"""Fetch a ChatGPT/Claude *share link* and parse its embedded conversation JSON. A pasted share URL should "just work": we GET the public share page, dig the conversation out of the JSON the page ships to the browser (no headless browser, no API key), and map it to the same `ParsedExport` the paste path returns (source="paste" → single-conversation flow). Where the JSON lives (web-verified shapes; both have several historical variants we tolerate): - ChatGPT: `` (Next.js) — or newer Remix/react-router builds embedding `window.__remixContext = {…}` / `__reactRouterContext`. We grep all of them for the message mapping (or `linear_conversation`) and reuse the parser's ChatGPT mapping walk where possible. - Claude: a JSON blob carrying `chat_messages: [{sender, text|content}]`, embedded in a ` _NEXT_DATA_RE = re.compile( r']*id=["\']__NEXT_DATA__["\'][^>]*>(.*?)', re.S | re.I) # window.__remixContext = {…}; / __reactRouterContext = {…}; _REMIX_RE = re.compile( r'(?:window\.)?(?:__remixContext|__reactRouterContext)\s*=\s*(\{.*?\})\s*;', re.S) # generic fallback: any _ANY_SCRIPT_RE = re.compile(r']*>(.*?)', re.S | re.I) def _json_blobs(html: str): """Yield candidate parsed-JSON objects embedded in the page, best matches first.""" seen = [] for rx in (_NEXT_DATA_RE, _REMIX_RE): for m in rx.finditer(html): obj = _try_json(m.group(1)) if obj is not None: seen.append(obj) # Last resort: scan every