#!/usr/bin/env python3 """ muse-auth.py: Muse.ai authentication state detection and flows. Handles the tricky parts of automation: - Sign IN (existing account) vs Sign UP (new account) have DIFFERENT verification flows - Meta account selection has UI quirks (+1 for extra accounts) - Login can get stuck in incomplete states that need detection and restart STATES (detectable via CDP): LANDING: Title == "muse.ai", not logged in ACCOUNT_SELECTION: Body contains "Your email matches multiple accounts" UI QUIRK: Meta shows "+1" for extra accounts. The "+1" option is NOT the right one — find the actual account name (e.g., "Nico Parada", not "auxfate"). The +1 is a collapsed list, not a selectable account. OTP_PROMPT: Body contains "To log in, enter the code" LOGGED_OUT: Body contains "Log in" button NOT_IN_CHAT: API messages times out (no chat UI to read) LOGGED_IN: Title contains "Chat —" or "Muse —", body has "Main chat"/"Side chats", message input present, API returns actual content. SIGN IN vs SIGN UP: Sign IN (existing account): 1. Enter email/phone 2. OTP prompt (code sent to email/phone) 3. Meta account selection (if multiple accounts match) 4. Chat UI Sign UP (new account): 1. Enter email/phone 2. OTP prompt (code sent to email/phone) 3. **DIFFERENT:** May require additional verification: - CAPTCHA - Phone verification (if email used) - Email verification (if phone used) - Terms acceptance - Profile setup (name, etc.) 4. **DIFFERENT:** May NOT have Meta account selection (new account = no existing Meta link) 5. Chat UI (possibly with onboarding tour) The sign-up flow is less predictable and may require human intervention for CAPTCHAs and other anti-bot measures. Sign-in is more automatable. RESTART LOGIC: If state != LOGGED_IN, restart from scratch: - Clear cookies/session - Navigate to muse.ai - Begin sign-in flow - Don't try to resume a stuck flow (OTP codes expire, sessions timeout) """ import json import urllib.request import websocket import time def get_state(cdp_url): """ Detect the current authentication state via CDP. Returns: (state, details) """ try: with urllib.request.urlopen(cdp_url, timeout=5) as r: ts = json.load(r) pages = [t for t in ts if t.get("type") == "page"] if not pages: return ("NO_PAGE", "No browser page found") p = pages[0] ws = websocket.create_connection(p["webSocketDebuggerUrl"], timeout=10) except Exception as e: return ("CDP_ERROR", str(e)) def ev(expr): try: ws.send(json.dumps({ "id": 1, "method": "Runtime.evaluate", "params": {"expression": expr, "returnByValue": True} })) r = json.loads(ws.recv()) return r["result"]["result"].get("value", "") except: return "" title = ev("document.title") or "" body = ev("document.body.innerText.slice(0,2000)") or "" ws.close() # Check states in order of specificity if "Your email matches multiple accounts" in body: return ("ACCOUNT_SELECTION", "Meta account selection - watch for +1 quirk") if "To log in, enter the code" in body: return ("OTP_PROMPT", "Waiting for OTP code") if "Main chat" in body and "Side chats" in body: return ("LOGGED_IN", "In chat UI") if title.strip() == "muse.ai": return ("LANDING", "On landing page, not logged in") if "Log in" in body: return ("LOGGED_OUT", "Logged out or not started") return ("UNKNOWN", f"Title: {title[:50]}") def is_logged_in(cdp_url): """Quick check: are we in the chat?""" state, _ = get_state(cdp_url) return state == "LOGGED_IN" if __name__ == "__main__": import sys cdp = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:9440/json/list" state, details = get_state(cdp) print(f"State: {state}") print(f"Details: {details}")