x-summary 0.1.0 → 0.2.0

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/env.ts", "../../src/scrape.ts", "../../src/config/load.ts", "../../src/browser/profile.ts", "../../src/validate/ajv.ts", "../../src/validate/json.ts", "../../src/logger.ts", "../../src/browser/article-fields.ts", "../../src/links/resolve.ts", "../../src/scrape-timeouts.ts", "../../src/browser/post-processor.ts", "../../src/state/assemble.ts", "../../src/browser/interactions.ts", "../../src/browser/post-stub.ts", "../../src/browser/tweet-body.ts", "../../src/browser/tweet-detail-api.ts", "../../src/browser/post-detail.ts", "../../src/browser/tab-pool.ts", "../../src/browser/timeline.ts", "../../src/browser/scrape.ts", "../../src/browser/session.ts", "../../src/browser/auth.ts", "../../src/browser/login-window.ts", "../../src/browser/stealth.ts", "../../src/cli.ts", "../../src/state/io.ts"],
4
- "sourcesContent": ["import { config } from 'dotenv';\n\n/** Load `.env` from the current working directory (no-op if missing). */\nconfig({ quiet: true });\n", "import './env.js';\nimport { access } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { scrapeTimelines } from './browser/scrape.js';\nimport { acquireBrowserSession, closeBrowser, ensureOwnerSession } from './browser/session.js';\nimport { parseCli, resolveAbortOnIncorrectOwnerHandle } from './cli.js';\nimport { loadConfig } from './config/load.js';\nimport { createScrapeLogger } from './logger.js';\nimport { loadState, saveState } from './state/io.js';\nimport type { AppState } from './types/state.js';\n\n/** Scrape timelines and persist state to `config.statePath`. */\nexport async function runScrape(argv: string[]): Promise<AppState> {\n const cli = parseCli(argv);\n const resolvedConfigPath = resolve(cli.configPath);\n await assertPathExists(resolvedConfigPath, 'Config file');\n\n const config = await loadConfig(resolvedConfigPath);\n const log = createScrapeLogger();\n const abortOnIncorrectOwnerHandle = resolveAbortOnIncorrectOwnerHandle(\n cli,\n config.abortOnIncorrectOwnerHandle,\n );\n\n const previousState = await loadState(config.statePath);\n let session = await acquireBrowserSession(config, log);\n\n try {\n session = await ensureOwnerSession(session, {\n ownerHandle: config.ownerHandle,\n headless: config.headless,\n abortOnIncorrectOwnerHandle,\n log,\n });\n\n const state = await scrapeTimelines(session.page, { config, previousState });\n await saveState(config.statePath, state);\n\n log.info(\n {\n statePath: config.statePath,\n following: state.following.length,\n forYouSuggestions: state.forYouSuggestions.length,\n monitored: Object.fromEntries(\n Object.entries(state.monitored).map(([handle, posts]) => [handle, posts.length]),\n ),\n },\n 'scrape complete; state saved',\n );\n\n return state;\n } finally {\n await closeBrowser(session, log);\n }\n}\n\nasync function assertPathExists(path: string, label: string): Promise<void> {\n try {\n await access(path);\n } catch {\n throw new Error(`${label} not found: ${path}`);\n }\n}\n\nasync function main(): Promise<void> {\n await runScrape(process.argv);\n}\n\nconst entryPath = process.argv[1];\nconst isMain = entryPath !== undefined && resolve(entryPath) === fileURLToPath(import.meta.url);\nif (isMain) {\n main().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(message);\n process.exitCode = 1;\n });\n}\n", "import { readFile } from 'node:fs/promises';\nimport { DEFAULT_BROWSER_PROFILE_PATH } from '../browser/profile.js';\nimport type { AppConfig } from '../types/config.js';\nimport { assertValid } from '../validate/ajv.js';\nimport { parseJson } from '../validate/json.js';\n\nconst DEFAULT_STATE_PATH = './tmp/state.json';\nexport const DEFAULT_PARALLEL_TABS = 4;\n\nexport async function loadConfig(configPath: string): Promise<AppConfig> {\n const raw = await readFile(configPath, 'utf8');\n const parsed = parseJson(raw);\n const config = await assertValid<AppConfig>('config.schema.json', parsed, 'Config');\n return {\n ...config,\n statePath: config.statePath ?? DEFAULT_STATE_PATH,\n browserProfilePath: config.browserProfilePath ?? DEFAULT_BROWSER_PROFILE_PATH,\n llm: {\n ...config.llm,\n ...(config.llm.temperature ? { temperature: config.llm.temperature } : {}),\n },\n parallelTabs: config.parallelTabs ?? DEFAULT_PARALLEL_TABS,\n };\n}\n", "import { resolve } from 'node:path';\nimport type { AppConfig } from '../types/config.js';\n\nexport const DEFAULT_BROWSER_PROFILE_PATH = './tmp/browser-profile';\n\n/** Absolute path to the on-disk Chrome user-data directory (cookies, localStorage, etc.). */\nexport function resolveBrowserProfilePath(\n config: Pick<AppConfig, 'browserProfilePath'>,\n cwd: string = process.cwd(),\n): string {\n const relative = config.browserProfilePath ?? DEFAULT_BROWSER_PROFILE_PATH;\n return resolve(cwd, relative);\n}\n", "import { readFile } from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Ajv, type AnySchema, type ErrorObject, type ValidateFunction } from 'ajv';\n\nconst require = createRequire(import.meta.url);\nconst addFormats = require('ajv-formats') as (ajv: Ajv) => Ajv;\n\nconst schemasDir = join(dirname(fileURLToPath(import.meta.url)), '../../schemas');\n\nlet ajvInstance: Ajv | undefined;\n\nfunction getAjv(): Ajv {\n if (ajvInstance) {\n return ajvInstance;\n }\n const ajv = new Ajv({\n allErrors: true,\n strict: true,\n validateSchema: false,\n removeAdditional: false,\n });\n addFormats(ajv);\n ajvInstance = ajv;\n return ajv;\n}\n\nasync function loadSchemaFile(name: string): Promise<AnySchema> {\n const path = join(schemasDir, name);\n const raw = await readFile(path, 'utf8');\n return JSON.parse(raw) as AnySchema;\n}\n\nconst validatorCache = new Map<string, Promise<ValidateFunction>>();\n\nexport async function getValidator(schemaFile: string): Promise<ValidateFunction> {\n const cached = validatorCache.get(schemaFile);\n if (cached) {\n return cached;\n }\n const promise = (async () => {\n const ajv = getAjv();\n const schema = await loadSchemaFile(schemaFile);\n const validate = ajv.compile(schema);\n return validate;\n })();\n validatorCache.set(schemaFile, promise);\n return promise;\n}\n\nexport function formatAjvErrors(errors: ErrorObject[] | null | undefined): string {\n if (!errors?.length) {\n return 'Unknown validation error';\n }\n return errors\n .map((error) => {\n const path = error.instancePath || '/';\n return `${path}: ${error.message ?? 'invalid'}`;\n })\n .join('\\n');\n}\n\nexport async function assertValid<T>(schemaFile: string, data: unknown, label: string): Promise<T> {\n const validate = await getValidator(schemaFile);\n if (!validate(data)) {\n throw new Error(`${label} validation failed:\\n${formatAjvErrors(validate.errors)}`);\n }\n return data as T;\n}\n", "/** Pretty-print JSON for human review of machine-readable artifacts. */\nexport function stringifyJson(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\nexport function parseJson(text: string): unknown {\n return JSON.parse(text) as unknown;\n}\n", "import pino from 'pino';\n\nfunction readLogLevel(): string {\n // biome-ignore lint/complexity/useLiteralKeys: Bracket access required by TS4111 (noPropertyAccessFromIndexSignature).\n return process.env['LOG_LEVEL'] ?? 'info';\n}\n\nexport const logger = pino({\n level: readLogLevel(),\n base: { app: 'x-summary' },\n});\n\nexport type ScrapeLogger = pino.Logger;\nexport type LogLevel = pino.Level;\n\nexport function createScrapeLogger(): ScrapeLogger {\n return logger.child({ module: 'scrape' });\n}\n\nexport function logScrapeFailure(\n log: ScrapeLogger,\n context: {\n action: string;\n expected?: string;\n missing?: string;\n href?: string;\n err: unknown;\n },\n): void {\n const error =\n context.err instanceof Error\n ? { message: context.err.message, stack: context.err.stack, name: context.err.name }\n : { message: String(context.err) };\n\n log.error(\n {\n action: context.action,\n expected: context.expected,\n missing: context.missing,\n href: context.href,\n err: error,\n },\n 'scrape step failed',\n );\n}\n", "import type { Locator } from 'playwright';\nimport type { ScrapeLogger } from '../logger.js';\nimport type { Post } from '../types/post.js';\n\n/** Status URL used in feed lists when a post is keyed by a synthetic repost href. */\nexport function canonicalFeedHref(href: string): string {\n const match = /^repost:\\/\\/[^@]+@(.+)$/.exec(href);\n return match?.[1] ?? href;\n}\n\nexport function syntheticRepostHref(handle: string, statusHref: string): string {\n const normalized = handle.replace(/^@/, '');\n return `repost://${normalized}@${statusHref}`;\n}\n\nexport function isSyntheticRepostHref(href: string): boolean {\n return href.startsWith('repost://');\n}\n\n/** Canonical status page URL (`/handle/status/id`) without `/photo` or query suffixes. */\nexport function canonicalStatusHref(raw: string): string {\n const absolute = raw.startsWith('http') ? raw : `https://x.com${raw}`;\n try {\n const url = new URL(absolute);\n const match = url.pathname.match(/^(\\/[^/]+\\/status\\/\\d+)/);\n if (match) {\n return `https://x.com${match[1]}`;\n }\n return absolute;\n } catch {\n return absolute;\n }\n}\n\nexport function statusIdFromHref(href: string): string | null {\n const match = canonicalStatusHref(href).match(/\\/status\\/(\\d+)/);\n return match?.[1] ?? null;\n}\n\nexport async function readPostHref(article: Locator): Promise<string | null> {\n const links = article.getByRole('link');\n const count = await links.count();\n let fallback: string | null = null;\n\n for (let i = 0; i < count; i++) {\n const raw = await links.nth(i).getAttribute('href');\n if (!raw?.includes('/status/')) {\n continue;\n }\n const canonical = canonicalStatusHref(raw);\n const path = new URL(canonical).pathname;\n if (/^\\/[^/]+\\/status\\/\\d+$/.test(path)) {\n return canonical;\n }\n fallback ??= canonical;\n }\n\n return fallback;\n}\n\nexport function normalizeStatusPageUrl(url: string): string {\n try {\n const parsed = new URL(url);\n parsed.hash = '';\n parsed.search = '';\n return parsed.toString();\n } catch {\n return url;\n }\n}\n\n/** Stats live in `[role=\"group\"]` \u2192 `[data-testid=\"reply|retweet|like\"]`. */\nexport async function readStats(article: Locator, log: ScrapeLogger): Promise<Post['stats']> {\n const readByTestId = async (testId: string): Promise<number> => {\n const button = article.getByTestId(testId).first();\n if (!(await button.count())) {\n log.debug({ testId }, 'stat control not found, defaulting to 0');\n return 0;\n }\n const text = await button.innerText().catch(() => '0');\n return parseMetricCount(text);\n };\n\n return {\n comments: await readByTestId('reply'),\n reposts: await readByTestId('retweet'),\n likes: await readByTestId('like'),\n };\n}\n\n/** Handle from `[data-testid=\"User-Name\"] a[role=\"link\"]` href (`/handle`). */\nexport async function readAuthor(article: Locator): Promise<{ author?: string }> {\n const userBlock = article.getByTestId('User-Name');\n if (await userBlock.count()) {\n const link = userBlock.getByRole('link').first();\n const href = await link.getAttribute('href');\n if (href) {\n const handle = href.replace(/^\\//, '').split('/')[0]?.replace(/^@/, '');\n if (handle) {\n return { author: handle };\n }\n }\n }\n\n return {};\n}\n\nexport async function readTimestamp(article: Locator): Promise<{ timestamp?: string }> {\n const time = article.locator('time').first();\n const datetime = await time.getAttribute('datetime').catch(() => null);\n return datetime ? { timestamp: datetime } : {};\n}\n\nfunction parseMetricCount(raw: string): number {\n const text = raw.trim().toUpperCase();\n if (!text || text === '\u2014') {\n return 0;\n }\n const match = /^([\\d,.]+)\\s*([KMB])?/.exec(text);\n if (!match) {\n return 0;\n }\n const base = Number.parseFloat(match[1]?.replace(/,/g, '') ?? '0');\n const suffix = match[2];\n const multiplier =\n suffix === 'K' ? 1_000 : suffix === 'M' ? 1_000_000 : suffix === 'B' ? 1_000_000_000 : 1;\n return Math.round(base * multiplier);\n}\n", "import { promises as dns } from 'node:dns';\nimport { isIP } from 'node:net';\nimport type { ResolvedLink } from '../types/post.js';\n\nconst MAX_REDIRECTS = 10;\nconst FETCH_TIMEOUT_MS = 30_000;\nconst MAX_HTML_BYTES = 512_000;\n\nconst DESCRIPTION_META_KEYS = ['twitter:description', 'og:description', 'description'] as const;\n\n/**\n * Follow redirects, fetch HTML, and extract title plus description meta tags.\n */\nexport async function resolveLink(\n url: string,\n options?: { maxRedirects?: number; signal?: AbortSignal },\n): Promise<ResolvedLink> {\n const finalUrl = await resolveFinalUrl(url, options);\n try {\n const fetched = await fetchHtmlIfHtml(finalUrl, options?.signal);\n if (!fetched) {\n return { url: finalUrl };\n }\n const { title, description } = extractPageMetadata(fetched);\n\n return {\n url: finalUrl,\n ...(title ? { title } : {}),\n ...(description ? { description } : {}),\n };\n } catch {\n return { url: finalUrl };\n }\n}\n\nexport async function resolveLinks(\n urls: string[],\n cache?: Map<string, ResolvedLink>,\n): Promise<ResolvedLink[]> {\n const unique = [...new Set(urls)];\n const results: ResolvedLink[] = [];\n\n for (const url of unique) {\n const cached = cache?.get(url);\n if (cached) {\n results.push(cached);\n continue;\n }\n const resolved = await resolveLink(url);\n cache?.set(url, resolved);\n results.push(resolved);\n }\n\n return results;\n}\n\n/** @internal Redirect resolution only (no HTML). */\nexport async function resolveFinalUrl(\n url: string,\n options?: { maxRedirects?: number; signal?: AbortSignal },\n): Promise<string> {\n const maxRedirects = options?.maxRedirects ?? MAX_REDIRECTS;\n let current = new URL(url);\n\n for (let i = 0; i <= maxRedirects; i++) {\n await assertSafeExternalHttpUrl(current);\n const head = await fetchWithTimeout(current.toString(), {\n method: 'HEAD',\n redirect: 'manual',\n ...signalInit(options?.signal),\n });\n const headNext = redirectTarget(current, head);\n if (headNext) {\n await assertSafeExternalHttpUrl(headNext);\n current = headNext;\n continue;\n }\n\n if (head.status === 405 || head.status === 501) {\n const get = await fetchWithTimeout(current.toString(), {\n method: 'GET',\n redirect: 'manual',\n ...signalInit(options?.signal),\n });\n const getNext = redirectTarget(current, get);\n if (getNext) {\n await assertSafeExternalHttpUrl(getNext);\n current = getNext;\n continue;\n }\n }\n\n return current.toString();\n }\n\n throw new Error(`Too many redirects resolving ${url}`);\n}\n\nexport function extractPageMetadata(html: string): { title?: string; description?: string } {\n const title = extractTitle(html);\n const description = extractDescription(html);\n return {\n ...(title ? { title } : {}),\n ...(description ? { description } : {}),\n };\n}\n\nfunction extractTitle(html: string): string | undefined {\n const match = /<title[^>]*>([^<]*)<\\/title>/i.exec(html);\n const title = match?.[1]?.trim();\n return title || undefined;\n}\n\nfunction extractDescription(html: string): string | undefined {\n const metas = parseMetaTags(html);\n for (const key of DESCRIPTION_META_KEYS) {\n const value = metas.get(key);\n if (value) {\n return value;\n }\n }\n for (const [name, value] of metas) {\n if (name.endsWith(':description') || name === 'description') {\n return value;\n }\n }\n return undefined;\n}\n\nfunction parseMetaTags(html: string): Map<string, string> {\n const map = new Map<string, string>();\n const tagPattern = /<meta\\s+[^>]*>/gi;\n\n for (const tag of html.matchAll(tagPattern)) {\n const attrs = parseAttributes(tag[0] ?? '');\n const name = attrs.name ?? attrs.property;\n const content = attrs.content;\n if (name && content) {\n map.set(name.toLowerCase(), decodeHtmlEntities(content));\n }\n }\n\n return map;\n}\n\ntype MetaAttributes = {\n name?: string;\n property?: string;\n content?: string;\n};\n\nfunction parseAttributes(tag: string): MetaAttributes {\n const attrs: MetaAttributes = {};\n const attrPattern = /([a-zA-Z_:.-]+)\\s*=\\s*(\"([^\"]*)\"|'([^']*)'|(\\S+))/g;\n for (const match of tag.matchAll(attrPattern)) {\n const key = match[1]?.toLowerCase();\n const value = match[3] ?? match[4] ?? match[5] ?? '';\n if (key === 'name' || key === 'property' || key === 'content') {\n attrs[key] = value;\n }\n }\n return attrs;\n}\n\nfunction decodeHtmlEntities(text: string): string {\n return text\n .replaceAll('&amp;', '&')\n .replaceAll('&lt;', '<')\n .replaceAll('&gt;', '>')\n .replaceAll('&quot;', '\"')\n .replaceAll('&#39;', \"'\");\n}\n\nfunction isHtmlContentType(contentType: string | null): boolean {\n if (!contentType) {\n return false;\n }\n const base = contentType.split(';')[0]?.trim().toLowerCase() ?? '';\n return base === 'text/html' || base === 'application/xhtml+xml';\n}\n\nasync function fetchHtmlIfHtml(url: string, signal?: AbortSignal): Promise<string | null> {\n let current = new URL(url);\n\n for (let i = 0; i <= MAX_REDIRECTS; i++) {\n await assertSafeExternalHttpUrl(current);\n const response = await fetchWithTimeout(current.toString(), {\n method: 'GET',\n redirect: 'manual',\n headers: { Accept: 'text/html,application/xhtml+xml' },\n ...signalInit(signal),\n });\n\n const next = redirectTarget(current, response);\n if (next) {\n await assertSafeExternalHttpUrl(next);\n current = next;\n continue;\n }\n\n return await readHtmlResponse(current.toString(), response);\n }\n\n throw new Error(`Too many redirects fetching HTML for ${url}`);\n}\n\nasync function readHtmlResponse(url: string, response: Response): Promise<string | null> {\n if (!response.ok) {\n throw new Error(`Failed to fetch HTML for ${url}: HTTP ${response.status}`);\n }\n\n if (!isHtmlContentType(response.headers.get('content-type'))) {\n return null;\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n return '';\n }\n\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (value) {\n total += value.length;\n if (total > MAX_HTML_BYTES) {\n break;\n }\n chunks.push(value);\n }\n }\n\n return new TextDecoder().decode(concatChunks(chunks));\n}\n\n/** @internal Exported for tests. */\nexport { isHtmlContentType };\n\nfunction concatChunks(chunks: Uint8Array[]): Uint8Array {\n const length = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const out = new Uint8Array(length);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nfunction redirectTarget(base: URL, response: Response): URL | null {\n if (!isRedirect(response.status)) {\n return null;\n }\n const location = response.headers.get('location');\n if (!location) {\n return base;\n }\n return new URL(location, base);\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nasync function assertSafeExternalHttpUrl(url: URL): Promise<void> {\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new Error(`Unsafe URL protocol: ${url.protocol}`);\n }\n\n const host = normalizeUrlHostname(url.hostname);\n if (isLocalHostname(host)) {\n throw new Error(`Unsafe local URL host: ${url.hostname}`);\n }\n\n if (isUnsafeIpAddress(host)) {\n throw new Error(`Unsafe private URL host: ${url.hostname}`);\n }\n\n if (isIP(host)) {\n return;\n }\n\n const addresses = await dns.lookup(host, { all: true, verbatim: true });\n if (!addresses.length) {\n throw new Error(`Could not resolve URL host: ${url.hostname}`);\n }\n\n for (const { address } of addresses) {\n if (isUnsafeIpAddress(address)) {\n throw new Error(`Unsafe private URL host: ${url.hostname}`);\n }\n }\n}\n\nfunction isLocalHostname(host: string): boolean {\n return host === 'localhost' || host.endsWith('.localhost');\n}\n\nfunction normalizeUrlHostname(hostname: string): string {\n const withoutTrailingDot = hostname.replace(/\\.$/, '').toLowerCase();\n return withoutTrailingDot.startsWith('[') && withoutTrailingDot.endsWith(']')\n ? withoutTrailingDot.slice(1, -1)\n : withoutTrailingDot;\n}\n\nfunction isUnsafeIpAddress(address: string): boolean {\n const family = isIP(address);\n if (family === 4) {\n return isUnsafeIpv4Address(address);\n }\n if (family === 6) {\n return isUnsafeIpv6Address(address);\n }\n return false;\n}\n\nfunction isUnsafeIpv4Address(address: string): boolean {\n const parts = address.split('.').map((part) => Number.parseInt(part, 10));\n if (\n parts.length !== 4 ||\n parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)\n ) {\n return true;\n }\n\n const [a = 0, b = 0] = parts;\n return (\n a === 0 ||\n a === 10 ||\n a === 127 ||\n (a === 100 && b >= 64 && b <= 127) ||\n (a === 169 && b === 254) ||\n (a === 172 && b >= 16 && b <= 31) ||\n (a === 192 && b === 168) ||\n (a === 198 && (b === 18 || b === 19)) ||\n a >= 224\n );\n}\n\nfunction isUnsafeIpv6Address(address: string): boolean {\n const normalized = address.toLowerCase();\n if (normalized.startsWith('::ffff:')) {\n const mapped = normalized.slice('::ffff:'.length);\n return isUnsafeIpv4Address(mapped);\n }\n return (\n normalized === '::' ||\n normalized === '::1' ||\n normalized.startsWith('fc') ||\n normalized.startsWith('fd') ||\n /^fe[89ab]/.test(normalized) ||\n normalized.startsWith('ff')\n );\n}\n\nfunction signalInit(signal?: AbortSignal): Pick<RequestInit, 'signal'> {\n return signal ? { signal } : {};\n}\n\nasync function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n const signal = init.signal\n ? AbortSignal.any([init.signal, controller.signal])\n : controller.signal;\n\n try {\n return await fetch(url, { ...init, signal });\n } finally {\n clearTimeout(timeout);\n }\n}\n", "/** Timeout for conversation DOM parsing (navigation + thread/quote nesting). */\nexport const POST_DETAIL_PARSE_TIMEOUT_MS = 60_000;\n\n/** Per-item timeout for external link resolution. */\nexport const SCRAPE_ITEM_TIMEOUT_MS = 30_000;\n\nexport class ScrapeTimeoutError extends Error {\n constructor(label: string, ms: number) {\n super(`${label} timed out after ${ms}ms`);\n this.name = 'ScrapeTimeoutError';\n }\n}\n\nexport async function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new ScrapeTimeoutError(label, ms));\n }, ms);\n });\n\n try {\n return await Promise.race([promise, timeoutPromise]);\n } finally {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n }\n}\n", "import { resolveLink } from '../links/resolve.js';\nimport type { ScrapeLogger } from '../logger.js';\nimport { SCRAPE_ITEM_TIMEOUT_MS, withTimeout } from '../scrape-timeouts.js';\nimport type { Post, ResolvedLink } from '../types/post.js';\n\nexport function normalizePostHref(href: string): string {\n if (href.startsWith('repost://')) {\n return href;\n }\n try {\n const url = new URL(href);\n url.hash = '';\n return url.toString();\n } catch {\n return href;\n }\n}\n\n/** Cache posts and resolved links; guard threads/references against cycles. */\nexport class PostProcessor {\n private readonly postCache = new Map<string, Post>();\n private readonly linkCache = new Map<string, ResolvedLink>();\n\n private readonly log: ScrapeLogger;\n\n constructor(log: ScrapeLogger) {\n this.log = log;\n }\n\n getCached(href: string): Post | undefined {\n return this.postCache.get(normalizePostHref(href));\n }\n\n remember(post: Post): void {\n this.postCache.set(normalizePostHref(post.href), post);\n }\n\n collectAllHrefs(post: Post, into: Set<string>): void {\n const key = normalizePostHref(post.href);\n if (into.has(key)) {\n return;\n }\n into.add(key);\n for (const ref of post.references ?? []) {\n this.collectAllHrefs(ref, into);\n }\n for (const item of post.thread ?? []) {\n this.collectAllHrefs(item, into);\n }\n }\n\n async finalize(post: Post, cycleGuard: Set<string>, remember = true): Promise<Post> {\n const key = normalizePostHref(post.href);\n const cached = this.postCache.get(key);\n if (cached) {\n return cached;\n }\n\n if (cycleGuard.has(key)) {\n this.log.debug({ href: key }, 'cycle detected; omitting nested content');\n return post;\n }\n\n cycleGuard.add(key);\n\n const urlList = post.linkUrls?.length\n ? post.linkUrls\n : extractUrlsFromMarkdown(post.body ?? '');\n const links = urlList.length ? await this.resolveLinksCached(urlList) : undefined;\n\n const references = await this.finalizeNested(post.references ?? [], cycleGuard, false);\n const thread = await this.finalizeNested(post.thread ?? [], cycleGuard, false);\n\n cycleGuard.delete(key);\n\n const {\n references: _refs,\n thread: _thread,\n links: _links,\n linkUrls: _linkUrls,\n ...base\n } = post;\n const finalized: Post = {\n ...base,\n ...(links?.length ? { links } : {}),\n ...(references.length ? { references } : {}),\n ...(thread.length ? { thread } : {}),\n };\n\n if (remember) {\n this.postCache.set(key, finalized);\n }\n return finalized;\n }\n\n private async finalizeNested(\n posts: Post[],\n cycleGuard: Set<string>,\n remember: boolean,\n ): Promise<Post[]> {\n const result: Post[] = [];\n for (const item of posts) {\n const key = normalizePostHref(item.href);\n if (cycleGuard.has(key)) {\n this.log.debug({ href: key }, 'cycle detected; skipping reference/thread insert');\n continue;\n }\n result.push(await this.finalize(item, cycleGuard, remember));\n }\n return result;\n }\n\n private async resolveLinksCached(urls: string[]): Promise<ResolvedLink[]> {\n const results: ResolvedLink[] = [];\n for (const url of urls) {\n const cached = this.linkCache.get(url);\n if (cached) {\n results.push(cached);\n continue;\n }\n if (isDirectMediaUrl(url)) {\n const link = { url };\n this.linkCache.set(url, link);\n results.push(link);\n continue;\n }\n try {\n const resolved = await withTimeout(\n resolveLink(url),\n SCRAPE_ITEM_TIMEOUT_MS,\n `external link ${url}`,\n );\n this.linkCache.set(url, resolved);\n results.push(resolved);\n } catch (err) {\n this.log.warn({ url, err }, 'external link resolution failed; keeping url only');\n const fallback = { url };\n this.linkCache.set(url, fallback);\n results.push(fallback);\n }\n }\n return results;\n }\n}\n\nfunction isDirectMediaUrl(url: string): boolean {\n try {\n const { pathname } = new URL(url);\n return (\n /\\.(mp4|m3u8|webm|mov)(\\?|$)/i.test(pathname) ||\n pathname.includes('/video/') ||\n pathname.includes('/amplify_video/')\n );\n } catch {\n return false;\n }\n}\n\nfunction extractUrlsFromMarkdown(markdown: string): string[] {\n const urls: string[] = [];\n const pattern = /https?:\\/\\/[^\\s)>\\]]+/g;\n for (const match of markdown.matchAll(pattern)) {\n urls.push(match[0].replace(/[.,;:!?)]+$/, ''));\n }\n return urls;\n}\n", "import { canonicalFeedHref } from '../browser/article-fields.js';\nimport { normalizePostHref } from '../browser/post-processor.js';\nimport type { AppConfig } from '../types/config.js';\nimport type { Post } from '../types/post.js';\nimport type { AppState, PostRecord } from '../types/state.js';\n\n/** Derive scrape window start as an absolute ISO8601 instant (stored in state.cutoffTimestamp). */\nexport function resolveScrapeCutoff(\n config: AppConfig,\n previousState: AppState | null,\n nowMs: number = Date.now(),\n): { cutoffMs: number; cutoffTimestamp: string } {\n if (previousState) {\n return {\n cutoffMs: Date.parse(previousState.timestamp),\n cutoffTimestamp: previousState.timestamp,\n };\n }\n const cutoffMs = nowMs - config.timeWindowMinutes * 60 * 1000;\n return {\n cutoffMs,\n cutoffTimestamp: new Date(cutoffMs).toISOString(),\n };\n}\n\n/** Flatten scraped posts into `posts` plus href-only feed lists. */\nexport function buildAppState(\n timestamp: string,\n cutoffTimestamp: string,\n following: Post[],\n forYouSuggestions: Post[],\n monitored: Record<string, Post[]>,\n): AppState {\n const posts: Record<string, PostRecord> = {};\n\n for (const post of following) {\n ingestPost(post, posts);\n }\n for (const post of forYouSuggestions) {\n ingestPost(post, posts);\n }\n for (const list of Object.values(monitored)) {\n for (const post of list) {\n ingestPost(post, posts);\n }\n }\n\n return {\n timestamp,\n cutoffTimestamp,\n posts,\n following: following.map((post) => normalizePostHref(canonicalFeedHref(post.href))),\n forYouSuggestions: forYouSuggestions.map((post) =>\n normalizePostHref(canonicalFeedHref(post.href)),\n ),\n monitored: Object.fromEntries(\n Object.entries(monitored).map(([handle, list]) => [\n handle,\n list.map((post) => normalizePostHref(canonicalFeedHref(post.href))),\n ]),\n ),\n };\n}\n\nfunction ingestPost(post: Post, posts: Record<string, PostRecord>): void {\n const key = normalizePostHref(post.href);\n for (const ref of post.references ?? []) {\n ingestPost(ref, posts);\n }\n for (const item of post.thread ?? []) {\n ingestPost(item, posts);\n }\n if (posts[key]) {\n return;\n }\n posts[key] = toPostRecord(post);\n}\n\nfunction toPostRecord(post: Post): PostRecord {\n return {\n stats: post.stats,\n ...(post.author ? { author: post.author } : {}),\n ...(post.timestamp ? { timestamp: post.timestamp } : {}),\n ...(post.body ? { body: post.body } : {}),\n ...(post.links?.length ? { links: post.links } : {}),\n ...(post.thread?.length\n ? { thread: post.thread.map((item) => normalizePostHref(item.href)) }\n : {}),\n ...(post.references?.length\n ? { references: post.references.map((item) => normalizePostHref(item.href)) }\n : {}),\n };\n}\n\n/** All post hrefs in a persisted state (feeds, references, thread). */\nexport function collectStateHrefs(state: AppState): Set<string> {\n const hrefs = new Set<string>();\n const add = (href: string): void => {\n hrefs.add(normalizePostHref(href));\n };\n\n for (const href of state.following) {\n add(href);\n }\n for (const href of state.forYouSuggestions) {\n add(href);\n }\n for (const list of Object.values(state.monitored)) {\n for (const href of list) {\n add(href);\n }\n }\n for (const key of Object.keys(state.posts)) {\n add(key);\n }\n for (const record of Object.values(state.posts)) {\n for (const href of record.references ?? []) {\n add(href);\n }\n for (const href of record.thread ?? []) {\n add(href);\n }\n }\n\n return hrefs;\n}\n", "import type { Page } from 'playwright';\nimport type { ScrapeLogger } from '../logger.js';\n\nconst MIN_ACTION_DELAY_MS = 500;\n\nexport async function humanDelay(): Promise<void> {\n const jitter = Math.floor(Math.random() * 500);\n await new Promise((resolve) => setTimeout(resolve, MIN_ACTION_DELAY_MS + jitter));\n}\n\n/**\n * Wait for client-side fetches to settle after a UI action on X (SPA).\n */\nexport async function waitForUiSettled(\n page: Page,\n log: ScrapeLogger,\n label: string,\n): Promise<void> {\n log.debug({ label }, 'waiting for UI to settle');\n await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => undefined);\n await waitForDomIdle(page);\n}\n\n/** Lighter settle for in-page actions (Show more, quote navigation) without networkidle. */\nexport async function waitAfterDomAction(\n page: Page,\n log: ScrapeLogger,\n label: string,\n): Promise<void> {\n log.debug({ label }, 'waiting after DOM action');\n await waitForDomIdle(page);\n}\n\n/** Wait for a post status page conversation timeline and first tweet article. */\nexport async function waitForConversationReady(\n page: Page,\n log: ScrapeLogger,\n label = 'post conversation',\n): Promise<void> {\n log.debug({ label }, 'waiting for conversation timeline');\n const timeline = page.getByLabel('Timeline: Conversation', { exact: true });\n await timeline.waitFor({ state: 'visible', timeout: 20_000 });\n const articles = timeline.locator('article[data-testid=\"tweet\"]');\n await articles\n .first()\n .waitFor({ state: 'visible', timeout: 20_000 })\n .catch(() => undefined);\n await humanDelay();\n}\n\nasync function waitForDomIdle(page: Page): Promise<void> {\n const busy = page.locator('[aria-busy=\"true\"]');\n if ((await busy.count()) > 0) {\n await busy\n .first()\n .waitFor({ state: 'hidden', timeout: 10_000 })\n .catch(() => undefined);\n }\n\n await humanDelay();\n}\n\n/** Close transient overlays (layers div) that block timeline controls. */\nexport async function dismissBlockingLayers(page: Page): Promise<void> {\n await page.keyboard.press('Escape');\n const dismissNames = [/^Close$/i, /^Not now$/i, /^Got it$/i, /^Dismiss$/i];\n for (const name of dismissNames) {\n const button = page.getByRole('button', { name }).first();\n if (await button.isVisible().catch(() => false)) {\n await button.click({ timeout: 2_000 }).catch(() => undefined);\n }\n }\n}\n\nexport async function tracedClick(\n page: Page,\n log: ScrapeLogger,\n target: { click: (options?: { force?: boolean }) => Promise<void> },\n action: string,\n options?: { force?: boolean },\n): Promise<void> {\n log.info({ action }, 'interaction');\n await target.click(options);\n await waitForUiSettled(page, log, action);\n}\n", "import type { Post } from '../types/post.js';\nimport { normalizePostHref } from './post-processor.js';\n\n/** Minimal post record when detail scraping or nesting fails. */\nexport function postStub(href: string): Post {\n return {\n href: normalizePostHref(href),\n stats: { comments: 0, reposts: 0, likes: 0 },\n };\n}\n", "import type { Locator } from 'playwright';\n\ntype TweetAnchor = {\n text: string;\n href: string;\n};\n\n/** Reposter/author text only \u2014 excludes quoted-tweet preview inside role=link cards. */\nexport async function readOwnTweetBodyMarkdown(article: Locator): Promise<string | undefined> {\n const tweetTexts = article.locator('[data-testid=\"tweetText\"]');\n const count = await tweetTexts.count();\n\n for (let i = 0; i < count; i++) {\n const tweetText = tweetTexts.nth(i);\n const inQuoteCard = await tweetText.evaluate((el) => !!el.closest('div[role=\"link\"]'));\n if (inQuoteCard) {\n continue;\n }\n\n const plain = (await tweetText.innerText()).trim();\n if (!plain) {\n continue;\n }\n\n const anchors = await tweetText.evaluate((el) => {\n const out: { text: string; href: string }[] = [];\n for (const anchor of el.querySelectorAll('a[href]')) {\n out.push({\n text: (anchor.textContent ?? '').trim(),\n href: anchor.getAttribute('href') ?? '',\n });\n }\n return out;\n });\n\n return plainTextToMarkdown(plain, anchors);\n }\n\n return undefined;\n}\n\nexport function plainTextToMarkdown(plain: string, anchors: TweetAnchor[]): string {\n let markdown = plain;\n for (const { text, href } of anchors) {\n const absolute = toAbsoluteAnchorHref(href);\n if (!text || markdown.includes(`](${absolute})`)) {\n continue;\n }\n markdown = markdown.replace(text, `[${text}](${absolute})`);\n }\n return markdown;\n}\n\nfunction toAbsoluteAnchorHref(href: string): string {\n if (href.startsWith('http')) {\n return href;\n }\n if (href.startsWith('/')) {\n return `https://x.com${href}`;\n }\n return href;\n}\n", "import type { Page } from 'playwright';\nimport type { ScrapeLogger } from '../logger.js';\nimport type { Post, Stats } from '../types/post.js';\nimport { normalizeStatusPageUrl, statusIdFromHref, syntheticRepostHref } from './article-fields.js';\nimport { waitForConversationReady } from './interactions.js';\nimport { plainTextToMarkdown } from './tweet-body.js';\n\ntype UrlEntity = {\n url?: string;\n expanded_url?: string;\n display_url?: string;\n};\n\ntype MediaEntity = {\n type?: string;\n url?: string;\n expanded_url?: string;\n display_url?: string;\n media_url_https?: string;\n video_info?: { variants?: Array<{ url?: string; content_type?: string }> };\n};\n\ntype LegacyTweet = {\n id_str?: string;\n created_at?: string;\n full_text?: string;\n reply_count?: number;\n retweet_count?: number;\n favorite_count?: number;\n is_quote_status?: boolean;\n in_reply_to_status_id_str?: string;\n conversation_id_str?: string;\n entities?: { urls?: UrlEntity[]; media?: MediaEntity[] };\n extended_entities?: { media?: MediaEntity[] };\n};\n\ntype RawTweetResult = {\n rest_id?: string;\n legacy?: LegacyTweet;\n core?: { user_results?: { result?: { core?: { screen_name?: string } } } };\n note_tweet?: { note_tweet_results?: { result?: { text?: string } } };\n quoted_status_result?: { result?: RawTweetResult };\n retweeted_status_result?: { result?: RawTweetResult };\n card?: {\n legacy?: { binding_values?: Array<{ key?: string; value?: { string_value?: string } }> };\n };\n};\n\nexport type TweetDetailListener = {\n waitFor: (timeoutMs?: number) => Promise<string | null>;\n detach: () => void;\n};\n\n/** Listen for TweetDetail GraphQL on the next matching response. */\nexport function attachTweetDetailListener(page: Page, focalTweetId: string): TweetDetailListener {\n let captured: string | null = null;\n let settled = false;\n const waiters: Array<(value: string | null) => void> = [];\n\n const notify = (value: string | null): void => {\n if (settled) {\n return;\n }\n settled = true;\n captured = value;\n for (const resolve of waiters) {\n resolve(value);\n }\n waiters.length = 0;\n };\n\n const handler = async (response: {\n url: () => string;\n text: () => Promise<string>;\n }): Promise<void> => {\n const url = response.url();\n if (!url.includes('TweetDetail') || !url.includes(focalTweetId)) {\n return;\n }\n try {\n notify(await response.text());\n } catch {\n // ignore truncated bodies\n }\n };\n\n page.on('response', handler);\n\n return {\n waitFor: (timeoutMs = 15_000) => {\n if (captured) {\n return Promise.resolve(captured);\n }\n return new Promise<string | null>((resolve) => {\n const timer = setTimeout(() => {\n page.off('response', handler);\n resolve(captured);\n }, timeoutMs);\n waiters.push((value) => {\n clearTimeout(timer);\n resolve(value);\n });\n });\n },\n detach: () => {\n page.off('response', handler);\n },\n };\n}\n\n/** Navigate (or reload) and wait for TweetDetail JSON. */\nexport async function loadTweetDetailJson(\n page: Page,\n href: string,\n log: ScrapeLogger,\n): Promise<string | null> {\n const focalId = statusIdFromHref(href);\n if (!focalId) {\n return null;\n }\n\n const listener = attachTweetDetailListener(page, focalId);\n const target = normalizeStatusPageUrl(href);\n\n try {\n if (normalizeStatusPageUrl(page.url()) !== target) {\n await page.goto(href, { waitUntil: 'domcontentloaded' });\n } else {\n log.debug({ focalId }, 'reloading conversation to capture TweetDetail');\n await page.reload({ waitUntil: 'domcontentloaded' });\n }\n await waitForConversationReady(page, log);\n return await listener.waitFor(15_000);\n } finally {\n listener.detach();\n }\n}\n\n/** Parse focal post (thread, quotes, media) from TweetDetail GraphQL JSON. */\nexport function parsePostFromTweetDetail(json: string, focalTweetId: string): Post | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n return null;\n }\n\n const graph = indexTweetResults(parsed);\n const focal = graph.get(focalTweetId);\n if (!focal) {\n return null;\n }\n\n return buildPostFromNode(focal, graph, {\n includeThread: true,\n includeQuotes: true,\n allowSyntheticRepost: true,\n });\n}\n\nfunction indexTweetResults(json: unknown): Map<string, RawTweetResult> {\n const graph = new Map<string, RawTweetResult>();\n\n const visit = (value: unknown): void => {\n if (!value || typeof value !== 'object') {\n return;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n visit(item);\n }\n return;\n }\n\n const node = value as RawTweetResult;\n const id = node.legacy?.id_str;\n const author = node.core?.user_results?.result?.core?.screen_name;\n if (id && author) {\n graph.set(id, node);\n }\n\n for (const child of Object.values(value)) {\n visit(child);\n }\n };\n\n visit(json);\n return graph;\n}\n\ntype BuildOptions = {\n includeThread: boolean;\n includeQuotes: boolean;\n allowSyntheticRepost: boolean;\n};\n\nfunction postBaseFields(\n author: string,\n legacy: LegacyTweet,\n): { stats: Stats; author?: string; timestamp?: string } {\n const timestamp = parseTwitterDate(legacy.created_at);\n return {\n stats: mapStats(legacy),\n ...(author ? { author } : {}),\n ...(timestamp ? { timestamp } : {}),\n };\n}\n\nfunction buildBareRepostPost(\n node: RawTweetResult,\n graph: Map<string, RawTweetResult>,\n author: string,\n href: string,\n legacy: LegacyTweet,\n): Post {\n const retweeted = node.retweeted_status_result?.result;\n if (!retweeted) {\n throw new Error('bare retweet missing retweeted_status_result');\n }\n return {\n href: syntheticRepostHref(author, href),\n ...postBaseFields(author, legacy),\n references: [\n buildPostFromNode(retweeted, graph, {\n includeThread: false,\n includeQuotes: true,\n allowSyntheticRepost: false,\n }),\n ],\n };\n}\n\nfunction buildQuoteReferences(\n node: RawTweetResult,\n graph: Map<string, RawTweetResult>,\n options: BuildOptions,\n): Post[] {\n const quoted = node.quoted_status_result?.result;\n if (!options.includeQuotes || !quoted) {\n return [];\n }\n return [\n buildPostFromNode(quoted, graph, {\n includeThread: false,\n includeQuotes: false,\n allowSyntheticRepost: false,\n }),\n ];\n}\n\nfunction buildPostFromNode(\n node: RawTweetResult,\n graph: Map<string, RawTweetResult>,\n options: BuildOptions,\n): Post {\n const legacy = node.legacy;\n if (!legacy?.id_str) {\n throw new Error('tweet node missing id_str');\n }\n\n const author = node.core?.user_results?.result?.core?.screen_name ?? '';\n const href = statusHref(author, legacy.id_str);\n\n if (options.allowSyntheticRepost && isBareRetweet(node)) {\n return buildBareRepostPost(node, graph, author, href, legacy);\n }\n\n const body = tweetBodyMarkdown(node);\n const linkUrls = collectLinkUrls(node);\n const references = buildQuoteReferences(node, graph, options);\n const thread = options.includeThread ? buildThreadChain(node, graph) : [];\n\n return {\n href,\n ...postBaseFields(author, legacy),\n ...(body ? { body } : {}),\n ...(linkUrls.length ? { linkUrls } : {}),\n ...(references.length ? { references } : {}),\n ...(thread.length ? { thread } : {}),\n };\n}\n\nfunction buildThreadChain(node: RawTweetResult, graph: Map<string, RawTweetResult>): Post[] {\n const thread: Post[] = [];\n const seen = new Set<string>();\n let current: RawTweetResult | undefined = node;\n\n while (current?.legacy?.in_reply_to_status_id_str) {\n const parentId = current.legacy.in_reply_to_status_id_str;\n if (seen.has(parentId)) {\n break;\n }\n seen.add(parentId);\n const parent = graph.get(parentId);\n if (!parent) {\n break;\n }\n thread.unshift(\n buildPostFromNode(parent, graph, {\n includeThread: false,\n includeQuotes: false,\n allowSyntheticRepost: false,\n }),\n );\n current = parent;\n }\n\n return thread;\n}\n\nfunction isBareRetweet(node: RawTweetResult): boolean {\n const retweeted = node.retweeted_status_result?.result;\n if (!retweeted) {\n return false;\n }\n if (node.legacy?.is_quote_status) {\n return false;\n }\n const text = tweetPlainText(node).trim();\n if (!text) {\n return true;\n }\n return /^RT @\\w+:/i.test(text);\n}\n\nfunction tweetPlainText(node: RawTweetResult): string {\n return node.note_tweet?.note_tweet_results?.result?.text ?? node.legacy?.full_text ?? '';\n}\n\nfunction tweetBodyMarkdown(node: RawTweetResult): string | undefined {\n const text = tweetPlainText(node).trim();\n if (!text) {\n return undefined;\n }\n\n const anchors: Array<{ text: string; href: string }> = [];\n for (const url of node.legacy?.entities?.urls ?? []) {\n if (url.expanded_url) {\n anchors.push({\n text: url.display_url ?? url.url ?? url.expanded_url,\n href: url.expanded_url,\n });\n }\n }\n for (const media of mediaEntities(node)) {\n if (media.expanded_url && media.display_url) {\n anchors.push({ text: media.display_url, href: media.expanded_url });\n }\n }\n\n return plainTextToMarkdown(text, anchors);\n}\n\nfunction mediaEntities(node: RawTweetResult): MediaEntity[] {\n return node.legacy?.extended_entities?.media ?? node.legacy?.entities?.media ?? [];\n}\n\nfunction collectLinkUrls(node: RawTweetResult): string[] {\n const urls = new Set<string>();\n const add = (raw?: string): void => {\n if (!raw || raw.startsWith('blob:')) {\n return;\n }\n try {\n const url = new URL(raw);\n if (isHttpUrl(url)) {\n urls.add(url.toString());\n }\n } catch {\n if (raw.startsWith('/')) {\n urls.add(new URL(raw, 'https://x.com').toString());\n }\n }\n };\n\n for (const url of node.legacy?.entities?.urls ?? []) {\n add(url.expanded_url);\n }\n\n for (const media of mediaEntities(node)) {\n add(media.expanded_url);\n add(media.media_url_https);\n for (const variant of media.video_info?.variants ?? []) {\n if (variant.content_type?.startsWith('video/')) {\n add(variant.url);\n }\n }\n }\n\n for (const binding of node.card?.legacy?.binding_values ?? []) {\n const value = binding.value?.string_value;\n if (binding.key?.includes('url') || value?.startsWith('http')) {\n add(value);\n }\n }\n\n for (const url of extractUrlsFromPlainText(tweetPlainText(node))) {\n add(url);\n }\n\n return [...urls];\n}\n\nfunction mapStats(legacy: LegacyTweet): Stats {\n return {\n comments: legacy.reply_count ?? 0,\n reposts: legacy.retweet_count ?? 0,\n likes: legacy.favorite_count ?? 0,\n };\n}\n\nfunction statusHref(author: string, id: string): string {\n return normalizeStatusPageUrl(`https://x.com/${author}/status/${id}`);\n}\n\nfunction parseTwitterDate(raw?: string): string | undefined {\n if (!raw) {\n return undefined;\n }\n const ms = Date.parse(raw);\n return Number.isNaN(ms) ? undefined : new Date(ms).toISOString();\n}\n\n/** Extract http(s) URLs from plain tweet text (handles line-broken x.com URLs). */\nexport function extractUrlsFromPlainText(text: string): string[] {\n const normalized = text.replace(/\\s+/g, '');\n const urls: string[] = [];\n const pattern = /https?:\\/\\/[^\\s]+|(?:https?:\\/\\/)?(?:x\\.com|twitter\\.com)\\/[^\\s]+/gi;\n for (const match of normalized.matchAll(pattern)) {\n let url = match[0].replace(/[.,;:!?)\u2026]+$/, '');\n if (!url.startsWith('http')) {\n url = `https://${url}`;\n }\n urls.push(url);\n }\n return urls;\n}\n\nfunction isHttpUrl(url: URL): boolean {\n return url.protocol === 'http:' || url.protocol === 'https:';\n}\n", "import type { Locator, Page } from 'playwright';\nimport { logScrapeFailure, type ScrapeLogger } from '../logger.js';\nimport { POST_DETAIL_PARSE_TIMEOUT_MS, withTimeout } from '../scrape-timeouts.js';\nimport type { Post } from '../types/post.js';\nimport {\n canonicalStatusHref,\n normalizeStatusPageUrl,\n readAuthor,\n readPostHref,\n readStats,\n readTimestamp,\n statusIdFromHref,\n syntheticRepostHref,\n} from './article-fields.js';\nimport { waitAfterDomAction, waitForConversationReady } from './interactions.js';\nimport { normalizePostHref, type PostProcessor } from './post-processor.js';\nimport { postStub } from './post-stub.js';\nimport type { TabPool } from './tab-pool.js';\nimport { readOwnTweetBodyMarkdown } from './tweet-body.js';\nimport { loadTweetDetailJson, parsePostFromTweetDetail } from './tweet-detail-api.js';\n\n/** When set, nested scrapes reuse this tab and restore `returnHref` afterward (avoids pool deadlock). */\nexport type NestedScrapeContext = {\n page: Page;\n returnHref: string;\n};\n\nconst CONVERSATION_LABEL = 'Timeline: Conversation';\n\n/** Pool-backed scraper with href deduplication for parallel detail parsing. */\nexport class PostDetailScraper {\n private readonly pool: TabPool;\n private readonly processor: PostProcessor;\n private readonly log: ScrapeLogger;\n private readonly inFlight = new Map<string, Promise<Post>>();\n\n constructor(pool: TabPool, processor: PostProcessor, log: ScrapeLogger) {\n this.pool = pool;\n this.processor = processor;\n this.log = log;\n }\n\n async scrapeMany(hrefs: string[]): Promise<Post[]> {\n return Promise.all(hrefs.map((href) => this.scrape(href)));\n }\n\n async scrape(href: string, nested?: NestedScrapeContext): Promise<Post> {\n const key = normalizePostHref(href);\n const cached = this.processor.getCached(key);\n if (cached) {\n return cached;\n }\n\n const pending = this.inFlight.get(key);\n if (pending) {\n if (nested) {\n this.log.warn(\n { href: key },\n 'nested scrape skipped; same href already in flight (would deadlock)',\n );\n return postStub(href);\n }\n this.log.debug({ href: key }, 'awaiting in-flight post detail scrape');\n return pending;\n }\n\n const task = this.runScrape(href, nested);\n this.inFlight.set(key, task);\n try {\n return await task;\n } finally {\n this.inFlight.delete(key);\n }\n }\n\n scrapeLinked(page: Page, href: string, returnHref: string): Promise<Post> {\n return this.scrape(href, { page, returnHref });\n }\n\n private async runScrape(href: string, nested?: NestedScrapeContext): Promise<Post> {\n const key = normalizePostHref(href);\n const parseWork = nested\n ? () => this.parseOnPage(nested.page, href, nested.returnHref)\n : () => this.pool.run((page) => this.parseOnPage(page, href));\n\n let post: Post;\n try {\n post = await withTimeout(parseWork(), POST_DETAIL_PARSE_TIMEOUT_MS, `post detail ${key}`);\n } catch (err: unknown) {\n return this.failPost(href, err);\n }\n\n const finalized = await this.processor.finalize(post, new Set());\n this.processor.remember(finalized);\n return finalized;\n }\n\n private failPost(href: string, err: unknown): Post {\n logScrapeFailure(this.log, {\n action: 'scrapePostDetail',\n expected: 'TweetDetail GraphQL or conversation timeline',\n href,\n err,\n });\n const stub = postStub(href);\n this.processor.remember(stub);\n return stub;\n }\n\n private async parseOnPage(page: Page, href: string, returnHref?: string): Promise<Post> {\n const restoreHref = returnHref ? normalizeStatusPageUrl(returnHref) : undefined;\n const focalId = statusIdFromHref(href);\n\n try {\n if (focalId) {\n const json = await loadTweetDetailJson(page, href, this.log);\n if (json) {\n const post = parsePostFromTweetDetail(json, focalId);\n if (post) {\n this.log.debug({ href, source: 'TweetDetail' }, 'parsed post from API');\n return post;\n }\n }\n }\n\n this.log.warn({ href }, 'TweetDetail unavailable; falling back to DOM');\n return await parseCurrentConversationDom(page, href, this.log);\n } finally {\n if (restoreHref && normalizeStatusPageUrl(page.url()) !== restoreHref) {\n await page.goto(restoreHref, { waitUntil: 'domcontentloaded' });\n await waitForConversationReady(page, this.log, 'restore focal conversation');\n }\n }\n }\n}\n\nasync function parseCurrentConversationDom(\n page: Page,\n href: string,\n log: ScrapeLogger,\n): Promise<Post> {\n const statusHref = normalizeStatusPageUrl(href);\n if (normalizeStatusPageUrl(page.url()) !== statusHref) {\n await page.goto(href, { waitUntil: 'domcontentloaded' });\n await waitForConversationReady(page, log);\n }\n\n const articles = conversationArticles(page);\n await articles\n .first()\n .waitFor({ state: 'visible', timeout: 20_000 })\n .catch(() => undefined);\n\n const count = await articles.count();\n if (count === 0) {\n log.warn({ href: statusHref }, 'no conversation articles; keeping href-only stub');\n return postStub(statusHref);\n }\n\n const focalIdx = await findFocalArticleIndex(articles, statusHref);\n for (let i = 0; i <= focalIdx; i++) {\n await expandArticleUi(page, articles.nth(i), log);\n }\n\n const thread: Post[] = [];\n for (let i = 0; i < focalIdx; i++) {\n thread.push(await parseArticleSnapshotDom(articles.nth(i), log));\n }\n\n const focalArticle = conversationArticles(page).nth(focalIdx);\n const bareRepostHandle = await readBareRepostHandle(focalArticle);\n const body = await readOwnTweetBodyMarkdown(focalArticle);\n\n if (bareRepostHandle && !body) {\n const nested = focalArticle.locator('article[data-testid=\"tweet\"]');\n const nestedHref = await readPostHref(nested.last());\n return {\n href: syntheticRepostHref(bareRepostHandle, statusHref),\n stats: await readStats(focalArticle, log),\n ...(await readAuthor(focalArticle)),\n ...(await readTimestamp(focalArticle)),\n references: nestedHref ? [postStub(normalizeStatusPageUrl(nestedHref))] : [],\n };\n }\n\n const linkUrls = await collectArticleLinkUrlsDom(focalArticle, body);\n return {\n href: statusHref,\n stats: await readStats(focalArticle, log),\n ...(await readAuthor(focalArticle)),\n ...(await readTimestamp(focalArticle)),\n ...(body ? { body } : {}),\n ...(linkUrls.length ? { linkUrls } : {}),\n ...(thread.length ? { thread } : {}),\n };\n}\n\nasync function findFocalArticleIndex(articles: Locator, statusHref: string): Promise<number> {\n const targetId = statusIdFromHref(statusHref);\n const count = await articles.count();\n\n for (let i = 0; i < count; i++) {\n const href = await readPostHref(articles.nth(i));\n if (href && statusIdFromHref(href) === targetId) {\n return i;\n }\n }\n\n return 0;\n}\n\nasync function parseArticleSnapshotDom(article: Locator, log: ScrapeLogger): Promise<Post> {\n const href = await readPostHref(article);\n if (!href) {\n throw new Error('thread article missing status href');\n }\n\n const body = await readOwnTweetBodyMarkdown(article);\n const linkUrls = await collectArticleLinkUrlsDom(article, body);\n return {\n href: normalizeStatusPageUrl(href),\n stats: await readStats(article, log),\n ...(await readAuthor(article)),\n ...(await readTimestamp(article)),\n ...(body ? { body } : {}),\n ...(linkUrls.length ? { linkUrls } : {}),\n };\n}\n\nfunction conversationTimeline(page: Page): Locator {\n return page.getByLabel(CONVERSATION_LABEL, { exact: true });\n}\n\nfunction conversationArticles(page: Page): Locator {\n return conversationTimeline(page).locator('article[data-testid=\"tweet\"]');\n}\n\nasync function expandArticleUi(page: Page, article: Locator, log: ScrapeLogger): Promise<void> {\n const showMore = article.getByRole('button', { name: /^Show more$/i });\n while (await showMore.isVisible().catch(() => false)) {\n log.info({ action: 'expand show more' }, 'interaction');\n await showMore.click();\n await waitAfterDomAction(page, log, 'expand show more');\n }\n\n const showPosts = article.getByRole('button', { name: /^Show \\d+ posts?$/i });\n while (await showPosts.isVisible().catch(() => false)) {\n log.info({ action: 'expand thread posts' }, 'interaction');\n await showPosts.click();\n await waitAfterDomAction(page, log, 'expand thread posts');\n }\n}\n\nasync function readBareRepostHandle(article: Locator): Promise<string | null> {\n const social = article.getByTestId('socialContext');\n if (!(await social.count())) {\n return null;\n }\n\n const profileLink = article.locator('a[href^=\"/\"]').filter({ has: social }).first();\n if (!(await profileLink.count())) {\n return null;\n }\n\n const href = await profileLink.getAttribute('href');\n if (!href || href.includes('/status/')) {\n return null;\n }\n\n const handle = href.replace(/^\\//, '').split('/')[0]?.replace(/^@/, '');\n return handle ?? null;\n}\n\nasync function collectArticleLinkUrlsDom(article: Locator, body?: string): Promise<string[]> {\n const urls: string[] = [];\n const seen = new Set<string>();\n\n const push = (raw: string | null | undefined): void => {\n if (!raw || raw.startsWith('blob:')) {\n return;\n }\n const absolute = toAbsoluteUrl(raw);\n if (!absolute || seen.has(absolute)) {\n return;\n }\n seen.add(absolute);\n urls.push(absolute);\n };\n\n const card = article.getByTestId('card.wrapper');\n if (await card.count()) {\n const cardLink = card.locator('a[role=\"link\"]');\n if (await cardLink.count()) {\n push(\n await cardLink\n .first()\n .getAttribute('href', { timeout: 3_000 })\n .catch(() => null),\n );\n }\n }\n\n const photos = article.locator('[data-testid=\"tweetPhoto\"] img[src*=\"twimg.com\"]');\n const photoCount = await photos.count();\n for (let i = 0; i < photoCount; i++) {\n push(await photos.nth(i).getAttribute('src'));\n }\n\n if (body) {\n for (const url of extractUrlsFromMarkdown(body)) {\n push(url);\n }\n }\n\n return urls;\n}\n\nfunction toAbsoluteUrl(href: string): string | null {\n try {\n if (href.startsWith('http')) {\n return new URL(href).toString();\n }\n if (href.startsWith('/')) {\n return new URL(href, 'https://x.com').toString();\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction extractUrlsFromMarkdown(markdown: string): string[] {\n const urls: string[] = [];\n const pattern = /\\]\\((https?:\\/\\/[^)]+)\\)|https?:\\/\\/[^\\s)>\\]]+/g;\n for (const match of markdown.matchAll(pattern)) {\n const url = (match[1] ?? match[0]).replace(/[.,;:!?)]+$/, '');\n urls.push(url);\n }\n return urls;\n}\n\n/** Normalize href for assertions in live parser tests. */\nexport function expectStatusId(href: string): string {\n const id = statusIdFromHref(href);\n if (!id) {\n throw new Error(`not a status href: ${href}`);\n }\n return id;\n}\n\n/** Resolve canonical status URL for assertions in live parser tests. */\nexport function expectStatusHref(href: string): string {\n return canonicalStatusHref(href);\n}\n", "import type { BrowserContext, Page } from 'playwright';\nimport type { ScrapeLogger } from '../logger.js';\n\n/** Fixed-size pool of Playwright tabs for parallel post-detail scraping. */\nexport class TabPool {\n private readonly pages: Page[] = [];\n private readonly available: Page[] = [];\n private readonly waiters: Array<(page: Page) => void> = [];\n private readonly log: ScrapeLogger;\n\n private constructor(log: ScrapeLogger) {\n this.log = log;\n }\n\n static async create(context: BrowserContext, size: number, log: ScrapeLogger): Promise<TabPool> {\n const pool = new TabPool(log);\n const tabCount = Math.max(1, size);\n for (let i = 0; i < tabCount; i++) {\n const page = await context.newPage();\n pool.pages.push(page);\n pool.available.push(page);\n }\n log.info({ parallelTabs: tabCount }, 'detail tab pool ready');\n return pool;\n }\n\n async run<T>(fn: (page: Page) => Promise<T>): Promise<T> {\n const page = await this.acquire();\n try {\n return await fn(page);\n } finally {\n this.release(page);\n }\n }\n\n async close(): Promise<void> {\n await Promise.all(this.pages.map((page) => page.close().catch(() => undefined)));\n this.pages.length = 0;\n this.available.length = 0;\n this.log.debug('detail tab pool closed');\n }\n\n private async acquire(): Promise<Page> {\n const page = this.available.pop();\n if (page) {\n return page;\n }\n return new Promise((resolve) => {\n this.waiters.push(resolve);\n });\n }\n\n private release(page: Page): void {\n const waiter = this.waiters.shift();\n if (waiter) {\n waiter(page);\n return;\n }\n this.available.push(page);\n }\n}\n", "import type { Locator, Page } from 'playwright';\nimport type { ScrapeLogger } from '../logger.js';\nimport { humanDelay, waitAfterDomAction } from './interactions.js';\n\n/** Virtualized home feed posts live under this landmark (inside Home timeline). */\nexport const HOME_TIMELINE_LABEL = 'Timeline: Your Home Timeline';\n\nexport type FeedScrollKind = 'home' | 'profile';\n\n/**\n * Timeline tweet articles: `[data-testid=\"tweet\"]` inside the home timeline landmark.\n * Ads are skipped \u2014 they sit under `[data-testid=\"placementTracking\"]`.\n */\nexport function timelineTweetArticles(page: Page): Locator {\n return page.getByLabel(HOME_TIMELINE_LABEL).locator('article[data-testid=\"tweet\"]');\n}\n\nexport async function isAdTweet(article: Locator): Promise<boolean> {\n return (\n (await article.locator('xpath=ancestor::*[@data-testid=\"placementTracking\"][1]').count()) > 0\n );\n}\n\n/** Scroll window to top so the Following sort menu (off-viewport group) can be interacted with. */\nexport async function scrollTimelineToTop(page: Page): Promise<void> {\n await page.evaluate('window.scrollTo(0, 0)');\n await humanDelay();\n}\n\nfunction feedScrollRegion(page: Page, kind: FeedScrollKind): Locator {\n if (kind === 'home') {\n return page.getByLabel(HOME_TIMELINE_LABEL);\n }\n return page.locator('[data-testid=\"primaryColumn\"]');\n}\n\nasync function moveMouseToFeed(page: Page, kind: FeedScrollKind): Promise<void> {\n const box = await feedScrollRegion(page, kind)\n .boundingBox()\n .catch(() => null);\n if (!box) {\n return;\n }\n await page.mouse.move(box.x + box.width / 2, box.y + Math.min(box.height * 0.45, 520));\n}\n\nasync function scrollFeedPixels(\n page: Page,\n deltaY: number,\n kind: FeedScrollKind,\n): Promise<boolean> {\n await moveMouseToFeed(page, kind);\n await page.mouse.wheel(0, deltaY);\n\n return page.evaluate<boolean>(\n `((delta, feedKind, label) => {\n const tryScroll = (el) => {\n if (!el) {\n return false;\n }\n const style = window.getComputedStyle(el);\n const scrollable =\n (style.overflowY === 'auto' || style.overflowY === 'scroll') &&\n el.scrollHeight > el.clientHeight + 1;\n if (!scrollable) {\n return false;\n }\n const before = el.scrollTop;\n el.scrollTop += delta;\n return el.scrollTop > before;\n };\n if (feedKind === 'home') {\n const timeline = document.querySelector('[aria-label=\"' + label + '\"]');\n let node = timeline;\n while (node) {\n if (tryScroll(node)) {\n return true;\n }\n node = node.parentElement;\n }\n }\n const primary = document.querySelector('[data-testid=\"primaryColumn\"]');\n if (tryScroll(primary)) {\n return true;\n }\n const before = window.scrollY;\n window.scrollBy(0, delta);\n return window.scrollY > before;\n })(${deltaY}, ${JSON.stringify(kind)}, ${JSON.stringify(HOME_TIMELINE_LABEL)})`,\n );\n}\n\n/** Scroll the feed so the next virtualized posts can load. */\nexport async function scrollTimelineDown(\n page: Page,\n log: ScrapeLogger,\n knownArticleCount: number,\n kind: FeedScrollKind,\n): Promise<boolean> {\n const moved = await scrollFeedPixels(page, 1_800, kind);\n await humanDelay();\n await waitAfterDomAction(page, log, 'timeline scroll');\n\n const afterCount =\n kind === 'home'\n ? await timelineTweetArticles(page).count()\n : await page.locator('article[data-testid=\"tweet\"]').count();\n if (afterCount > knownArticleCount) {\n return true;\n }\n\n return moved;\n}\n\n/** After scraping a post, scroll it out of view so X loads the next timeline items. */\nexport async function scrollPastArticle(\n page: Page,\n article: Locator,\n log: ScrapeLogger,\n kind: FeedScrollKind,\n): Promise<void> {\n await article.scrollIntoViewIfNeeded().catch(() => undefined);\n await article.evaluate((el) => {\n el.scrollIntoView({ block: 'end', inline: 'nearest' });\n });\n await humanDelay();\n\n const box = await article.boundingBox().catch(() => null);\n const deltaY = box ? Math.ceil(box.height) + 480 : 1_200;\n await scrollFeedPixels(page, deltaY, kind);\n await humanDelay();\n await waitAfterDomAction(page, log, 'scroll past post');\n}\n", "import type { Locator, Page } from 'playwright';\nimport { DEFAULT_PARALLEL_TABS } from '../config/load.js';\nimport { createScrapeLogger, logScrapeFailure } from '../logger.js';\nimport { buildAppState, collectStateHrefs, resolveScrapeCutoff } from '../state/assemble.js';\nimport type { AppConfig } from '../types/config.js';\nimport type { Post } from '../types/post.js';\nimport type { AppState } from '../types/state.js';\nimport { canonicalFeedHref, readPostHref, readTimestamp } from './article-fields.js';\nimport { tracedClick, waitAfterDomAction, waitForUiSettled } from './interactions.js';\nimport { PostDetailScraper } from './post-detail.js';\nimport { normalizePostHref, PostProcessor } from './post-processor.js';\nimport { TabPool } from './tab-pool.js';\nimport {\n type FeedScrollKind,\n isAdTweet,\n scrollPastArticle,\n scrollTimelineDown,\n timelineTweetArticles,\n} from './timeline.js';\n\nexport type ScrapeOptions = {\n config: AppConfig;\n previousState: AppState | null;\n};\n\nconst FOLLOWING_TAB = 'Following';\nconst FOR_YOU_TAB = 'For you';\nconst RECENT_SORT = 'Recent';\n\ntype TimelineContext = {\n cutoffMs: number;\n stopHrefs: Set<string>;\n skipHrefs?: Set<string>;\n processor: PostProcessor;\n detailScraper: PostDetailScraper;\n};\n\ntype FeedKind = 'home' | 'profile';\n\n/**\n * Collect posts from Following (Recent), For You suggestions, and monitored profiles.\n * Following is scraped first; For You skips posts already seen in Following.\n */\nexport async function scrapeTimelines(page: Page, options: ScrapeOptions): Promise<AppState> {\n const log = createScrapeLogger();\n const { cutoffMs, cutoffTimestamp } = resolveScrapeCutoff(options.config, options.previousState);\n const previousHrefs = collectPreviousHrefs(options.previousState);\n const processor = new PostProcessor(log);\n const tabPool = await TabPool.create(\n page.context(),\n options.config.parallelTabs ?? DEFAULT_PARALLEL_TABS,\n log,\n );\n const detailScraper = new PostDetailScraper(tabPool, processor, log);\n\n log.info(\n {\n timeWindowMinutes: options.config.timeWindowMinutes,\n cutoffTimestamp,\n incremental: Boolean(options.previousState),\n parallelTabs: options.config.parallelTabs ?? DEFAULT_PARALLEL_TABS,\n },\n 'starting scrape',\n );\n\n try {\n const followingCtx: TimelineContext = {\n cutoffMs,\n stopHrefs: previousHrefs,\n processor,\n detailScraper,\n };\n\n const following = await scrapeFollowingRecent(page, followingCtx, log);\n\n const followingHrefs = new Set<string>();\n for (const post of following) {\n followingHrefs.add(normalizePostHref(canonicalFeedHref(post.href)));\n processor.collectAllHrefs(post, followingHrefs);\n }\n log.info({ count: following.length, unique: followingHrefs.size }, 'following feed complete');\n\n const forYouSuggestions = await scrapeForYouSuggestions(\n page,\n {\n cutoffMs,\n stopHrefs: previousHrefs,\n skipHrefs: followingHrefs,\n processor,\n detailScraper,\n },\n log,\n );\n\n const monitored: Record<string, Post[]> = {};\n for (const handle of options.config.monitored) {\n log.info({ handle }, 'scraping monitored profile');\n monitored[handle] = await scrapeMonitoredProfile(\n page,\n handle,\n {\n cutoffMs,\n stopHrefs: previousHrefs,\n processor,\n detailScraper,\n },\n log,\n );\n }\n\n return buildAppState(\n new Date().toISOString(),\n cutoffTimestamp,\n following,\n forYouSuggestions,\n monitored,\n );\n } finally {\n await tabPool.close();\n }\n}\n\nasync function scrapeFollowingRecent(\n page: Page,\n ctx: TimelineContext,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<Post[]> {\n log.info({ tab: FOLLOWING_TAB, sort: RECENT_SORT }, 'scraping following');\n await page.goto('https://x.com/home', { waitUntil: 'domcontentloaded' });\n await waitForUiSettled(page, log, 'home');\n await ensureFollowingTabSelected(page, log);\n await selectFollowingRecentSort(page, log);\n await page.keyboard.press('Escape');\n await waitAfterDomAction(page, log, 'close following sort menu');\n await timelineTweetArticles(page)\n .first()\n .waitFor({ state: 'visible', timeout: 20_000 })\n .catch(() => undefined);\n return scrollAndCollectPosts(page, ctx, log, 'following', 'home');\n}\n\nasync function scrapeForYouSuggestions(\n page: Page,\n ctx: TimelineContext,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<Post[]> {\n log.info({ tab: FOR_YOU_TAB }, 'scraping for-you suggestions');\n await selectHomeTab(page, FOR_YOU_TAB, log);\n return scrollAndCollectPosts(page, ctx, log, 'forYouSuggestions', 'home');\n}\n\nasync function scrapeMonitoredProfile(\n page: Page,\n handle: string,\n ctx: TimelineContext,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<Post[]> {\n const normalized = handle.replace(/^@/, '');\n log.info({ handle: normalized }, 'navigating to profile');\n await page.goto(`https://x.com/${normalized}`, { waitUntil: 'domcontentloaded' });\n await waitForUiSettled(page, log, `profile:${normalized}`);\n return scrollAndCollectPosts(page, ctx, log, `monitored:${normalized}`, 'profile');\n}\n\nasync function selectHomeTab(\n page: Page,\n label: string,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<void> {\n await page.goto('https://x.com/home', { waitUntil: 'domcontentloaded' });\n await waitForUiSettled(page, log, 'home');\n\n const tab = homeFeedTab(page, label);\n if (!(await tab.isVisible().catch(() => false))) {\n logScrapeFailure(log, {\n action: 'selectHomeTab',\n expected: `tab \"${label}\" visible`,\n missing: 'home tab',\n err: new Error(`Tab not found: ${label}`),\n });\n throw new Error(`Home tab not found: ${label}`);\n }\n\n await tracedClick(page, log, tab, `select tab ${label}`);\n}\n\nfunction homeFeedTab(page: Page, label: string): Locator {\n return page\n .locator('[data-testid=\"ScrollSnap-List\"]')\n .getByRole('tab', { name: label, exact: true });\n}\n\nasync function ensureFollowingTabSelected(\n page: Page,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<void> {\n const tab = homeFeedTab(page, FOLLOWING_TAB);\n if ((await tab.getAttribute('aria-selected')) === 'true') {\n return;\n }\n log.info({ action: 'select Following tab' }, 'interaction');\n await tab.click();\n await waitAfterDomAction(page, log, 'select Following tab');\n}\n\nfunction followingSortDropdown(page: Page): Locator {\n return page.getByRole('menu').filter({\n has: page.getByRole('menuitem', { name: RECENT_SORT, exact: true }),\n });\n}\n\nasync function selectFollowingRecentSort(\n page: Page,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<void> {\n await ensureFollowingSortMenuOpen(page, log);\n\n if (await isFollowingSortSelected(page, RECENT_SORT)) {\n log.info({ sort: RECENT_SORT }, 'following sort already selected');\n await page.keyboard.press('Escape');\n return;\n }\n\n const recent = followingSortDropdown(page).getByRole('menuitem', {\n name: RECENT_SORT,\n exact: true,\n });\n if (!(await recent.isVisible().catch(() => false))) {\n log.warn({ sort: RECENT_SORT }, 'could not find Following sort menuitem; continuing');\n await page.keyboard.press('Escape');\n return;\n }\n\n await tracedClick(page, log, recent, `select following sort ${RECENT_SORT}`, { force: true });\n}\n\nasync function ensureFollowingSortMenuOpen(\n page: Page,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<void> {\n const tab = homeFeedTab(page, FOLLOWING_TAB);\n await tab.waitFor({ state: 'visible', timeout: 15_000 });\n\n const dropdown = followingSortDropdown(page);\n if (await dropdown.isVisible().catch(() => false)) {\n return;\n }\n\n log.info({ action: 'Following tab (open sort menu)' }, 'interaction');\n await tab.click();\n await waitAfterDomAction(page, log, 'Following tab (open sort menu)');\n\n await followingSortDropdown(page)\n .waitFor({ state: 'visible', timeout: 10_000 })\n .catch(() => undefined);\n}\n\nasync function isFollowingSortSelected(page: Page, sort: string): Promise<boolean> {\n const item = followingSortDropdown(page).getByRole('menuitem', { name: sort, exact: true });\n if (!(await item.count())) {\n return false;\n }\n return (await item.locator(':scope > div').nth(1).locator('svg').count()) > 0;\n}\n\ntype ArticleStep = 'advanced' | 'continue' | 'stop';\n\nasync function stepTimelineArticle(\n page: Page,\n ctx: TimelineContext,\n article: Locator,\n seenInFeed: Set<string>,\n posts: Post[],\n feed: string,\n scrollKind: FeedScrollKind,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<ArticleStep> {\n if (await isAdTweet(article)) {\n return 'continue';\n }\n\n const href = await readPostHref(article);\n if (!href) {\n return 'continue';\n }\n\n const hrefKey = normalizePostHref(href);\n if (seenInFeed.has(hrefKey)) {\n return 'continue';\n }\n seenInFeed.add(hrefKey);\n\n if (ctx.skipHrefs?.has(hrefKey)) {\n log.debug({ href: hrefKey, feed }, 'skipping post already collected from Following');\n await scrollPastArticle(page, article, log, scrollKind);\n return 'advanced';\n }\n\n const timestamp = (await readTimestamp(article)).timestamp;\n const feedHrefKey = normalizePostHref(canonicalFeedHref(hrefKey));\n if (ctx.stopHrefs.has(feedHrefKey) || isOlderThanCutoffTimestamp(timestamp, ctx.cutoffMs)) {\n return 'stop';\n }\n\n log.debug({ href: hrefKey, feed }, 'scraping post detail');\n posts.push(await ctx.detailScraper.scrape(href));\n await scrollPastArticle(page, article, log, scrollKind);\n return 'advanced';\n}\n\nasync function scrollForMoreTimelinePosts(\n page: Page,\n kind: FeedKind,\n scrollKind: FeedScrollKind,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<'moved' | 'stalled'> {\n const articleCount = await feedArticles(page, kind).count();\n const scrolled = await scrollTimelineDown(page, log, articleCount, scrollKind);\n return scrolled ? 'moved' : 'stalled';\n}\n\nasync function advanceTimelineOnce(\n page: Page,\n ctx: TimelineContext,\n kind: FeedKind,\n seenInFeed: Set<string>,\n posts: Post[],\n feed: string,\n scrollKind: FeedScrollKind,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<ArticleStep> {\n const articles = feedArticles(page, kind);\n const count = await articles.count();\n\n for (let i = 0; i < count; i++) {\n const step = await stepTimelineArticle(\n page,\n ctx,\n articles.nth(i),\n seenInFeed,\n posts,\n feed,\n scrollKind,\n log,\n );\n if (step !== 'continue') {\n return step;\n }\n }\n\n return 'continue';\n}\n\nasync function scrollAndCollectPosts(\n page: Page,\n ctx: TimelineContext,\n log: ReturnType<typeof createScrapeLogger>,\n feed: string,\n kind: FeedKind,\n): Promise<Post[]> {\n const posts: Post[] = [];\n const seenInFeed = new Set<string>();\n let staleScrolls = 0;\n const scrollKind: FeedScrollKind = kind;\n\n for (let attempts = 0; attempts < 600; attempts++) {\n const step = await advanceTimelineOnce(\n page,\n ctx,\n kind,\n seenInFeed,\n posts,\n feed,\n scrollKind,\n log,\n );\n\n if (step === 'stop') {\n log.info(\n { feed, timelineItems: posts.length, reason: 'stop condition' },\n 'timeline walk ended',\n );\n return posts;\n }\n\n if (step === 'advanced') {\n staleScrolls = 0;\n continue;\n }\n\n const scrollResult = await scrollForMoreTimelinePosts(page, kind, scrollKind, log);\n if (scrollResult === 'stalled') {\n staleScrolls++;\n if (staleScrolls >= 4) {\n log.info(\n { feed, timelineItems: posts.length, reason: 'stalled scroll' },\n 'timeline walk ended',\n );\n break;\n }\n } else {\n staleScrolls = 0;\n }\n }\n\n log.info({ feed, timelineItems: posts.length, reason: 'iteration limit' }, 'timeline walk ended');\n return posts;\n}\n\nfunction feedArticles(page: Page, kind: FeedKind): Locator {\n if (kind === 'home') {\n return timelineTweetArticles(page);\n }\n return page.locator('article[data-testid=\"tweet\"]');\n}\n\nfunction isOlderThanCutoffTimestamp(timestamp: string | undefined, cutoffMs: number): boolean {\n const ts = timestamp ? Date.parse(timestamp) : Number.NaN;\n return !Number.isNaN(ts) && ts < cutoffMs;\n}\n\nfunction collectPreviousHrefs(state: AppState | null): Set<string> {\n if (!state) {\n return new Set();\n }\n return collectStateHrefs(state);\n}\n", "import { mkdir } from 'node:fs/promises';\nimport {\n type Browser,\n type BrowserContext,\n type ConsoleMessage,\n chromium,\n type Page,\n} from 'playwright';\nimport {\n createScrapeLogger,\n type LogLevel,\n logScrapeFailure,\n type ScrapeLogger,\n} from '../logger.js';\nimport type { AppConfig } from '../types/config.js';\nimport { hasXAuthCookies, manualLoginWindowGuidance, X_LOGIN_URL } from './auth.js';\nimport { waitForUiSettled } from './interactions.js';\nimport { runManualLoginWindow } from './login-window.js';\nimport { resolveBrowserProfilePath } from './profile.js';\nimport { applyStealthToContext, persistentContextOptions } from './stealth.js';\n\nconst X_HOME_URL = 'https://x.com/home';\nconst OWNER_POLL_MS = 2_000;\nconst OWNER_LOG_EVERY_MS = 30_000;\n\nexport type BrowserSession = {\n context: BrowserContext;\n page: Page;\n profilePath: string;\n cdpAttached: boolean;\n cdpBrowser?: Browser;\n};\n\nexport type EnsureOwnerSessionOptions = {\n ownerHandle: string;\n abortOnIncorrectOwnerHandle: boolean;\n log: ScrapeLogger;\n headless: boolean;\n};\n\nexport class OwnerSessionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'OwnerSessionError';\n }\n}\n\nlet activeSession: BrowserSession | null = null;\nlet activeProfileKey: string | null = null;\nconst loggedPages = new WeakSet<Page>();\n\nexport async function acquireBrowserSession(\n config: AppConfig,\n log: ScrapeLogger = createScrapeLogger(),\n): Promise<BrowserSession> {\n const profilePath = resolveBrowserProfilePath(config);\n const sessionKey = config.browserCdpEndpoint?.trim() || profilePath;\n\n if (activeSession && activeProfileKey === sessionKey) {\n log.debug({ sessionKey }, 'reusing in-process browser session');\n await activeSession.page.bringToFront().catch(() => undefined);\n return activeSession;\n }\n\n if (activeSession) {\n log.info({ sessionKey: activeProfileKey }, 'closing previous browser before new session');\n await closeBrowser(activeSession, log);\n }\n\n activeSession = config.browserCdpEndpoint?.trim()\n ? await attachOverCdp(config.browserCdpEndpoint.trim(), profilePath, log)\n : await openPersistentSession(profilePath, log, config.ownerHandle, config.headless);\n\n activeProfileKey = sessionKey;\n return activeSession;\n}\n\nasync function attachOverCdp(\n endpoint: string,\n profilePath: string,\n log: ScrapeLogger,\n): Promise<BrowserSession> {\n log.info({ endpoint }, 'attaching to Chrome over CDP');\n\n const browser = await chromium.connectOverCDP(endpoint);\n const context = browser.contexts()[0];\n if (!context) {\n throw new Error(\n `No browser context at ${endpoint}. Start Chrome with remote debugging (see README).`,\n );\n }\n\n await applyStealthToContext(context);\n wireContextPages(context);\n\n const page = context.pages()[0] ?? (await context.newPage());\n attachBrowserLogging(page);\n\n await page.goto((await hasXAuthCookies(context)) ? X_HOME_URL : X_LOGIN_URL, {\n waitUntil: 'domcontentloaded',\n });\n await waitForUiSettled(page, log, 'cdp attach');\n\n return { context, page, profilePath, cdpAttached: true, cdpBrowser: browser };\n}\n\n/**\n * Launch scrape-capable Chrome. If unauthenticated, run manual login window first (separate launch).\n */\nasync function openPersistentSession(\n profilePath: string,\n log: ScrapeLogger,\n expectedOwner: string,\n headless: boolean,\n): Promise<BrowserSession> {\n await mkdir(profilePath, { recursive: true });\n\n let session = await launchScrapeContext(profilePath, log, headless);\n\n if (!(await hasXAuthCookies(session.context))) {\n log.info('no auth cookies in scrape session; starting manual login window');\n await closeBrowser(session, log);\n await runManualLoginWindow(profilePath, log, 'login', expectedOwner);\n session = await launchScrapeContext(profilePath, log, headless);\n }\n\n if (!(await hasXAuthCookies(session.context))) {\n throw new OwnerSessionError(\n `Login did not persist to ${profilePath} (missing auth_token/ct0). ${manualLoginWindowGuidance('login', expectedOwner)}`,\n );\n }\n\n return session;\n}\n\nasync function launchScrapeContext(\n profilePath: string,\n log: ScrapeLogger,\n headless: boolean,\n): Promise<BrowserSession> {\n log.info({ profilePath }, 'opening scrape Chrome profile (Playwright-controlled)');\n\n let context: BrowserContext;\n try {\n context = await chromium.launchPersistentContext(\n profilePath,\n persistentContextOptions(headless),\n );\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (/process_singleton|singleton|user data dir|profile/i.test(message)) {\n throw new Error(\n `Chrome profile is locked at ${profilePath}. Close any other Chrome window using this profile.`,\n );\n }\n throw error;\n }\n\n await applyStealthToContext(context);\n wireContextPages(context);\n\n const page = context.pages()[0] ?? (await context.newPage());\n attachBrowserLogging(page);\n\n await page.goto(X_HOME_URL, { waitUntil: 'domcontentloaded' });\n await waitForUiSettled(page, log, 'scrape session launch');\n\n const hasAuth = await hasXAuthCookies(context);\n log.info({ profilePath, hasAuth }, 'scrape Chrome session ready');\n\n return { context, page, profilePath, cdpAttached: false };\n}\n\nfunction wireContextPages(context: BrowserContext): void {\n context.on('page', (page) => {\n attachBrowserLogging(page);\n });\n}\n\nfunction attachBrowserLogging(page: Page): void {\n if (loggedPages.has(page)) {\n return;\n }\n loggedPages.add(page);\n\n const browserLog = createScrapeLogger().child({ source: 'browser' });\n\n const messageRemap: { match: RegExp; note: string; level: LogLevel }[] = [\n {\n match:\n /^The resource \\S+ was preloaded using link preload but not used within a few seconds/i,\n note: 'Preload warning; ignore',\n level: 'debug',\n },\n { match: /^Banner not shown/i, note: 'Banner not shown; ignore', level: 'debug' },\n {\n match: /GSI_LOGGER|FedCM/i,\n note: 'Google Sign-In noise; use X email/password login instead',\n level: 'error',\n },\n ];\n const levelMap: Record<ReturnType<ConsoleMessage['type']>, LogLevel> = {\n assert: 'fatal',\n clear: 'debug',\n count: 'debug',\n dir: 'debug',\n dirxml: 'debug',\n endGroup: 'debug',\n error: 'error',\n warning: 'warn',\n info: 'info',\n debug: 'debug',\n log: 'debug',\n profile: 'trace',\n profileEnd: 'trace',\n startGroup: 'debug',\n startGroupCollapsed: 'debug',\n table: 'debug',\n time: 'debug',\n timeEnd: 'debug',\n trace: 'trace',\n };\n\n page.on('console', (message: ConsoleMessage) => {\n const type = message.type();\n const text = message.text();\n const payload = { type, text, location: message.location() };\n\n for (const { match, note, level } of messageRemap) {\n if (match.test(text)) {\n browserLog[level]({ ...payload, note }, 'browser console: %s', text);\n return;\n }\n }\n\n browserLog[levelMap[type]](payload, 'browser console: %s', text);\n });\n\n page.on('pageerror', (error) => {\n browserLog.error({ err: error.message, stack: error.stack }, 'browser page error');\n });\n\n page.on('response', (response) => {\n const url = response.url();\n if (url.includes('onboarding/task.json') && response.status() >= 400) {\n browserLog.warn(\n {\n url,\n status: response.status(),\n hint: 'X onboarding API failed \u2014 finish login in the manual login window',\n },\n 'x api response',\n );\n }\n });\n}\n\n/**\n * Ensure owner is logged in. May close the scrape session and open a manual login window.\n * Returns an up-to-date session (possibly replaced after login).\n */\nexport async function ensureOwnerSession(\n session: BrowserSession,\n options: EnsureOwnerSessionOptions,\n): Promise<BrowserSession> {\n const log = options.log ?? createScrapeLogger();\n const expected = normalizeOwnerHandle(options.ownerHandle);\n\n if (await isOwnerSessionActive(session.page, expected)) {\n log.info({ ownerHandle: expected }, 'owner session verified');\n return session;\n }\n\n const loginRequired = await isLoginRequired(session.page);\n const ownerMismatch = !loginRequired;\n\n if (options.abortOnIncorrectOwnerHandle) {\n const message = loginRequired\n ? `Login required for @${expected}. ${manualLoginWindowGuidance('login', expected)}`\n : `Active session does not match ownerHandle @${expected}. ${manualLoginWindowGuidance('owner-mismatch', expected)}`;\n\n logScrapeFailure(log, {\n action: 'ensureOwnerSession',\n expected: `logged in as @${expected}`,\n missing: loginRequired ? 'auth_token and ct0 cookies' : `profile for @${expected}`,\n err: new OwnerSessionError(message),\n });\n throw new OwnerSessionError(message);\n }\n\n if (!session.cdpAttached) {\n const manualReason = loginRequired ? 'login' : 'owner-mismatch';\n log.warn(\n { expectedOwner: expected, reason: manualReason },\n 'opening manual login window to fix session',\n );\n return retryAfterManualLoginWindow(session, options, manualReason);\n }\n\n if (ownerMismatch) {\n await waitForOwnerOnCdpSession(session, expected, log);\n }\n\n return session;\n}\n\nasync function retryAfterManualLoginWindow(\n session: BrowserSession,\n options: EnsureOwnerSessionOptions,\n reason: 'login' | 'owner-mismatch',\n): Promise<BrowserSession> {\n const expected = normalizeOwnerHandle(options.ownerHandle);\n\n await closeBrowser(session, options.log);\n activeSession = null;\n activeProfileKey = null;\n\n await runManualLoginWindow(session.profilePath, options.log, reason, expected);\n\n const fresh = await openPersistentSession(\n session.profilePath,\n options.log,\n options.ownerHandle,\n options.headless,\n );\n activeSession = fresh;\n activeProfileKey = session.profilePath;\n\n return ensureOwnerSession(fresh, options);\n}\n\n/** CDP-attached Chrome cannot use the manual login launcher; poll the live browser instead. */\nasync function waitForOwnerOnCdpSession(\n session: BrowserSession,\n expected: string,\n log: ScrapeLogger,\n): Promise<void> {\n const { page } = session;\n\n log.warn(\n {\n expectedOwner: expected,\n guidance: manualLoginWindowGuidance('owner-mismatch', expected),\n },\n 'wrong account on CDP browser \u2014 switch to the configured owner in that Chrome window',\n );\n\n await page.bringToFront();\n\n const started = Date.now();\n let lastLog = started;\n\n while (true) {\n if (await isOwnerSessionActive(page, expected)) {\n log.info({ ownerHandle: expected }, 'owner session verified after waiting');\n await page.goto(X_HOME_URL, { waitUntil: 'domcontentloaded' });\n await waitForUiSettled(page, log, 'post-login home');\n return;\n }\n\n const now = Date.now();\n if (now - lastLog >= OWNER_LOG_EVERY_MS) {\n log.info(\n {\n expectedOwner: expected,\n waitedMs: now - started,\n hasAuthCookies: await hasXAuthCookies(page.context()),\n },\n 'still waiting for correct owner on scrape session',\n );\n lastLog = now;\n }\n\n await page.waitForTimeout(OWNER_POLL_MS);\n }\n}\n\nexport function normalizeOwnerHandle(handle: string): string {\n return handle.replace(/^@/, '').toLowerCase();\n}\n\nexport async function isOwnerSessionActive(page: Page, normalizedOwner: string): Promise<boolean> {\n if (!(await hasXAuthCookies(page.context()))) {\n return false;\n }\n\n if (await isLoginRequired(page)) {\n return false;\n }\n\n const profile = page.getByRole('link', { name: new RegExp(`@${normalizedOwner}`, 'i') }).first();\n if (await profile.isVisible().catch(() => false)) {\n return true;\n }\n\n const accountSwitcher = page.getByRole('button', {\n name: new RegExp(`@${normalizedOwner}|account menu`, 'i'),\n });\n if (\n await accountSwitcher\n .first()\n .isVisible()\n .catch(() => false)\n ) {\n return true;\n }\n\n const profileNav = page.getByTestId('AppTabBar_Profile_Link');\n const href = await profileNav.getAttribute('href').catch(() => null);\n if (href?.toLowerCase().includes(`/${normalizedOwner}`)) {\n return true;\n }\n\n return false;\n}\n\nexport async function isLoginRequired(page: Page): Promise<boolean> {\n if (await hasXAuthCookies(page.context())) {\n return false;\n }\n\n const url = page.url();\n if (/\\/login|\\/flow\\/login/i.test(url)) {\n return true;\n }\n\n const signIn = page.getByRole('button', { name: /^(log in|sign in)$/i });\n if (\n await signIn\n .first()\n .isVisible()\n .catch(() => false)\n ) {\n return true;\n }\n\n const signInLink = page.getByRole('link', { name: /^(log in|sign in)$/i });\n if (\n await signInLink\n .first()\n .isVisible()\n .catch(() => false)\n ) {\n return true;\n }\n\n return true;\n}\n\nexport async function closeBrowser(\n session: BrowserSession,\n log: ScrapeLogger = createScrapeLogger(),\n): Promise<void> {\n if (session.cdpAttached && session.cdpBrowser) {\n log.info('detaching from CDP (leaving your Chrome running)');\n await session.cdpBrowser.close();\n } else {\n log.info({ profilePath: session.profilePath }, 'closing browser; persisting profile to disk');\n await session.context.close();\n }\n\n if (activeSession === session) {\n activeSession = null;\n activeProfileKey = null;\n }\n}\n", "import type { BrowserContext } from 'playwright';\n\n/** X session cookies set after a successful login (not present during FedCM/onboarding errors). */\nexport async function hasXAuthCookies(context: BrowserContext): Promise<boolean> {\n const cookies = await context.cookies();\n const xCookies = cookies.filter((cookie) => /x\\.com|twitter\\.com/i.test(cookie.domain));\n\n const authToken = xCookies.find((c) => c.name === 'auth_token' && c.value.length > 0);\n const ct0 = xCookies.find((c) => c.name === 'ct0' && c.value.length > 0);\n\n return Boolean(authToken && ct0);\n}\n\nexport const X_LOGIN_URL = 'https://x.com/i/flow/login';\n\nexport type ManualLoginReason = 'login' | 'owner-mismatch';\n\nexport function manualLoginWindowGuidance(\n reason: ManualLoginReason,\n expectedOwner: string,\n): string {\n const base = [\n 'A separate Chrome window opens (no Playwright remote debugging).',\n 'Use X username/email and password \u2014 not Google Sign-In.',\n ];\n\n if (reason === 'owner-mismatch') {\n return [\n ...base,\n `Sign in as @${expectedOwner} (or switch to that account).`,\n 'Quit Chrome completely when the correct account is active (close the browser, not just a tab).',\n ].join(' ');\n }\n\n return [\n ...base,\n 'Complete onboarding until you reach the home timeline, then quit Chrome completely.',\n ].join(' ');\n}\n", "import { type BrowserContext, chromium } from 'playwright';\nimport type { ScrapeLogger } from '../logger.js';\nimport { type ManualLoginReason, manualLoginWindowGuidance, X_LOGIN_URL } from './auth.js';\nimport { loginContextOptions } from './stealth.js';\n\n/**\n * Launch Chrome for manual login (no automation pipes). Blocks until the user closes the browser.\n * Cookies are written to `profilePath` when the browser process exits.\n */\nexport async function runManualLoginWindow(\n profilePath: string,\n log: ScrapeLogger,\n reason: ManualLoginReason,\n expectedOwner: string,\n): Promise<void> {\n const guidance = manualLoginWindowGuidance(reason, expectedOwner);\n const action = reason === 'owner-mismatch' ? 'change user' : 'sign in with email/password';\n\n log.warn(\n {\n profilePath,\n reason,\n expectedOwner,\n guidance,\n loginUrl: X_LOGIN_URL,\n },\n 'opening manual login window \u2014 %s for owner %s at %s, then quit Chrome when done',\n action,\n expectedOwner,\n X_LOGIN_URL,\n );\n\n let context: BrowserContext;\n try {\n context = await chromium.launchPersistentContext(profilePath, loginContextOptions());\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (/process_singleton|singleton|user data dir|profile/i.test(message)) {\n if (message.includes('browser has been closed')) {\n log.info({ profilePath }, 'login browser closed; profile saved to disk');\n return;\n }\n\n log.error(\n { profilePath, err: error },\n 'cannot open login window \u2014 profile locked at %s. Close other Chrome windows using this profile. Error: %s',\n profilePath,\n error,\n );\n throw new Error(\n `Cannot open login window \u2014 profile locked at ${profilePath}. Close other Chrome windows using this profile.`,\n );\n }\n throw error;\n }\n\n await context.close();\n throw new Error(\n 'Unexpected: launching a browser window without remote debugging pipes should fail',\n );\n}\n", "import type { BrowserContext, chromium } from 'playwright';\n\ntype PersistentContextOptions = NonNullable<Parameters<typeof chromium.launchPersistentContext>[1]>;\n\nexport const STEALTH_INIT_SCRIPT = `\n(() => {\n Object.defineProperty(navigator, \"webdriver\", {\n get: () => undefined,\n configurable: true,\n });\n if (!window.chrome) {\n window.chrome = { runtime: {} };\n }\n})();\n`;\n\n/** Options tuned for post-login scraping (Playwright-controlled). */\nexport function persistentContextOptions(headless: boolean): PersistentContextOptions {\n return {\n headless,\n channel: 'chrome',\n locale: 'en-US',\n ignoreDefaultArgs: ['--use-mock-keychain'], // X seems to detect this, maybe due disabled FedCM?\n acceptDownloads: false,\n serviceWorkers: 'allow',\n chromiumSandbox: true,\n };\n}\n\n/**\n * Opens X login in a normal Chrome window without remote-debugging pipes since X will detect it and block login.\n * Playwright only waits for the user to close the browser; do not call page.goto here.\n */\nexport function loginContextOptions(): PersistentContextOptions {\n const headless = false; // we always want the browser to be visible when logging in\n const baseOptions = persistentContextOptions(headless);\n return {\n ...baseOptions,\n ignoreDefaultArgs: [\n '--remote-debugging-pipe', // X detects this and blocks login\n ...(baseOptions.ignoreDefaultArgs as string[]),\n ],\n };\n}\n\nexport async function applyStealthToContext(context: BrowserContext): Promise<void> {\n await context.addInitScript(STEALTH_INIT_SCRIPT);\n}\n", "const DEFAULT_CONFIG_PATH = './config.json';\n\nexport type CliOptions = {\n configPath: string;\n /** CLI flag overrides config when set. */\n abortOnIncorrectOwnerHandle?: boolean;\n};\n\nexport function parseCli(argv: string[]): CliOptions {\n let configPath = DEFAULT_CONFIG_PATH;\n let abortOnIncorrectOwnerHandle: boolean | undefined;\n\n for (let i = 2; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === undefined) {\n continue;\n }\n if (arg === '--abort-on-incorrect-ownerHandle') {\n abortOnIncorrectOwnerHandle = true;\n continue;\n }\n if (arg.startsWith('-')) {\n throw new Error(`Unknown option: ${arg}`);\n }\n configPath = arg;\n }\n\n return {\n configPath,\n ...(abortOnIncorrectOwnerHandle !== undefined ? { abortOnIncorrectOwnerHandle } : {}),\n };\n}\n\nexport function resolveAbortOnIncorrectOwnerHandle(\n cli: CliOptions,\n configValue: boolean | undefined,\n): boolean {\n return cli.abortOnIncorrectOwnerHandle ?? configValue ?? false;\n}\n", "import { access, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport type { AppState } from '../types/state.js';\nimport { assertValid } from '../validate/ajv.js';\nimport { parseJson, stringifyJson } from '../validate/json.js';\n\nexport async function loadState(statePath: string): Promise<AppState | null> {\n try {\n const raw = await readFile(statePath, 'utf8');\n const parsed = parseJson(raw);\n return await assertValid<AppState>('state.schema.json', parsed, 'State');\n } catch (error) {\n if (isENOENT(error)) {\n return null;\n }\n throw error;\n }\n}\n\nexport async function saveState(statePath: string, state: AppState): Promise<void> {\n await assertValid('state.schema.json', state, 'State');\n await mkdir(dirname(statePath), { recursive: true });\n await backupExistingState(statePath);\n await writeFile(statePath, stringifyJson(state), 'utf8');\n}\n\nasync function backupExistingState(statePath: string): Promise<void> {\n const backupPath = `${statePath}.bkp`;\n try {\n await access(statePath);\n } catch (error) {\n if (isENOENT(error)) {\n return;\n }\n throw error;\n }\n try {\n await access(backupPath);\n await unlink(backupPath);\n } catch (error) {\n if (!isENOENT(error)) {\n throw error;\n }\n }\n await rename(statePath, backupPath);\n}\n\nfunction isENOENT(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n (error as NodeJS.ErrnoException).code === 'ENOENT'\n );\n}\n"],
5
- "mappings": "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",
6
- "names": ["config", "access", "resolve", "fileURLToPath", "readFile", "resolve", "DEFAULT_BROWSER_PROFILE_PATH", "resolveBrowserProfilePath", "config", "cwd", "relative", "readFile", "createRequire", "dirname", "join", "fileURLToPath", "Ajv", "require", "addFormats", "schemasDir", "ajvInstance", "getAjv", "ajv", "loadSchemaFile", "name", "path", "raw", "validatorCache", "getValidator", "schemaFile", "cached", "promise", "schema", "formatAjvErrors", "errors", "error", "assertValid", "data", "label", "validate", "stringifyJson", "value", "parseJson", "text", "DEFAULT_STATE_PATH", "DEFAULT_PARALLEL_TABS", "loadConfig", "configPath", "raw", "readFile", "parsed", "parseJson", "config", "assertValid", "DEFAULT_BROWSER_PROFILE_PATH", "pino", "readLogLevel", "logger", "createScrapeLogger", "logScrapeFailure", "log", "context", "error", "canonicalFeedHref", "href", "syntheticRepostHref", "handle", "statusHref", "canonicalStatusHref", "raw", "absolute", "match", "statusIdFromHref", "href", "readPostHref", "article", "links", "count", "fallback", "i", "canonical", "path", "normalizeStatusPageUrl", "url", "parsed", "readStats", "log", "readByTestId", "testId", "button", "text", "parseMetricCount", "readAuthor", "userBlock", "handle", "readTimestamp", "datetime", "base", "suffix", "dns", "isIP", "MAX_REDIRECTS", "FETCH_TIMEOUT_MS", "MAX_HTML_BYTES", "DESCRIPTION_META_KEYS", "resolveLink", "url", "options", "finalUrl", "resolveFinalUrl", "fetched", "fetchHtmlIfHtml", "title", "description", "extractPageMetadata", "resolveFinalUrl", "url", "options", "maxRedirects", "MAX_REDIRECTS", "current", "i", "assertSafeExternalHttpUrl", "head", "fetchWithTimeout", "signalInit", "headNext", "redirectTarget", "get", "getNext", "extractPageMetadata", "html", "title", "extractTitle", "description", "extractDescription", "metas", "parseMetaTags", "key", "DESCRIPTION_META_KEYS", "value", "name", "map", "tagPattern", "tag", "attrs", "parseAttributes", "content", "decodeHtmlEntities", "attrPattern", "match", "text", "isHtmlContentType", "contentType", "base", "fetchHtmlIfHtml", "signal", "response", "next", "readHtmlResponse", "reader", "chunks", "total", "done", "MAX_HTML_BYTES", "concatChunks", "concatChunks", "chunks", "length", "sum", "chunk", "out", "offset", "redirectTarget", "base", "response", "isRedirect", "location", "status", "assertSafeExternalHttpUrl", "url", "host", "normalizeUrlHostname", "isLocalHostname", "isUnsafeIpAddress", "isIP", "addresses", "dns", "address", "hostname", "withoutTrailingDot", "family", "isUnsafeIpv4Address", "isUnsafeIpv6Address", "parts", "part", "a", "b", "normalized", "mapped", "signalInit", "signal", "fetchWithTimeout", "init", "controller", "timeout", "FETCH_TIMEOUT_MS", "ScrapeTimeoutError", "label", "ms", "withTimeout", "promise", "timeoutId", "timeoutPromise", "_", "reject", "normalizePostHref", "href", "url", "PostProcessor", "log", "post", "into", "key", "ref", "item", "cycleGuard", "remember", "cached", "urlList", "extractUrlsFromMarkdown", "links", "references", "thread", "_refs", "_thread", "_links", "_linkUrls", "base", "finalized", "posts", "result", "urls", "results", "isDirectMediaUrl", "link", "resolved", "withTimeout", "resolveLink", "err", "fallback", "pathname", "markdown", "pattern", "match", "resolveScrapeCutoff", "config", "previousState", "nowMs", "cutoffMs", "buildAppState", "timestamp", "cutoffTimestamp", "following", "forYouSuggestions", "monitored", "posts", "post", "ingestPost", "list", "normalizePostHref", "canonicalFeedHref", "handle", "key", "ref", "item", "toPostRecord", "collectStateHrefs", "state", "hrefs", "add", "href", "record", "humanDelay", "jitter", "resolve", "waitForUiSettled", "page", "log", "label", "waitForDomIdle", "waitAfterDomAction", "waitForConversationReady", "timeline", "busy", "tracedClick", "page", "log", "target", "action", "options", "waitForUiSettled", "postStub", "href", "normalizePostHref", "readOwnTweetBodyMarkdown", "article", "tweetTexts", "count", "i", "tweetText", "el", "plain", "anchors", "out", "anchor", "plainTextToMarkdown", "markdown", "text", "href", "absolute", "toAbsoluteAnchorHref", "attachTweetDetailListener", "page", "focalTweetId", "captured", "settled", "waiters", "notify", "value", "resolve", "handler", "response", "url", "timeoutMs", "timer", "loadTweetDetailJson", "href", "log", "focalId", "statusIdFromHref", "listener", "target", "normalizeStatusPageUrl", "waitForConversationReady", "parsePostFromTweetDetail", "json", "parsed", "graph", "indexTweetResults", "focal", "buildPostFromNode", "visit", "item", "node", "id", "author", "child", "postBaseFields", "legacy", "timestamp", "parseTwitterDate", "mapStats", "buildBareRepostPost", "retweeted", "syntheticRepostHref", "buildQuoteReferences", "options", "quoted", "statusHref", "isBareRetweet", "body", "tweetBodyMarkdown", "linkUrls", "collectLinkUrls", "references", "thread", "buildThreadChain", "seen", "current", "parentId", "parent", "text", "tweetPlainText", "anchors", "media", "mediaEntities", "plainTextToMarkdown", "urls", "add", "raw", "isHttpUrl", "variant", "binding", "extractUrlsFromPlainText", "ms", "normalized", "pattern", "match", "CONVERSATION_LABEL", "PostDetailScraper", "pool", "processor", "log", "hrefs", "href", "nested", "key", "normalizePostHref", "cached", "pending", "postStub", "task", "page", "returnHref", "parseWork", "post", "withTimeout", "err", "finalized", "logScrapeFailure", "stub", "restoreHref", "normalizeStatusPageUrl", "focalId", "statusIdFromHref", "json", "loadTweetDetailJson", "parsePostFromTweetDetail", "parseCurrentConversationDom", "waitForConversationReady", "statusHref", "articles", "conversationArticles", "focalIdx", "findFocalArticleIndex", "i", "expandArticleUi", "thread", "parseArticleSnapshotDom", "focalArticle", "bareRepostHandle", "readBareRepostHandle", "body", "readOwnTweetBodyMarkdown", "nestedHref", "readPostHref", "syntheticRepostHref", "readStats", "readAuthor", "readTimestamp", "linkUrls", "collectArticleLinkUrlsDom", "targetId", "count", "article", "conversationTimeline", "showMore", "waitAfterDomAction", "showPosts", "social", "profileLink", "urls", "seen", "push", "raw", "absolute", "toAbsoluteUrl", "card", "cardLink", "photos", "photoCount", "url", "extractUrlsFromMarkdown", "markdown", "pattern", "match", "TabPool", "_TabPool", "log", "context", "size", "pool", "tabCount", "i", "page", "fn", "resolve", "waiter", "HOME_TIMELINE_LABEL", "timelineTweetArticles", "page", "isAdTweet", "article", "feedScrollRegion", "page", "kind", "HOME_TIMELINE_LABEL", "moveMouseToFeed", "box", "scrollFeedPixels", "deltaY", "scrollTimelineDown", "log", "knownArticleCount", "moved", "humanDelay", "waitAfterDomAction", "timelineTweetArticles", "scrollPastArticle", "article", "el", "FOLLOWING_TAB", "FOR_YOU_TAB", "RECENT_SORT", "scrapeTimelines", "page", "options", "log", "createScrapeLogger", "cutoffMs", "cutoffTimestamp", "resolveScrapeCutoff", "previousHrefs", "collectPreviousHrefs", "processor", "PostProcessor", "tabPool", "TabPool", "DEFAULT_PARALLEL_TABS", "detailScraper", "PostDetailScraper", "following", "scrapeFollowingRecent", "followingHrefs", "post", "normalizePostHref", "canonicalFeedHref", "forYouSuggestions", "scrapeForYouSuggestions", "monitored", "handle", "scrapeMonitoredProfile", "buildAppState", "ctx", "waitForUiSettled", "ensureFollowingTabSelected", "selectFollowingRecentSort", "waitAfterDomAction", "timelineTweetArticles", "scrollAndCollectPosts", "selectHomeTab", "normalized", "label", "tab", "homeFeedTab", "logScrapeFailure", "tracedClick", "followingSortDropdown", "ensureFollowingSortMenuOpen", "isFollowingSortSelected", "recent", "sort", "item", "stepTimelineArticle", "article", "seenInFeed", "posts", "feed", "scrollKind", "isAdTweet", "href", "readPostHref", "hrefKey", "scrollPastArticle", "timestamp", "readTimestamp", "feedHrefKey", "isOlderThanCutoffTimestamp", "scrollForMoreTimelinePosts", "kind", "articleCount", "feedArticles", "scrollTimelineDown", "advanceTimelineOnce", "articles", "count", "i", "step", "staleScrolls", "attempts", "ts", "state", "collectStateHrefs", "mkdir", "chromium", "hasXAuthCookies", "context", "xCookies", "cookie", "authToken", "c", "ct0", "X_LOGIN_URL", "manualLoginWindowGuidance", "reason", "expectedOwner", "base", "chromium", "STEALTH_INIT_SCRIPT", "persistentContextOptions", "headless", "loginContextOptions", "baseOptions", "applyStealthToContext", "context", "runManualLoginWindow", "profilePath", "log", "reason", "expectedOwner", "guidance", "manualLoginWindowGuidance", "action", "X_LOGIN_URL", "context", "chromium", "loginContextOptions", "error", "message", "X_HOME_URL", "OWNER_POLL_MS", "OWNER_LOG_EVERY_MS", "OwnerSessionError", "message", "activeSession", "activeProfileKey", "loggedPages", "acquireBrowserSession", "config", "log", "createScrapeLogger", "profilePath", "resolveBrowserProfilePath", "sessionKey", "closeBrowser", "attachOverCdp", "openPersistentSession", "endpoint", "browser", "chromium", "context", "applyStealthToContext", "wireContextPages", "page", "attachBrowserLogging", "hasXAuthCookies", "X_LOGIN_URL", "waitForUiSettled", "expectedOwner", "headless", "mkdir", "session", "launchScrapeContext", "runManualLoginWindow", "manualLoginWindowGuidance", "persistentContextOptions", "error", "hasAuth", "browserLog", "messageRemap", "levelMap", "type", "text", "payload", "match", "note", "level", "response", "url", "ensureOwnerSession", "options", "expected", "normalizeOwnerHandle", "isOwnerSessionActive", "loginRequired", "isLoginRequired", "ownerMismatch", "logScrapeFailure", "manualReason", "retryAfterManualLoginWindow", "waitForOwnerOnCdpSession", "reason", "fresh", "started", "lastLog", "now", "handle", "normalizedOwner", "DEFAULT_CONFIG_PATH", "parseCli", "argv", "configPath", "abortOnIncorrectOwnerHandle", "i", "arg", "resolveAbortOnIncorrectOwnerHandle", "cli", "configValue", "access", "mkdir", "readFile", "rename", "unlink", "writeFile", "dirname", "loadState", "statePath", "raw", "readFile", "parsed", "parseJson", "assertValid", "error", "isENOENT", "saveState", "state", "mkdir", "dirname", "backupExistingState", "writeFile", "stringifyJson", "backupPath", "access", "unlink", "rename", "runScrape", "argv", "cli", "parseCli", "resolvedConfigPath", "resolve", "assertPathExists", "config", "loadConfig", "log", "createScrapeLogger", "abortOnIncorrectOwnerHandle", "resolveAbortOnIncorrectOwnerHandle", "previousState", "loadState", "session", "acquireBrowserSession", "ensureOwnerSession", "state", "scrapeTimelines", "saveState", "handle", "posts", "closeBrowser", "path", "label", "access", "main", "entryPath", "isMain", "fileURLToPath", "error", "message"]
4
+ "sourcesContent": ["import { config } from 'dotenv';\n\n/** Load `.env` from the current working directory (no-op if missing). */\nconfig({ quiet: true });\n", "import './env.js';\nimport { access } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { scrapeTimelines } from './browser/scrape.js';\nimport {\n acquireBrowserSession,\n type BrowserSession,\n closeBrowser,\n ensureOwnerSession,\n} from './browser/session.js';\nimport { parseCli, resolveAbortOnIncorrectOwnerHandle } from './cli.js';\nimport { loadConfig } from './config/load.js';\nimport { createScrapeLogger, logger } from './logger.js';\nimport { loadState, saveState } from './state/io.js';\nimport type { AppConfig } from './types/config.js';\nimport type { AppState } from './types/state.js';\n\n/** Scrape timelines and persist state to `config.statePath`. */\nexport async function runScrape(argv: string[]): Promise<{ state: AppState; config: AppConfig }> {\n const start = Date.now();\n const getElapsedInSeconds = () => (Date.now() - start) / 1000;\n\n const cli = parseCli(argv);\n const resolvedConfigPath = resolve(cli.configPath);\n await assertPathExists(resolvedConfigPath, 'Config file');\n\n const config = await loadConfig(resolvedConfigPath);\n const log = createScrapeLogger();\n const abortOnIncorrectOwnerHandle = resolveAbortOnIncorrectOwnerHandle(\n cli,\n config.abortOnIncorrectOwnerHandle,\n );\n\n const previousState = await loadState(config.statePath);\n let session: BrowserSession | null = null;\n let sigtermReceived = false;\n let sigtermCleanup: Promise<void> | null = null;\n\n const handleSigterm = (): void => {\n if (sigtermReceived) {\n return;\n }\n\n sigtermReceived = true;\n process.exitCode = 1;\n log.warn(\n { elapsedInSeconds: getElapsedInSeconds() },\n 'SIGTERM received; stopping scrape early',\n );\n\n if (!session) {\n process.exit(1);\n }\n\n sigtermCleanup = closeBrowser(session, log)\n .catch((err: unknown) => {\n log.error(\n { err, elapsedInSeconds: getElapsedInSeconds() },\n 'failed to close browser after SIGTERM: %s',\n err,\n );\n })\n .finally(() => {\n process.exit(1);\n });\n };\n\n process.once('SIGTERM', handleSigterm);\n\n try {\n session = await acquireBrowserSession(config, log);\n\n session = await ensureOwnerSession(session, {\n ownerHandle: config.ownerHandle,\n headless: config.headless,\n abortOnIncorrectOwnerHandle,\n log,\n });\n\n const state = await scrapeTimelines(session.page, { config, previousState });\n if (sigtermReceived) {\n throw new Error('Scrape stopped early after SIGTERM');\n }\n await saveState(config.statePath, state);\n\n log.info(\n {\n statePath: config.statePath,\n following: state.following.length,\n forYouSuggestions: state.forYouSuggestions.length,\n elapsedInSeconds: getElapsedInSeconds(),\n monitored: Object.fromEntries(\n Object.entries(state.monitored).map(([handle, posts]) => [handle, posts.length]),\n ),\n },\n 'scrape complete; state saved',\n );\n\n return { state, config };\n } finally {\n process.off('SIGTERM', handleSigterm);\n\n if (sigtermCleanup) {\n await sigtermCleanup;\n } else if (session) {\n await closeBrowser(session, log);\n }\n }\n}\n\nasync function assertPathExists(path: string, label: string): Promise<void> {\n try {\n await access(path);\n } catch {\n throw new Error(`${label} not found: ${path}`);\n }\n}\n\nasync function main(): Promise<void> {\n await runScrape(process.argv);\n}\n\n/** Replaced at build time by esbuild with the entry-point name; undefined otherwise. */\ndeclare const __BUNDLE_ENTRY_NAME: string | undefined;\n\nconst entryPath = process.argv[1];\nconst isMain =\n entryPath !== undefined &&\n resolve(entryPath) === fileURLToPath(import.meta.url) &&\n (typeof __BUNDLE_ENTRY_NAME === 'undefined' || __BUNDLE_ENTRY_NAME === 'scrape');\nif (isMain) {\n main().catch((error: unknown) => {\n logger.fatal({ err: error }, 'scrape failed: %s', error);\n process.exit(1);\n });\n}\n", "import { readFile } from 'node:fs/promises';\nimport { DEFAULT_BROWSER_PROFILE_PATH } from '../browser/profile.js';\nimport type { AppConfig } from '../types/config.js';\nimport { assertValid } from '../validate/ajv.js';\nimport { parseJson } from '../validate/json.js';\n\nconst DEFAULT_STATE_PATH = './tmp/state.json';\nexport const DEFAULT_PARALLEL_TABS = 4;\n\nexport async function loadConfig(configPath: string): Promise<AppConfig> {\n const raw = await readFile(configPath, 'utf8');\n const parsed = parseJson(raw);\n const config = await assertValid<AppConfig>('config.schema.json', parsed, 'Config');\n return {\n ...config,\n statePath: config.statePath ?? DEFAULT_STATE_PATH,\n browserProfilePath: config.browserProfilePath ?? DEFAULT_BROWSER_PROFILE_PATH,\n llm: {\n ...config.llm,\n ...(config.llm.temperature ? { temperature: config.llm.temperature } : {}),\n },\n parallelTabs: config.parallelTabs ?? DEFAULT_PARALLEL_TABS,\n };\n}\n", "import { resolve } from 'node:path';\nimport type { AppConfig } from '../types/config.js';\n\nexport const DEFAULT_BROWSER_PROFILE_PATH = './tmp/browser-profile';\n\n/** Absolute path to the on-disk Chrome user-data directory (cookies, localStorage, etc.). */\nexport function resolveBrowserProfilePath(\n config: Pick<AppConfig, 'browserProfilePath'>,\n cwd: string = process.cwd(),\n): string {\n const relative = config.browserProfilePath ?? DEFAULT_BROWSER_PROFILE_PATH;\n return resolve(cwd, relative);\n}\n", "import { readFile } from 'node:fs/promises';\nimport { createRequire } from 'node:module';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { Ajv, type AnySchema, type ErrorObject, type ValidateFunction } from 'ajv';\n\nconst require = createRequire(import.meta.url);\nconst addFormats = require('ajv-formats') as (ajv: Ajv) => Ajv;\n\nconst schemasDir = join(dirname(fileURLToPath(import.meta.url)), '../../schemas');\n\nlet ajvInstance: Ajv | undefined;\n\nfunction getAjv(): Ajv {\n if (ajvInstance) {\n return ajvInstance;\n }\n const ajv = new Ajv({\n allErrors: true,\n strict: true,\n validateSchema: false,\n removeAdditional: false,\n });\n addFormats(ajv);\n ajvInstance = ajv;\n return ajv;\n}\n\nasync function loadSchemaFile(name: string): Promise<AnySchema> {\n const path = join(schemasDir, name);\n const raw = await readFile(path, 'utf8');\n return JSON.parse(raw) as AnySchema;\n}\n\nconst validatorCache = new Map<string, Promise<ValidateFunction>>();\n\nexport async function getValidator(schemaFile: string): Promise<ValidateFunction> {\n const cached = validatorCache.get(schemaFile);\n if (cached) {\n return cached;\n }\n const promise = (async () => {\n const ajv = getAjv();\n const schema = await loadSchemaFile(schemaFile);\n const validate = ajv.compile(schema);\n return validate;\n })();\n validatorCache.set(schemaFile, promise);\n return promise;\n}\n\nexport function formatAjvErrors(errors: ErrorObject[] | null | undefined): string {\n if (!errors?.length) {\n return 'Unknown validation error';\n }\n return errors\n .map((error) => {\n const path = error.instancePath || '/';\n return `${path}: ${error.message ?? 'invalid'}`;\n })\n .join('\\n');\n}\n\nexport async function assertValid<T>(schemaFile: string, data: unknown, label: string): Promise<T> {\n const validate = await getValidator(schemaFile);\n if (!validate(data)) {\n throw new Error(`${label} validation failed:\\n${formatAjvErrors(validate.errors)}`);\n }\n return data as T;\n}\n", "/** Pretty-print JSON for human review of machine-readable artifacts. */\nexport function stringifyJson(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\nexport function parseJson(text: string): unknown {\n return JSON.parse(text) as unknown;\n}\n", "import pino from 'pino';\n\nfunction readLogLevel(): string {\n // biome-ignore lint/complexity/useLiteralKeys: Bracket access required by TS4111 (noPropertyAccessFromIndexSignature).\n return process.env['LOG_LEVEL'] ?? 'info';\n}\n\nexport const logger = pino(\n {\n level: readLogLevel(),\n base: { app: 'x-summary' },\n },\n pino.destination(2), // all logs should go to stderr\n);\n\nexport type ScrapeLogger = pino.Logger;\nexport type LogLevel = pino.Level;\n\nexport function createScrapeLogger(): ScrapeLogger {\n return logger.child({ module: 'scrape' });\n}\n\nexport function logScrapeFailure(\n log: ScrapeLogger,\n context: {\n action: string;\n expected?: string;\n missing?: string;\n href?: string;\n err: unknown;\n },\n): void {\n const error =\n context.err instanceof Error\n ? { message: context.err.message, stack: context.err.stack, name: context.err.name }\n : { message: String(context.err) };\n\n log.error(\n {\n action: context.action,\n expected: context.expected,\n missing: context.missing,\n href: context.href,\n err: error,\n },\n 'scrape step failed',\n );\n}\n", "import type { Locator } from 'playwright';\nimport type { ScrapeLogger } from '../logger.js';\nimport type { Post } from '../types/post.js';\n\n/** Status URL used in feed lists when a post is keyed by a synthetic repost href. */\nexport function canonicalFeedHref(href: string): string {\n const match = /^repost:\\/\\/[^@]+@(.+)$/.exec(href);\n return match?.[1] ?? href;\n}\n\nexport function syntheticRepostHref(handle: string, statusHref: string): string {\n const normalized = handle.replace(/^@/, '');\n return `repost://${normalized}@${statusHref}`;\n}\n\nexport function isSyntheticRepostHref(href: string): boolean {\n return href.startsWith('repost://');\n}\n\n/** Canonical status page URL (`/handle/status/id`) without `/photo` or query suffixes. */\nexport function canonicalStatusHref(raw: string): string {\n const absolute = raw.startsWith('http') ? raw : `https://x.com${raw}`;\n try {\n const url = new URL(absolute);\n const match = url.pathname.match(/^(\\/[^/]+\\/status\\/\\d+)/);\n if (match) {\n return `https://x.com${match[1]}`;\n }\n return absolute;\n } catch {\n return absolute;\n }\n}\n\nexport function statusIdFromHref(href: string): string | null {\n const match = canonicalStatusHref(href).match(/\\/status\\/(\\d+)/);\n return match?.[1] ?? null;\n}\n\nexport async function readPostHref(article: Locator): Promise<string | null> {\n const links = article.getByRole('link');\n const count = await links.count();\n let fallback: string | null = null;\n\n for (let i = 0; i < count; i++) {\n const raw = await links.nth(i).getAttribute('href');\n if (!raw?.includes('/status/')) {\n continue;\n }\n const canonical = canonicalStatusHref(raw);\n const path = new URL(canonical).pathname;\n if (/^\\/[^/]+\\/status\\/\\d+$/.test(path)) {\n return canonical;\n }\n fallback ??= canonical;\n }\n\n return fallback;\n}\n\nexport function normalizeStatusPageUrl(url: string): string {\n try {\n const parsed = new URL(url);\n parsed.hash = '';\n parsed.search = '';\n return parsed.toString();\n } catch {\n return url;\n }\n}\n\n/** Stats live in `[role=\"group\"]` \u2192 `[data-testid=\"reply|retweet|like\"]`. */\nexport async function readStats(article: Locator, log: ScrapeLogger): Promise<Post['stats']> {\n const readByTestId = async (testId: string): Promise<number> => {\n const button = article.getByTestId(testId).first();\n if (!(await button.count())) {\n log.debug({ testId }, 'stat control not found, defaulting to 0');\n return 0;\n }\n const text = await button.innerText().catch(() => '0');\n return parseMetricCount(text);\n };\n\n return {\n comments: await readByTestId('reply'),\n reposts: await readByTestId('retweet'),\n likes: await readByTestId('like'),\n };\n}\n\n/** Handle from `[data-testid=\"User-Name\"] a[role=\"link\"]` href (`/handle`). */\nexport async function readAuthor(article: Locator): Promise<{ author?: string }> {\n const userBlock = article.getByTestId('User-Name');\n if (await userBlock.count()) {\n const link = userBlock.getByRole('link').first();\n const href = await link.getAttribute('href');\n if (href) {\n const handle = href.replace(/^\\//, '').split('/')[0]?.replace(/^@/, '');\n if (handle) {\n return { author: handle };\n }\n }\n }\n\n return {};\n}\n\nexport async function readTimestamp(article: Locator): Promise<{ timestamp?: string }> {\n const time = article.locator('time').first();\n const datetime = await time.getAttribute('datetime').catch(() => null);\n return datetime ? { timestamp: datetime } : {};\n}\n\nfunction parseMetricCount(raw: string): number {\n const text = raw.trim().toUpperCase();\n if (!text || text === '\u2014') {\n return 0;\n }\n const match = /^([\\d,.]+)\\s*([KMB])?/.exec(text);\n if (!match) {\n return 0;\n }\n const base = Number.parseFloat(match[1]?.replace(/,/g, '') ?? '0');\n const suffix = match[2];\n const multiplier =\n suffix === 'K' ? 1_000 : suffix === 'M' ? 1_000_000 : suffix === 'B' ? 1_000_000_000 : 1;\n return Math.round(base * multiplier);\n}\n", "import { promises as dns } from 'node:dns';\nimport { isIP } from 'node:net';\nimport type { ResolvedLink } from '../types/post.js';\n\nconst MAX_REDIRECTS = 10;\nconst FETCH_TIMEOUT_MS = 30_000;\nconst MAX_HTML_BYTES = 512_000;\n\nconst DESCRIPTION_META_KEYS = ['twitter:description', 'og:description', 'description'] as const;\n\n/**\n * Follow redirects, fetch HTML, and extract title plus description meta tags.\n */\nexport async function resolveLink(\n url: string,\n options?: { maxRedirects?: number; signal?: AbortSignal },\n): Promise<ResolvedLink> {\n const finalUrl = await resolveFinalUrl(url, options);\n try {\n const fetched = await fetchHtmlIfHtml(finalUrl, options?.signal);\n if (!fetched) {\n return { url: finalUrl };\n }\n const { title, description } = extractPageMetadata(fetched);\n\n return {\n url: finalUrl,\n ...(title ? { title } : {}),\n ...(description ? { description } : {}),\n };\n } catch {\n return { url: finalUrl };\n }\n}\n\nexport async function resolveLinks(\n urls: string[],\n cache?: Map<string, ResolvedLink>,\n): Promise<ResolvedLink[]> {\n const unique = [...new Set(urls)];\n const results: ResolvedLink[] = [];\n\n for (const url of unique) {\n const cached = cache?.get(url);\n if (cached) {\n results.push(cached);\n continue;\n }\n const resolved = await resolveLink(url);\n cache?.set(url, resolved);\n results.push(resolved);\n }\n\n return results;\n}\n\n/** @internal Redirect resolution only (no HTML). */\nexport async function resolveFinalUrl(\n url: string,\n options?: { maxRedirects?: number; signal?: AbortSignal },\n): Promise<string> {\n const maxRedirects = options?.maxRedirects ?? MAX_REDIRECTS;\n let current = new URL(url);\n\n for (let i = 0; i <= maxRedirects; i++) {\n await assertSafeExternalHttpUrl(current);\n const head = await fetchWithTimeout(current.toString(), {\n method: 'HEAD',\n redirect: 'manual',\n ...signalInit(options?.signal),\n });\n const headNext = redirectTarget(current, head);\n if (headNext) {\n await assertSafeExternalHttpUrl(headNext);\n current = headNext;\n continue;\n }\n\n if (head.status === 405 || head.status === 501) {\n const get = await fetchWithTimeout(current.toString(), {\n method: 'GET',\n redirect: 'manual',\n ...signalInit(options?.signal),\n });\n const getNext = redirectTarget(current, get);\n if (getNext) {\n await assertSafeExternalHttpUrl(getNext);\n current = getNext;\n continue;\n }\n }\n\n return current.toString();\n }\n\n throw new Error(`Too many redirects resolving ${url}`);\n}\n\nexport function extractPageMetadata(html: string): { title?: string; description?: string } {\n const title = extractTitle(html);\n const description = extractDescription(html);\n return {\n ...(title ? { title } : {}),\n ...(description ? { description } : {}),\n };\n}\n\nfunction extractTitle(html: string): string | undefined {\n const match = /<title[^>]*>([^<]*)<\\/title>/i.exec(html);\n const title = match?.[1]?.trim();\n return title || undefined;\n}\n\nfunction extractDescription(html: string): string | undefined {\n const metas = parseMetaTags(html);\n for (const key of DESCRIPTION_META_KEYS) {\n const value = metas.get(key);\n if (value) {\n return value;\n }\n }\n for (const [name, value] of metas) {\n if (name.endsWith(':description') || name === 'description') {\n return value;\n }\n }\n return undefined;\n}\n\nfunction parseMetaTags(html: string): Map<string, string> {\n const map = new Map<string, string>();\n const tagPattern = /<meta\\s+[^>]*>/gi;\n\n for (const tag of html.matchAll(tagPattern)) {\n const attrs = parseAttributes(tag[0] ?? '');\n const name = attrs.name ?? attrs.property;\n const content = attrs.content;\n if (name && content) {\n map.set(name.toLowerCase(), decodeHtmlEntities(content));\n }\n }\n\n return map;\n}\n\ntype MetaAttributes = {\n name?: string;\n property?: string;\n content?: string;\n};\n\nfunction parseAttributes(tag: string): MetaAttributes {\n const attrs: MetaAttributes = {};\n const attrPattern = /([a-zA-Z_:.-]+)\\s*=\\s*(\"([^\"]*)\"|'([^']*)'|(\\S+))/g;\n for (const match of tag.matchAll(attrPattern)) {\n const key = match[1]?.toLowerCase();\n const value = match[3] ?? match[4] ?? match[5] ?? '';\n if (key === 'name' || key === 'property' || key === 'content') {\n attrs[key] = value;\n }\n }\n return attrs;\n}\n\nfunction decodeHtmlEntities(text: string): string {\n return text\n .replaceAll('&amp;', '&')\n .replaceAll('&lt;', '<')\n .replaceAll('&gt;', '>')\n .replaceAll('&quot;', '\"')\n .replaceAll('&#39;', \"'\");\n}\n\nfunction isHtmlContentType(contentType: string | null): boolean {\n if (!contentType) {\n return false;\n }\n const base = contentType.split(';')[0]?.trim().toLowerCase() ?? '';\n return base === 'text/html' || base === 'application/xhtml+xml';\n}\n\nasync function fetchHtmlIfHtml(url: string, signal?: AbortSignal): Promise<string | null> {\n let current = new URL(url);\n\n for (let i = 0; i <= MAX_REDIRECTS; i++) {\n await assertSafeExternalHttpUrl(current);\n const response = await fetchWithTimeout(current.toString(), {\n method: 'GET',\n redirect: 'manual',\n headers: { Accept: 'text/html,application/xhtml+xml' },\n ...signalInit(signal),\n });\n\n const next = redirectTarget(current, response);\n if (next) {\n await assertSafeExternalHttpUrl(next);\n current = next;\n continue;\n }\n\n return await readHtmlResponse(current.toString(), response);\n }\n\n throw new Error(`Too many redirects fetching HTML for ${url}`);\n}\n\nasync function readHtmlResponse(url: string, response: Response): Promise<string | null> {\n if (!response.ok) {\n throw new Error(`Failed to fetch HTML for ${url}: HTTP ${response.status}`);\n }\n\n if (!isHtmlContentType(response.headers.get('content-type'))) {\n return null;\n }\n\n const reader = response.body?.getReader();\n if (!reader) {\n return '';\n }\n\n const chunks: Uint8Array[] = [];\n let total = 0;\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n if (value) {\n total += value.length;\n if (total > MAX_HTML_BYTES) {\n break;\n }\n chunks.push(value);\n }\n }\n\n return new TextDecoder().decode(concatChunks(chunks));\n}\n\n/** @internal Exported for tests. */\nexport { isHtmlContentType };\n\nfunction concatChunks(chunks: Uint8Array[]): Uint8Array {\n const length = chunks.reduce((sum, chunk) => sum + chunk.length, 0);\n const out = new Uint8Array(length);\n let offset = 0;\n for (const chunk of chunks) {\n out.set(chunk, offset);\n offset += chunk.length;\n }\n return out;\n}\n\nfunction redirectTarget(base: URL, response: Response): URL | null {\n if (!isRedirect(response.status)) {\n return null;\n }\n const location = response.headers.get('location');\n if (!location) {\n return base;\n }\n return new URL(location, base);\n}\n\nfunction isRedirect(status: number): boolean {\n return status >= 300 && status < 400;\n}\n\nasync function assertSafeExternalHttpUrl(url: URL): Promise<void> {\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n throw new Error(`Unsafe URL protocol: ${url.protocol}`);\n }\n\n const host = normalizeUrlHostname(url.hostname);\n if (isLocalHostname(host)) {\n throw new Error(`Unsafe local URL host: ${url.hostname}`);\n }\n\n if (isUnsafeIpAddress(host)) {\n throw new Error(`Unsafe private URL host: ${url.hostname}`);\n }\n\n if (isIP(host)) {\n return;\n }\n\n const addresses = await dns.lookup(host, { all: true, verbatim: true });\n if (!addresses.length) {\n throw new Error(`Could not resolve URL host: ${url.hostname}`);\n }\n\n for (const { address } of addresses) {\n if (isUnsafeIpAddress(address)) {\n throw new Error(`Unsafe private URL host: ${url.hostname}`);\n }\n }\n}\n\nfunction isLocalHostname(host: string): boolean {\n return host === 'localhost' || host.endsWith('.localhost');\n}\n\nfunction normalizeUrlHostname(hostname: string): string {\n const withoutTrailingDot = hostname.replace(/\\.$/, '').toLowerCase();\n return withoutTrailingDot.startsWith('[') && withoutTrailingDot.endsWith(']')\n ? withoutTrailingDot.slice(1, -1)\n : withoutTrailingDot;\n}\n\nfunction isUnsafeIpAddress(address: string): boolean {\n const family = isIP(address);\n if (family === 4) {\n return isUnsafeIpv4Address(address);\n }\n if (family === 6) {\n return isUnsafeIpv6Address(address);\n }\n return false;\n}\n\nfunction isUnsafeIpv4Address(address: string): boolean {\n const parts = address.split('.').map((part) => Number.parseInt(part, 10));\n if (\n parts.length !== 4 ||\n parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)\n ) {\n return true;\n }\n\n const [a = 0, b = 0] = parts;\n return (\n a === 0 ||\n a === 10 ||\n a === 127 ||\n (a === 100 && b >= 64 && b <= 127) ||\n (a === 169 && b === 254) ||\n (a === 172 && b >= 16 && b <= 31) ||\n (a === 192 && b === 168) ||\n (a === 198 && (b === 18 || b === 19)) ||\n a >= 224\n );\n}\n\nfunction isUnsafeIpv6Address(address: string): boolean {\n const normalized = address.toLowerCase();\n if (normalized.startsWith('::ffff:')) {\n const mapped = normalized.slice('::ffff:'.length);\n return isUnsafeIpv4Address(mapped);\n }\n return (\n normalized === '::' ||\n normalized === '::1' ||\n normalized.startsWith('fc') ||\n normalized.startsWith('fd') ||\n /^fe[89ab]/.test(normalized) ||\n normalized.startsWith('ff')\n );\n}\n\nfunction signalInit(signal?: AbortSignal): Pick<RequestInit, 'signal'> {\n return signal ? { signal } : {};\n}\n\nasync function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);\n const signal = init.signal\n ? AbortSignal.any([init.signal, controller.signal])\n : controller.signal;\n\n try {\n return await fetch(url, { ...init, signal });\n } finally {\n clearTimeout(timeout);\n }\n}\n", "/** Timeout for conversation DOM parsing (navigation + thread/quote nesting). */\nexport const POST_DETAIL_PARSE_TIMEOUT_MS = 60_000;\n\n/** Per-item timeout for external link resolution. */\nexport const SCRAPE_ITEM_TIMEOUT_MS = 30_000;\n\nexport class ScrapeTimeoutError extends Error {\n constructor(label: string, ms: number) {\n super(`${label} timed out after ${ms}ms`);\n this.name = 'ScrapeTimeoutError';\n }\n}\n\nexport async function withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {\n let timeoutId: ReturnType<typeof setTimeout> | undefined;\n const timeoutPromise = new Promise<never>((_, reject) => {\n timeoutId = setTimeout(() => {\n reject(new ScrapeTimeoutError(label, ms));\n }, ms);\n });\n\n try {\n return await Promise.race([promise, timeoutPromise]);\n } finally {\n if (timeoutId !== undefined) {\n clearTimeout(timeoutId);\n }\n }\n}\n", "import { resolveLink } from '../links/resolve.js';\nimport type { ScrapeLogger } from '../logger.js';\nimport { SCRAPE_ITEM_TIMEOUT_MS, withTimeout } from '../scrape-timeouts.js';\nimport type { Post, ResolvedLink } from '../types/post.js';\n\nexport function normalizePostHref(href: string): string {\n if (href.startsWith('repost://')) {\n return href;\n }\n try {\n const url = new URL(href);\n url.hash = '';\n return url.toString();\n } catch {\n return href;\n }\n}\n\n/** Cache posts and resolved links; guard threads/references against cycles. */\nexport class PostProcessor {\n private readonly postCache = new Map<string, Post>();\n private readonly linkCache = new Map<string, ResolvedLink>();\n\n private readonly log: ScrapeLogger;\n\n constructor(log: ScrapeLogger) {\n this.log = log;\n }\n\n getCached(href: string): Post | undefined {\n return this.postCache.get(normalizePostHref(href));\n }\n\n remember(post: Post): void {\n this.postCache.set(normalizePostHref(post.href), post);\n }\n\n collectAllHrefs(post: Post, into: Set<string>): void {\n const key = normalizePostHref(post.href);\n if (into.has(key)) {\n return;\n }\n into.add(key);\n for (const ref of post.references ?? []) {\n this.collectAllHrefs(ref, into);\n }\n for (const item of post.thread ?? []) {\n this.collectAllHrefs(item, into);\n }\n }\n\n async finalize(post: Post, cycleGuard: Set<string>, remember = true): Promise<Post> {\n const key = normalizePostHref(post.href);\n const cached = this.postCache.get(key);\n if (cached) {\n return cached;\n }\n\n if (cycleGuard.has(key)) {\n this.log.debug({ href: key }, 'cycle detected; omitting nested content');\n return post;\n }\n\n cycleGuard.add(key);\n\n const urlList = post.linkUrls?.length\n ? post.linkUrls\n : extractUrlsFromMarkdown(post.body ?? '');\n const links = urlList.length ? await this.resolveLinksCached(urlList) : undefined;\n\n const references = await this.finalizeNested(post.references ?? [], cycleGuard, false);\n const thread = await this.finalizeNested(post.thread ?? [], cycleGuard, false);\n\n cycleGuard.delete(key);\n\n const {\n references: _refs,\n thread: _thread,\n links: _links,\n linkUrls: _linkUrls,\n ...base\n } = post;\n const finalized: Post = {\n ...base,\n ...(links?.length ? { links } : {}),\n ...(references.length ? { references } : {}),\n ...(thread.length ? { thread } : {}),\n };\n\n if (remember) {\n this.postCache.set(key, finalized);\n }\n return finalized;\n }\n\n private async finalizeNested(\n posts: Post[],\n cycleGuard: Set<string>,\n remember: boolean,\n ): Promise<Post[]> {\n const result: Post[] = [];\n for (const item of posts) {\n const key = normalizePostHref(item.href);\n if (cycleGuard.has(key)) {\n this.log.debug({ href: key }, 'cycle detected; skipping reference/thread insert');\n continue;\n }\n result.push(await this.finalize(item, cycleGuard, remember));\n }\n return result;\n }\n\n private async resolveLinksCached(urls: string[]): Promise<ResolvedLink[]> {\n const results: ResolvedLink[] = [];\n const seen = new Set<string>();\n for (const rawUrl of urls) {\n const url = normalizeLinkUrl(rawUrl);\n if (!url) {\n this.log.warn({ url: rawUrl }, 'invalid external link skipped');\n continue;\n }\n if (seen.has(url)) {\n continue;\n }\n seen.add(url);\n const cached = this.linkCache.get(url);\n if (cached) {\n results.push(cached);\n continue;\n }\n if (isDirectMediaUrl(url)) {\n const link = { url };\n this.linkCache.set(url, link);\n results.push(link);\n continue;\n }\n try {\n const resolved = await withTimeout(\n resolveLink(url),\n SCRAPE_ITEM_TIMEOUT_MS,\n `external link ${url}`,\n );\n const resolvedUrl = normalizeLinkUrl(resolved.url);\n if (!resolvedUrl) {\n this.log.warn({ url, resolved }, 'resolved external link is invalid; skipping');\n continue;\n }\n const link = { ...resolved, url: resolvedUrl };\n this.linkCache.set(url, link);\n results.push(link);\n } catch (err) {\n this.log.warn({ url, err }, 'external link resolution failed; keeping url only');\n const fallback = { url };\n this.linkCache.set(url, fallback);\n results.push(fallback);\n }\n }\n return results;\n }\n}\n\nfunction normalizeLinkUrl(raw: string): string | null {\n if (raw.startsWith('blob:')) {\n return null;\n }\n try {\n const url = new URL(raw);\n if (url.protocol !== 'http:' && url.protocol !== 'https:') {\n return null;\n }\n return url.toString();\n } catch {\n return null;\n }\n}\n\nfunction isDirectMediaUrl(url: string): boolean {\n try {\n const { pathname } = new URL(url);\n return (\n /\\.(mp4|m3u8|webm|mov)(\\?|$)/i.test(pathname) ||\n pathname.includes('/video/') ||\n pathname.includes('/amplify_video/')\n );\n } catch {\n return false;\n }\n}\n\nfunction extractUrlsFromMarkdown(markdown: string): string[] {\n const urls: string[] = [];\n const pattern = /https?:\\/\\/[^\\s)>\\]]+/g;\n for (const match of markdown.matchAll(pattern)) {\n urls.push(match[0].replace(/[.,;:!?)]+$/, ''));\n }\n return urls;\n}\n", "import { canonicalFeedHref } from '../browser/article-fields.js';\nimport { normalizePostHref } from '../browser/post-processor.js';\nimport type { AppConfig } from '../types/config.js';\nimport type { Post } from '../types/post.js';\nimport type { AppState, PostRecord } from '../types/state.js';\n\n/** Derive scrape window start as an absolute ISO8601 instant (stored in state.cutoffTimestamp). */\nexport function resolveScrapeCutoff(\n config: AppConfig,\n previousState: AppState | null,\n nowMs: number = Date.now(),\n): { cutoffMs: number; cutoffTimestamp: string } {\n if (previousState) {\n return {\n cutoffMs: Date.parse(previousState.timestamp),\n cutoffTimestamp: previousState.timestamp,\n };\n }\n const cutoffMs = nowMs - config.timeWindowMinutes * 60 * 1000;\n return {\n cutoffMs,\n cutoffTimestamp: new Date(cutoffMs).toISOString(),\n };\n}\n\n/** Flatten scraped posts into `posts` plus href-only feed lists. */\nexport function buildAppState(\n timestamp: string,\n cutoffTimestamp: string,\n following: Post[],\n forYouSuggestions: Post[],\n monitored: Record<string, Post[]>,\n): AppState {\n const posts: Record<string, PostRecord> = {};\n\n for (const post of following) {\n ingestPost(post, posts);\n }\n for (const post of forYouSuggestions) {\n ingestPost(post, posts);\n }\n for (const list of Object.values(monitored)) {\n for (const post of list) {\n ingestPost(post, posts);\n }\n }\n\n return {\n timestamp,\n cutoffTimestamp,\n posts,\n following: following.map((post) => normalizePostHref(canonicalFeedHref(post.href))),\n forYouSuggestions: forYouSuggestions.map((post) =>\n normalizePostHref(canonicalFeedHref(post.href)),\n ),\n monitored: Object.fromEntries(\n Object.entries(monitored).map(([handle, list]) => [\n handle,\n list.map((post) => normalizePostHref(canonicalFeedHref(post.href))),\n ]),\n ),\n };\n}\n\nfunction ingestPost(post: Post, posts: Record<string, PostRecord>): void {\n const key = normalizePostHref(post.href);\n for (const ref of post.references ?? []) {\n ingestPost(ref, posts);\n }\n for (const item of post.thread ?? []) {\n ingestPost(item, posts);\n }\n if (posts[key]) {\n return;\n }\n posts[key] = toPostRecord(post);\n}\n\nfunction toPostRecord(post: Post): PostRecord {\n return {\n stats: post.stats,\n ...(post.author ? { author: post.author } : {}),\n ...(post.timestamp ? { timestamp: post.timestamp } : {}),\n ...(post.body ? { body: post.body } : {}),\n ...(post.links?.length ? { links: post.links } : {}),\n ...(post.thread?.length\n ? { thread: post.thread.map((item) => normalizePostHref(item.href)) }\n : {}),\n ...(post.references?.length\n ? { references: post.references.map((item) => normalizePostHref(item.href)) }\n : {}),\n };\n}\n\n/** All post hrefs in a persisted state (feeds, references, thread). */\nexport function collectStateHrefs(state: AppState): Set<string> {\n const hrefs = new Set<string>();\n const add = (href: string): void => {\n hrefs.add(normalizePostHref(href));\n };\n\n for (const href of state.following) {\n add(href);\n }\n for (const href of state.forYouSuggestions) {\n add(href);\n }\n for (const list of Object.values(state.monitored)) {\n for (const href of list) {\n add(href);\n }\n }\n for (const key of Object.keys(state.posts)) {\n add(key);\n }\n for (const record of Object.values(state.posts)) {\n for (const href of record.references ?? []) {\n add(href);\n }\n for (const href of record.thread ?? []) {\n add(href);\n }\n }\n\n return hrefs;\n}\n", "import type { Page } from 'playwright';\nimport type { ScrapeLogger } from '../logger.js';\n\nconst MIN_ACTION_DELAY_MS = 500;\n\nexport async function humanDelay(): Promise<void> {\n const jitter = Math.floor(Math.random() * 500);\n await new Promise((resolve) => setTimeout(resolve, MIN_ACTION_DELAY_MS + jitter));\n}\n\n/**\n * Wait for client-side fetches to settle after a UI action on X (SPA).\n */\nexport async function waitForUiSettled(\n page: Page,\n log: ScrapeLogger,\n label: string,\n): Promise<void> {\n log.debug({ label }, 'waiting for UI to settle');\n await page.waitForLoadState('networkidle', { timeout: 15_000 }).catch(() => undefined);\n await waitForDomIdle(page);\n}\n\n/** Lighter settle for in-page actions (Show more, quote navigation) without networkidle. */\nexport async function waitAfterDomAction(\n page: Page,\n log: ScrapeLogger,\n label: string,\n): Promise<void> {\n log.debug({ label }, 'waiting after DOM action');\n await waitForDomIdle(page);\n}\n\n/** Wait for a post status page conversation timeline and first tweet article. */\nexport async function waitForConversationReady(\n page: Page,\n log: ScrapeLogger,\n label = 'post conversation',\n): Promise<void> {\n log.debug({ label }, 'waiting for conversation timeline');\n const timeline = page.getByLabel('Timeline: Conversation', { exact: true });\n await timeline.waitFor({ state: 'visible', timeout: 20_000 });\n const articles = timeline.locator('article[data-testid=\"tweet\"]');\n await articles\n .first()\n .waitFor({ state: 'visible', timeout: 20_000 })\n .catch(() => undefined);\n await humanDelay();\n}\n\nasync function waitForDomIdle(page: Page): Promise<void> {\n const busy = page.locator('[aria-busy=\"true\"]');\n if ((await busy.count()) > 0) {\n await busy\n .first()\n .waitFor({ state: 'hidden', timeout: 10_000 })\n .catch(() => undefined);\n }\n\n await humanDelay();\n}\n\n/** Close transient overlays (layers div) that block timeline controls. */\nexport async function dismissBlockingLayers(page: Page): Promise<void> {\n await page.keyboard.press('Escape');\n const dismissNames = [/^Close$/i, /^Not now$/i, /^Got it$/i, /^Dismiss$/i];\n for (const name of dismissNames) {\n const button = page.getByRole('button', { name }).first();\n if (await button.isVisible().catch(() => false)) {\n await button.click({ timeout: 2_000 }).catch(() => undefined);\n }\n }\n}\n\nexport async function tracedClick(\n page: Page,\n log: ScrapeLogger,\n target: { click: (options?: { force?: boolean }) => Promise<void> },\n action: string,\n options?: { force?: boolean },\n): Promise<void> {\n log.info({ action }, 'interaction');\n await target.click(options);\n await waitForUiSettled(page, log, action);\n}\n", "import type { Post } from '../types/post.js';\nimport { normalizePostHref } from './post-processor.js';\n\n/** Minimal post record when detail scraping or nesting fails. */\nexport function postStub(href: string): Post {\n return {\n href: normalizePostHref(href),\n stats: { comments: 0, reposts: 0, likes: 0 },\n };\n}\n", "import type { Locator } from 'playwright';\n\ntype TweetAnchor = {\n text: string;\n href: string;\n};\n\n/** Reposter/author text only \u2014 excludes quoted-tweet preview inside role=link cards. */\nexport async function readOwnTweetBodyMarkdown(article: Locator): Promise<string | undefined> {\n const tweetTexts = article.locator('[data-testid=\"tweetText\"]');\n const count = await tweetTexts.count();\n\n for (let i = 0; i < count; i++) {\n const tweetText = tweetTexts.nth(i);\n const inQuoteCard = await tweetText.evaluate((el) => !!el.closest('div[role=\"link\"]'));\n if (inQuoteCard) {\n continue;\n }\n\n const plain = (await tweetText.innerText()).trim();\n if (!plain) {\n continue;\n }\n\n const anchors = await tweetText.evaluate((el) => {\n const out: { text: string; href: string }[] = [];\n for (const anchor of el.querySelectorAll('a[href]')) {\n out.push({\n text: (anchor.textContent ?? '').trim(),\n href: anchor.getAttribute('href') ?? '',\n });\n }\n return out;\n });\n\n return plainTextToMarkdown(plain, anchors);\n }\n\n return undefined;\n}\n\nexport function plainTextToMarkdown(plain: string, anchors: TweetAnchor[]): string {\n let markdown = plain;\n for (const { text, href } of anchors) {\n const absolute = toAbsoluteAnchorHref(href);\n if (!text || markdown.includes(`](${absolute})`)) {\n continue;\n }\n markdown = markdown.replace(text, `[${text}](${absolute})`);\n }\n return markdown;\n}\n\nfunction toAbsoluteAnchorHref(href: string): string {\n if (href.startsWith('http')) {\n return href;\n }\n if (href.startsWith('/')) {\n return `https://x.com${href}`;\n }\n return href;\n}\n", "import type { Page } from 'playwright';\nimport type { ScrapeLogger } from '../logger.js';\nimport type { Post, Stats } from '../types/post.js';\nimport { normalizeStatusPageUrl, statusIdFromHref, syntheticRepostHref } from './article-fields.js';\nimport { waitForConversationReady } from './interactions.js';\nimport { plainTextToMarkdown } from './tweet-body.js';\n\ntype UrlEntity = {\n url?: string;\n expanded_url?: string;\n display_url?: string;\n};\n\ntype MediaEntity = {\n type?: string;\n url?: string;\n expanded_url?: string;\n display_url?: string;\n media_url_https?: string;\n video_info?: { variants?: Array<{ url?: string; content_type?: string }> };\n};\n\ntype LegacyTweet = {\n id_str?: string;\n created_at?: string;\n full_text?: string;\n reply_count?: number;\n retweet_count?: number;\n favorite_count?: number;\n is_quote_status?: boolean;\n in_reply_to_status_id_str?: string;\n conversation_id_str?: string;\n entities?: { urls?: UrlEntity[]; media?: MediaEntity[] };\n extended_entities?: { media?: MediaEntity[] };\n};\n\ntype RawTweetResult = {\n rest_id?: string;\n legacy?: LegacyTweet;\n core?: { user_results?: { result?: { core?: { screen_name?: string } } } };\n note_tweet?: { note_tweet_results?: { result?: { text?: string } } };\n quoted_status_result?: { result?: RawTweetResult };\n retweeted_status_result?: { result?: RawTweetResult };\n card?: {\n legacy?: { binding_values?: Array<{ key?: string; value?: { string_value?: string } }> };\n };\n};\n\nexport type TweetDetailListener = {\n waitFor: (timeoutMs?: number) => Promise<string | null>;\n detach: () => void;\n};\n\n/** Listen for TweetDetail GraphQL on the next matching response. */\nexport function attachTweetDetailListener(page: Page, focalTweetId: string): TweetDetailListener {\n let captured: string | null = null;\n let settled = false;\n const waiters: Array<(value: string | null) => void> = [];\n\n const notify = (value: string | null): void => {\n if (settled) {\n return;\n }\n settled = true;\n captured = value;\n for (const resolve of waiters) {\n resolve(value);\n }\n waiters.length = 0;\n };\n\n const handler = async (response: {\n url: () => string;\n text: () => Promise<string>;\n }): Promise<void> => {\n const url = response.url();\n if (!url.includes('TweetDetail') || !url.includes(focalTweetId)) {\n return;\n }\n try {\n notify(await response.text());\n } catch {\n // ignore truncated bodies\n }\n };\n\n page.on('response', handler);\n\n return {\n waitFor: (timeoutMs = 15_000) => {\n if (captured) {\n return Promise.resolve(captured);\n }\n return new Promise<string | null>((resolve) => {\n const timer = setTimeout(() => {\n page.off('response', handler);\n resolve(captured);\n }, timeoutMs);\n waiters.push((value) => {\n clearTimeout(timer);\n resolve(value);\n });\n });\n },\n detach: () => {\n page.off('response', handler);\n },\n };\n}\n\n/** Navigate (or reload) and wait for TweetDetail JSON. */\nexport async function loadTweetDetailJson(\n page: Page,\n href: string,\n log: ScrapeLogger,\n): Promise<string | null> {\n const focalId = statusIdFromHref(href);\n if (!focalId) {\n return null;\n }\n\n const listener = attachTweetDetailListener(page, focalId);\n const target = normalizeStatusPageUrl(href);\n\n try {\n if (normalizeStatusPageUrl(page.url()) !== target) {\n await page.goto(href, { waitUntil: 'domcontentloaded' });\n } else {\n log.debug({ focalId }, 'reloading conversation to capture TweetDetail');\n await page.reload({ waitUntil: 'domcontentloaded' });\n }\n await waitForConversationReady(page, log);\n return await listener.waitFor(15_000);\n } finally {\n listener.detach();\n }\n}\n\n/** Parse focal post (thread, quotes, media) from TweetDetail GraphQL JSON. */\nexport function parsePostFromTweetDetail(json: string, focalTweetId: string): Post | null {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n return null;\n }\n\n const graph = indexTweetResults(parsed);\n const focal = graph.get(focalTweetId);\n if (!focal) {\n return null;\n }\n\n return buildPostFromNode(focal, graph, {\n includeThread: true,\n includeQuotes: true,\n allowSyntheticRepost: true,\n });\n}\n\nfunction indexTweetResults(json: unknown): Map<string, RawTweetResult> {\n const graph = new Map<string, RawTweetResult>();\n\n const visit = (value: unknown): void => {\n if (!value || typeof value !== 'object') {\n return;\n }\n if (Array.isArray(value)) {\n for (const item of value) {\n visit(item);\n }\n return;\n }\n\n const node = value as RawTweetResult;\n const id = node.legacy?.id_str;\n const author = node.core?.user_results?.result?.core?.screen_name;\n if (id && author) {\n graph.set(id, node);\n }\n\n for (const child of Object.values(value)) {\n visit(child);\n }\n };\n\n visit(json);\n return graph;\n}\n\ntype BuildOptions = {\n includeThread: boolean;\n includeQuotes: boolean;\n allowSyntheticRepost: boolean;\n};\n\nfunction postBaseFields(\n author: string,\n legacy: LegacyTweet,\n): { stats: Stats; author?: string; timestamp?: string } {\n const timestamp = parseTwitterDate(legacy.created_at);\n return {\n stats: mapStats(legacy),\n ...(author ? { author } : {}),\n ...(timestamp ? { timestamp } : {}),\n };\n}\n\nfunction buildBareRepostPost(\n node: RawTweetResult,\n graph: Map<string, RawTweetResult>,\n author: string,\n href: string,\n legacy: LegacyTweet,\n): Post {\n const retweeted = node.retweeted_status_result?.result;\n if (!retweeted) {\n throw new Error('bare retweet missing retweeted_status_result');\n }\n return {\n href: syntheticRepostHref(author, href),\n ...postBaseFields(author, legacy),\n references: [\n buildPostFromNode(retweeted, graph, {\n includeThread: false,\n includeQuotes: true,\n allowSyntheticRepost: false,\n }),\n ],\n };\n}\n\nfunction buildQuoteReferences(\n node: RawTweetResult,\n graph: Map<string, RawTweetResult>,\n options: BuildOptions,\n): Post[] {\n const quoted = node.quoted_status_result?.result;\n if (!options.includeQuotes || !quoted) {\n return [];\n }\n return [\n buildPostFromNode(quoted, graph, {\n includeThread: false,\n includeQuotes: false,\n allowSyntheticRepost: false,\n }),\n ];\n}\n\nfunction buildPostFromNode(\n node: RawTweetResult,\n graph: Map<string, RawTweetResult>,\n options: BuildOptions,\n): Post {\n const legacy = node.legacy;\n if (!legacy?.id_str) {\n throw new Error('tweet node missing id_str');\n }\n\n const author = node.core?.user_results?.result?.core?.screen_name ?? '';\n const href = statusHref(author, legacy.id_str);\n\n if (options.allowSyntheticRepost && isBareRetweet(node)) {\n return buildBareRepostPost(node, graph, author, href, legacy);\n }\n\n const body = tweetBodyMarkdown(node);\n const linkUrls = collectLinkUrls(node);\n const references = buildQuoteReferences(node, graph, options);\n const thread = options.includeThread ? buildThreadChain(node, graph) : [];\n\n return {\n href,\n ...postBaseFields(author, legacy),\n ...(body ? { body } : {}),\n ...(linkUrls.length ? { linkUrls } : {}),\n ...(references.length ? { references } : {}),\n ...(thread.length ? { thread } : {}),\n };\n}\n\nfunction buildThreadChain(node: RawTweetResult, graph: Map<string, RawTweetResult>): Post[] {\n const thread: Post[] = [];\n const seen = new Set<string>();\n let current: RawTweetResult | undefined = node;\n\n while (current?.legacy?.in_reply_to_status_id_str) {\n const parentId = current.legacy.in_reply_to_status_id_str;\n if (seen.has(parentId)) {\n break;\n }\n seen.add(parentId);\n const parent = graph.get(parentId);\n if (!parent) {\n break;\n }\n thread.unshift(\n buildPostFromNode(parent, graph, {\n includeThread: false,\n includeQuotes: false,\n allowSyntheticRepost: false,\n }),\n );\n current = parent;\n }\n\n return thread;\n}\n\nfunction isBareRetweet(node: RawTweetResult): boolean {\n const retweeted = node.retweeted_status_result?.result;\n if (!retweeted) {\n return false;\n }\n if (node.legacy?.is_quote_status) {\n return false;\n }\n const text = tweetPlainText(node).trim();\n if (!text) {\n return true;\n }\n return /^RT @\\w+:/i.test(text);\n}\n\nfunction tweetPlainText(node: RawTweetResult): string {\n return node.note_tweet?.note_tweet_results?.result?.text ?? node.legacy?.full_text ?? '';\n}\n\nfunction tweetBodyMarkdown(node: RawTweetResult): string | undefined {\n const text = tweetPlainText(node).trim();\n if (!text) {\n return undefined;\n }\n\n const anchors: Array<{ text: string; href: string }> = [];\n for (const url of node.legacy?.entities?.urls ?? []) {\n if (url.expanded_url) {\n anchors.push({\n text: url.display_url ?? url.url ?? url.expanded_url,\n href: url.expanded_url,\n });\n }\n }\n for (const media of mediaEntities(node)) {\n if (media.expanded_url && media.display_url) {\n anchors.push({ text: media.display_url, href: media.expanded_url });\n }\n }\n\n return plainTextToMarkdown(text, anchors);\n}\n\nfunction mediaEntities(node: RawTweetResult): MediaEntity[] {\n return node.legacy?.extended_entities?.media ?? node.legacy?.entities?.media ?? [];\n}\n\nfunction collectLinkUrls(node: RawTweetResult): string[] {\n const urls = new Set<string>();\n const add = (raw?: string): void => {\n if (!raw || raw.startsWith('blob:')) {\n return;\n }\n try {\n const url = new URL(raw);\n if (isHttpUrl(url)) {\n urls.add(url.toString());\n }\n } catch {\n if (raw.startsWith('/')) {\n urls.add(new URL(raw, 'https://x.com').toString());\n }\n }\n };\n\n for (const url of node.legacy?.entities?.urls ?? []) {\n add(url.expanded_url);\n }\n\n for (const media of mediaEntities(node)) {\n add(media.expanded_url);\n add(media.media_url_https);\n for (const variant of media.video_info?.variants ?? []) {\n if (variant.content_type?.startsWith('video/')) {\n add(variant.url);\n }\n }\n }\n\n for (const binding of node.card?.legacy?.binding_values ?? []) {\n const value = binding.value?.string_value;\n if (binding.key?.includes('url') || value?.startsWith('http')) {\n add(value);\n }\n }\n\n for (const url of extractUrlsFromPlainText(tweetPlainText(node))) {\n add(url);\n }\n\n return [...urls];\n}\n\nfunction mapStats(legacy: LegacyTweet): Stats {\n return {\n comments: legacy.reply_count ?? 0,\n reposts: legacy.retweet_count ?? 0,\n likes: legacy.favorite_count ?? 0,\n };\n}\n\nfunction statusHref(author: string, id: string): string {\n return normalizeStatusPageUrl(`https://x.com/${author}/status/${id}`);\n}\n\nfunction parseTwitterDate(raw?: string): string | undefined {\n if (!raw) {\n return undefined;\n }\n const ms = Date.parse(raw);\n return Number.isNaN(ms) ? undefined : new Date(ms).toISOString();\n}\n\n/** Extract http(s) URLs from plain tweet text (handles line-broken x.com URLs). */\nexport function extractUrlsFromPlainText(text: string): string[] {\n const normalized = text.replace(/\\s+/g, '');\n const urls: string[] = [];\n const pattern = /https?:\\/\\/[^\\s]+|(?:https?:\\/\\/)?(?:x\\.com|twitter\\.com)\\/[^\\s]+/gi;\n for (const match of normalized.matchAll(pattern)) {\n let url = match[0].replace(/[.,;:!?)\u2026]+$/, '');\n if (!url.startsWith('http')) {\n url = `https://${url}`;\n }\n urls.push(url);\n }\n return urls;\n}\n\nfunction isHttpUrl(url: URL): boolean {\n return url.protocol === 'http:' || url.protocol === 'https:';\n}\n", "import type { Locator, Page } from 'playwright';\nimport { logScrapeFailure, type ScrapeLogger } from '../logger.js';\nimport { POST_DETAIL_PARSE_TIMEOUT_MS, withTimeout } from '../scrape-timeouts.js';\nimport type { Post } from '../types/post.js';\nimport {\n canonicalStatusHref,\n normalizeStatusPageUrl,\n readAuthor,\n readPostHref,\n readStats,\n readTimestamp,\n statusIdFromHref,\n syntheticRepostHref,\n} from './article-fields.js';\nimport { waitAfterDomAction, waitForConversationReady } from './interactions.js';\nimport { normalizePostHref, type PostProcessor } from './post-processor.js';\nimport { postStub } from './post-stub.js';\nimport type { TabPool } from './tab-pool.js';\nimport { readOwnTweetBodyMarkdown } from './tweet-body.js';\nimport { loadTweetDetailJson, parsePostFromTweetDetail } from './tweet-detail-api.js';\n\n/** When set, nested scrapes reuse this tab and restore `returnHref` afterward (avoids pool deadlock). */\nexport type NestedScrapeContext = {\n page: Page;\n returnHref: string;\n};\n\nconst CONVERSATION_LABEL = 'Timeline: Conversation';\n\n/** Pool-backed scraper with href deduplication for parallel detail parsing. */\nexport class PostDetailScraper {\n private readonly pool: TabPool;\n private readonly processor: PostProcessor;\n private readonly log: ScrapeLogger;\n private readonly inFlight = new Map<string, Promise<Post>>();\n\n constructor(pool: TabPool, processor: PostProcessor, log: ScrapeLogger) {\n this.pool = pool;\n this.processor = processor;\n this.log = log;\n }\n\n async scrapeMany(hrefs: string[]): Promise<Post[]> {\n return Promise.all(hrefs.map((href) => this.scrape(href)));\n }\n\n async scrape(href: string, nested?: NestedScrapeContext): Promise<Post> {\n const key = normalizePostHref(href);\n const cached = this.processor.getCached(key);\n if (cached) {\n return cached;\n }\n\n const pending = this.inFlight.get(key);\n if (pending) {\n if (nested) {\n this.log.warn(\n { href: key },\n 'nested scrape skipped; same href already in flight (would deadlock)',\n );\n return postStub(href);\n }\n this.log.debug({ href: key }, 'awaiting in-flight post detail scrape');\n return pending;\n }\n\n const task = this.runScrape(href, nested);\n this.inFlight.set(key, task);\n try {\n return await task;\n } finally {\n this.inFlight.delete(key);\n }\n }\n\n scrapeLinked(page: Page, href: string, returnHref: string): Promise<Post> {\n return this.scrape(href, { page, returnHref });\n }\n\n private async runScrape(href: string, nested?: NestedScrapeContext): Promise<Post> {\n const key = normalizePostHref(href);\n const parseWork = nested\n ? () => this.parseOnPage(nested.page, href, nested.returnHref)\n : () => this.pool.run((page) => this.parseOnPage(page, href));\n\n let post: Post;\n try {\n post = await withTimeout(parseWork(), POST_DETAIL_PARSE_TIMEOUT_MS, `post detail ${key}`);\n } catch (err: unknown) {\n return this.failPost(href, err);\n }\n\n const finalized = await this.processor.finalize(post, new Set());\n this.processor.remember(finalized);\n return finalized;\n }\n\n private failPost(href: string, err: unknown): Post {\n logScrapeFailure(this.log, {\n action: 'scrapePostDetail',\n expected: 'TweetDetail GraphQL or conversation timeline',\n href,\n err,\n });\n const stub = postStub(href);\n this.processor.remember(stub);\n return stub;\n }\n\n private async parseOnPage(page: Page, href: string, returnHref?: string): Promise<Post> {\n const restoreHref = returnHref ? normalizeStatusPageUrl(returnHref) : undefined;\n const focalId = statusIdFromHref(href);\n\n try {\n if (focalId) {\n const json = await loadTweetDetailJson(page, href, this.log);\n if (json) {\n const post = parsePostFromTweetDetail(json, focalId);\n if (post) {\n this.log.debug({ href, source: 'TweetDetail' }, 'parsed post from API');\n return post;\n }\n }\n }\n\n this.log.warn({ href }, 'TweetDetail unavailable; falling back to DOM');\n return await parseCurrentConversationDom(page, href, this.log);\n } finally {\n if (restoreHref && normalizeStatusPageUrl(page.url()) !== restoreHref) {\n await page.goto(restoreHref, { waitUntil: 'domcontentloaded' });\n await waitForConversationReady(page, this.log, 'restore focal conversation');\n }\n }\n }\n}\n\nasync function parseCurrentConversationDom(\n page: Page,\n href: string,\n log: ScrapeLogger,\n): Promise<Post> {\n const statusHref = normalizeStatusPageUrl(href);\n if (normalizeStatusPageUrl(page.url()) !== statusHref) {\n await page.goto(href, { waitUntil: 'domcontentloaded' });\n await waitForConversationReady(page, log);\n }\n\n const articles = conversationArticles(page);\n await articles\n .first()\n .waitFor({ state: 'visible', timeout: 20_000 })\n .catch(() => undefined);\n\n const count = await articles.count();\n if (count === 0) {\n log.warn({ href: statusHref }, 'no conversation articles; keeping href-only stub');\n return postStub(statusHref);\n }\n\n const focalIdx = await findFocalArticleIndex(articles, statusHref);\n for (let i = 0; i <= focalIdx; i++) {\n await expandArticleUi(page, articles.nth(i), log);\n }\n\n const thread: Post[] = [];\n for (let i = 0; i < focalIdx; i++) {\n thread.push(await parseArticleSnapshotDom(articles.nth(i), log));\n }\n\n const focalArticle = conversationArticles(page).nth(focalIdx);\n const bareRepostHandle = await readBareRepostHandle(focalArticle);\n const body = await readOwnTweetBodyMarkdown(focalArticle);\n\n if (bareRepostHandle && !body) {\n const nested = focalArticle.locator('article[data-testid=\"tweet\"]');\n const nestedHref = await readPostHref(nested.last());\n return {\n href: syntheticRepostHref(bareRepostHandle, statusHref),\n stats: await readStats(focalArticle, log),\n ...(await readAuthor(focalArticle)),\n ...(await readTimestamp(focalArticle)),\n references: nestedHref ? [postStub(normalizeStatusPageUrl(nestedHref))] : [],\n };\n }\n\n const linkUrls = await collectArticleLinkUrlsDom(focalArticle, body);\n return {\n href: statusHref,\n stats: await readStats(focalArticle, log),\n ...(await readAuthor(focalArticle)),\n ...(await readTimestamp(focalArticle)),\n ...(body ? { body } : {}),\n ...(linkUrls.length ? { linkUrls } : {}),\n ...(thread.length ? { thread } : {}),\n };\n}\n\nasync function findFocalArticleIndex(articles: Locator, statusHref: string): Promise<number> {\n const targetId = statusIdFromHref(statusHref);\n const count = await articles.count();\n\n for (let i = 0; i < count; i++) {\n const href = await readPostHref(articles.nth(i));\n if (href && statusIdFromHref(href) === targetId) {\n return i;\n }\n }\n\n return 0;\n}\n\nasync function parseArticleSnapshotDom(article: Locator, log: ScrapeLogger): Promise<Post> {\n const href = await readPostHref(article);\n if (!href) {\n throw new Error('thread article missing status href');\n }\n\n const body = await readOwnTweetBodyMarkdown(article);\n const linkUrls = await collectArticleLinkUrlsDom(article, body);\n return {\n href: normalizeStatusPageUrl(href),\n stats: await readStats(article, log),\n ...(await readAuthor(article)),\n ...(await readTimestamp(article)),\n ...(body ? { body } : {}),\n ...(linkUrls.length ? { linkUrls } : {}),\n };\n}\n\nfunction conversationTimeline(page: Page): Locator {\n return page.getByLabel(CONVERSATION_LABEL, { exact: true });\n}\n\nfunction conversationArticles(page: Page): Locator {\n return conversationTimeline(page).locator('article[data-testid=\"tweet\"]');\n}\n\nasync function expandArticleUi(page: Page, article: Locator, log: ScrapeLogger): Promise<void> {\n const showMore = article.getByRole('button', { name: /^Show more$/i });\n while (await showMore.isVisible().catch(() => false)) {\n log.info({ action: 'expand show more' }, 'interaction');\n await showMore.click();\n await waitAfterDomAction(page, log, 'expand show more');\n }\n\n const showPosts = article.getByRole('button', { name: /^Show \\d+ posts?$/i });\n while (await showPosts.isVisible().catch(() => false)) {\n log.info({ action: 'expand thread posts' }, 'interaction');\n await showPosts.click();\n await waitAfterDomAction(page, log, 'expand thread posts');\n }\n}\n\nasync function readBareRepostHandle(article: Locator): Promise<string | null> {\n const social = article.getByTestId('socialContext');\n if (!(await social.count())) {\n return null;\n }\n\n const profileLink = article.locator('a[href^=\"/\"]').filter({ has: social }).first();\n if (!(await profileLink.count())) {\n return null;\n }\n\n const href = await profileLink.getAttribute('href');\n if (!href || href.includes('/status/')) {\n return null;\n }\n\n const handle = href.replace(/^\\//, '').split('/')[0]?.replace(/^@/, '');\n return handle ?? null;\n}\n\nasync function collectArticleLinkUrlsDom(article: Locator, body?: string): Promise<string[]> {\n const urls: string[] = [];\n const seen = new Set<string>();\n\n const push = (raw: string | null | undefined): void => {\n if (!raw || raw.startsWith('blob:')) {\n return;\n }\n const absolute = toAbsoluteUrl(raw);\n if (!absolute || seen.has(absolute)) {\n return;\n }\n seen.add(absolute);\n urls.push(absolute);\n };\n\n const card = article.getByTestId('card.wrapper');\n if (await card.count()) {\n const cardLink = card.locator('a[role=\"link\"]');\n if (await cardLink.count()) {\n push(\n await cardLink\n .first()\n .getAttribute('href', { timeout: 3_000 })\n .catch(() => null),\n );\n }\n }\n\n const photos = article.locator('[data-testid=\"tweetPhoto\"] img[src*=\"twimg.com\"]');\n const photoCount = await photos.count();\n for (let i = 0; i < photoCount; i++) {\n push(await photos.nth(i).getAttribute('src'));\n }\n\n if (body) {\n for (const url of extractUrlsFromMarkdown(body)) {\n push(url);\n }\n }\n\n return urls;\n}\n\nfunction toAbsoluteUrl(href: string): string | null {\n try {\n if (href.startsWith('http')) {\n return new URL(href).toString();\n }\n if (href.startsWith('/')) {\n return new URL(href, 'https://x.com').toString();\n }\n return null;\n } catch {\n return null;\n }\n}\n\nfunction extractUrlsFromMarkdown(markdown: string): string[] {\n const urls: string[] = [];\n const pattern = /\\]\\((https?:\\/\\/[^)]+)\\)|https?:\\/\\/[^\\s)>\\]]+/g;\n for (const match of markdown.matchAll(pattern)) {\n const url = (match[1] ?? match[0]).replace(/[.,;:!?)]+$/, '');\n urls.push(url);\n }\n return urls;\n}\n\n/** Normalize href for assertions in live parser tests. */\nexport function expectStatusId(href: string): string {\n const id = statusIdFromHref(href);\n if (!id) {\n throw new Error(`not a status href: ${href}`);\n }\n return id;\n}\n\n/** Resolve canonical status URL for assertions in live parser tests. */\nexport function expectStatusHref(href: string): string {\n return canonicalStatusHref(href);\n}\n", "import type { BrowserContext, Page } from 'playwright';\nimport type { ScrapeLogger } from '../logger.js';\n\n/** Fixed-size pool of Playwright tabs for parallel post-detail scraping. */\nexport class TabPool {\n private readonly pages: Page[] = [];\n private readonly available: Page[] = [];\n private readonly waiters: Array<(page: Page) => void> = [];\n private readonly log: ScrapeLogger;\n\n private constructor(log: ScrapeLogger) {\n this.log = log;\n }\n\n static async create(context: BrowserContext, size: number, log: ScrapeLogger): Promise<TabPool> {\n const pool = new TabPool(log);\n const tabCount = Math.max(1, size);\n for (let i = 0; i < tabCount; i++) {\n const page = await context.newPage();\n pool.pages.push(page);\n pool.available.push(page);\n }\n log.info({ parallelTabs: tabCount }, 'detail tab pool ready');\n return pool;\n }\n\n async run<T>(fn: (page: Page) => Promise<T>): Promise<T> {\n const page = await this.acquire();\n try {\n return await fn(page);\n } finally {\n this.release(page);\n }\n }\n\n async close(): Promise<void> {\n await Promise.all(this.pages.map((page) => page.close().catch(() => undefined)));\n this.pages.length = 0;\n this.available.length = 0;\n this.log.debug('detail tab pool closed');\n }\n\n private async acquire(): Promise<Page> {\n const page = this.available.pop();\n if (page) {\n return page;\n }\n return new Promise((resolve) => {\n this.waiters.push(resolve);\n });\n }\n\n private release(page: Page): void {\n const waiter = this.waiters.shift();\n if (waiter) {\n waiter(page);\n return;\n }\n this.available.push(page);\n }\n}\n", "import type { Locator, Page } from 'playwright';\nimport type { ScrapeLogger } from '../logger.js';\nimport { humanDelay, waitAfterDomAction } from './interactions.js';\n\n/** Virtualized home feed posts live under this landmark (inside Home timeline). */\nexport const HOME_TIMELINE_LABEL = 'Timeline: Your Home Timeline';\n\nexport type FeedScrollKind = 'home' | 'profile';\n\n/**\n * Timeline tweet articles: `[data-testid=\"tweet\"]` inside the home timeline landmark.\n * Ads are skipped \u2014 they sit under `[data-testid=\"placementTracking\"]`.\n */\nexport function timelineTweetArticles(page: Page): Locator {\n return page.getByLabel(HOME_TIMELINE_LABEL).locator('article[data-testid=\"tweet\"]');\n}\n\nexport async function isAdTweet(article: Locator): Promise<boolean> {\n return (\n (await article.locator('xpath=ancestor::*[@data-testid=\"placementTracking\"][1]').count()) > 0\n );\n}\n\n/** Scroll window to top so the Following sort menu (off-viewport group) can be interacted with. */\nexport async function scrollTimelineToTop(page: Page): Promise<void> {\n await page.evaluate('window.scrollTo(0, 0)');\n await humanDelay();\n}\n\nfunction feedScrollRegion(page: Page, kind: FeedScrollKind): Locator {\n if (kind === 'home') {\n return page.getByLabel(HOME_TIMELINE_LABEL);\n }\n return page.locator('[data-testid=\"primaryColumn\"]');\n}\n\nasync function moveMouseToFeed(page: Page, kind: FeedScrollKind): Promise<void> {\n const box = await feedScrollRegion(page, kind)\n .boundingBox()\n .catch(() => null);\n if (!box) {\n return;\n }\n await page.mouse.move(box.x + box.width / 2, box.y + Math.min(box.height * 0.45, 520));\n}\n\nasync function scrollFeedPixels(\n page: Page,\n deltaY: number,\n kind: FeedScrollKind,\n): Promise<boolean> {\n await moveMouseToFeed(page, kind);\n await page.mouse.wheel(0, deltaY);\n\n return page.evaluate<boolean>(\n `((delta, feedKind, label) => {\n const tryScroll = (el) => {\n if (!el) {\n return false;\n }\n const style = window.getComputedStyle(el);\n const scrollable =\n (style.overflowY === 'auto' || style.overflowY === 'scroll') &&\n el.scrollHeight > el.clientHeight + 1;\n if (!scrollable) {\n return false;\n }\n const before = el.scrollTop;\n el.scrollTop += delta;\n return el.scrollTop > before;\n };\n if (feedKind === 'home') {\n const timeline = document.querySelector('[aria-label=\"' + label + '\"]');\n let node = timeline;\n while (node) {\n if (tryScroll(node)) {\n return true;\n }\n node = node.parentElement;\n }\n }\n const primary = document.querySelector('[data-testid=\"primaryColumn\"]');\n if (tryScroll(primary)) {\n return true;\n }\n const before = window.scrollY;\n window.scrollBy(0, delta);\n return window.scrollY > before;\n })(${deltaY}, ${JSON.stringify(kind)}, ${JSON.stringify(HOME_TIMELINE_LABEL)})`,\n );\n}\n\n/** Scroll the feed so the next virtualized posts can load. */\nexport async function scrollTimelineDown(\n page: Page,\n log: ScrapeLogger,\n knownArticleCount: number,\n kind: FeedScrollKind,\n): Promise<boolean> {\n const moved = await scrollFeedPixels(page, 1_800, kind);\n await humanDelay();\n await waitAfterDomAction(page, log, 'timeline scroll');\n\n const afterCount =\n kind === 'home'\n ? await timelineTweetArticles(page).count()\n : await page.locator('article[data-testid=\"tweet\"]').count();\n if (afterCount > knownArticleCount) {\n return true;\n }\n\n return moved;\n}\n\n/** After scraping a post, scroll it out of view so X loads the next timeline items. */\nexport async function scrollPastArticle(\n page: Page,\n article: Locator,\n log: ScrapeLogger,\n kind: FeedScrollKind,\n): Promise<void> {\n await article.scrollIntoViewIfNeeded().catch(() => undefined);\n await article.evaluate((el) => {\n el.scrollIntoView({ block: 'end', inline: 'nearest' });\n });\n await humanDelay();\n\n const box = await article.boundingBox().catch(() => null);\n const deltaY = box ? Math.ceil(box.height) + 480 : 1_200;\n await scrollFeedPixels(page, deltaY, kind);\n await humanDelay();\n await waitAfterDomAction(page, log, 'scroll past post');\n}\n", "import type { Locator, Page } from 'playwright';\nimport { DEFAULT_PARALLEL_TABS } from '../config/load.js';\nimport { createScrapeLogger, logScrapeFailure } from '../logger.js';\nimport { buildAppState, collectStateHrefs, resolveScrapeCutoff } from '../state/assemble.js';\nimport type { AppConfig } from '../types/config.js';\nimport type { Post } from '../types/post.js';\nimport type { AppState } from '../types/state.js';\nimport { canonicalFeedHref, readPostHref, readTimestamp } from './article-fields.js';\nimport { tracedClick, waitAfterDomAction, waitForUiSettled } from './interactions.js';\nimport { PostDetailScraper } from './post-detail.js';\nimport { normalizePostHref, PostProcessor } from './post-processor.js';\nimport { TabPool } from './tab-pool.js';\nimport {\n type FeedScrollKind,\n isAdTweet,\n scrollPastArticle,\n scrollTimelineDown,\n timelineTweetArticles,\n} from './timeline.js';\n\nexport type ScrapeOptions = {\n config: AppConfig;\n previousState: AppState | null;\n};\n\nconst FOLLOWING_TAB = 'Following';\nconst FOR_YOU_TAB = 'For you';\nconst RECENT_SORT = 'Recent';\n\ntype TimelineContext = {\n cutoffMs: number;\n stopHrefs: Set<string>;\n skipHrefs?: Set<string>;\n processor: PostProcessor;\n detailScraper: PostDetailScraper;\n};\n\ntype FeedKind = 'home' | 'profile';\n\n/**\n * Collect posts from Following (Recent), For You suggestions, and monitored profiles.\n * Following is scraped first; For You skips posts already seen in Following.\n */\nexport async function scrapeTimelines(page: Page, options: ScrapeOptions): Promise<AppState> {\n const log = createScrapeLogger();\n const { cutoffMs, cutoffTimestamp } = resolveScrapeCutoff(options.config, options.previousState);\n const previousHrefs = collectPreviousHrefs(options.previousState);\n const processor = new PostProcessor(log);\n const tabPool = await TabPool.create(\n page.context(),\n options.config.parallelTabs ?? DEFAULT_PARALLEL_TABS,\n log,\n );\n const detailScraper = new PostDetailScraper(tabPool, processor, log);\n\n log.info(\n {\n timeWindowMinutes: options.config.timeWindowMinutes,\n cutoffTimestamp,\n incremental: Boolean(options.previousState),\n parallelTabs: options.config.parallelTabs ?? DEFAULT_PARALLEL_TABS,\n },\n 'starting scrape',\n );\n\n try {\n const followingCtx: TimelineContext = {\n cutoffMs,\n stopHrefs: previousHrefs,\n processor,\n detailScraper,\n };\n\n const following = await scrapeFollowingRecent(page, followingCtx, log);\n\n const followingHrefs = new Set<string>();\n for (const post of following) {\n followingHrefs.add(normalizePostHref(canonicalFeedHref(post.href)));\n processor.collectAllHrefs(post, followingHrefs);\n }\n log.info({ count: following.length, unique: followingHrefs.size }, 'following feed complete');\n\n const forYouSuggestions = await scrapeForYouSuggestions(\n page,\n {\n cutoffMs,\n stopHrefs: previousHrefs,\n skipHrefs: followingHrefs,\n processor,\n detailScraper,\n },\n log,\n );\n\n const monitored: Record<string, Post[]> = {};\n for (const handle of options.config.monitored) {\n log.info({ handle }, 'scraping monitored profile');\n monitored[handle] = await scrapeMonitoredProfile(\n page,\n handle,\n {\n cutoffMs,\n stopHrefs: previousHrefs,\n processor,\n detailScraper,\n },\n log,\n );\n }\n\n return buildAppState(\n new Date().toISOString(),\n cutoffTimestamp,\n following,\n forYouSuggestions,\n monitored,\n );\n } finally {\n await tabPool.close();\n }\n}\n\nasync function scrapeFollowingRecent(\n page: Page,\n ctx: TimelineContext,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<Post[]> {\n log.info({ tab: FOLLOWING_TAB, sort: RECENT_SORT }, 'scraping following');\n await page.goto('https://x.com/home', { waitUntil: 'domcontentloaded' });\n await waitForUiSettled(page, log, 'home');\n await ensureFollowingTabSelected(page, log);\n await selectFollowingRecentSort(page, log);\n await page.keyboard.press('Escape');\n await waitAfterDomAction(page, log, 'close following sort menu');\n await timelineTweetArticles(page)\n .first()\n .waitFor({ state: 'visible', timeout: 20_000 })\n .catch(() => undefined);\n return scrollAndCollectPosts(page, ctx, log, 'following', 'home');\n}\n\nasync function scrapeForYouSuggestions(\n page: Page,\n ctx: TimelineContext,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<Post[]> {\n log.info({ tab: FOR_YOU_TAB }, 'scraping for-you suggestions');\n await selectHomeTab(page, FOR_YOU_TAB, log);\n return scrollAndCollectPosts(page, ctx, log, 'forYouSuggestions', 'home');\n}\n\nasync function scrapeMonitoredProfile(\n page: Page,\n handle: string,\n ctx: TimelineContext,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<Post[]> {\n const normalized = handle.replace(/^@/, '');\n log.info({ handle: normalized }, 'navigating to profile');\n await page.goto(`https://x.com/${normalized}`, { waitUntil: 'domcontentloaded' });\n await waitForUiSettled(page, log, `profile:${normalized}`);\n return scrollAndCollectPosts(page, ctx, log, `monitored:${normalized}`, 'profile');\n}\n\nasync function selectHomeTab(\n page: Page,\n label: string,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<void> {\n await page.goto('https://x.com/home', { waitUntil: 'domcontentloaded' });\n await waitForUiSettled(page, log, 'home');\n\n const tab = homeFeedTab(page, label);\n if (!(await tab.isVisible().catch(() => false))) {\n logScrapeFailure(log, {\n action: 'selectHomeTab',\n expected: `tab \"${label}\" visible`,\n missing: 'home tab',\n err: new Error(`Tab not found: ${label}`),\n });\n throw new Error(`Home tab not found: ${label}`);\n }\n\n await tracedClick(page, log, tab, `select tab ${label}`);\n}\n\nfunction homeFeedTab(page: Page, label: string): Locator {\n return page\n .locator('[data-testid=\"ScrollSnap-List\"]')\n .getByRole('tab', { name: label, exact: true });\n}\n\nasync function ensureFollowingTabSelected(\n page: Page,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<void> {\n const tab = homeFeedTab(page, FOLLOWING_TAB);\n if ((await tab.getAttribute('aria-selected')) === 'true') {\n return;\n }\n log.info({ action: 'select Following tab' }, 'interaction');\n await tab.click();\n await waitAfterDomAction(page, log, 'select Following tab');\n}\n\nfunction followingSortDropdown(page: Page): Locator {\n return page.getByRole('menu').filter({\n has: page.getByRole('menuitem', { name: RECENT_SORT, exact: true }),\n });\n}\n\nasync function selectFollowingRecentSort(\n page: Page,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<void> {\n await ensureFollowingSortMenuOpen(page, log);\n\n if (await isFollowingSortSelected(page, RECENT_SORT)) {\n log.info({ sort: RECENT_SORT }, 'following sort already selected');\n await page.keyboard.press('Escape');\n return;\n }\n\n const recent = followingSortDropdown(page).getByRole('menuitem', {\n name: RECENT_SORT,\n exact: true,\n });\n if (!(await recent.isVisible().catch(() => false))) {\n log.warn({ sort: RECENT_SORT }, 'could not find Following sort menuitem; continuing');\n await page.keyboard.press('Escape');\n return;\n }\n\n await tracedClick(page, log, recent, `select following sort ${RECENT_SORT}`, { force: true });\n}\n\nasync function ensureFollowingSortMenuOpen(\n page: Page,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<void> {\n const tab = homeFeedTab(page, FOLLOWING_TAB);\n await tab.waitFor({ state: 'visible', timeout: 15_000 });\n\n const dropdown = followingSortDropdown(page);\n if (await dropdown.isVisible().catch(() => false)) {\n return;\n }\n\n log.info({ action: 'Following tab (open sort menu)' }, 'interaction');\n await tab.click();\n await waitAfterDomAction(page, log, 'Following tab (open sort menu)');\n\n await followingSortDropdown(page)\n .waitFor({ state: 'visible', timeout: 10_000 })\n .catch(() => undefined);\n}\n\nasync function isFollowingSortSelected(page: Page, sort: string): Promise<boolean> {\n const item = followingSortDropdown(page).getByRole('menuitem', { name: sort, exact: true });\n if (!(await item.count())) {\n return false;\n }\n return (await item.locator(':scope > div').nth(1).locator('svg').count()) > 0;\n}\n\ntype ArticleStep = 'advanced' | 'continue' | 'stop';\n\nasync function stepTimelineArticle(\n page: Page,\n ctx: TimelineContext,\n article: Locator,\n seenInFeed: Set<string>,\n posts: Post[],\n feed: string,\n scrollKind: FeedScrollKind,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<ArticleStep> {\n if (await isAdTweet(article)) {\n return 'continue';\n }\n\n const href = await readPostHref(article);\n if (!href) {\n return 'continue';\n }\n\n const hrefKey = normalizePostHref(href);\n if (seenInFeed.has(hrefKey)) {\n return 'continue';\n }\n seenInFeed.add(hrefKey);\n\n if (ctx.skipHrefs?.has(hrefKey)) {\n log.debug({ href: hrefKey, feed }, 'skipping post already collected from Following');\n await scrollPastArticle(page, article, log, scrollKind);\n return 'advanced';\n }\n\n const timestamp = (await readTimestamp(article)).timestamp;\n const feedHrefKey = normalizePostHref(canonicalFeedHref(hrefKey));\n if (ctx.stopHrefs.has(feedHrefKey) || isOlderThanCutoffTimestamp(timestamp, ctx.cutoffMs)) {\n return 'stop';\n }\n\n log.debug({ href: hrefKey, feed }, 'scraping post detail');\n posts.push(await ctx.detailScraper.scrape(href));\n await scrollPastArticle(page, article, log, scrollKind);\n return 'advanced';\n}\n\nasync function scrollForMoreTimelinePosts(\n page: Page,\n kind: FeedKind,\n scrollKind: FeedScrollKind,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<'moved' | 'stalled'> {\n const articleCount = await feedArticles(page, kind).count();\n const scrolled = await scrollTimelineDown(page, log, articleCount, scrollKind);\n return scrolled ? 'moved' : 'stalled';\n}\n\nasync function advanceTimelineOnce(\n page: Page,\n ctx: TimelineContext,\n kind: FeedKind,\n seenInFeed: Set<string>,\n posts: Post[],\n feed: string,\n scrollKind: FeedScrollKind,\n log: ReturnType<typeof createScrapeLogger>,\n): Promise<ArticleStep> {\n const articles = feedArticles(page, kind);\n const count = await articles.count();\n\n for (let i = 0; i < count; i++) {\n const step = await stepTimelineArticle(\n page,\n ctx,\n articles.nth(i),\n seenInFeed,\n posts,\n feed,\n scrollKind,\n log,\n );\n if (step !== 'continue') {\n return step;\n }\n }\n\n return 'continue';\n}\n\nasync function scrollAndCollectPosts(\n page: Page,\n ctx: TimelineContext,\n log: ReturnType<typeof createScrapeLogger>,\n feed: string,\n kind: FeedKind,\n): Promise<Post[]> {\n const posts: Post[] = [];\n const seenInFeed = new Set<string>();\n let staleScrolls = 0;\n const scrollKind: FeedScrollKind = kind;\n\n for (let attempts = 0; attempts < 600; attempts++) {\n const step = await advanceTimelineOnce(\n page,\n ctx,\n kind,\n seenInFeed,\n posts,\n feed,\n scrollKind,\n log,\n );\n\n if (step === 'stop') {\n log.info(\n { feed, timelineItems: posts.length, reason: 'stop condition' },\n 'timeline walk ended',\n );\n return posts;\n }\n\n if (step === 'advanced') {\n staleScrolls = 0;\n continue;\n }\n\n const scrollResult = await scrollForMoreTimelinePosts(page, kind, scrollKind, log);\n if (scrollResult === 'stalled') {\n staleScrolls++;\n if (staleScrolls >= 4) {\n log.info(\n { feed, timelineItems: posts.length, reason: 'stalled scroll' },\n 'timeline walk ended',\n );\n break;\n }\n } else {\n staleScrolls = 0;\n }\n }\n\n log.info({ feed, timelineItems: posts.length, reason: 'iteration limit' }, 'timeline walk ended');\n return posts;\n}\n\nfunction feedArticles(page: Page, kind: FeedKind): Locator {\n if (kind === 'home') {\n return timelineTweetArticles(page);\n }\n return page.locator('article[data-testid=\"tweet\"]');\n}\n\nfunction isOlderThanCutoffTimestamp(timestamp: string | undefined, cutoffMs: number): boolean {\n const ts = timestamp ? Date.parse(timestamp) : Number.NaN;\n return !Number.isNaN(ts) && ts < cutoffMs;\n}\n\nfunction collectPreviousHrefs(state: AppState | null): Set<string> {\n if (!state) {\n return new Set();\n }\n return collectStateHrefs(state);\n}\n", "import { mkdir } from 'node:fs/promises';\nimport {\n type Browser,\n type BrowserContext,\n type ConsoleMessage,\n chromium,\n type Page,\n} from 'playwright';\nimport {\n createScrapeLogger,\n type LogLevel,\n logScrapeFailure,\n type ScrapeLogger,\n} from '../logger.js';\nimport type { AppConfig } from '../types/config.js';\nimport { hasXAuthCookies, manualLoginWindowGuidance, X_LOGIN_URL } from './auth.js';\nimport { waitForUiSettled } from './interactions.js';\nimport { runManualLoginWindow } from './login-window.js';\nimport { resolveBrowserProfilePath } from './profile.js';\nimport { applyStealthToContext, persistentContextOptions } from './stealth.js';\n\nconst X_HOME_URL = 'https://x.com/home';\nconst OWNER_POLL_MS = 2_000;\nconst OWNER_LOG_EVERY_MS = 30_000;\n\nexport type BrowserSession = {\n context: BrowserContext;\n page: Page;\n profilePath: string;\n cdpAttached: boolean;\n cdpBrowser?: Browser;\n};\n\nexport type EnsureOwnerSessionOptions = {\n ownerHandle: string;\n abortOnIncorrectOwnerHandle: boolean;\n log: ScrapeLogger;\n headless: boolean;\n};\n\nexport class OwnerSessionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'OwnerSessionError';\n }\n}\n\nlet activeSession: BrowserSession | null = null;\nlet activeProfileKey: string | null = null;\nconst loggedPages = new WeakSet<Page>();\n\nexport async function acquireBrowserSession(\n config: AppConfig,\n log: ScrapeLogger = createScrapeLogger(),\n): Promise<BrowserSession> {\n const profilePath = resolveBrowserProfilePath(config);\n const sessionKey = config.browserCdpEndpoint?.trim() || profilePath;\n\n if (activeSession && activeProfileKey === sessionKey) {\n log.debug({ sessionKey }, 'reusing in-process browser session');\n await activeSession.page.bringToFront().catch(() => undefined);\n return activeSession;\n }\n\n if (activeSession) {\n log.info({ sessionKey: activeProfileKey }, 'closing previous browser before new session');\n await closeBrowser(activeSession, log);\n }\n\n activeSession = config.browserCdpEndpoint?.trim()\n ? await attachOverCdp(config.browserCdpEndpoint.trim(), profilePath, log)\n : await openPersistentSession(profilePath, log, config.ownerHandle, config.headless);\n\n activeProfileKey = sessionKey;\n return activeSession;\n}\n\nasync function attachOverCdp(\n endpoint: string,\n profilePath: string,\n log: ScrapeLogger,\n): Promise<BrowserSession> {\n log.info({ endpoint }, 'attaching to Chrome over CDP');\n\n const browser = await chromium.connectOverCDP(endpoint);\n const context = browser.contexts()[0];\n if (!context) {\n throw new Error(\n `No browser context at ${endpoint}. Start Chrome with remote debugging (see README).`,\n );\n }\n\n await applyStealthToContext(context);\n wireContextPages(context);\n\n const page = context.pages()[0] ?? (await context.newPage());\n attachBrowserLogging(page);\n\n await page.goto((await hasXAuthCookies(context)) ? X_HOME_URL : X_LOGIN_URL, {\n waitUntil: 'domcontentloaded',\n });\n await waitForUiSettled(page, log, 'cdp attach');\n\n return { context, page, profilePath, cdpAttached: true, cdpBrowser: browser };\n}\n\n/**\n * Launch scrape-capable Chrome. If unauthenticated, run manual login window first (separate launch).\n */\nasync function openPersistentSession(\n profilePath: string,\n log: ScrapeLogger,\n expectedOwner: string,\n headless: boolean,\n): Promise<BrowserSession> {\n await mkdir(profilePath, { recursive: true });\n\n let session = await launchScrapeContext(profilePath, log, headless);\n\n if (!(await hasXAuthCookies(session.context))) {\n log.info('no auth cookies in scrape session; starting manual login window');\n await closeBrowser(session, log);\n await runManualLoginWindow(profilePath, log, 'login', expectedOwner);\n session = await launchScrapeContext(profilePath, log, headless);\n }\n\n if (!(await hasXAuthCookies(session.context))) {\n throw new OwnerSessionError(\n `Login did not persist to ${profilePath} (missing auth_token/ct0). ${manualLoginWindowGuidance('login', expectedOwner)}`,\n );\n }\n\n return session;\n}\n\nasync function launchScrapeContext(\n profilePath: string,\n log: ScrapeLogger,\n headless: boolean,\n): Promise<BrowserSession> {\n log.info({ profilePath }, 'opening scrape Chrome profile (Playwright-controlled)');\n\n const options = persistentContextOptions(headless);\n let context: BrowserContext;\n try {\n context = await chromium.launchPersistentContext(profilePath, options);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (/process_singleton|singleton|user data dir|profile/i.test(message)) {\n log.error(\n { err: error, profilePath, options },\n 'Failed to launch persistent session: %s',\n error,\n );\n throw new Error(\n `Chrome profile is locked at ${profilePath}. Close any other Chrome window using this profile.`,\n );\n }\n throw error;\n }\n\n await applyStealthToContext(context);\n wireContextPages(context);\n\n const page = context.pages()[0] ?? (await context.newPage());\n attachBrowserLogging(page);\n\n await page.goto(X_HOME_URL, { waitUntil: 'domcontentloaded' });\n await waitForUiSettled(page, log, 'scrape session launch');\n\n const hasAuth = await hasXAuthCookies(context);\n log.info({ profilePath, hasAuth }, 'scrape Chrome session ready');\n\n return { context, page, profilePath, cdpAttached: false };\n}\n\nfunction wireContextPages(context: BrowserContext): void {\n context.on('page', (page) => {\n attachBrowserLogging(page);\n });\n}\n\nfunction attachBrowserLogging(page: Page): void {\n if (loggedPages.has(page)) {\n return;\n }\n loggedPages.add(page);\n\n const browserLog = createScrapeLogger().child({ source: 'browser' });\n\n const messageRemap: { match: RegExp; matchUrl?: RegExp; note: string; level: LogLevel }[] = [\n {\n match:\n /^The resource \\S+ was preloaded using link preload but not used within a few seconds/i,\n note: 'Preload warning; ignore',\n level: 'debug',\n },\n { match: /^Banner not shown/i, note: 'Banner not shown; ignore', level: 'debug' },\n {\n match: /GSI_LOGGER|FedCM/i,\n note: 'Google Sign-In noise; use X email/password login instead',\n level: 'error',\n },\n {\n match: /Failed to load resource: the server responded with a status of 503/i,\n matchUrl: /[/][/]ads-api[.]x/i,\n note: 'Failed to load resource advertisement resource',\n level: 'debug',\n },\n ];\n const levelMap: Record<ReturnType<ConsoleMessage['type']> | 'verbose', LogLevel> = {\n assert: 'fatal',\n clear: 'debug',\n count: 'debug',\n dir: 'debug',\n dirxml: 'debug',\n endGroup: 'debug',\n error: 'error',\n warning: 'warn',\n info: 'info',\n debug: 'debug',\n log: 'debug',\n profile: 'trace',\n profileEnd: 'trace',\n startGroup: 'debug',\n startGroupCollapsed: 'debug',\n table: 'debug',\n time: 'debug',\n timeEnd: 'debug',\n trace: 'trace',\n verbose: 'debug',\n };\n\n page.on('console', (message: ConsoleMessage) => {\n const type = message.type();\n const text = message.text();\n const location = message.location();\n const payload = { type, text, location };\n\n for (const { match, matchUrl, note, level } of messageRemap) {\n if (match.test(text) && (matchUrl?.test(location.url) ?? true)) {\n browserLog[level]({ ...payload, note }, 'browser console: %s', text);\n return;\n }\n }\n\n browserLog[levelMap[type]](payload, 'browser console: %s', text);\n });\n\n page.on('pageerror', (error) => {\n browserLog.error({ err: error.message, stack: error.stack }, 'browser page error');\n });\n\n page.on('response', (response) => {\n const url = response.url();\n if (url.includes('onboarding/task.json') && response.status() >= 400) {\n browserLog.warn(\n {\n url,\n status: response.status(),\n hint: 'X onboarding API failed \u2014 finish login in the manual login window',\n },\n 'x api response',\n );\n }\n });\n}\n\n/**\n * Ensure owner is logged in. May close the scrape session and open a manual login window.\n * Returns an up-to-date session (possibly replaced after login).\n */\nexport async function ensureOwnerSession(\n session: BrowserSession,\n options: EnsureOwnerSessionOptions,\n): Promise<BrowserSession> {\n const log = options.log ?? createScrapeLogger();\n const expected = normalizeOwnerHandle(options.ownerHandle);\n\n if (await isOwnerSessionActive(session.page, expected)) {\n log.info({ ownerHandle: expected }, 'owner session verified');\n return session;\n }\n\n const loginRequired = await isLoginRequired(session.page);\n const ownerMismatch = !loginRequired;\n\n if (options.abortOnIncorrectOwnerHandle) {\n const message = loginRequired\n ? `Login required for @${expected}. ${manualLoginWindowGuidance('login', expected)}`\n : `Active session does not match ownerHandle @${expected}. ${manualLoginWindowGuidance('owner-mismatch', expected)}`;\n\n logScrapeFailure(log, {\n action: 'ensureOwnerSession',\n expected: `logged in as @${expected}`,\n missing: loginRequired ? 'auth_token and ct0 cookies' : `profile for @${expected}`,\n err: new OwnerSessionError(message),\n });\n throw new OwnerSessionError(message);\n }\n\n if (!session.cdpAttached) {\n const manualReason = loginRequired ? 'login' : 'owner-mismatch';\n log.warn(\n { expectedOwner: expected, reason: manualReason },\n 'opening manual login window to fix session',\n );\n return retryAfterManualLoginWindow(session, options, manualReason);\n }\n\n if (ownerMismatch) {\n await waitForOwnerOnCdpSession(session, expected, log);\n }\n\n return session;\n}\n\nasync function retryAfterManualLoginWindow(\n session: BrowserSession,\n options: EnsureOwnerSessionOptions,\n reason: 'login' | 'owner-mismatch',\n): Promise<BrowserSession> {\n const expected = normalizeOwnerHandle(options.ownerHandle);\n\n await closeBrowser(session, options.log);\n activeSession = null;\n activeProfileKey = null;\n\n await runManualLoginWindow(session.profilePath, options.log, reason, expected);\n\n const fresh = await openPersistentSession(\n session.profilePath,\n options.log,\n options.ownerHandle,\n options.headless,\n );\n activeSession = fresh;\n activeProfileKey = session.profilePath;\n\n return ensureOwnerSession(fresh, options);\n}\n\n/** CDP-attached Chrome cannot use the manual login launcher; poll the live browser instead. */\nasync function waitForOwnerOnCdpSession(\n session: BrowserSession,\n expected: string,\n log: ScrapeLogger,\n): Promise<void> {\n const { page } = session;\n\n log.warn(\n {\n expectedOwner: expected,\n guidance: manualLoginWindowGuidance('owner-mismatch', expected),\n },\n 'wrong account on CDP browser \u2014 switch to the configured owner in that Chrome window',\n );\n\n await page.bringToFront();\n\n const started = Date.now();\n let lastLog = started;\n\n while (true) {\n if (await isOwnerSessionActive(page, expected)) {\n log.info({ ownerHandle: expected }, 'owner session verified after waiting');\n await page.goto(X_HOME_URL, { waitUntil: 'domcontentloaded' });\n await waitForUiSettled(page, log, 'post-login home');\n return;\n }\n\n const now = Date.now();\n if (now - lastLog >= OWNER_LOG_EVERY_MS) {\n log.info(\n {\n expectedOwner: expected,\n waitedMs: now - started,\n hasAuthCookies: await hasXAuthCookies(page.context()),\n },\n 'still waiting for correct owner on scrape session',\n );\n lastLog = now;\n }\n\n await page.waitForTimeout(OWNER_POLL_MS);\n }\n}\n\nexport function normalizeOwnerHandle(handle: string): string {\n return handle.replace(/^@/, '').toLowerCase();\n}\n\nexport async function isOwnerSessionActive(page: Page, normalizedOwner: string): Promise<boolean> {\n if (!(await hasXAuthCookies(page.context()))) {\n return false;\n }\n\n if (await isLoginRequired(page)) {\n return false;\n }\n\n const profile = page.getByRole('link', { name: new RegExp(`@${normalizedOwner}`, 'i') }).first();\n if (await profile.isVisible().catch(() => false)) {\n return true;\n }\n\n const accountSwitcher = page.getByRole('button', {\n name: new RegExp(`@${normalizedOwner}|account menu`, 'i'),\n });\n if (\n await accountSwitcher\n .first()\n .isVisible()\n .catch(() => false)\n ) {\n return true;\n }\n\n const profileNav = page.getByTestId('AppTabBar_Profile_Link');\n const href = await profileNav.getAttribute('href').catch(() => null);\n if (href?.toLowerCase().includes(`/${normalizedOwner}`)) {\n return true;\n }\n\n return false;\n}\n\nexport async function isLoginRequired(page: Page): Promise<boolean> {\n if (await hasXAuthCookies(page.context())) {\n return false;\n }\n\n const url = page.url();\n if (/\\/login|\\/flow\\/login/i.test(url)) {\n return true;\n }\n\n const signIn = page.getByRole('button', { name: /^(log in|sign in)$/i });\n if (\n await signIn\n .first()\n .isVisible()\n .catch(() => false)\n ) {\n return true;\n }\n\n const signInLink = page.getByRole('link', { name: /^(log in|sign in)$/i });\n if (\n await signInLink\n .first()\n .isVisible()\n .catch(() => false)\n ) {\n return true;\n }\n\n return true;\n}\n\nexport async function closeBrowser(\n session: BrowserSession,\n log: ScrapeLogger = createScrapeLogger(),\n): Promise<void> {\n if (session.cdpAttached && session.cdpBrowser) {\n log.info('detaching from CDP (leaving your Chrome running)');\n await session.cdpBrowser.close();\n } else {\n log.info({ profilePath: session.profilePath }, 'closing browser; persisting profile to disk');\n await session.context.close();\n }\n\n if (activeSession === session) {\n activeSession = null;\n activeProfileKey = null;\n }\n}\n", "import type { BrowserContext } from 'playwright';\n\n/** X session cookies set after a successful login (not present during FedCM/onboarding errors). */\nexport async function hasXAuthCookies(context: BrowserContext): Promise<boolean> {\n const cookies = await context.cookies();\n const xCookies = cookies.filter((cookie) => /x\\.com|twitter\\.com/i.test(cookie.domain));\n\n const authToken = xCookies.find((c) => c.name === 'auth_token' && c.value.length > 0);\n const ct0 = xCookies.find((c) => c.name === 'ct0' && c.value.length > 0);\n\n return Boolean(authToken && ct0);\n}\n\nexport const X_LOGIN_URL = 'https://x.com/i/flow/login';\n\nexport type ManualLoginReason = 'login' | 'owner-mismatch';\n\nexport function manualLoginWindowGuidance(\n reason: ManualLoginReason,\n expectedOwner: string,\n): string {\n const base = [\n 'A separate Chrome window opens (no Playwright remote debugging).',\n 'Use X username/email and password \u2014 not Google Sign-In.',\n ];\n\n if (reason === 'owner-mismatch') {\n return [\n ...base,\n `Sign in as @${expectedOwner} (or switch to that account).`,\n 'Quit Chrome completely when the correct account is active (close the browser, not just a tab).',\n ].join(' ');\n }\n\n return [\n ...base,\n 'Complete onboarding until you reach the home timeline, then quit Chrome completely.',\n ].join(' ');\n}\n", "import { type BrowserContext, chromium } from 'playwright';\nimport type { ScrapeLogger } from '../logger.js';\nimport { type ManualLoginReason, manualLoginWindowGuidance, X_LOGIN_URL } from './auth.js';\nimport { loginContextOptions } from './stealth.js';\n\n/**\n * Launch Chrome for manual login (no automation pipes). Blocks until the user closes the browser.\n * Cookies are written to `profilePath` when the browser process exits.\n */\nexport async function runManualLoginWindow(\n profilePath: string,\n log: ScrapeLogger,\n reason: ManualLoginReason,\n expectedOwner: string,\n): Promise<void> {\n const guidance = manualLoginWindowGuidance(reason, expectedOwner);\n const action = reason === 'owner-mismatch' ? 'change user' : 'sign in with email/password';\n\n log.warn(\n {\n profilePath,\n reason,\n expectedOwner,\n guidance,\n loginUrl: X_LOGIN_URL,\n },\n 'opening manual login window \u2014 %s for owner %s at %s, then quit Chrome when done',\n action,\n expectedOwner,\n X_LOGIN_URL,\n );\n\n let context: BrowserContext;\n try {\n context = await chromium.launchPersistentContext(profilePath, loginContextOptions());\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n if (/process_singleton|singleton|user data dir|profile/i.test(message)) {\n if (message.includes('browser has been closed')) {\n log.info({ profilePath }, 'login browser closed; profile saved to disk');\n return;\n }\n\n log.error(\n { profilePath, err: error },\n 'cannot open login window \u2014 profile locked at %s. Close other Chrome windows using this profile. Error: %s',\n profilePath,\n error,\n );\n throw new Error(\n `Cannot open login window \u2014 profile locked at ${profilePath}. Close other Chrome windows using this profile.`,\n );\n }\n throw error;\n }\n\n await context.close();\n throw new Error(\n 'Unexpected: launching a browser window without remote debugging pipes should fail',\n );\n}\n", "import type { BrowserContext, chromium } from 'playwright';\n\ntype PersistentContextOptions = NonNullable<Parameters<typeof chromium.launchPersistentContext>[1]>;\n\nexport const STEALTH_INIT_SCRIPT = `\n(() => {\n Object.defineProperty(navigator, \"webdriver\", {\n get: () => undefined,\n configurable: true,\n });\n if (!window.chrome) {\n window.chrome = { runtime: {} };\n }\n})();\n`;\n\n/** Options tuned for post-login scraping (Playwright-controlled). */\nexport function persistentContextOptions(headless: boolean): PersistentContextOptions {\n return {\n headless,\n channel: 'chrome',\n locale: 'en-US',\n ignoreDefaultArgs: ['--use-mock-keychain'], // X seems to detect this, maybe due disabled FedCM?\n acceptDownloads: false,\n serviceWorkers: 'allow',\n chromiumSandbox: true,\n };\n}\n\n/**\n * Opens X login in a normal Chrome window without remote-debugging pipes since X will detect it and block login.\n * Playwright only waits for the user to close the browser; do not call page.goto here.\n */\nexport function loginContextOptions(): PersistentContextOptions {\n const headless = false; // we always want the browser to be visible when logging in\n const baseOptions = persistentContextOptions(headless);\n return {\n ...baseOptions,\n ignoreDefaultArgs: [\n '--remote-debugging-pipe', // X detects this and blocks login\n ...(baseOptions.ignoreDefaultArgs as string[]),\n ],\n };\n}\n\nexport async function applyStealthToContext(context: BrowserContext): Promise<void> {\n await context.addInitScript(STEALTH_INIT_SCRIPT);\n}\n", "const DEFAULT_CONFIG_PATH = './config.json';\n\nexport type CliOptions = {\n configPath: string;\n /** CLI flag overrides config when set. */\n abortOnIncorrectOwnerHandle?: boolean;\n};\n\nexport function parseCli(argv: string[]): CliOptions {\n let configPath = DEFAULT_CONFIG_PATH;\n let abortOnIncorrectOwnerHandle: boolean | undefined;\n\n for (let i = 2; i < argv.length; i++) {\n const arg = argv[i];\n if (arg === undefined) {\n continue;\n }\n if (arg === '--abort-on-incorrect-ownerHandle') {\n abortOnIncorrectOwnerHandle = true;\n continue;\n }\n if (arg.startsWith('-')) {\n throw new Error(`Unknown option: ${arg}`);\n }\n configPath = arg;\n }\n\n return {\n configPath,\n ...(abortOnIncorrectOwnerHandle !== undefined ? { abortOnIncorrectOwnerHandle } : {}),\n };\n}\n\nexport function resolveAbortOnIncorrectOwnerHandle(\n cli: CliOptions,\n configValue: boolean | undefined,\n): boolean {\n return cli.abortOnIncorrectOwnerHandle ?? configValue ?? false;\n}\n", "import { access, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';\nimport { dirname } from 'node:path';\nimport type { AppState } from '../types/state.js';\nimport { assertValid } from '../validate/ajv.js';\nimport { parseJson, stringifyJson } from '../validate/json.js';\n\nexport async function loadState(statePath: string): Promise<AppState | null> {\n try {\n const raw = await readFile(statePath, 'utf8');\n const parsed = parseJson(raw);\n return await assertValid<AppState>('state.schema.json', parsed, 'State');\n } catch (error) {\n if (isENOENT(error)) {\n return null;\n }\n throw error;\n }\n}\n\nexport async function saveState(statePath: string, state: AppState): Promise<void> {\n await assertValid('state.schema.json', state, 'State');\n await mkdir(dirname(statePath), { recursive: true });\n await backupExistingState(statePath);\n await writeFile(statePath, stringifyJson(state), 'utf8');\n}\n\nasync function backupExistingState(statePath: string): Promise<void> {\n const backupPath = `${statePath}.bkp`;\n try {\n await access(statePath);\n } catch (error) {\n if (isENOENT(error)) {\n return;\n }\n throw error;\n }\n try {\n await access(backupPath);\n await unlink(backupPath);\n } catch (error) {\n if (!isENOENT(error)) {\n throw error;\n }\n }\n await rename(statePath, backupPath);\n}\n\nfunction isENOENT(error: unknown): boolean {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n (error as NodeJS.ErrnoException).code === 'ENOENT'\n );\n}\n"],
5
+ "mappings": ";;AAAA,OAAS,UAAAA,OAAc,SAGvBA,GAAO,CAAE,MAAO,EAAK,CAAC,ECFtB,OAAS,UAAAC,OAAc,mBACvB,OAAS,WAAAC,OAAe,YACxB,OAAS,iBAAAC,OAAqB,WCH9B,OAAS,YAAAC,OAAgB,mBCAzB,OAAS,WAAAC,OAAe,YAGjB,IAAMC,EAA+B,wBAGrC,SAASC,GACdC,EACAC,EAAc,QAAQ,IAAI,EAClB,CACR,IAAMC,EAAWF,EAAO,oBAAsBF,EAC9C,OAAOD,GAAQI,EAAKC,CAAQ,CAC9B,CCZA,OAAS,YAAAC,OAAgB,mBACzB,OAAS,iBAAAC,OAAqB,cAC9B,OAAS,WAAAC,GAAS,QAAAC,OAAY,YAC9B,OAAS,iBAAAC,OAAqB,WAC9B,OAAS,OAAAC,OAAoE,MAE7E,IAAMC,GAAUL,GAAc,YAAY,GAAG,EACvCM,GAAaD,GAAQ,aAAa,EAElCE,GAAaL,GAAKD,GAAQE,GAAc,YAAY,GAAG,CAAC,EAAG,eAAe,EAE5EK,EAEJ,SAASC,IAAc,CACrB,GAAID,EACF,OAAOA,EAET,IAAME,EAAM,IAAIN,GAAI,CAClB,UAAW,GACX,OAAQ,GACR,eAAgB,GAChB,iBAAkB,EACpB,CAAC,EACD,OAAAE,GAAWI,CAAG,EACdF,EAAcE,EACPA,CACT,CAEA,eAAeC,GAAeC,EAAkC,CAC9D,IAAMC,EAAOX,GAAKK,GAAYK,CAAI,EAC5BE,EAAM,MAAMf,GAASc,EAAM,MAAM,EACvC,OAAO,KAAK,MAAMC,CAAG,CACvB,CAEA,IAAMC,GAAiB,IAAI,IAE3B,eAAsBC,GAAaC,EAA+C,CAChF,IAAMC,EAASH,GAAe,IAAIE,CAAU,EAC5C,GAAIC,EACF,OAAOA,EAET,IAAMC,GAAW,SAAY,CAC3B,IAAMT,EAAMD,GAAO,EACbW,EAAS,MAAMT,GAAeM,CAAU,EAE9C,OADiBP,EAAI,QAAQU,CAAM,CAErC,GAAG,EACH,OAAAL,GAAe,IAAIE,EAAYE,CAAO,EAC/BA,CACT,CAEO,SAASE,GAAgBC,EAAkD,CAChF,OAAKA,GAAQ,OAGNA,EACJ,IAAKC,GAEG,GADMA,EAAM,cAAgB,GACrB,KAAKA,EAAM,SAAW,SAAS,EAC9C,EACA,KAAK;AAAA,CAAI,EAPH,0BAQX,CAEA,eAAsBC,EAAeP,EAAoBQ,EAAeC,EAA2B,CACjG,IAAMC,EAAW,MAAMX,GAAaC,CAAU,EAC9C,GAAI,CAACU,EAASF,CAAI,EAChB,MAAM,IAAI,MAAM,GAAGC,CAAK;AAAA,EAAwBL,GAAgBM,EAAS,MAAM,CAAC,EAAE,EAEpF,OAAOF,CACT,CCpEO,SAASG,GAAcC,EAAwB,CACpD,MAAO,GAAG,KAAK,UAAUA,EAAO,KAAM,CAAC,CAAC;AAAA,CAC1C,CAEO,SAASC,EAAUC,EAAuB,CAC/C,OAAO,KAAK,MAAMA,CAAI,CACxB,CHDA,IAAMC,GAAqB,mBACdC,EAAwB,EAErC,eAAsBC,GAAWC,EAAwC,CACvE,IAAMC,EAAM,MAAMC,GAASF,EAAY,MAAM,EACvCG,EAASC,EAAUH,CAAG,EACtBI,EAAS,MAAMC,EAAuB,qBAAsBH,EAAQ,QAAQ,EAClF,MAAO,CACL,GAAGE,EACH,UAAWA,EAAO,WAAaR,GAC/B,mBAAoBQ,EAAO,oBAAsBE,EACjD,IAAK,CACH,GAAGF,EAAO,IACV,GAAIA,EAAO,IAAI,YAAc,CAAE,YAAaA,EAAO,IAAI,WAAY,EAAI,CAAC,CAC1E,EACA,aAAcA,EAAO,cAAgBP,CACvC,CACF,CIvBA,OAAOU,OAAU,OAEjB,SAASC,IAAuB,CAE9B,OAAO,QAAQ,IAAI,WAAgB,MACrC,CAEO,IAAMC,GAASF,GACpB,CACE,MAAOC,GAAa,EACpB,KAAM,CAAE,IAAK,WAAY,CAC3B,EACAD,GAAK,YAAY,CAAC,CACpB,EAKO,SAASG,GAAmC,CACjD,OAAOD,GAAO,MAAM,CAAE,OAAQ,QAAS,CAAC,CAC1C,CAEO,SAASE,EACdC,EACAC,EAOM,CACN,IAAMC,EACJD,EAAQ,eAAe,MACnB,CAAE,QAASA,EAAQ,IAAI,QAAS,MAAOA,EAAQ,IAAI,MAAO,KAAMA,EAAQ,IAAI,IAAK,EACjF,CAAE,QAAS,OAAOA,EAAQ,GAAG,CAAE,EAErCD,EAAI,MACF,CACE,OAAQC,EAAQ,OAChB,SAAUA,EAAQ,SAClB,QAASA,EAAQ,QACjB,KAAMA,EAAQ,KACd,IAAKC,CACP,EACA,oBACF,CACF,CC1CO,SAASC,EAAkBC,EAAsB,CAEtD,MADc,0BAA0B,KAAKA,CAAI,IAClC,CAAC,GAAKA,CACvB,CAEO,SAASC,EAAoBC,EAAgBC,EAA4B,CAE9E,MAAO,YADYD,EAAO,QAAQ,KAAM,EAAE,CACb,IAAIC,CAAU,EAC7C,CAOO,SAASC,GAAoBC,EAAqB,CACvD,IAAMC,EAAWD,EAAI,WAAW,MAAM,EAAIA,EAAM,gBAAgBA,CAAG,GACnE,GAAI,CAEF,IAAME,EADM,IAAI,IAAID,CAAQ,EACV,SAAS,MAAM,yBAAyB,EAC1D,OAAIC,EACK,gBAAgBA,EAAM,CAAC,CAAC,GAE1BD,CACT,MAAQ,CACN,OAAOA,CACT,CACF,CAEO,SAASE,EAAiBC,EAA6B,CAE5D,OADcL,GAAoBK,CAAI,EAAE,MAAM,iBAAiB,IAChD,CAAC,GAAK,IACvB,CAEA,eAAsBC,EAAaC,EAA0C,CAC3E,IAAMC,EAAQD,EAAQ,UAAU,MAAM,EAChCE,EAAQ,MAAMD,EAAM,MAAM,EAC5BE,EAA0B,KAE9B,QAASC,EAAI,EAAGA,EAAIF,EAAOE,IAAK,CAC9B,IAAMV,EAAM,MAAMO,EAAM,IAAIG,CAAC,EAAE,aAAa,MAAM,EAClD,GAAI,CAACV,GAAK,SAAS,UAAU,EAC3B,SAEF,IAAMW,EAAYZ,GAAoBC,CAAG,EACnCY,EAAO,IAAI,IAAID,CAAS,EAAE,SAChC,GAAI,yBAAyB,KAAKC,CAAI,EACpC,OAAOD,EAETF,IAAaE,CACf,CAEA,OAAOF,CACT,CAEO,SAASI,EAAuBC,EAAqB,CAC1D,GAAI,CACF,IAAMC,EAAS,IAAI,IAAID,CAAG,EAC1B,OAAAC,EAAO,KAAO,GACdA,EAAO,OAAS,GACTA,EAAO,SAAS,CACzB,MAAQ,CACN,OAAOD,CACT,CACF,CAGA,eAAsBE,EAAUV,EAAkBW,EAA2C,CAC3F,IAAMC,EAAe,MAAOC,GAAoC,CAC9D,IAAMC,EAASd,EAAQ,YAAYa,CAAM,EAAE,MAAM,EACjD,GAAI,CAAE,MAAMC,EAAO,MAAM,EACvB,OAAAH,EAAI,MAAM,CAAE,OAAAE,CAAO,EAAG,yCAAyC,EACxD,EAET,IAAME,EAAO,MAAMD,EAAO,UAAU,EAAE,MAAM,IAAM,GAAG,EACrD,OAAOE,GAAiBD,CAAI,CAC9B,EAEA,MAAO,CACL,SAAU,MAAMH,EAAa,OAAO,EACpC,QAAS,MAAMA,EAAa,SAAS,EACrC,MAAO,MAAMA,EAAa,MAAM,CAClC,CACF,CAGA,eAAsBK,EAAWjB,EAAgD,CAC/E,IAAMkB,EAAYlB,EAAQ,YAAY,WAAW,EACjD,GAAI,MAAMkB,EAAU,MAAM,EAAG,CAE3B,IAAMpB,EAAO,MADAoB,EAAU,UAAU,MAAM,EAAE,MAAM,EACvB,aAAa,MAAM,EAC3C,GAAIpB,EAAM,CACR,IAAMqB,EAASrB,EAAK,QAAQ,MAAO,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC,GAAG,QAAQ,KAAM,EAAE,EACtE,GAAIqB,EACF,MAAO,CAAE,OAAQA,CAAO,CAE5B,CACF,CAEA,MAAO,CAAC,CACV,CAEA,eAAsBC,EAAcpB,EAAmD,CAErF,IAAMqB,EAAW,MADJrB,EAAQ,QAAQ,MAAM,EAAE,MAAM,EACf,aAAa,UAAU,EAAE,MAAM,IAAM,IAAI,EACrE,OAAOqB,EAAW,CAAE,UAAWA,CAAS,EAAI,CAAC,CAC/C,CAEA,SAASL,GAAiBtB,EAAqB,CAC7C,IAAMqB,EAAOrB,EAAI,KAAK,EAAE,YAAY,EACpC,GAAI,CAACqB,GAAQA,IAAS,SACpB,MAAO,GAET,IAAMnB,EAAQ,wBAAwB,KAAKmB,CAAI,EAC/C,GAAI,CAACnB,EACH,MAAO,GAET,IAAM0B,EAAO,OAAO,WAAW1B,EAAM,CAAC,GAAG,QAAQ,KAAM,EAAE,GAAK,GAAG,EAC3D2B,EAAS3B,EAAM,CAAC,EAGtB,OAAO,KAAK,MAAM0B,GADhBC,IAAW,IAAM,IAAQA,IAAW,IAAM,IAAYA,IAAW,IAAM,IAAgB,EACtD,CACrC,CC/HA,OAAS,YAAYC,OAAW,WAChC,OAAS,QAAAC,OAAY,WAGrB,IAAMC,GAAgB,GAChBC,GAAmB,IACnBC,GAAiB,MAEjBC,GAAwB,CAAC,sBAAuB,iBAAkB,aAAa,EAKrF,eAAsBC,GACpBC,EACAC,EACuB,CACvB,IAAMC,EAAW,MAAMC,GAAgBH,EAAKC,CAAO,EACnD,GAAI,CACF,IAAMG,EAAU,MAAMC,GAAgBH,EAAUD,GAAS,MAAM,EAC/D,GAAI,CAACG,EACH,MAAO,CAAE,IAAKF,CAAS,EAEzB,GAAM,CAAE,MAAAI,EAAO,YAAAC,CAAY,EAAIC,GAAoBJ,CAAO,EAE1D,MAAO,CACL,IAAKF,EACL,GAAII,EAAQ,CAAE,MAAAA,CAAM,EAAI,CAAC,EACzB,GAAIC,EAAc,CAAE,YAAAA,CAAY,EAAI,CAAC,CACvC,CACF,MAAQ,CACN,MAAO,CAAE,IAAKL,CAAS,CACzB,CACF,CAwBA,eAAsBO,GACpBC,EACAC,EACiB,CACjB,IAAMC,EAAeD,GAAS,cAAgBE,GAC1CC,EAAU,IAAI,IAAIJ,CAAG,EAEzB,QAASK,EAAI,EAAGA,GAAKH,EAAcG,IAAK,CACtC,MAAMC,EAA0BF,CAAO,EACvC,IAAMG,EAAO,MAAMC,GAAiBJ,EAAQ,SAAS,EAAG,CACtD,OAAQ,OACR,SAAU,SACV,GAAGK,GAAWR,GAAS,MAAM,CAC/B,CAAC,EACKS,EAAWC,GAAeP,EAASG,CAAI,EAC7C,GAAIG,EAAU,CACZ,MAAMJ,EAA0BI,CAAQ,EACxCN,EAAUM,EACV,QACF,CAEA,GAAIH,EAAK,SAAW,KAAOA,EAAK,SAAW,IAAK,CAC9C,IAAMK,EAAM,MAAMJ,GAAiBJ,EAAQ,SAAS,EAAG,CACrD,OAAQ,MACR,SAAU,SACV,GAAGK,GAAWR,GAAS,MAAM,CAC/B,CAAC,EACKY,EAAUF,GAAeP,EAASQ,CAAG,EAC3C,GAAIC,EAAS,CACX,MAAMP,EAA0BO,CAAO,EACvCT,EAAUS,EACV,QACF,CACF,CAEA,OAAOT,EAAQ,SAAS,CAC1B,CAEA,MAAM,IAAI,MAAM,gCAAgCJ,CAAG,EAAE,CACvD,CAEO,SAASc,GAAoBC,EAAwD,CAC1F,IAAMC,EAAQC,GAAaF,CAAI,EACzBG,EAAcC,GAAmBJ,CAAI,EAC3C,MAAO,CACL,GAAIC,EAAQ,CAAE,MAAAA,CAAM,EAAI,CAAC,EACzB,GAAIE,EAAc,CAAE,YAAAA,CAAY,EAAI,CAAC,CACvC,CACF,CAEA,SAASD,GAAaF,EAAkC,CAGtD,MAFc,gCAAgC,KAAKA,CAAI,IACjC,CAAC,GAAG,KAAK,GACf,MAClB,CAEA,SAASI,GAAmBJ,EAAkC,CAC5D,IAAMK,EAAQC,GAAcN,CAAI,EAChC,QAAWO,KAAOC,GAAuB,CACvC,IAAMC,EAAQJ,EAAM,IAAIE,CAAG,EAC3B,GAAIE,EACF,OAAOA,CAEX,CACA,OAAW,CAACC,EAAMD,CAAK,IAAKJ,EAC1B,GAAIK,EAAK,SAAS,cAAc,GAAKA,IAAS,cAC5C,OAAOD,CAIb,CAEA,SAASH,GAAcN,EAAmC,CACxD,IAAMW,EAAM,IAAI,IACVC,EAAa,mBAEnB,QAAWC,KAAOb,EAAK,SAASY,CAAU,EAAG,CAC3C,IAAME,EAAQC,GAAgBF,EAAI,CAAC,GAAK,EAAE,EACpCH,EAAOI,EAAM,MAAQA,EAAM,SAC3BE,EAAUF,EAAM,QAClBJ,GAAQM,GACVL,EAAI,IAAID,EAAK,YAAY,EAAGO,GAAmBD,CAAO,CAAC,CAE3D,CAEA,OAAOL,CACT,CAQA,SAASI,GAAgBF,EAA6B,CACpD,IAAMC,EAAwB,CAAC,EACzBI,EAAc,qDACpB,QAAWC,KAASN,EAAI,SAASK,CAAW,EAAG,CAC7C,IAAMX,EAAMY,EAAM,CAAC,GAAG,YAAY,EAC5BV,EAAQU,EAAM,CAAC,GAAKA,EAAM,CAAC,GAAKA,EAAM,CAAC,GAAK,IAC9CZ,IAAQ,QAAUA,IAAQ,YAAcA,IAAQ,aAClDO,EAAMP,CAAG,EAAIE,EAEjB,CACA,OAAOK,CACT,CAEA,SAASG,GAAmBG,EAAsB,CAChD,OAAOA,EACJ,WAAW,QAAS,GAAG,EACvB,WAAW,OAAQ,GAAG,EACtB,WAAW,OAAQ,GAAG,EACtB,WAAW,SAAU,GAAG,EACxB,WAAW,QAAS,GAAG,CAC5B,CAEA,SAASC,GAAkBC,EAAqC,CAC9D,GAAI,CAACA,EACH,MAAO,GAET,IAAMC,EAAOD,EAAY,MAAM,GAAG,EAAE,CAAC,GAAG,KAAK,EAAE,YAAY,GAAK,GAChE,OAAOC,IAAS,aAAeA,IAAS,uBAC1C,CAEA,eAAeC,GAAgBvC,EAAawC,EAA8C,CACxF,IAAIpC,EAAU,IAAI,IAAIJ,CAAG,EAEzB,QAASK,EAAI,EAAGA,GAAKF,GAAeE,IAAK,CACvC,MAAMC,EAA0BF,CAAO,EACvC,IAAMqC,EAAW,MAAMjC,GAAiBJ,EAAQ,SAAS,EAAG,CAC1D,OAAQ,MACR,SAAU,SACV,QAAS,CAAE,OAAQ,iCAAkC,EACrD,GAAGK,GAAW+B,CAAM,CACtB,CAAC,EAEKE,EAAO/B,GAAeP,EAASqC,CAAQ,EAC7C,GAAIC,EAAM,CACR,MAAMpC,EAA0BoC,CAAI,EACpCtC,EAAUsC,EACV,QACF,CAEA,OAAO,MAAMC,GAAiBvC,EAAQ,SAAS,EAAGqC,CAAQ,CAC5D,CAEA,MAAM,IAAI,MAAM,wCAAwCzC,CAAG,EAAE,CAC/D,CAEA,eAAe2C,GAAiB3C,EAAayC,EAA4C,CACvF,GAAI,CAACA,EAAS,GACZ,MAAM,IAAI,MAAM,4BAA4BzC,CAAG,UAAUyC,EAAS,MAAM,EAAE,EAG5E,GAAI,CAACL,GAAkBK,EAAS,QAAQ,IAAI,cAAc,CAAC,EACzD,OAAO,KAGT,IAAMG,EAASH,EAAS,MAAM,UAAU,EACxC,GAAI,CAACG,EACH,MAAO,GAGT,IAAMC,EAAuB,CAAC,EAC1BC,EAAQ,EAEZ,OAAa,CACX,GAAM,CAAE,KAAAC,EAAM,MAAAvB,CAAM,EAAI,MAAMoB,EAAO,KAAK,EAC1C,GAAIG,EACF,MAEF,GAAIvB,EAAO,CAET,GADAsB,GAAStB,EAAM,OACXsB,EAAQE,GACV,MAEFH,EAAO,KAAKrB,CAAK,CACnB,CACF,CAEA,OAAO,IAAI,YAAY,EAAE,OAAOyB,GAAaJ,CAAM,CAAC,CACtD,CAKA,SAASK,GAAaC,EAAkC,CACtD,IAAMC,EAASD,EAAO,OAAO,CAACE,EAAKC,IAAUD,EAAMC,EAAM,OAAQ,CAAC,EAC5DC,EAAM,IAAI,WAAWH,CAAM,EAC7BI,EAAS,EACb,QAAWF,KAASH,EAClBI,EAAI,IAAID,EAAOE,CAAM,EACrBA,GAAUF,EAAM,OAElB,OAAOC,CACT,CAEA,SAASE,GAAeC,EAAWC,EAAgC,CACjE,GAAI,CAACC,GAAWD,EAAS,MAAM,EAC7B,OAAO,KAET,IAAME,EAAWF,EAAS,QAAQ,IAAI,UAAU,EAChD,OAAKE,EAGE,IAAI,IAAIA,EAAUH,CAAI,EAFpBA,CAGX,CAEA,SAASE,GAAWE,EAAyB,CAC3C,OAAOA,GAAU,KAAOA,EAAS,GACnC,CAEA,eAAeC,EAA0BC,EAAyB,CAChE,GAAIA,EAAI,WAAa,SAAWA,EAAI,WAAa,SAC/C,MAAM,IAAI,MAAM,wBAAwBA,EAAI,QAAQ,EAAE,EAGxD,IAAMC,EAAOC,GAAqBF,EAAI,QAAQ,EAC9C,GAAIG,GAAgBF,CAAI,EACtB,MAAM,IAAI,MAAM,0BAA0BD,EAAI,QAAQ,EAAE,EAG1D,GAAII,GAAkBH,CAAI,EACxB,MAAM,IAAI,MAAM,4BAA4BD,EAAI,QAAQ,EAAE,EAG5D,GAAIK,GAAKJ,CAAI,EACX,OAGF,IAAMK,EAAY,MAAMC,GAAI,OAAON,EAAM,CAAE,IAAK,GAAM,SAAU,EAAK,CAAC,EACtE,GAAI,CAACK,EAAU,OACb,MAAM,IAAI,MAAM,+BAA+BN,EAAI,QAAQ,EAAE,EAG/D,OAAW,CAAE,QAAAQ,CAAQ,IAAKF,EACxB,GAAIF,GAAkBI,CAAO,EAC3B,MAAM,IAAI,MAAM,4BAA4BR,EAAI,QAAQ,EAAE,CAGhE,CAEA,SAASG,GAAgBF,EAAuB,CAC9C,OAAOA,IAAS,aAAeA,EAAK,SAAS,YAAY,CAC3D,CAEA,SAASC,GAAqBO,EAA0B,CACtD,IAAMC,EAAqBD,EAAS,QAAQ,MAAO,EAAE,EAAE,YAAY,EACnE,OAAOC,EAAmB,WAAW,GAAG,GAAKA,EAAmB,SAAS,GAAG,EACxEA,EAAmB,MAAM,EAAG,EAAE,EAC9BA,CACN,CAEA,SAASN,GAAkBI,EAA0B,CACnD,IAAMG,EAASN,GAAKG,CAAO,EAC3B,OAAIG,IAAW,EACNC,GAAoBJ,CAAO,EAEhCG,IAAW,EACNE,GAAoBL,CAAO,EAE7B,EACT,CAEA,SAASI,GAAoBJ,EAA0B,CACrD,IAAMM,EAAQN,EAAQ,MAAM,GAAG,EAAE,IAAKO,GAAS,OAAO,SAASA,EAAM,EAAE,CAAC,EACxE,GACED,EAAM,SAAW,GACjBA,EAAM,KAAMC,GAAS,CAAC,OAAO,UAAUA,CAAI,GAAKA,EAAO,GAAKA,EAAO,GAAG,EAEtE,MAAO,GAGT,GAAM,CAACC,EAAI,EAAGC,EAAI,CAAC,EAAIH,EACvB,OACEE,IAAM,GACNA,IAAM,IACNA,IAAM,KACLA,IAAM,KAAOC,GAAK,IAAMA,GAAK,KAC7BD,IAAM,KAAOC,IAAM,KACnBD,IAAM,KAAOC,GAAK,IAAMA,GAAK,IAC7BD,IAAM,KAAOC,IAAM,KACnBD,IAAM,MAAQC,IAAM,IAAMA,IAAM,KACjCD,GAAK,GAET,CAEA,SAASH,GAAoBL,EAA0B,CACrD,IAAMU,EAAaV,EAAQ,YAAY,EACvC,GAAIU,EAAW,WAAW,SAAS,EAAG,CACpC,IAAMC,EAASD,EAAW,MAAM,CAAgB,EAChD,OAAON,GAAoBO,CAAM,CACnC,CACA,OACED,IAAe,MACfA,IAAe,OACfA,EAAW,WAAW,IAAI,GAC1BA,EAAW,WAAW,IAAI,GAC1B,YAAY,KAAKA,CAAU,GAC3BA,EAAW,WAAW,IAAI,CAE9B,CAEA,SAASE,GAAWC,EAAmD,CACrE,OAAOA,EAAS,CAAE,OAAAA,CAAO,EAAI,CAAC,CAChC,CAEA,eAAeC,GAAiBtB,EAAauB,EAAsC,CACjF,IAAMC,EAAa,IAAI,gBACjBC,EAAU,WAAW,IAAMD,EAAW,MAAM,EAAGE,EAAgB,EAC/DL,EAASE,EAAK,OAChB,YAAY,IAAI,CAACA,EAAK,OAAQC,EAAW,MAAM,CAAC,EAChDA,EAAW,OAEf,GAAI,CACF,OAAO,MAAM,MAAMxB,EAAK,CAAE,GAAGuB,EAAM,OAAAF,CAAO,CAAC,CAC7C,QAAE,CACA,aAAaI,CAAO,CACtB,CACF,CClXO,IAAME,GAAN,cAAiC,KAAM,CAC5C,YAAYC,EAAeC,EAAY,CACrC,MAAM,GAAGD,CAAK,oBAAoBC,CAAE,IAAI,EACxC,KAAK,KAAO,oBACd,CACF,EAEA,eAAsBC,EAAeC,EAAqBF,EAAYD,EAA2B,CAC/F,IAAII,EACEC,EAAiB,IAAI,QAAe,CAACC,EAAGC,IAAW,CACvDH,EAAY,WAAW,IAAM,CAC3BG,EAAO,IAAIR,GAAmBC,EAAOC,CAAE,CAAC,CAC1C,EAAGA,CAAE,CACP,CAAC,EAED,GAAI,CACF,OAAO,MAAM,QAAQ,KAAK,CAACE,EAASE,CAAc,CAAC,CACrD,QAAE,CACID,IAAc,QAChB,aAAaA,CAAS,CAE1B,CACF,CCvBO,SAASI,EAAkBC,EAAsB,CACtD,GAAIA,EAAK,WAAW,WAAW,EAC7B,OAAOA,EAET,GAAI,CACF,IAAMC,EAAM,IAAI,IAAID,CAAI,EACxB,OAAAC,EAAI,KAAO,GACJA,EAAI,SAAS,CACtB,MAAQ,CACN,OAAOD,CACT,CACF,CAGO,IAAME,EAAN,KAAoB,CACR,UAAY,IAAI,IAChB,UAAY,IAAI,IAEhB,IAEjB,YAAYC,EAAmB,CAC7B,KAAK,IAAMA,CACb,CAEA,UAAUH,EAAgC,CACxC,OAAO,KAAK,UAAU,IAAID,EAAkBC,CAAI,CAAC,CACnD,CAEA,SAASI,EAAkB,CACzB,KAAK,UAAU,IAAIL,EAAkBK,EAAK,IAAI,EAAGA,CAAI,CACvD,CAEA,gBAAgBA,EAAYC,EAAyB,CACnD,IAAMC,EAAMP,EAAkBK,EAAK,IAAI,EACvC,GAAI,CAAAC,EAAK,IAAIC,CAAG,EAGhB,CAAAD,EAAK,IAAIC,CAAG,EACZ,QAAWC,KAAOH,EAAK,YAAc,CAAC,EACpC,KAAK,gBAAgBG,EAAKF,CAAI,EAEhC,QAAWG,KAAQJ,EAAK,QAAU,CAAC,EACjC,KAAK,gBAAgBI,EAAMH,CAAI,EAEnC,CAEA,MAAM,SAASD,EAAYK,EAAyBC,EAAW,GAAqB,CAClF,IAAMJ,EAAMP,EAAkBK,EAAK,IAAI,EACjCO,EAAS,KAAK,UAAU,IAAIL,CAAG,EACrC,GAAIK,EACF,OAAOA,EAGT,GAAIF,EAAW,IAAIH,CAAG,EACpB,YAAK,IAAI,MAAM,CAAE,KAAMA,CAAI,EAAG,yCAAyC,EAChEF,EAGTK,EAAW,IAAIH,CAAG,EAElB,IAAMM,EAAUR,EAAK,UAAU,OAC3BA,EAAK,SACLS,GAAwBT,EAAK,MAAQ,EAAE,EACrCU,EAAQF,EAAQ,OAAS,MAAM,KAAK,mBAAmBA,CAAO,EAAI,OAElEG,EAAa,MAAM,KAAK,eAAeX,EAAK,YAAc,CAAC,EAAGK,EAAY,EAAK,EAC/EO,EAAS,MAAM,KAAK,eAAeZ,EAAK,QAAU,CAAC,EAAGK,EAAY,EAAK,EAE7EA,EAAW,OAAOH,CAAG,EAErB,GAAM,CACJ,WAAYW,EACZ,OAAQC,EACR,MAAOC,EACP,SAAUC,EACV,GAAGC,CACL,EAAIjB,EACEkB,EAAkB,CACtB,GAAGD,EACH,GAAIP,GAAO,OAAS,CAAE,MAAAA,CAAM,EAAI,CAAC,EACjC,GAAIC,EAAW,OAAS,CAAE,WAAAA,CAAW,EAAI,CAAC,EAC1C,GAAIC,EAAO,OAAS,CAAE,OAAAA,CAAO,EAAI,CAAC,CACpC,EAEA,OAAIN,GACF,KAAK,UAAU,IAAIJ,EAAKgB,CAAS,EAE5BA,CACT,CAEA,MAAc,eACZC,EACAd,EACAC,EACiB,CACjB,IAAMc,EAAiB,CAAC,EACxB,QAAWhB,KAAQe,EAAO,CACxB,IAAMjB,EAAMP,EAAkBS,EAAK,IAAI,EACvC,GAAIC,EAAW,IAAIH,CAAG,EAAG,CACvB,KAAK,IAAI,MAAM,CAAE,KAAMA,CAAI,EAAG,kDAAkD,EAChF,QACF,CACAkB,EAAO,KAAK,MAAM,KAAK,SAAShB,EAAMC,EAAYC,CAAQ,CAAC,CAC7D,CACA,OAAOc,CACT,CAEA,MAAc,mBAAmBC,EAAyC,CACxE,IAAMC,EAA0B,CAAC,EAC3BC,EAAO,IAAI,IACjB,QAAWC,KAAUH,EAAM,CACzB,IAAMxB,EAAM4B,GAAiBD,CAAM,EACnC,GAAI,CAAC3B,EAAK,CACR,KAAK,IAAI,KAAK,CAAE,IAAK2B,CAAO,EAAG,+BAA+B,EAC9D,QACF,CACA,GAAID,EAAK,IAAI1B,CAAG,EACd,SAEF0B,EAAK,IAAI1B,CAAG,EACZ,IAAMU,EAAS,KAAK,UAAU,IAAIV,CAAG,EACrC,GAAIU,EAAQ,CACVe,EAAQ,KAAKf,CAAM,EACnB,QACF,CACA,GAAImB,GAAiB7B,CAAG,EAAG,CACzB,IAAM8B,EAAO,CAAE,IAAA9B,CAAI,EACnB,KAAK,UAAU,IAAIA,EAAK8B,CAAI,EAC5BL,EAAQ,KAAKK,CAAI,EACjB,QACF,CACA,GAAI,CACF,IAAMC,EAAW,MAAMC,EACrBC,GAAYjC,CAAG,EACf,IACA,iBAAiBA,CAAG,EACtB,EACMkC,EAAcN,GAAiBG,EAAS,GAAG,EACjD,GAAI,CAACG,EAAa,CAChB,KAAK,IAAI,KAAK,CAAE,IAAAlC,EAAK,SAAA+B,CAAS,EAAG,6CAA6C,EAC9E,QACF,CACA,IAAMD,EAAO,CAAE,GAAGC,EAAU,IAAKG,CAAY,EAC7C,KAAK,UAAU,IAAIlC,EAAK8B,CAAI,EAC5BL,EAAQ,KAAKK,CAAI,CACnB,OAASK,EAAK,CACZ,KAAK,IAAI,KAAK,CAAE,IAAAnC,EAAK,IAAAmC,CAAI,EAAG,mDAAmD,EAC/E,IAAMC,EAAW,CAAE,IAAApC,CAAI,EACvB,KAAK,UAAU,IAAIA,EAAKoC,CAAQ,EAChCX,EAAQ,KAAKW,CAAQ,CACvB,CACF,CACA,OAAOX,CACT,CACF,EAEA,SAASG,GAAiBS,EAA4B,CACpD,GAAIA,EAAI,WAAW,OAAO,EACxB,OAAO,KAET,GAAI,CACF,IAAMrC,EAAM,IAAI,IAAIqC,CAAG,EACvB,OAAIrC,EAAI,WAAa,SAAWA,EAAI,WAAa,SACxC,KAEFA,EAAI,SAAS,CACtB,MAAQ,CACN,OAAO,IACT,CACF,CAEA,SAAS6B,GAAiB7B,EAAsB,CAC9C,GAAI,CACF,GAAM,CAAE,SAAAsC,CAAS,EAAI,IAAI,IAAItC,CAAG,EAChC,MACE,+BAA+B,KAAKsC,CAAQ,GAC5CA,EAAS,SAAS,SAAS,GAC3BA,EAAS,SAAS,iBAAiB,CAEvC,MAAQ,CACN,MAAO,EACT,CACF,CAEA,SAAS1B,GAAwB2B,EAA4B,CAC3D,IAAMf,EAAiB,CAAC,EAClBgB,EAAU,yBAChB,QAAWC,KAASF,EAAS,SAASC,CAAO,EAC3ChB,EAAK,KAAKiB,EAAM,CAAC,EAAE,QAAQ,cAAe,EAAE,CAAC,EAE/C,OAAOjB,CACT,CC7LO,SAASkB,GACdC,EACAC,EACAC,EAAgB,KAAK,IAAI,EACsB,CAC/C,GAAID,EACF,MAAO,CACL,SAAU,KAAK,MAAMA,EAAc,SAAS,EAC5C,gBAAiBA,EAAc,SACjC,EAEF,IAAME,EAAWD,EAAQF,EAAO,kBAAoB,GAAK,IACzD,MAAO,CACL,SAAAG,EACA,gBAAiB,IAAI,KAAKA,CAAQ,EAAE,YAAY,CAClD,CACF,CAGO,SAASC,GACdC,EACAC,EACAC,EACAC,EACAC,EACU,CACV,IAAMC,EAAoC,CAAC,EAE3C,QAAWC,KAAQJ,EACjBK,EAAWD,EAAMD,CAAK,EAExB,QAAWC,KAAQH,EACjBI,EAAWD,EAAMD,CAAK,EAExB,QAAWG,KAAQ,OAAO,OAAOJ,CAAS,EACxC,QAAWE,KAAQE,EACjBD,EAAWD,EAAMD,CAAK,EAI1B,MAAO,CACL,UAAAL,EACA,gBAAAC,EACA,MAAAI,EACA,UAAWH,EAAU,IAAKI,GAASG,EAAkBC,EAAkBJ,EAAK,IAAI,CAAC,CAAC,EAClF,kBAAmBH,EAAkB,IAAKG,GACxCG,EAAkBC,EAAkBJ,EAAK,IAAI,CAAC,CAChD,EACA,UAAW,OAAO,YAChB,OAAO,QAAQF,CAAS,EAAE,IAAI,CAAC,CAACO,EAAQH,CAAI,IAAM,CAChDG,EACAH,EAAK,IAAKF,GAASG,EAAkBC,EAAkBJ,EAAK,IAAI,CAAC,CAAC,CACpE,CAAC,CACH,CACF,CACF,CAEA,SAASC,EAAWD,EAAYD,EAAyC,CACvE,IAAMO,EAAMH,EAAkBH,EAAK,IAAI,EACvC,QAAWO,KAAOP,EAAK,YAAc,CAAC,EACpCC,EAAWM,EAAKR,CAAK,EAEvB,QAAWS,KAAQR,EAAK,QAAU,CAAC,EACjCC,EAAWO,EAAMT,CAAK,EAEpBA,EAAMO,CAAG,IAGbP,EAAMO,CAAG,EAAIG,GAAaT,CAAI,EAChC,CAEA,SAASS,GAAaT,EAAwB,CAC5C,MAAO,CACL,MAAOA,EAAK,MACZ,GAAIA,EAAK,OAAS,CAAE,OAAQA,EAAK,MAAO,EAAI,CAAC,EAC7C,GAAIA,EAAK,UAAY,CAAE,UAAWA,EAAK,SAAU,EAAI,CAAC,EACtD,GAAIA,EAAK,KAAO,CAAE,KAAMA,EAAK,IAAK,EAAI,CAAC,EACvC,GAAIA,EAAK,OAAO,OAAS,CAAE,MAAOA,EAAK,KAAM,EAAI,CAAC,EAClD,GAAIA,EAAK,QAAQ,OACb,CAAE,OAAQA,EAAK,OAAO,IAAKQ,GAASL,EAAkBK,EAAK,IAAI,CAAC,CAAE,EAClE,CAAC,EACL,GAAIR,EAAK,YAAY,OACjB,CAAE,WAAYA,EAAK,WAAW,IAAKQ,GAASL,EAAkBK,EAAK,IAAI,CAAC,CAAE,EAC1E,CAAC,CACP,CACF,CAGO,SAASE,GAAkBC,EAA8B,CAC9D,IAAMC,EAAQ,IAAI,IACZC,EAAOC,GAAuB,CAClCF,EAAM,IAAIT,EAAkBW,CAAI,CAAC,CACnC,EAEA,QAAWA,KAAQH,EAAM,UACvBE,EAAIC,CAAI,EAEV,QAAWA,KAAQH,EAAM,kBACvBE,EAAIC,CAAI,EAEV,QAAWZ,KAAQ,OAAO,OAAOS,EAAM,SAAS,EAC9C,QAAWG,KAAQZ,EACjBW,EAAIC,CAAI,EAGZ,QAAWR,KAAO,OAAO,KAAKK,EAAM,KAAK,EACvCE,EAAIP,CAAG,EAET,QAAWS,KAAU,OAAO,OAAOJ,EAAM,KAAK,EAAG,CAC/C,QAAWG,KAAQC,EAAO,YAAc,CAAC,EACvCF,EAAIC,CAAI,EAEV,QAAWA,KAAQC,EAAO,QAAU,CAAC,EACnCF,EAAIC,CAAI,CAEZ,CAEA,OAAOF,CACT,CCxHA,eAAsBI,GAA4B,CAChD,IAAMC,EAAS,KAAK,MAAM,KAAK,OAAO,EAAI,GAAG,EAC7C,MAAM,IAAI,QAASC,GAAY,WAAWA,EAAS,IAAsBD,CAAM,CAAC,CAClF,CAKA,eAAsBE,EACpBC,EACAC,EACAC,EACe,CACfD,EAAI,MAAM,CAAE,MAAAC,CAAM,EAAG,0BAA0B,EAC/C,MAAMF,EAAK,iBAAiB,cAAe,CAAE,QAAS,IAAO,CAAC,EAAE,MAAM,IAAG,EAAY,EACrF,MAAMG,GAAeH,CAAI,CAC3B,CAGA,eAAsBI,EACpBJ,EACAC,EACAC,EACe,CACfD,EAAI,MAAM,CAAE,MAAAC,CAAM,EAAG,0BAA0B,EAC/C,MAAMC,GAAeH,CAAI,CAC3B,CAGA,eAAsBK,EACpBL,EACAC,EACAC,EAAQ,oBACO,CACfD,EAAI,MAAM,CAAE,MAAAC,CAAM,EAAG,mCAAmC,EACxD,IAAMI,EAAWN,EAAK,WAAW,yBAA0B,CAAE,MAAO,EAAK,CAAC,EAC1E,MAAMM,EAAS,QAAQ,CAAE,MAAO,UAAW,QAAS,GAAO,CAAC,EAE5D,MADiBA,EAAS,QAAQ,8BAA8B,EAE7D,MAAM,EACN,QAAQ,CAAE,MAAO,UAAW,QAAS,GAAO,CAAC,EAC7C,MAAM,IAAG,EAAY,EACxB,MAAMV,EAAW,CACnB,CAEA,eAAeO,GAAeH,EAA2B,CACvD,IAAMO,EAAOP,EAAK,QAAQ,oBAAoB,EACzC,MAAMO,EAAK,MAAM,EAAK,GACzB,MAAMA,EACH,MAAM,EACN,QAAQ,CAAE,MAAO,SAAU,QAAS,GAAO,CAAC,EAC5C,MAAM,IAAG,EAAY,EAG1B,MAAMX,EAAW,CACnB,CAcA,eAAsBY,GACpBC,EACAC,EACAC,EACAC,EACAC,EACe,CACfH,EAAI,KAAK,CAAE,OAAAE,CAAO,EAAG,aAAa,EAClC,MAAMD,EAAO,MAAME,CAAO,EAC1B,MAAMC,EAAiBL,EAAMC,EAAKE,CAAM,CAC1C,CChFO,SAASG,EAASC,EAAoB,CAC3C,MAAO,CACL,KAAMC,EAAkBD,CAAI,EAC5B,MAAO,CAAE,SAAU,EAAG,QAAS,EAAG,MAAO,CAAE,CAC7C,CACF,CCDA,eAAsBE,GAAyBC,EAA+C,CAC5F,IAAMC,EAAaD,EAAQ,QAAQ,2BAA2B,EACxDE,EAAQ,MAAMD,EAAW,MAAM,EAErC,QAASE,EAAI,EAAGA,EAAID,EAAOC,IAAK,CAC9B,IAAMC,EAAYH,EAAW,IAAIE,CAAC,EAElC,GADoB,MAAMC,EAAU,SAAUC,GAAO,CAAC,CAACA,EAAG,QAAQ,kBAAkB,CAAC,EAEnF,SAGF,IAAMC,GAAS,MAAMF,EAAU,UAAU,GAAG,KAAK,EACjD,GAAI,CAACE,EACH,SAGF,IAAMC,EAAU,MAAMH,EAAU,SAAUC,GAAO,CAC/C,IAAMG,EAAwC,CAAC,EAC/C,QAAWC,KAAUJ,EAAG,iBAAiB,SAAS,EAChDG,EAAI,KAAK,CACP,MAAOC,EAAO,aAAe,IAAI,KAAK,EACtC,KAAMA,EAAO,aAAa,MAAM,GAAK,EACvC,CAAC,EAEH,OAAOD,CACT,CAAC,EAED,OAAOE,GAAoBJ,EAAOC,CAAO,CAC3C,CAGF,CAEO,SAASG,GAAoBJ,EAAeC,EAAgC,CACjF,IAAII,EAAWL,EACf,OAAW,CAAE,KAAAM,EAAM,KAAAC,CAAK,IAAKN,EAAS,CACpC,IAAMO,EAAWC,GAAqBF,CAAI,EACtC,CAACD,GAAQD,EAAS,SAAS,KAAKG,CAAQ,GAAG,IAG/CH,EAAWA,EAAS,QAAQC,EAAM,IAAIA,CAAI,KAAKE,CAAQ,GAAG,EAC5D,CACA,OAAOH,CACT,CAEA,SAASI,GAAqBF,EAAsB,CAClD,OAAIA,EAAK,WAAW,MAAM,EACjBA,EAELA,EAAK,WAAW,GAAG,EACd,gBAAgBA,CAAI,GAEtBA,CACT,CCPO,SAASG,GAA0BC,EAAYC,EAA2C,CAC/F,IAAIC,EAA0B,KAC1BC,EAAU,GACRC,EAAiD,CAAC,EAElDC,EAAUC,GAA+B,CAC7C,GAAI,CAAAH,EAGJ,CAAAA,EAAU,GACVD,EAAWI,EACX,QAAWC,KAAWH,EACpBG,EAAQD,CAAK,EAEfF,EAAQ,OAAS,EACnB,EAEMI,EAAU,MAAOC,GAGF,CACnB,IAAMC,EAAMD,EAAS,IAAI,EACzB,GAAI,GAACC,EAAI,SAAS,aAAa,GAAK,CAACA,EAAI,SAAST,CAAY,GAG9D,GAAI,CACFI,EAAO,MAAMI,EAAS,KAAK,CAAC,CAC9B,MAAQ,CAER,CACF,EAEA,OAAAT,EAAK,GAAG,WAAYQ,CAAO,EAEpB,CACL,QAAS,CAACG,EAAY,OAChBT,EACK,QAAQ,QAAQA,CAAQ,EAE1B,IAAI,QAAwBK,GAAY,CAC7C,IAAMK,EAAQ,WAAW,IAAM,CAC7BZ,EAAK,IAAI,WAAYQ,CAAO,EAC5BD,EAAQL,CAAQ,CAClB,EAAGS,CAAS,EACZP,EAAQ,KAAME,GAAU,CACtB,aAAaM,CAAK,EAClBL,EAAQD,CAAK,CACf,CAAC,CACH,CAAC,EAEH,OAAQ,IAAM,CACZN,EAAK,IAAI,WAAYQ,CAAO,CAC9B,CACF,CACF,CAGA,eAAsBK,GACpBb,EACAc,EACAC,EACwB,CACxB,IAAMC,EAAUC,EAAiBH,CAAI,EACrC,GAAI,CAACE,EACH,OAAO,KAGT,IAAME,EAAWnB,GAA0BC,EAAMgB,CAAO,EAClDG,EAASC,EAAuBN,CAAI,EAE1C,GAAI,CACF,OAAIM,EAAuBpB,EAAK,IAAI,CAAC,IAAMmB,EACzC,MAAMnB,EAAK,KAAKc,EAAM,CAAE,UAAW,kBAAmB,CAAC,GAEvDC,EAAI,MAAM,CAAE,QAAAC,CAAQ,EAAG,+CAA+C,EACtE,MAAMhB,EAAK,OAAO,CAAE,UAAW,kBAAmB,CAAC,GAErD,MAAMqB,EAAyBrB,EAAMe,CAAG,EACjC,MAAMG,EAAS,QAAQ,IAAM,CACtC,QAAE,CACAA,EAAS,OAAO,CAClB,CACF,CAGO,SAASI,GAAyBC,EAActB,EAAmC,CACxF,IAAIuB,EACJ,GAAI,CACFA,EAAS,KAAK,MAAMD,CAAI,CAC1B,MAAQ,CACN,OAAO,IACT,CAEA,IAAME,EAAQC,GAAkBF,CAAM,EAChCG,EAAQF,EAAM,IAAIxB,CAAY,EACpC,OAAK0B,EAIEC,EAAkBD,EAAOF,EAAO,CACrC,cAAe,GACf,cAAe,GACf,qBAAsB,EACxB,CAAC,EAPQ,IAQX,CAEA,SAASC,GAAkBH,EAA4C,CACrE,IAAME,EAAQ,IAAI,IAEZI,EAASvB,GAAyB,CACtC,GAAI,CAACA,GAAS,OAAOA,GAAU,SAC7B,OAEF,GAAI,MAAM,QAAQA,CAAK,EAAG,CACxB,QAAWwB,KAAQxB,EACjBuB,EAAMC,CAAI,EAEZ,MACF,CAEA,IAAMC,EAAOzB,EACP0B,EAAKD,EAAK,QAAQ,OAClBE,EAASF,EAAK,MAAM,cAAc,QAAQ,MAAM,YAClDC,GAAMC,GACRR,EAAM,IAAIO,EAAID,CAAI,EAGpB,QAAWG,KAAS,OAAO,OAAO5B,CAAK,EACrCuB,EAAMK,CAAK,CAEf,EAEA,OAAAL,EAAMN,CAAI,EACHE,CACT,CAQA,SAASU,GACPF,EACAG,EACuD,CACvD,IAAMC,EAAYC,GAAiBF,EAAO,UAAU,EACpD,MAAO,CACL,MAAOG,GAASH,CAAM,EACtB,GAAIH,EAAS,CAAE,OAAAA,CAAO,EAAI,CAAC,EAC3B,GAAII,EAAY,CAAE,UAAAA,CAAU,EAAI,CAAC,CACnC,CACF,CAEA,SAASG,GACPT,EACAN,EACAQ,EACAnB,EACAsB,EACM,CACN,IAAMK,EAAYV,EAAK,yBAAyB,OAChD,GAAI,CAACU,EACH,MAAM,IAAI,MAAM,8CAA8C,EAEhE,MAAO,CACL,KAAMC,EAAoBT,EAAQnB,CAAI,EACtC,GAAGqB,GAAeF,EAAQG,CAAM,EAChC,WAAY,CACVR,EAAkBa,EAAWhB,EAAO,CAClC,cAAe,GACf,cAAe,GACf,qBAAsB,EACxB,CAAC,CACH,CACF,CACF,CAEA,SAASkB,GACPZ,EACAN,EACAmB,EACQ,CACR,IAAMC,EAASd,EAAK,sBAAsB,OAC1C,MAAI,CAACa,EAAQ,eAAiB,CAACC,EACtB,CAAC,EAEH,CACLjB,EAAkBiB,EAAQpB,EAAO,CAC/B,cAAe,GACf,cAAe,GACf,qBAAsB,EACxB,CAAC,CACH,CACF,CAEA,SAASG,EACPG,EACAN,EACAmB,EACM,CACN,IAAMR,EAASL,EAAK,OACpB,GAAI,CAACK,GAAQ,OACX,MAAM,IAAI,MAAM,2BAA2B,EAG7C,IAAMH,EAASF,EAAK,MAAM,cAAc,QAAQ,MAAM,aAAe,GAC/DjB,EAAOgC,GAAWb,EAAQG,EAAO,MAAM,EAE7C,GAAIQ,EAAQ,sBAAwBG,GAAchB,CAAI,EACpD,OAAOS,GAAoBT,EAAMN,EAAOQ,EAAQnB,EAAMsB,CAAM,EAG9D,IAAMY,EAAOC,GAAkBlB,CAAI,EAC7BmB,EAAWC,GAAgBpB,CAAI,EAC/BqB,EAAaT,GAAqBZ,EAAMN,EAAOmB,CAAO,EACtDS,EAAST,EAAQ,cAAgBU,GAAiBvB,EAAMN,CAAK,EAAI,CAAC,EAExE,MAAO,CACL,KAAAX,EACA,GAAGqB,GAAeF,EAAQG,CAAM,EAChC,GAAIY,EAAO,CAAE,KAAAA,CAAK,EAAI,CAAC,EACvB,GAAIE,EAAS,OAAS,CAAE,SAAAA,CAAS,EAAI,CAAC,EACtC,GAAIE,EAAW,OAAS,CAAE,WAAAA,CAAW,EAAI,CAAC,EAC1C,GAAIC,EAAO,OAAS,CAAE,OAAAA,CAAO,EAAI,CAAC,CACpC,CACF,CAEA,SAASC,GAAiBvB,EAAsBN,EAA4C,CAC1F,IAAM4B,EAAiB,CAAC,EAClBE,EAAO,IAAI,IACbC,EAAsCzB,EAE1C,KAAOyB,GAAS,QAAQ,2BAA2B,CACjD,IAAMC,EAAWD,EAAQ,OAAO,0BAChC,GAAID,EAAK,IAAIE,CAAQ,EACnB,MAEFF,EAAK,IAAIE,CAAQ,EACjB,IAAMC,EAASjC,EAAM,IAAIgC,CAAQ,EACjC,GAAI,CAACC,EACH,MAEFL,EAAO,QACLzB,EAAkB8B,EAAQjC,EAAO,CAC/B,cAAe,GACf,cAAe,GACf,qBAAsB,EACxB,CAAC,CACH,EACA+B,EAAUE,CACZ,CAEA,OAAOL,CACT,CAEA,SAASN,GAAchB,EAA+B,CAKpD,GAHI,CADcA,EAAK,yBAAyB,QAI5CA,EAAK,QAAQ,gBACf,MAAO,GAET,IAAM4B,EAAOC,GAAe7B,CAAI,EAAE,KAAK,EACvC,OAAK4B,EAGE,aAAa,KAAKA,CAAI,EAFpB,EAGX,CAEA,SAASC,GAAe7B,EAA8B,CACpD,OAAOA,EAAK,YAAY,oBAAoB,QAAQ,MAAQA,EAAK,QAAQ,WAAa,EACxF,CAEA,SAASkB,GAAkBlB,EAA0C,CACnE,IAAM4B,EAAOC,GAAe7B,CAAI,EAAE,KAAK,EACvC,GAAI,CAAC4B,EACH,OAGF,IAAME,EAAiD,CAAC,EACxD,QAAWnD,KAAOqB,EAAK,QAAQ,UAAU,MAAQ,CAAC,EAC5CrB,EAAI,cACNmD,EAAQ,KAAK,CACX,KAAMnD,EAAI,aAAeA,EAAI,KAAOA,EAAI,aACxC,KAAMA,EAAI,YACZ,CAAC,EAGL,QAAWoD,KAASC,GAAchC,CAAI,EAChC+B,EAAM,cAAgBA,EAAM,aAC9BD,EAAQ,KAAK,CAAE,KAAMC,EAAM,YAAa,KAAMA,EAAM,YAAa,CAAC,EAItE,OAAOE,GAAoBL,EAAME,CAAO,CAC1C,CAEA,SAASE,GAAchC,EAAqC,CAC1D,OAAOA,EAAK,QAAQ,mBAAmB,OAASA,EAAK,QAAQ,UAAU,OAAS,CAAC,CACnF,CAEA,SAASoB,GAAgBpB,EAAgC,CACvD,IAAMkC,EAAO,IAAI,IACXC,EAAOC,GAAuB,CAClC,GAAI,GAACA,GAAOA,EAAI,WAAW,OAAO,GAGlC,GAAI,CACF,IAAMzD,EAAM,IAAI,IAAIyD,CAAG,EACnBC,GAAU1D,CAAG,GACfuD,EAAK,IAAIvD,EAAI,SAAS,CAAC,CAE3B,MAAQ,CACFyD,EAAI,WAAW,GAAG,GACpBF,EAAK,IAAI,IAAI,IAAIE,EAAK,eAAe,EAAE,SAAS,CAAC,CAErD,CACF,EAEA,QAAWzD,KAAOqB,EAAK,QAAQ,UAAU,MAAQ,CAAC,EAChDmC,EAAIxD,EAAI,YAAY,EAGtB,QAAWoD,KAASC,GAAchC,CAAI,EAAG,CACvCmC,EAAIJ,EAAM,YAAY,EACtBI,EAAIJ,EAAM,eAAe,EACzB,QAAWO,KAAWP,EAAM,YAAY,UAAY,CAAC,EAC/CO,EAAQ,cAAc,WAAW,QAAQ,GAC3CH,EAAIG,EAAQ,GAAG,CAGrB,CAEA,QAAWC,KAAWvC,EAAK,MAAM,QAAQ,gBAAkB,CAAC,EAAG,CAC7D,IAAMzB,EAAQgE,EAAQ,OAAO,cACzBA,EAAQ,KAAK,SAAS,KAAK,GAAKhE,GAAO,WAAW,MAAM,IAC1D4D,EAAI5D,CAAK,CAEb,CAEA,QAAWI,KAAO6D,GAAyBX,GAAe7B,CAAI,CAAC,EAC7DmC,EAAIxD,CAAG,EAGT,MAAO,CAAC,GAAGuD,CAAI,CACjB,CAEA,SAAS1B,GAASH,EAA4B,CAC5C,MAAO,CACL,SAAUA,EAAO,aAAe,EAChC,QAASA,EAAO,eAAiB,EACjC,MAAOA,EAAO,gBAAkB,CAClC,CACF,CAEA,SAASU,GAAWb,EAAgBD,EAAoB,CACtD,OAAOZ,EAAuB,iBAAiBa,CAAM,WAAWD,CAAE,EAAE,CACtE,CAEA,SAASM,GAAiB6B,EAAkC,CAC1D,GAAI,CAACA,EACH,OAEF,IAAMK,EAAK,KAAK,MAAML,CAAG,EACzB,OAAO,OAAO,MAAMK,CAAE,EAAI,OAAY,IAAI,KAAKA,CAAE,EAAE,YAAY,CACjE,CAGO,SAASD,GAAyBZ,EAAwB,CAC/D,IAAMc,EAAad,EAAK,QAAQ,OAAQ,EAAE,EACpCM,EAAiB,CAAC,EAClBS,EAAU,sEAChB,QAAWC,KAASF,EAAW,SAASC,CAAO,EAAG,CAChD,IAAIhE,EAAMiE,EAAM,CAAC,EAAE,QAAQ,eAAgB,EAAE,EACxCjE,EAAI,WAAW,MAAM,IACxBA,EAAM,WAAWA,CAAG,IAEtBuD,EAAK,KAAKvD,CAAG,CACf,CACA,OAAOuD,CACT,CAEA,SAASG,GAAU1D,EAAmB,CACpC,OAAOA,EAAI,WAAa,SAAWA,EAAI,WAAa,QACtD,CC7ZA,IAAMkE,GAAqB,yBAGdC,EAAN,KAAwB,CACZ,KACA,UACA,IACA,SAAW,IAAI,IAEhC,YAAYC,EAAeC,EAA0BC,EAAmB,CACtE,KAAK,KAAOF,EACZ,KAAK,UAAYC,EACjB,KAAK,IAAMC,CACb,CAEA,MAAM,WAAWC,EAAkC,CACjD,OAAO,QAAQ,IAAIA,EAAM,IAAKC,GAAS,KAAK,OAAOA,CAAI,CAAC,CAAC,CAC3D,CAEA,MAAM,OAAOA,EAAcC,EAA6C,CACtE,IAAMC,EAAMC,EAAkBH,CAAI,EAC5BI,EAAS,KAAK,UAAU,UAAUF,CAAG,EAC3C,GAAIE,EACF,OAAOA,EAGT,IAAMC,EAAU,KAAK,SAAS,IAAIH,CAAG,EACrC,GAAIG,EACF,OAAIJ,GACF,KAAK,IAAI,KACP,CAAE,KAAMC,CAAI,EACZ,qEACF,EACOI,EAASN,CAAI,IAEtB,KAAK,IAAI,MAAM,CAAE,KAAME,CAAI,EAAG,uCAAuC,EAC9DG,GAGT,IAAME,EAAO,KAAK,UAAUP,EAAMC,CAAM,EACxC,KAAK,SAAS,IAAIC,EAAKK,CAAI,EAC3B,GAAI,CACF,OAAO,MAAMA,CACf,QAAE,CACA,KAAK,SAAS,OAAOL,CAAG,CAC1B,CACF,CAEA,aAAaM,EAAYR,EAAcS,EAAmC,CACxE,OAAO,KAAK,OAAOT,EAAM,CAAE,KAAAQ,EAAM,WAAAC,CAAW,CAAC,CAC/C,CAEA,MAAc,UAAUT,EAAcC,EAA6C,CACjF,IAAMC,EAAMC,EAAkBH,CAAI,EAC5BU,EAAYT,EACd,IAAM,KAAK,YAAYA,EAAO,KAAMD,EAAMC,EAAO,UAAU,EAC3D,IAAM,KAAK,KAAK,IAAKO,GAAS,KAAK,YAAYA,EAAMR,CAAI,CAAC,EAE1DW,EACJ,GAAI,CACFA,EAAO,MAAMC,EAAYF,EAAU,EAAG,IAA8B,eAAeR,CAAG,EAAE,CAC1F,OAASW,EAAc,CACrB,OAAO,KAAK,SAASb,EAAMa,CAAG,CAChC,CAEA,IAAMC,EAAY,MAAM,KAAK,UAAU,SAASH,EAAM,IAAI,GAAK,EAC/D,YAAK,UAAU,SAASG,CAAS,EAC1BA,CACT,CAEQ,SAASd,EAAca,EAAoB,CACjDE,EAAiB,KAAK,IAAK,CACzB,OAAQ,mBACR,SAAU,+CACV,KAAAf,EACA,IAAAa,CACF,CAAC,EACD,IAAMG,EAAOV,EAASN,CAAI,EAC1B,YAAK,UAAU,SAASgB,CAAI,EACrBA,CACT,CAEA,MAAc,YAAYR,EAAYR,EAAcS,EAAoC,CACtF,IAAMQ,EAAcR,EAAaS,EAAuBT,CAAU,EAAI,OAChEU,EAAUC,EAAiBpB,CAAI,EAErC,GAAI,CACF,GAAImB,EAAS,CACX,IAAME,EAAO,MAAMC,GAAoBd,EAAMR,EAAM,KAAK,GAAG,EAC3D,GAAIqB,EAAM,CACR,IAAMV,EAAOY,GAAyBF,EAAMF,CAAO,EACnD,GAAIR,EACF,YAAK,IAAI,MAAM,CAAE,KAAAX,EAAM,OAAQ,aAAc,EAAG,sBAAsB,EAC/DW,CAEX,CACF,CAEA,YAAK,IAAI,KAAK,CAAE,KAAAX,CAAK,EAAG,8CAA8C,EAC/D,MAAMwB,GAA4BhB,EAAMR,EAAM,KAAK,GAAG,CAC/D,QAAE,CACIiB,GAAeC,EAAuBV,EAAK,IAAI,CAAC,IAAMS,IACxD,MAAMT,EAAK,KAAKS,EAAa,CAAE,UAAW,kBAAmB,CAAC,EAC9D,MAAMQ,EAAyBjB,EAAM,KAAK,IAAK,4BAA4B,EAE/E,CACF,CACF,EAEA,eAAegB,GACbhB,EACAR,EACAF,EACe,CACf,IAAM4B,EAAaR,EAAuBlB,CAAI,EAC1CkB,EAAuBV,EAAK,IAAI,CAAC,IAAMkB,IACzC,MAAMlB,EAAK,KAAKR,EAAM,CAAE,UAAW,kBAAmB,CAAC,EACvD,MAAMyB,EAAyBjB,EAAMV,CAAG,GAG1C,IAAM6B,EAAWC,GAAqBpB,CAAI,EAO1C,GANA,MAAMmB,EACH,MAAM,EACN,QAAQ,CAAE,MAAO,UAAW,QAAS,GAAO,CAAC,EAC7C,MAAM,IAAG,EAAY,EAEV,MAAMA,EAAS,MAAM,IACrB,EACZ,OAAA7B,EAAI,KAAK,CAAE,KAAM4B,CAAW,EAAG,kDAAkD,EAC1EpB,EAASoB,CAAU,EAG5B,IAAMG,EAAW,MAAMC,GAAsBH,EAAUD,CAAU,EACjE,QAASK,EAAI,EAAGA,GAAKF,EAAUE,IAC7B,MAAMC,GAAgBxB,EAAMmB,EAAS,IAAII,CAAC,EAAGjC,CAAG,EAGlD,IAAMmC,EAAiB,CAAC,EACxB,QAASF,EAAI,EAAGA,EAAIF,EAAUE,IAC5BE,EAAO,KAAK,MAAMC,GAAwBP,EAAS,IAAII,CAAC,EAAGjC,CAAG,CAAC,EAGjE,IAAMqC,EAAeP,GAAqBpB,CAAI,EAAE,IAAIqB,CAAQ,EACtDO,EAAmB,MAAMC,GAAqBF,CAAY,EAC1DG,EAAO,MAAMC,GAAyBJ,CAAY,EAExD,GAAIC,GAAoB,CAACE,EAAM,CAC7B,IAAMrC,EAASkC,EAAa,QAAQ,8BAA8B,EAC5DK,EAAa,MAAMC,EAAaxC,EAAO,KAAK,CAAC,EACnD,MAAO,CACL,KAAMyC,EAAoBN,EAAkBV,CAAU,EACtD,MAAO,MAAMiB,EAAUR,EAAcrC,CAAG,EACxC,GAAI,MAAM8C,EAAWT,CAAY,EACjC,GAAI,MAAMU,EAAcV,CAAY,EACpC,WAAYK,EAAa,CAAClC,EAASY,EAAuBsB,CAAU,CAAC,CAAC,EAAI,CAAC,CAC7E,CACF,CAEA,IAAMM,EAAW,MAAMC,GAA0BZ,EAAcG,CAAI,EACnE,MAAO,CACL,KAAMZ,EACN,MAAO,MAAMiB,EAAUR,EAAcrC,CAAG,EACxC,GAAI,MAAM8C,EAAWT,CAAY,EACjC,GAAI,MAAMU,EAAcV,CAAY,EACpC,GAAIG,EAAO,CAAE,KAAAA,CAAK,EAAI,CAAC,EACvB,GAAIQ,EAAS,OAAS,CAAE,SAAAA,CAAS,EAAI,CAAC,EACtC,GAAIb,EAAO,OAAS,CAAE,OAAAA,CAAO,EAAI,CAAC,CACpC,CACF,CAEA,eAAeH,GAAsBH,EAAmBD,EAAqC,CAC3F,IAAMsB,EAAW5B,EAAiBM,CAAU,EACtCuB,EAAQ,MAAMtB,EAAS,MAAM,EAEnC,QAASI,EAAI,EAAGA,EAAIkB,EAAOlB,IAAK,CAC9B,IAAM/B,EAAO,MAAMyC,EAAad,EAAS,IAAII,CAAC,CAAC,EAC/C,GAAI/B,GAAQoB,EAAiBpB,CAAI,IAAMgD,EACrC,OAAOjB,CAEX,CAEA,MAAO,EACT,CAEA,eAAeG,GAAwBgB,EAAkBpD,EAAkC,CACzF,IAAME,EAAO,MAAMyC,EAAaS,CAAO,EACvC,GAAI,CAAClD,EACH,MAAM,IAAI,MAAM,oCAAoC,EAGtD,IAAMsC,EAAO,MAAMC,GAAyBW,CAAO,EAC7CJ,EAAW,MAAMC,GAA0BG,EAASZ,CAAI,EAC9D,MAAO,CACL,KAAMpB,EAAuBlB,CAAI,EACjC,MAAO,MAAM2C,EAAUO,EAASpD,CAAG,EACnC,GAAI,MAAM8C,EAAWM,CAAO,EAC5B,GAAI,MAAML,EAAcK,CAAO,EAC/B,GAAIZ,EAAO,CAAE,KAAAA,CAAK,EAAI,CAAC,EACvB,GAAIQ,EAAS,OAAS,CAAE,SAAAA,CAAS,EAAI,CAAC,CACxC,CACF,CAEA,SAASK,GAAqB3C,EAAqB,CACjD,OAAOA,EAAK,WAAWd,GAAoB,CAAE,MAAO,EAAK,CAAC,CAC5D,CAEA,SAASkC,GAAqBpB,EAAqB,CACjD,OAAO2C,GAAqB3C,CAAI,EAAE,QAAQ,8BAA8B,CAC1E,CAEA,eAAewB,GAAgBxB,EAAY0C,EAAkBpD,EAAkC,CAC7F,IAAMsD,EAAWF,EAAQ,UAAU,SAAU,CAAE,KAAM,cAAe,CAAC,EACrE,KAAO,MAAME,EAAS,UAAU,EAAE,MAAM,IAAM,EAAK,GACjDtD,EAAI,KAAK,CAAE,OAAQ,kBAAmB,EAAG,aAAa,EACtD,MAAMsD,EAAS,MAAM,EACrB,MAAMC,EAAmB7C,EAAMV,EAAK,kBAAkB,EAGxD,IAAMwD,EAAYJ,EAAQ,UAAU,SAAU,CAAE,KAAM,oBAAqB,CAAC,EAC5E,KAAO,MAAMI,EAAU,UAAU,EAAE,MAAM,IAAM,EAAK,GAClDxD,EAAI,KAAK,CAAE,OAAQ,qBAAsB,EAAG,aAAa,EACzD,MAAMwD,EAAU,MAAM,EACtB,MAAMD,EAAmB7C,EAAMV,EAAK,qBAAqB,CAE7D,CAEA,eAAeuC,GAAqBa,EAA0C,CAC5E,IAAMK,EAASL,EAAQ,YAAY,eAAe,EAClD,GAAI,CAAE,MAAMK,EAAO,MAAM,EACvB,OAAO,KAGT,IAAMC,EAAcN,EAAQ,QAAQ,cAAc,EAAE,OAAO,CAAE,IAAKK,CAAO,CAAC,EAAE,MAAM,EAClF,GAAI,CAAE,MAAMC,EAAY,MAAM,EAC5B,OAAO,KAGT,IAAMxD,EAAO,MAAMwD,EAAY,aAAa,MAAM,EAClD,MAAI,CAACxD,GAAQA,EAAK,SAAS,UAAU,EAC5B,KAGMA,EAAK,QAAQ,MAAO,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC,GAAG,QAAQ,KAAM,EAAE,GACrD,IACnB,CAEA,eAAe+C,GAA0BG,EAAkBZ,EAAkC,CAC3F,IAAMmB,EAAiB,CAAC,EAClBC,EAAO,IAAI,IAEXC,EAAQC,GAAyC,CACrD,GAAI,CAACA,GAAOA,EAAI,WAAW,OAAO,EAChC,OAEF,IAAMC,EAAWC,GAAcF,CAAG,EAC9B,CAACC,GAAYH,EAAK,IAAIG,CAAQ,IAGlCH,EAAK,IAAIG,CAAQ,EACjBJ,EAAK,KAAKI,CAAQ,EACpB,EAEME,EAAOb,EAAQ,YAAY,cAAc,EAC/C,GAAI,MAAMa,EAAK,MAAM,EAAG,CACtB,IAAMC,EAAWD,EAAK,QAAQ,gBAAgB,EAC1C,MAAMC,EAAS,MAAM,GACvBL,EACE,MAAMK,EACH,MAAM,EACN,aAAa,OAAQ,CAAE,QAAS,GAAM,CAAC,EACvC,MAAM,IAAM,IAAI,CACrB,CAEJ,CAEA,IAAMC,EAASf,EAAQ,QAAQ,kDAAkD,EAC3EgB,EAAa,MAAMD,EAAO,MAAM,EACtC,QAASlC,EAAI,EAAGA,EAAImC,EAAYnC,IAC9B4B,EAAK,MAAMM,EAAO,IAAIlC,CAAC,EAAE,aAAa,KAAK,CAAC,EAG9C,GAAIO,EACF,QAAW6B,KAAOC,GAAwB9B,CAAI,EAC5CqB,EAAKQ,CAAG,EAIZ,OAAOV,CACT,CAEA,SAASK,GAAc9D,EAA6B,CAClD,GAAI,CACF,OAAIA,EAAK,WAAW,MAAM,EACjB,IAAI,IAAIA,CAAI,EAAE,SAAS,EAE5BA,EAAK,WAAW,GAAG,EACd,IAAI,IAAIA,EAAM,eAAe,EAAE,SAAS,EAE1C,IACT,MAAQ,CACN,OAAO,IACT,CACF,CAEA,SAASoE,GAAwBC,EAA4B,CAC3D,IAAMZ,EAAiB,CAAC,EAClBa,EAAU,kDAChB,QAAWC,KAASF,EAAS,SAASC,CAAO,EAAG,CAC9C,IAAMH,GAAOI,EAAM,CAAC,GAAKA,EAAM,CAAC,GAAG,QAAQ,cAAe,EAAE,EAC5Dd,EAAK,KAAKU,CAAG,CACf,CACA,OAAOV,CACT,CC/UO,IAAMe,EAAN,MAAMC,CAAQ,CACF,MAAgB,CAAC,EACjB,UAAoB,CAAC,EACrB,QAAuC,CAAC,EACxC,IAET,YAAYC,EAAmB,CACrC,KAAK,IAAMA,CACb,CAEA,aAAa,OAAOC,EAAyBC,EAAcF,EAAqC,CAC9F,IAAMG,EAAO,IAAIJ,EAAQC,CAAG,EACtBI,EAAW,KAAK,IAAI,EAAGF,CAAI,EACjC,QAASG,EAAI,EAAGA,EAAID,EAAUC,IAAK,CACjC,IAAMC,EAAO,MAAML,EAAQ,QAAQ,EACnCE,EAAK,MAAM,KAAKG,CAAI,EACpBH,EAAK,UAAU,KAAKG,CAAI,CAC1B,CACA,OAAAN,EAAI,KAAK,CAAE,aAAcI,CAAS,EAAG,uBAAuB,EACrDD,CACT,CAEA,MAAM,IAAOI,EAA4C,CACvD,IAAMD,EAAO,MAAM,KAAK,QAAQ,EAChC,GAAI,CACF,OAAO,MAAMC,EAAGD,CAAI,CACtB,QAAE,CACA,KAAK,QAAQA,CAAI,CACnB,CACF,CAEA,MAAM,OAAuB,CAC3B,MAAM,QAAQ,IAAI,KAAK,MAAM,IAAKA,GAASA,EAAK,MAAM,EAAE,MAAM,IAAG,EAAY,CAAC,CAAC,EAC/E,KAAK,MAAM,OAAS,EACpB,KAAK,UAAU,OAAS,EACxB,KAAK,IAAI,MAAM,wBAAwB,CACzC,CAEA,MAAc,SAAyB,CACrC,IAAMA,EAAO,KAAK,UAAU,IAAI,EAChC,OAAIA,GAGG,IAAI,QAASE,GAAY,CAC9B,KAAK,QAAQ,KAAKA,CAAO,CAC3B,CAAC,CACH,CAEQ,QAAQF,EAAkB,CAChC,IAAMG,EAAS,KAAK,QAAQ,MAAM,EAClC,GAAIA,EAAQ,CACVA,EAAOH,CAAI,EACX,MACF,CACA,KAAK,UAAU,KAAKA,CAAI,CAC1B,CACF,ECvDO,IAAMI,GAAsB,+BAQ5B,SAASC,EAAsBC,EAAqB,CACzD,OAAOA,EAAK,WAAWF,EAAmB,EAAE,QAAQ,8BAA8B,CACpF,CAEA,eAAsBG,GAAUC,EAAoC,CAClE,OACG,MAAMA,EAAQ,QAAQ,wDAAwD,EAAE,MAAM,EAAK,CAEhG,CAQA,SAASC,GAAiBC,EAAYC,EAA+B,CACnE,OAAIA,IAAS,OACJD,EAAK,WAAWE,EAAmB,EAErCF,EAAK,QAAQ,+BAA+B,CACrD,CAEA,eAAeG,GAAgBH,EAAYC,EAAqC,CAC9E,IAAMG,EAAM,MAAML,GAAiBC,EAAMC,CAAI,EAC1C,YAAY,EACZ,MAAM,IAAM,IAAI,EACdG,GAGL,MAAMJ,EAAK,MAAM,KAAKI,EAAI,EAAIA,EAAI,MAAQ,EAAGA,EAAI,EAAI,KAAK,IAAIA,EAAI,OAAS,IAAM,GAAG,CAAC,CACvF,CAEA,eAAeC,GACbL,EACAM,EACAL,EACkB,CAClB,aAAME,GAAgBH,EAAMC,CAAI,EAChC,MAAMD,EAAK,MAAM,MAAM,EAAGM,CAAM,EAEzBN,EAAK,SACV;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,SAiCKM,CAAM,KAAK,KAAK,UAAUL,CAAI,CAAC,KAAK,KAAK,UAAUC,EAAmB,CAAC,GAC9E,CACF,CAGA,eAAsBK,GACpBP,EACAQ,EACAC,EACAR,EACkB,CAClB,IAAMS,EAAQ,MAAML,GAAiBL,EAAM,KAAOC,CAAI,EAQtD,OAPA,MAAMU,EAAW,EACjB,MAAMC,EAAmBZ,EAAMQ,EAAK,iBAAiB,GAGnDP,IAAS,OACL,MAAMY,EAAsBb,CAAI,EAAE,MAAM,EACxC,MAAMA,EAAK,QAAQ,8BAA8B,EAAE,MAAM,GAC9CS,EACR,GAGFC,CACT,CAGA,eAAsBI,GACpBd,EACAe,EACAP,EACAP,EACe,CACf,MAAMc,EAAQ,uBAAuB,EAAE,MAAM,IAAG,EAAY,EAC5D,MAAMA,EAAQ,SAAUC,GAAO,CAC7BA,EAAG,eAAe,CAAE,MAAO,MAAO,OAAQ,SAAU,CAAC,CACvD,CAAC,EACD,MAAML,EAAW,EAEjB,IAAMP,EAAM,MAAMW,EAAQ,YAAY,EAAE,MAAM,IAAM,IAAI,EAClDT,EAASF,EAAM,KAAK,KAAKA,EAAI,MAAM,EAAI,IAAM,KACnD,MAAMC,GAAiBL,EAAMM,EAAQL,CAAI,EACzC,MAAMU,EAAW,EACjB,MAAMC,EAAmBZ,EAAMQ,EAAK,kBAAkB,CACxD,CC3GA,IAAMS,GAAgB,YAChBC,GAAc,UACdC,EAAc,SAgBpB,eAAsBC,GAAgBC,EAAYC,EAA2C,CAC3F,IAAMC,EAAMC,EAAmB,EACzB,CAAE,SAAAC,EAAU,gBAAAC,CAAgB,EAAIC,GAAoBL,EAAQ,OAAQA,EAAQ,aAAa,EACzFM,EAAgBC,GAAqBP,EAAQ,aAAa,EAC1DQ,EAAY,IAAIC,EAAcR,CAAG,EACjCS,EAAU,MAAMC,EAAQ,OAC5BZ,EAAK,QAAQ,EACbC,EAAQ,OAAO,cAAgBY,EAC/BX,CACF,EACMY,EAAgB,IAAIC,EAAkBJ,EAASF,EAAWP,CAAG,EAEnEA,EAAI,KACF,CACE,kBAAmBD,EAAQ,OAAO,kBAClC,gBAAAI,EACA,YAAa,EAAQJ,EAAQ,cAC7B,aAAcA,EAAQ,OAAO,cAAgBY,CAC/C,EACA,iBACF,EAEA,GAAI,CAQF,IAAMG,EAAY,MAAMC,GAAsBjB,EAPR,CACpC,SAAAI,EACA,UAAWG,EACX,UAAAE,EACA,cAAAK,CACF,EAEkEZ,CAAG,EAE/DgB,EAAiB,IAAI,IAC3B,QAAWC,KAAQH,EACjBE,EAAe,IAAIE,EAAkBC,EAAkBF,EAAK,IAAI,CAAC,CAAC,EAClEV,EAAU,gBAAgBU,EAAMD,CAAc,EAEhDhB,EAAI,KAAK,CAAE,MAAOc,EAAU,OAAQ,OAAQE,EAAe,IAAK,EAAG,yBAAyB,EAE5F,IAAMI,EAAoB,MAAMC,GAC9BvB,EACA,CACE,SAAAI,EACA,UAAWG,EACX,UAAWW,EACX,UAAAT,EACA,cAAAK,CACF,EACAZ,CACF,EAEMsB,EAAoC,CAAC,EAC3C,QAAWC,KAAUxB,EAAQ,OAAO,UAClCC,EAAI,KAAK,CAAE,OAAAuB,CAAO,EAAG,4BAA4B,EACjDD,EAAUC,CAAM,EAAI,MAAMC,GACxB1B,EACAyB,EACA,CACE,SAAArB,EACA,UAAWG,EACX,UAAAE,EACA,cAAAK,CACF,EACAZ,CACF,EAGF,OAAOyB,GACL,IAAI,KAAK,EAAE,YAAY,EACvBtB,EACAW,EACAM,EACAE,CACF,CACF,QAAE,CACA,MAAMb,EAAQ,MAAM,CACtB,CACF,CAEA,eAAeM,GACbjB,EACA4B,EACA1B,EACiB,CACjB,OAAAA,EAAI,KAAK,CAAE,IAAKN,GAAe,KAAME,CAAY,EAAG,oBAAoB,EACxE,MAAME,EAAK,KAAK,qBAAsB,CAAE,UAAW,kBAAmB,CAAC,EACvE,MAAM6B,EAAiB7B,EAAME,EAAK,MAAM,EACxC,MAAM4B,GAA2B9B,EAAME,CAAG,EAC1C,MAAM6B,GAA0B/B,EAAME,CAAG,EACzC,MAAMF,EAAK,SAAS,MAAM,QAAQ,EAClC,MAAMgC,EAAmBhC,EAAME,EAAK,2BAA2B,EAC/D,MAAM+B,EAAsBjC,CAAI,EAC7B,MAAM,EACN,QAAQ,CAAE,MAAO,UAAW,QAAS,GAAO,CAAC,EAC7C,MAAM,IAAG,EAAY,EACjBkC,GAAsBlC,EAAM4B,EAAK1B,EAAK,YAAa,MAAM,CAClE,CAEA,eAAeqB,GACbvB,EACA4B,EACA1B,EACiB,CACjB,OAAAA,EAAI,KAAK,CAAE,IAAKL,EAAY,EAAG,8BAA8B,EAC7D,MAAMsC,GAAcnC,EAAMH,GAAaK,CAAG,EACnCgC,GAAsBlC,EAAM4B,EAAK1B,EAAK,oBAAqB,MAAM,CAC1E,CAEA,eAAewB,GACb1B,EACAyB,EACAG,EACA1B,EACiB,CACjB,IAAMkC,EAAaX,EAAO,QAAQ,KAAM,EAAE,EAC1C,OAAAvB,EAAI,KAAK,CAAE,OAAQkC,CAAW,EAAG,uBAAuB,EACxD,MAAMpC,EAAK,KAAK,iBAAiBoC,CAAU,GAAI,CAAE,UAAW,kBAAmB,CAAC,EAChF,MAAMP,EAAiB7B,EAAME,EAAK,WAAWkC,CAAU,EAAE,EAClDF,GAAsBlC,EAAM4B,EAAK1B,EAAK,aAAakC,CAAU,GAAI,SAAS,CACnF,CAEA,eAAeD,GACbnC,EACAqC,EACAnC,EACe,CACf,MAAMF,EAAK,KAAK,qBAAsB,CAAE,UAAW,kBAAmB,CAAC,EACvE,MAAM6B,EAAiB7B,EAAME,EAAK,MAAM,EAExC,IAAMoC,EAAMC,GAAYvC,EAAMqC,CAAK,EACnC,GAAI,CAAE,MAAMC,EAAI,UAAU,EAAE,MAAM,IAAM,EAAK,EAC3C,MAAAE,EAAiBtC,EAAK,CACpB,OAAQ,gBACR,SAAU,QAAQmC,CAAK,YACvB,QAAS,WACT,IAAK,IAAI,MAAM,kBAAkBA,CAAK,EAAE,CAC1C,CAAC,EACK,IAAI,MAAM,uBAAuBA,CAAK,EAAE,EAGhD,MAAMI,GAAYzC,EAAME,EAAKoC,EAAK,cAAcD,CAAK,EAAE,CACzD,CAEA,SAASE,GAAYvC,EAAYqC,EAAwB,CACvD,OAAOrC,EACJ,QAAQ,iCAAiC,EACzC,UAAU,MAAO,CAAE,KAAMqC,EAAO,MAAO,EAAK,CAAC,CAClD,CAEA,eAAeP,GACb9B,EACAE,EACe,CACf,IAAMoC,EAAMC,GAAYvC,EAAMJ,EAAa,EACtC,MAAM0C,EAAI,aAAa,eAAe,IAAO,SAGlDpC,EAAI,KAAK,CAAE,OAAQ,sBAAuB,EAAG,aAAa,EAC1D,MAAMoC,EAAI,MAAM,EAChB,MAAMN,EAAmBhC,EAAME,EAAK,sBAAsB,EAC5D,CAEA,SAASwC,EAAsB1C,EAAqB,CAClD,OAAOA,EAAK,UAAU,MAAM,EAAE,OAAO,CACnC,IAAKA,EAAK,UAAU,WAAY,CAAE,KAAMF,EAAa,MAAO,EAAK,CAAC,CACpE,CAAC,CACH,CAEA,eAAeiC,GACb/B,EACAE,EACe,CAGf,GAFA,MAAMyC,GAA4B3C,EAAME,CAAG,EAEvC,MAAM0C,GAAwB5C,EAAMF,CAAW,EAAG,CACpDI,EAAI,KAAK,CAAE,KAAMJ,CAAY,EAAG,iCAAiC,EACjE,MAAME,EAAK,SAAS,MAAM,QAAQ,EAClC,MACF,CAEA,IAAM6C,EAASH,EAAsB1C,CAAI,EAAE,UAAU,WAAY,CAC/D,KAAMF,EACN,MAAO,EACT,CAAC,EACD,GAAI,CAAE,MAAM+C,EAAO,UAAU,EAAE,MAAM,IAAM,EAAK,EAAI,CAClD3C,EAAI,KAAK,CAAE,KAAMJ,CAAY,EAAG,oDAAoD,EACpF,MAAME,EAAK,SAAS,MAAM,QAAQ,EAClC,MACF,CAEA,MAAMyC,GAAYzC,EAAME,EAAK2C,EAAQ,yBAAyB/C,CAAW,GAAI,CAAE,MAAO,EAAK,CAAC,CAC9F,CAEA,eAAe6C,GACb3C,EACAE,EACe,CACf,IAAMoC,EAAMC,GAAYvC,EAAMJ,EAAa,EAC3C,MAAM0C,EAAI,QAAQ,CAAE,MAAO,UAAW,QAAS,IAAO,CAAC,EAGnD,OADaI,EAAsB1C,CAAI,EACxB,UAAU,EAAE,MAAM,IAAM,EAAK,IAIhDE,EAAI,KAAK,CAAE,OAAQ,gCAAiC,EAAG,aAAa,EACpE,MAAMoC,EAAI,MAAM,EAChB,MAAMN,EAAmBhC,EAAME,EAAK,gCAAgC,EAEpE,MAAMwC,EAAsB1C,CAAI,EAC7B,QAAQ,CAAE,MAAO,UAAW,QAAS,GAAO,CAAC,EAC7C,MAAM,IAAG,EAAY,EAC1B,CAEA,eAAe4C,GAAwB5C,EAAY8C,EAAgC,CACjF,IAAMC,EAAOL,EAAsB1C,CAAI,EAAE,UAAU,WAAY,CAAE,KAAM8C,EAAM,MAAO,EAAK,CAAC,EAC1F,OAAM,MAAMC,EAAK,MAAM,EAGf,MAAMA,EAAK,QAAQ,cAAc,EAAE,IAAI,CAAC,EAAE,QAAQ,KAAK,EAAE,MAAM,EAAK,EAFnE,EAGX,CAIA,eAAeC,GACbhD,EACA4B,EACAqB,EACAC,EACAC,EACAC,EACAC,EACAnD,EACsB,CACtB,GAAI,MAAMoD,GAAUL,CAAO,EACzB,MAAO,WAGT,IAAMM,EAAO,MAAMC,EAAaP,CAAO,EACvC,GAAI,CAACM,EACH,MAAO,WAGT,IAAME,EAAUrC,EAAkBmC,CAAI,EACtC,GAAIL,EAAW,IAAIO,CAAO,EACxB,MAAO,WAIT,GAFAP,EAAW,IAAIO,CAAO,EAElB7B,EAAI,WAAW,IAAI6B,CAAO,EAC5B,OAAAvD,EAAI,MAAM,CAAE,KAAMuD,EAAS,KAAAL,CAAK,EAAG,gDAAgD,EACnF,MAAMM,GAAkB1D,EAAMiD,EAAS/C,EAAKmD,CAAU,EAC/C,WAGT,IAAMM,GAAa,MAAMC,EAAcX,CAAO,GAAG,UAC3CY,EAAczC,EAAkBC,EAAkBoC,CAAO,CAAC,EAChE,OAAI7B,EAAI,UAAU,IAAIiC,CAAW,GAAKC,GAA2BH,EAAW/B,EAAI,QAAQ,EAC/E,QAGT1B,EAAI,MAAM,CAAE,KAAMuD,EAAS,KAAAL,CAAK,EAAG,sBAAsB,EACzDD,EAAM,KAAK,MAAMvB,EAAI,cAAc,OAAO2B,CAAI,CAAC,EAC/C,MAAMG,GAAkB1D,EAAMiD,EAAS/C,EAAKmD,CAAU,EAC/C,WACT,CAEA,eAAeU,GACb/D,EACAgE,EACAX,EACAnD,EAC8B,CAC9B,IAAM+D,EAAe,MAAMC,GAAalE,EAAMgE,CAAI,EAAE,MAAM,EAE1D,OADiB,MAAMG,GAAmBnE,EAAME,EAAK+D,EAAcZ,CAAU,EAC3D,QAAU,SAC9B,CAEA,eAAee,GACbpE,EACA4B,EACAoC,EACAd,EACAC,EACAC,EACAC,EACAnD,EACsB,CACtB,IAAMmE,EAAWH,GAAalE,EAAMgE,CAAI,EAClCM,EAAQ,MAAMD,EAAS,MAAM,EAEnC,QAASE,EAAI,EAAGA,EAAID,EAAOC,IAAK,CAC9B,IAAMC,EAAO,MAAMxB,GACjBhD,EACA4B,EACAyC,EAAS,IAAIE,CAAC,EACdrB,EACAC,EACAC,EACAC,EACAnD,CACF,EACA,GAAIsE,IAAS,WACX,OAAOA,CAEX,CAEA,MAAO,UACT,CAEA,eAAetC,GACblC,EACA4B,EACA1B,EACAkD,EACAY,EACiB,CACjB,IAAMb,EAAgB,CAAC,EACjBD,EAAa,IAAI,IACnBuB,EAAe,EACbpB,EAA6BW,EAEnC,QAASU,EAAW,EAAGA,EAAW,IAAKA,IAAY,CACjD,IAAMF,EAAO,MAAMJ,GACjBpE,EACA4B,EACAoC,EACAd,EACAC,EACAC,EACAC,EACAnD,CACF,EAEA,GAAIsE,IAAS,OACX,OAAAtE,EAAI,KACF,CAAE,KAAAkD,EAAM,cAAeD,EAAM,OAAQ,OAAQ,gBAAiB,EAC9D,qBACF,EACOA,EAGT,GAAIqB,IAAS,WAAY,CACvBC,EAAe,EACf,QACF,CAGA,GADqB,MAAMV,GAA2B/D,EAAMgE,EAAMX,EAAYnD,CAAG,IAC5D,WAEnB,GADAuE,IACIA,GAAgB,EAAG,CACrBvE,EAAI,KACF,CAAE,KAAAkD,EAAM,cAAeD,EAAM,OAAQ,OAAQ,gBAAiB,EAC9D,qBACF,EACA,KACF,OAEAsB,EAAe,CAEnB,CAEA,OAAAvE,EAAI,KAAK,CAAE,KAAAkD,EAAM,cAAeD,EAAM,OAAQ,OAAQ,iBAAkB,EAAG,qBAAqB,EACzFA,CACT,CAEA,SAASe,GAAalE,EAAYgE,EAAyB,CACzD,OAAIA,IAAS,OACJ/B,EAAsBjC,CAAI,EAE5BA,EAAK,QAAQ,8BAA8B,CACpD,CAEA,SAAS8D,GAA2BH,EAA+BvD,EAA2B,CAC5F,IAAMuE,EAAKhB,EAAY,KAAK,MAAMA,CAAS,EAAI,OAAO,IACtD,MAAO,CAAC,OAAO,MAAMgB,CAAE,GAAKA,EAAKvE,CACnC,CAEA,SAASI,GAAqBoE,EAAqC,CACjE,OAAKA,EAGEC,GAAkBD,CAAK,EAFrB,IAAI,GAGf,CC1aA,OAAS,SAAAE,OAAa,mBACtB,OAIE,YAAAC,OAEK,aCJP,eAAsBC,EAAgBC,EAA2C,CAE/E,IAAMC,GADU,MAAMD,EAAQ,QAAQ,GACb,OAAQE,GAAW,uBAAuB,KAAKA,EAAO,MAAM,CAAC,EAEhFC,EAAYF,EAAS,KAAMG,GAAMA,EAAE,OAAS,cAAgBA,EAAE,MAAM,OAAS,CAAC,EAC9EC,EAAMJ,EAAS,KAAMG,GAAMA,EAAE,OAAS,OAASA,EAAE,MAAM,OAAS,CAAC,EAEvE,MAAO,GAAQD,GAAaE,EAC9B,CAEO,IAAMC,EAAc,6BAIpB,SAASC,EACdC,EACAC,EACQ,CACR,IAAMC,EAAO,CACX,mEACA,8DACF,EAEA,OAAIF,IAAW,iBACN,CACL,GAAGE,EACH,eAAeD,CAAa,gCAC5B,gGACF,EAAE,KAAK,GAAG,EAGL,CACL,GAAGC,EACH,qFACF,EAAE,KAAK,GAAG,CACZ,CCtCA,OAA8B,YAAAC,OAAgB,aCIvC,IAAMC,GAAsB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAa5B,SAASC,GAAyBC,EAA6C,CACpF,MAAO,CACL,SAAAA,EACA,QAAS,SACT,OAAQ,QACR,kBAAmB,CAAC,qBAAqB,EACzC,gBAAiB,GACjB,eAAgB,QAChB,gBAAiB,EACnB,CACF,CAMO,SAASC,IAAgD,CAE9D,IAAMC,EAAcH,GAAyB,EAAQ,EACrD,MAAO,CACL,GAAGG,EACH,kBAAmB,CACjB,0BACA,GAAIA,EAAY,iBAClB,CACF,CACF,CAEA,eAAsBC,GAAsBC,EAAwC,CAClF,MAAMA,EAAQ,cAAcN,EAAmB,CACjD,CDtCA,eAAsBO,GACpBC,EACAC,EACAC,EACAC,EACe,CACf,IAAMC,EAAWC,EAA0BH,EAAQC,CAAa,EAC1DG,EAASJ,IAAW,iBAAmB,cAAgB,8BAE7DD,EAAI,KACF,CACE,YAAAD,EACA,OAAAE,EACA,cAAAC,EACA,SAAAC,EACA,SAAUG,CACZ,EACA,uFACAD,EACAH,EACAI,CACF,EAEA,IAAIC,EACJ,GAAI,CACFA,EAAU,MAAMC,GAAS,wBAAwBT,EAAaU,GAAoB,CAAC,CACrF,OAASC,EAAO,CACd,IAAMC,EAAUD,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,EACrE,GAAI,qDAAqD,KAAKC,CAAO,EAAG,CACtE,GAAIA,EAAQ,SAAS,yBAAyB,EAAG,CAC/CX,EAAI,KAAK,CAAE,YAAAD,CAAY,EAAG,6CAA6C,EACvE,MACF,CAEA,MAAAC,EAAI,MACF,CAAE,YAAAD,EAAa,IAAKW,CAAM,EAC1B,iHACAX,EACAW,CACF,EACM,IAAI,MACR,qDAAgDX,CAAW,kDAC7D,CACF,CACA,MAAMW,CACR,CAEA,YAAMH,EAAQ,MAAM,EACd,IAAI,MACR,mFACF,CACF,CFvCA,IAAMK,GAAa,qBACbC,GAAgB,IAChBC,GAAqB,IAiBdC,EAAN,cAAgC,KAAM,CAC3C,YAAYC,EAAiB,CAC3B,MAAMA,CAAO,EACb,KAAK,KAAO,mBACd,CACF,EAEIC,EAAuC,KACvCC,EAAkC,KAChCC,GAAc,IAAI,QAExB,eAAsBC,GACpBC,EACAC,EAAoBC,EAAmB,EACd,CACzB,IAAMC,EAAcC,GAA0BJ,CAAM,EAC9CK,EAAaL,EAAO,oBAAoB,KAAK,GAAKG,EAExD,OAAIP,GAAiBC,IAAqBQ,GACxCJ,EAAI,MAAM,CAAE,WAAAI,CAAW,EAAG,oCAAoC,EAC9D,MAAMT,EAAc,KAAK,aAAa,EAAE,MAAM,IAAG,EAAY,EACtDA,IAGLA,IACFK,EAAI,KAAK,CAAE,WAAYJ,CAAiB,EAAG,6CAA6C,EACxF,MAAMS,EAAaV,EAAeK,CAAG,GAGvCL,EAAgBI,EAAO,oBAAoB,KAAK,EAC5C,MAAMO,GAAcP,EAAO,mBAAmB,KAAK,EAAGG,EAAaF,CAAG,EACtE,MAAMO,GAAsBL,EAAaF,EAAKD,EAAO,YAAaA,EAAO,QAAQ,EAErFH,EAAmBQ,EACZT,EACT,CAEA,eAAeW,GACbE,EACAN,EACAF,EACyB,CACzBA,EAAI,KAAK,CAAE,SAAAQ,CAAS,EAAG,8BAA8B,EAErD,IAAMC,EAAU,MAAMC,GAAS,eAAeF,CAAQ,EAChDG,EAAUF,EAAQ,SAAS,EAAE,CAAC,EACpC,GAAI,CAACE,EACH,MAAM,IAAI,MACR,yBAAyBH,CAAQ,oDACnC,EAGF,MAAMI,GAAsBD,CAAO,EACnCE,GAAiBF,CAAO,EAExB,IAAMG,EAAOH,EAAQ,MAAM,EAAE,CAAC,GAAM,MAAMA,EAAQ,QAAQ,EAC1D,OAAAI,GAAqBD,CAAI,EAEzB,MAAMA,EAAK,KAAM,MAAME,EAAgBL,CAAO,EAAKrB,GAAa2B,EAAa,CAC3E,UAAW,kBACb,CAAC,EACD,MAAMC,EAAiBJ,EAAMd,EAAK,YAAY,EAEvC,CAAE,QAAAW,EAAS,KAAAG,EAAM,YAAAZ,EAAa,YAAa,GAAM,WAAYO,CAAQ,CAC9E,CAKA,eAAeF,GACbL,EACAF,EACAmB,EACAC,EACyB,CACzB,MAAMC,GAAMnB,EAAa,CAAE,UAAW,EAAK,CAAC,EAE5C,IAAIoB,EAAU,MAAMC,GAAoBrB,EAAaF,EAAKoB,CAAQ,EASlE,GAPM,MAAMJ,EAAgBM,EAAQ,OAAO,IACzCtB,EAAI,KAAK,iEAAiE,EAC1E,MAAMK,EAAaiB,EAAStB,CAAG,EAC/B,MAAMwB,GAAqBtB,EAAaF,EAAK,QAASmB,CAAa,EACnEG,EAAU,MAAMC,GAAoBrB,EAAaF,EAAKoB,CAAQ,GAG5D,CAAE,MAAMJ,EAAgBM,EAAQ,OAAO,EACzC,MAAM,IAAI7B,EACR,4BAA4BS,CAAW,8BAA8BuB,EAA0B,QAASN,CAAa,CAAC,EACxH,EAGF,OAAOG,CACT,CAEA,eAAeC,GACbrB,EACAF,EACAoB,EACyB,CACzBpB,EAAI,KAAK,CAAE,YAAAE,CAAY,EAAG,uDAAuD,EAEjF,IAAMwB,EAAUC,GAAyBP,CAAQ,EAC7CT,EACJ,GAAI,CACFA,EAAU,MAAMD,GAAS,wBAAwBR,EAAawB,CAAO,CACvE,OAASE,EAAO,CACd,IAAMlC,EAAUkC,aAAiB,MAAQA,EAAM,QAAU,OAAOA,CAAK,EACrE,KAAI,qDAAqD,KAAKlC,CAAO,GACnEM,EAAI,MACF,CAAE,IAAK4B,EAAO,YAAA1B,EAAa,QAAAwB,CAAQ,EACnC,0CACAE,CACF,EACM,IAAI,MACR,+BAA+B1B,CAAW,qDAC5C,GAEI0B,CACR,CAEA,MAAMhB,GAAsBD,CAAO,EACnCE,GAAiBF,CAAO,EAExB,IAAMG,EAAOH,EAAQ,MAAM,EAAE,CAAC,GAAM,MAAMA,EAAQ,QAAQ,EAC1DI,GAAqBD,CAAI,EAEzB,MAAMA,EAAK,KAAKxB,GAAY,CAAE,UAAW,kBAAmB,CAAC,EAC7D,MAAM4B,EAAiBJ,EAAMd,EAAK,uBAAuB,EAEzD,IAAM6B,EAAU,MAAMb,EAAgBL,CAAO,EAC7C,OAAAX,EAAI,KAAK,CAAE,YAAAE,EAAa,QAAA2B,CAAQ,EAAG,6BAA6B,EAEzD,CAAE,QAAAlB,EAAS,KAAAG,EAAM,YAAAZ,EAAa,YAAa,EAAM,CAC1D,CAEA,SAASW,GAAiBF,EAA+B,CACvDA,EAAQ,GAAG,OAASG,GAAS,CAC3BC,GAAqBD,CAAI,CAC3B,CAAC,CACH,CAEA,SAASC,GAAqBD,EAAkB,CAC9C,GAAIjB,GAAY,IAAIiB,CAAI,EACtB,OAEFjB,GAAY,IAAIiB,CAAI,EAEpB,IAAMgB,EAAa7B,EAAmB,EAAE,MAAM,CAAE,OAAQ,SAAU,CAAC,EAE7D8B,EAAsF,CAC1F,CACE,MACE,wFACF,KAAM,0BACN,MAAO,OACT,EACA,CAAE,MAAO,qBAAsB,KAAM,2BAA4B,MAAO,OAAQ,EAChF,CACE,MAAO,oBACP,KAAM,2DACN,MAAO,OACT,EACA,CACE,MAAO,sEACP,SAAU,qBACV,KAAM,iDACN,MAAO,OACT,CACF,EACMC,EAA6E,CACjF,OAAQ,QACR,MAAO,QACP,MAAO,QACP,IAAK,QACL,OAAQ,QACR,SAAU,QACV,MAAO,QACP,QAAS,OACT,KAAM,OACN,MAAO,QACP,IAAK,QACL,QAAS,QACT,WAAY,QACZ,WAAY,QACZ,oBAAqB,QACrB,MAAO,QACP,KAAM,QACN,QAAS,QACT,MAAO,QACP,QAAS,OACX,EAEAlB,EAAK,GAAG,UAAYpB,GAA4B,CAC9C,IAAMuC,EAAOvC,EAAQ,KAAK,EACpBwC,EAAOxC,EAAQ,KAAK,EACpByC,EAAWzC,EAAQ,SAAS,EAC5B0C,EAAU,CAAE,KAAAH,EAAM,KAAAC,EAAM,SAAAC,CAAS,EAEvC,OAAW,CAAE,MAAAE,EAAO,SAAAC,EAAU,KAAAC,EAAM,MAAAC,CAAM,IAAKT,EAC7C,GAAIM,EAAM,KAAKH,CAAI,IAAMI,GAAU,KAAKH,EAAS,GAAG,GAAK,IAAO,CAC9DL,EAAWU,CAAK,EAAE,CAAE,GAAGJ,EAAS,KAAAG,CAAK,EAAG,sBAAuBL,CAAI,EACnE,MACF,CAGFJ,EAAWE,EAASC,CAAI,CAAC,EAAEG,EAAS,sBAAuBF,CAAI,CACjE,CAAC,EAEDpB,EAAK,GAAG,YAAcc,GAAU,CAC9BE,EAAW,MAAM,CAAE,IAAKF,EAAM,QAAS,MAAOA,EAAM,KAAM,EAAG,oBAAoB,CACnF,CAAC,EAEDd,EAAK,GAAG,WAAa2B,GAAa,CAChC,IAAMC,EAAMD,EAAS,IAAI,EACrBC,EAAI,SAAS,sBAAsB,GAAKD,EAAS,OAAO,GAAK,KAC/DX,EAAW,KACT,CACE,IAAAY,EACA,OAAQD,EAAS,OAAO,EACxB,KAAM,wEACR,EACA,gBACF,CAEJ,CAAC,CACH,CAMA,eAAsBE,GACpBrB,EACAI,EACyB,CACzB,IAAM1B,EAAM0B,EAAQ,KAAOzB,EAAmB,EACxC2C,EAAWC,GAAqBnB,EAAQ,WAAW,EAEzD,GAAI,MAAMoB,GAAqBxB,EAAQ,KAAMsB,CAAQ,EACnD,OAAA5C,EAAI,KAAK,CAAE,YAAa4C,CAAS,EAAG,wBAAwB,EACrDtB,EAGT,IAAMyB,EAAgB,MAAMC,GAAgB1B,EAAQ,IAAI,EAClD2B,EAAgB,CAACF,EAEvB,GAAIrB,EAAQ,4BAA6B,CACvC,IAAMhC,EAAUqD,EACZ,uBAAuBH,CAAQ,KAAKnB,EAA0B,QAASmB,CAAQ,CAAC,GAChF,8CAA8CA,CAAQ,KAAKnB,EAA0B,iBAAkBmB,CAAQ,CAAC,GAEpH,MAAAM,EAAiBlD,EAAK,CACpB,OAAQ,qBACR,SAAU,iBAAiB4C,CAAQ,GACnC,QAASG,EAAgB,6BAA+B,gBAAgBH,CAAQ,GAChF,IAAK,IAAInD,EAAkBC,CAAO,CACpC,CAAC,EACK,IAAID,EAAkBC,CAAO,CACrC,CAEA,GAAI,CAAC4B,EAAQ,YAAa,CACxB,IAAM6B,EAAeJ,EAAgB,QAAU,iBAC/C,OAAA/C,EAAI,KACF,CAAE,cAAe4C,EAAU,OAAQO,CAAa,EAChD,4CACF,EACOC,GAA4B9B,EAASI,EAASyB,CAAY,CACnE,CAEA,OAAIF,GACF,MAAMI,GAAyB/B,EAASsB,EAAU5C,CAAG,EAGhDsB,CACT,CAEA,eAAe8B,GACb9B,EACAI,EACA4B,EACyB,CACzB,IAAMV,EAAWC,GAAqBnB,EAAQ,WAAW,EAEzD,MAAMrB,EAAaiB,EAASI,EAAQ,GAAG,EACvC/B,EAAgB,KAChBC,EAAmB,KAEnB,MAAM4B,GAAqBF,EAAQ,YAAaI,EAAQ,IAAK4B,EAAQV,CAAQ,EAE7E,IAAMW,EAAQ,MAAMhD,GAClBe,EAAQ,YACRI,EAAQ,IACRA,EAAQ,YACRA,EAAQ,QACV,EACA,OAAA/B,EAAgB4D,EAChB3D,EAAmB0B,EAAQ,YAEpBqB,GAAmBY,EAAO7B,CAAO,CAC1C,CAGA,eAAe2B,GACb/B,EACAsB,EACA5C,EACe,CACf,GAAM,CAAE,KAAAc,CAAK,EAAIQ,EAEjBtB,EAAI,KACF,CACE,cAAe4C,EACf,SAAUnB,EAA0B,iBAAkBmB,CAAQ,CAChE,EACA,0FACF,EAEA,MAAM9B,EAAK,aAAa,EAExB,IAAM0C,EAAU,KAAK,IAAI,EACrBC,EAAUD,EAEd,OAAa,CACX,GAAI,MAAMV,GAAqBhC,EAAM8B,CAAQ,EAAG,CAC9C5C,EAAI,KAAK,CAAE,YAAa4C,CAAS,EAAG,sCAAsC,EAC1E,MAAM9B,EAAK,KAAKxB,GAAY,CAAE,UAAW,kBAAmB,CAAC,EAC7D,MAAM4B,EAAiBJ,EAAMd,EAAK,iBAAiB,EACnD,MACF,CAEA,IAAM0D,EAAM,KAAK,IAAI,EACjBA,EAAMD,GAAWjE,KACnBQ,EAAI,KACF,CACE,cAAe4C,EACf,SAAUc,EAAMF,EAChB,eAAgB,MAAMxC,EAAgBF,EAAK,QAAQ,CAAC,CACtD,EACA,mDACF,EACA2C,EAAUC,GAGZ,MAAM5C,EAAK,eAAevB,EAAa,CACzC,CACF,CAEO,SAASsD,GAAqBc,EAAwB,CAC3D,OAAOA,EAAO,QAAQ,KAAM,EAAE,EAAE,YAAY,CAC9C,CAEA,eAAsBb,GAAqBhC,EAAY8C,EAA2C,CAKhG,MAJI,CAAE,MAAM5C,EAAgBF,EAAK,QAAQ,CAAC,GAItC,MAAMkC,GAAgBlC,CAAI,EACrB,GAIL,SADYA,EAAK,UAAU,OAAQ,CAAE,KAAM,IAAI,OAAO,IAAI8C,CAAe,GAAI,GAAG,CAAE,CAAC,EAAE,MAAM,EAC7E,UAAU,EAAE,MAAM,IAAM,EAAK,GAQ7C,MAJsB9C,EAAK,UAAU,SAAU,CAC/C,KAAM,IAAI,OAAO,IAAI8C,CAAe,gBAAiB,GAAG,CAC1D,CAAC,EAGI,MAAM,EACN,UAAU,EACV,MAAM,IAAM,EAAK,IAMT,MADM9C,EAAK,YAAY,wBAAwB,EAC9B,aAAa,MAAM,EAAE,MAAM,IAAM,IAAI,IACzD,YAAY,EAAE,SAAS,IAAI8C,CAAe,EAAE,EAKxD,CAEA,eAAsBZ,GAAgBlC,EAA8B,CAClE,GAAI,MAAME,EAAgBF,EAAK,QAAQ,CAAC,EACtC,MAAO,GAGT,IAAM4B,EAAM5B,EAAK,IAAI,EAgBrB,MAfI,yBAAyB,KAAK4B,CAAG,GAMnC,MAFa5B,EAAK,UAAU,SAAU,CAAE,KAAM,qBAAsB,CAAC,EAGlE,MAAM,EACN,UAAU,EACV,MAAM,IAAM,EAAK,GAOpB,MAFiBA,EAAK,UAAU,OAAQ,CAAE,KAAM,qBAAsB,CAAC,EAGpE,MAAM,EACN,UAAU,EACV,MAAM,IAAM,EAAK,EAEb,EAIX,CAEA,eAAsBT,EACpBiB,EACAtB,EAAoBC,EAAmB,EACxB,CACXqB,EAAQ,aAAeA,EAAQ,YACjCtB,EAAI,KAAK,kDAAkD,EAC3D,MAAMsB,EAAQ,WAAW,MAAM,IAE/BtB,EAAI,KAAK,CAAE,YAAasB,EAAQ,WAAY,EAAG,6CAA6C,EAC5F,MAAMA,EAAQ,QAAQ,MAAM,GAG1B3B,IAAkB2B,IACpB3B,EAAgB,KAChBC,EAAmB,KAEvB,CI5dA,IAAMiE,GAAsB,gBAQrB,SAASC,GAASC,EAA4B,CACnD,IAAIC,EAAaH,GACbI,EAEJ,QAASC,EAAI,EAAGA,EAAIH,EAAK,OAAQG,IAAK,CACpC,IAAMC,EAAMJ,EAAKG,CAAC,EAClB,GAAIC,IAAQ,OAGZ,IAAIA,IAAQ,mCAAoC,CAC9CF,EAA8B,GAC9B,QACF,CACA,GAAIE,EAAI,WAAW,GAAG,EACpB,MAAM,IAAI,MAAM,mBAAmBA,CAAG,EAAE,EAE1CH,EAAaG,EACf,CAEA,MAAO,CACL,WAAAH,EACA,GAAIC,IAAgC,OAAY,CAAE,4BAAAA,CAA4B,EAAI,CAAC,CACrF,CACF,CAEO,SAASG,GACdC,EACAC,EACS,CACT,OAAOD,EAAI,6BAA+BC,GAAe,EAC3D,CCtCA,OAAS,UAAAC,GAAQ,SAAAC,GAAO,YAAAC,GAAU,UAAAC,GAAQ,UAAAC,GAAQ,aAAAC,OAAiB,mBACnE,OAAS,WAAAC,OAAe,YAKxB,eAAsBC,GAAUC,EAA6C,CAC3E,GAAI,CACF,IAAMC,EAAM,MAAMC,GAASF,EAAW,MAAM,EACtCG,EAASC,EAAUH,CAAG,EAC5B,OAAO,MAAMI,EAAsB,oBAAqBF,EAAQ,OAAO,CACzE,OAASG,EAAO,CACd,GAAIC,GAASD,CAAK,EAChB,OAAO,KAET,MAAMA,CACR,CACF,CAEA,eAAsBE,GAAUR,EAAmBS,EAAgC,CACjF,MAAMJ,EAAY,oBAAqBI,EAAO,OAAO,EACrD,MAAMC,GAAMC,GAAQX,CAAS,EAAG,CAAE,UAAW,EAAK,CAAC,EACnD,MAAMY,GAAoBZ,CAAS,EACnC,MAAMa,GAAUb,EAAWc,GAAcL,CAAK,EAAG,MAAM,CACzD,CAEA,eAAeG,GAAoBZ,EAAkC,CACnE,IAAMe,EAAa,GAAGf,CAAS,OAC/B,GAAI,CACF,MAAMgB,GAAOhB,CAAS,CACxB,OAASM,EAAO,CACd,GAAIC,GAASD,CAAK,EAChB,OAEF,MAAMA,CACR,CACA,GAAI,CACF,MAAMU,GAAOD,CAAU,EACvB,MAAME,GAAOF,CAAU,CACzB,OAAST,EAAO,CACd,GAAI,CAACC,GAASD,CAAK,EACjB,MAAMA,CAEV,CACA,MAAMY,GAAOlB,EAAWe,CAAU,CACpC,CAEA,SAASR,GAASD,EAAyB,CACzC,OACE,OAAOA,GAAU,UACjBA,IAAU,MACV,SAAUA,GACTA,EAAgC,OAAS,QAE9C,CxBnCA,eAAsBa,GAAUC,EAAiE,CAC/F,IAAMC,EAAQ,KAAK,IAAI,EACjBC,EAAsB,KAAO,KAAK,IAAI,EAAID,GAAS,IAEnDE,EAAMC,GAASJ,CAAI,EACnBK,EAAqBC,GAAQH,EAAI,UAAU,EACjD,MAAMI,GAAiBF,EAAoB,aAAa,EAExD,IAAMG,EAAS,MAAMC,GAAWJ,CAAkB,EAC5CK,EAAMC,EAAmB,EACzBC,EAA8BC,GAClCV,EACAK,EAAO,2BACT,EAEMM,EAAgB,MAAMC,GAAUP,EAAO,SAAS,EAClDQ,EAAiC,KACjCC,EAAkB,GAClBC,EAAuC,KAErCC,EAAgB,IAAY,CAC5BF,IAIJA,EAAkB,GAClB,QAAQ,SAAW,EACnBP,EAAI,KACF,CAAE,iBAAkBR,EAAoB,CAAE,EAC1C,yCACF,EAEKc,GACH,QAAQ,KAAK,CAAC,EAGhBE,EAAiBE,EAAaJ,EAASN,CAAG,EACvC,MAAOW,GAAiB,CACvBX,EAAI,MACF,CAAE,IAAAW,EAAK,iBAAkBnB,EAAoB,CAAE,EAC/C,4CACAmB,CACF,CACF,CAAC,EACA,QAAQ,IAAM,CACb,QAAQ,KAAK,CAAC,CAChB,CAAC,EACL,EAEA,QAAQ,KAAK,UAAWF,CAAa,EAErC,GAAI,CACFH,EAAU,MAAMM,GAAsBd,EAAQE,CAAG,EAEjDM,EAAU,MAAMO,GAAmBP,EAAS,CAC1C,YAAaR,EAAO,YACpB,SAAUA,EAAO,SACjB,4BAAAI,EACA,IAAAF,CACF,CAAC,EAED,IAAMc,EAAQ,MAAMC,GAAgBT,EAAQ,KAAM,CAAE,OAAAR,EAAQ,cAAAM,CAAc,CAAC,EAC3E,GAAIG,EACF,MAAM,IAAI,MAAM,oCAAoC,EAEtD,aAAMS,GAAUlB,EAAO,UAAWgB,CAAK,EAEvCd,EAAI,KACF,CACE,UAAWF,EAAO,UAClB,UAAWgB,EAAM,UAAU,OAC3B,kBAAmBA,EAAM,kBAAkB,OAC3C,iBAAkBtB,EAAoB,EACtC,UAAW,OAAO,YAChB,OAAO,QAAQsB,EAAM,SAAS,EAAE,IAAI,CAAC,CAACG,EAAQC,CAAK,IAAM,CAACD,EAAQC,EAAM,MAAM,CAAC,CACjF,CACF,EACA,8BACF,EAEO,CAAE,MAAAJ,EAAO,OAAAhB,CAAO,CACzB,QAAE,CACA,QAAQ,IAAI,UAAWW,CAAa,EAEhCD,EACF,MAAMA,EACGF,GACT,MAAMI,EAAaJ,EAASN,CAAG,CAEnC,CACF,CAEA,eAAeH,GAAiBsB,EAAcC,EAA8B,CAC1E,GAAI,CACF,MAAMC,GAAOF,CAAI,CACnB,MAAQ,CACN,MAAM,IAAI,MAAM,GAAGC,CAAK,eAAeD,CAAI,EAAE,CAC/C,CACF,CAEA,eAAeG,IAAsB,CACnC,MAAMjC,GAAU,QAAQ,IAAI,CAC9B,CAKA,IAAMkC,GAAY,QAAQ,KAAK,CAAC,EAC1BC,GACJD,KAAc,QACd3B,GAAQ2B,EAAS,IAAME,GAAc,YAAY,GAAG,GACL,GAC7CD,IACFF,GAAK,EAAE,MAAOI,GAAmB,CAC/BC,GAAO,MAAM,CAAE,IAAKD,CAAM,EAAG,oBAAqBA,CAAK,EACvD,QAAQ,KAAK,CAAC,CAChB,CAAC",
6
+ "names": ["config", "access", "resolve", "fileURLToPath", "readFile", "resolve", "DEFAULT_BROWSER_PROFILE_PATH", "resolveBrowserProfilePath", "config", "cwd", "relative", "readFile", "createRequire", "dirname", "join", "fileURLToPath", "Ajv", "require", "addFormats", "schemasDir", "ajvInstance", "getAjv", "ajv", "loadSchemaFile", "name", "path", "raw", "validatorCache", "getValidator", "schemaFile", "cached", "promise", "schema", "formatAjvErrors", "errors", "error", "assertValid", "data", "label", "validate", "stringifyJson", "value", "parseJson", "text", "DEFAULT_STATE_PATH", "DEFAULT_PARALLEL_TABS", "loadConfig", "configPath", "raw", "readFile", "parsed", "parseJson", "config", "assertValid", "DEFAULT_BROWSER_PROFILE_PATH", "pino", "readLogLevel", "logger", "createScrapeLogger", "logScrapeFailure", "log", "context", "error", "canonicalFeedHref", "href", "syntheticRepostHref", "handle", "statusHref", "canonicalStatusHref", "raw", "absolute", "match", "statusIdFromHref", "href", "readPostHref", "article", "links", "count", "fallback", "i", "canonical", "path", "normalizeStatusPageUrl", "url", "parsed", "readStats", "log", "readByTestId", "testId", "button", "text", "parseMetricCount", "readAuthor", "userBlock", "handle", "readTimestamp", "datetime", "base", "suffix", "dns", "isIP", "MAX_REDIRECTS", "FETCH_TIMEOUT_MS", "MAX_HTML_BYTES", "DESCRIPTION_META_KEYS", "resolveLink", "url", "options", "finalUrl", "resolveFinalUrl", "fetched", "fetchHtmlIfHtml", "title", "description", "extractPageMetadata", "resolveFinalUrl", "url", "options", "maxRedirects", "MAX_REDIRECTS", "current", "i", "assertSafeExternalHttpUrl", "head", "fetchWithTimeout", "signalInit", "headNext", "redirectTarget", "get", "getNext", "extractPageMetadata", "html", "title", "extractTitle", "description", "extractDescription", "metas", "parseMetaTags", "key", "DESCRIPTION_META_KEYS", "value", "name", "map", "tagPattern", "tag", "attrs", "parseAttributes", "content", "decodeHtmlEntities", "attrPattern", "match", "text", "isHtmlContentType", "contentType", "base", "fetchHtmlIfHtml", "signal", "response", "next", "readHtmlResponse", "reader", "chunks", "total", "done", "MAX_HTML_BYTES", "concatChunks", "concatChunks", "chunks", "length", "sum", "chunk", "out", "offset", "redirectTarget", "base", "response", "isRedirect", "location", "status", "assertSafeExternalHttpUrl", "url", "host", "normalizeUrlHostname", "isLocalHostname", "isUnsafeIpAddress", "isIP", "addresses", "dns", "address", "hostname", "withoutTrailingDot", "family", "isUnsafeIpv4Address", "isUnsafeIpv6Address", "parts", "part", "a", "b", "normalized", "mapped", "signalInit", "signal", "fetchWithTimeout", "init", "controller", "timeout", "FETCH_TIMEOUT_MS", "ScrapeTimeoutError", "label", "ms", "withTimeout", "promise", "timeoutId", "timeoutPromise", "_", "reject", "normalizePostHref", "href", "url", "PostProcessor", "log", "post", "into", "key", "ref", "item", "cycleGuard", "remember", "cached", "urlList", "extractUrlsFromMarkdown", "links", "references", "thread", "_refs", "_thread", "_links", "_linkUrls", "base", "finalized", "posts", "result", "urls", "results", "seen", "rawUrl", "normalizeLinkUrl", "isDirectMediaUrl", "link", "resolved", "withTimeout", "resolveLink", "resolvedUrl", "err", "fallback", "raw", "pathname", "markdown", "pattern", "match", "resolveScrapeCutoff", "config", "previousState", "nowMs", "cutoffMs", "buildAppState", "timestamp", "cutoffTimestamp", "following", "forYouSuggestions", "monitored", "posts", "post", "ingestPost", "list", "normalizePostHref", "canonicalFeedHref", "handle", "key", "ref", "item", "toPostRecord", "collectStateHrefs", "state", "hrefs", "add", "href", "record", "humanDelay", "jitter", "resolve", "waitForUiSettled", "page", "log", "label", "waitForDomIdle", "waitAfterDomAction", "waitForConversationReady", "timeline", "busy", "tracedClick", "page", "log", "target", "action", "options", "waitForUiSettled", "postStub", "href", "normalizePostHref", "readOwnTweetBodyMarkdown", "article", "tweetTexts", "count", "i", "tweetText", "el", "plain", "anchors", "out", "anchor", "plainTextToMarkdown", "markdown", "text", "href", "absolute", "toAbsoluteAnchorHref", "attachTweetDetailListener", "page", "focalTweetId", "captured", "settled", "waiters", "notify", "value", "resolve", "handler", "response", "url", "timeoutMs", "timer", "loadTweetDetailJson", "href", "log", "focalId", "statusIdFromHref", "listener", "target", "normalizeStatusPageUrl", "waitForConversationReady", "parsePostFromTweetDetail", "json", "parsed", "graph", "indexTweetResults", "focal", "buildPostFromNode", "visit", "item", "node", "id", "author", "child", "postBaseFields", "legacy", "timestamp", "parseTwitterDate", "mapStats", "buildBareRepostPost", "retweeted", "syntheticRepostHref", "buildQuoteReferences", "options", "quoted", "statusHref", "isBareRetweet", "body", "tweetBodyMarkdown", "linkUrls", "collectLinkUrls", "references", "thread", "buildThreadChain", "seen", "current", "parentId", "parent", "text", "tweetPlainText", "anchors", "media", "mediaEntities", "plainTextToMarkdown", "urls", "add", "raw", "isHttpUrl", "variant", "binding", "extractUrlsFromPlainText", "ms", "normalized", "pattern", "match", "CONVERSATION_LABEL", "PostDetailScraper", "pool", "processor", "log", "hrefs", "href", "nested", "key", "normalizePostHref", "cached", "pending", "postStub", "task", "page", "returnHref", "parseWork", "post", "withTimeout", "err", "finalized", "logScrapeFailure", "stub", "restoreHref", "normalizeStatusPageUrl", "focalId", "statusIdFromHref", "json", "loadTweetDetailJson", "parsePostFromTweetDetail", "parseCurrentConversationDom", "waitForConversationReady", "statusHref", "articles", "conversationArticles", "focalIdx", "findFocalArticleIndex", "i", "expandArticleUi", "thread", "parseArticleSnapshotDom", "focalArticle", "bareRepostHandle", "readBareRepostHandle", "body", "readOwnTweetBodyMarkdown", "nestedHref", "readPostHref", "syntheticRepostHref", "readStats", "readAuthor", "readTimestamp", "linkUrls", "collectArticleLinkUrlsDom", "targetId", "count", "article", "conversationTimeline", "showMore", "waitAfterDomAction", "showPosts", "social", "profileLink", "urls", "seen", "push", "raw", "absolute", "toAbsoluteUrl", "card", "cardLink", "photos", "photoCount", "url", "extractUrlsFromMarkdown", "markdown", "pattern", "match", "TabPool", "_TabPool", "log", "context", "size", "pool", "tabCount", "i", "page", "fn", "resolve", "waiter", "HOME_TIMELINE_LABEL", "timelineTweetArticles", "page", "isAdTweet", "article", "feedScrollRegion", "page", "kind", "HOME_TIMELINE_LABEL", "moveMouseToFeed", "box", "scrollFeedPixels", "deltaY", "scrollTimelineDown", "log", "knownArticleCount", "moved", "humanDelay", "waitAfterDomAction", "timelineTweetArticles", "scrollPastArticle", "article", "el", "FOLLOWING_TAB", "FOR_YOU_TAB", "RECENT_SORT", "scrapeTimelines", "page", "options", "log", "createScrapeLogger", "cutoffMs", "cutoffTimestamp", "resolveScrapeCutoff", "previousHrefs", "collectPreviousHrefs", "processor", "PostProcessor", "tabPool", "TabPool", "DEFAULT_PARALLEL_TABS", "detailScraper", "PostDetailScraper", "following", "scrapeFollowingRecent", "followingHrefs", "post", "normalizePostHref", "canonicalFeedHref", "forYouSuggestions", "scrapeForYouSuggestions", "monitored", "handle", "scrapeMonitoredProfile", "buildAppState", "ctx", "waitForUiSettled", "ensureFollowingTabSelected", "selectFollowingRecentSort", "waitAfterDomAction", "timelineTweetArticles", "scrollAndCollectPosts", "selectHomeTab", "normalized", "label", "tab", "homeFeedTab", "logScrapeFailure", "tracedClick", "followingSortDropdown", "ensureFollowingSortMenuOpen", "isFollowingSortSelected", "recent", "sort", "item", "stepTimelineArticle", "article", "seenInFeed", "posts", "feed", "scrollKind", "isAdTweet", "href", "readPostHref", "hrefKey", "scrollPastArticle", "timestamp", "readTimestamp", "feedHrefKey", "isOlderThanCutoffTimestamp", "scrollForMoreTimelinePosts", "kind", "articleCount", "feedArticles", "scrollTimelineDown", "advanceTimelineOnce", "articles", "count", "i", "step", "staleScrolls", "attempts", "ts", "state", "collectStateHrefs", "mkdir", "chromium", "hasXAuthCookies", "context", "xCookies", "cookie", "authToken", "c", "ct0", "X_LOGIN_URL", "manualLoginWindowGuidance", "reason", "expectedOwner", "base", "chromium", "STEALTH_INIT_SCRIPT", "persistentContextOptions", "headless", "loginContextOptions", "baseOptions", "applyStealthToContext", "context", "runManualLoginWindow", "profilePath", "log", "reason", "expectedOwner", "guidance", "manualLoginWindowGuidance", "action", "X_LOGIN_URL", "context", "chromium", "loginContextOptions", "error", "message", "X_HOME_URL", "OWNER_POLL_MS", "OWNER_LOG_EVERY_MS", "OwnerSessionError", "message", "activeSession", "activeProfileKey", "loggedPages", "acquireBrowserSession", "config", "log", "createScrapeLogger", "profilePath", "resolveBrowserProfilePath", "sessionKey", "closeBrowser", "attachOverCdp", "openPersistentSession", "endpoint", "browser", "chromium", "context", "applyStealthToContext", "wireContextPages", "page", "attachBrowserLogging", "hasXAuthCookies", "X_LOGIN_URL", "waitForUiSettled", "expectedOwner", "headless", "mkdir", "session", "launchScrapeContext", "runManualLoginWindow", "manualLoginWindowGuidance", "options", "persistentContextOptions", "error", "hasAuth", "browserLog", "messageRemap", "levelMap", "type", "text", "location", "payload", "match", "matchUrl", "note", "level", "response", "url", "ensureOwnerSession", "expected", "normalizeOwnerHandle", "isOwnerSessionActive", "loginRequired", "isLoginRequired", "ownerMismatch", "logScrapeFailure", "manualReason", "retryAfterManualLoginWindow", "waitForOwnerOnCdpSession", "reason", "fresh", "started", "lastLog", "now", "handle", "normalizedOwner", "DEFAULT_CONFIG_PATH", "parseCli", "argv", "configPath", "abortOnIncorrectOwnerHandle", "i", "arg", "resolveAbortOnIncorrectOwnerHandle", "cli", "configValue", "access", "mkdir", "readFile", "rename", "unlink", "writeFile", "dirname", "loadState", "statePath", "raw", "readFile", "parsed", "parseJson", "assertValid", "error", "isENOENT", "saveState", "state", "mkdir", "dirname", "backupExistingState", "writeFile", "stringifyJson", "backupPath", "access", "unlink", "rename", "runScrape", "argv", "start", "getElapsedInSeconds", "cli", "parseCli", "resolvedConfigPath", "resolve", "assertPathExists", "config", "loadConfig", "log", "createScrapeLogger", "abortOnIncorrectOwnerHandle", "resolveAbortOnIncorrectOwnerHandle", "previousState", "loadState", "session", "sigtermReceived", "sigtermCleanup", "handleSigterm", "closeBrowser", "err", "acquireBrowserSession", "ensureOwnerSession", "state", "scrapeTimelines", "saveState", "handle", "posts", "path", "label", "access", "main", "entryPath", "isMain", "fileURLToPath", "error", "logger"]
7
7
  }