main
John Lauer v1.3.15 (insiders): Second route and wake for Grok's agent computer: grok_chat_send types into the grok-bridge chat through UIA when the webhook is refused, grok_wake keeps the paused agent computer running for N minutes, and Grok's rate-limit refusal reports its limit and count. b702936 5d ago
"""grok_describe — machine-readable verb catalog (hint / related / pitfalls)."""

from __future__ import annotations

PREFIX = "grok_"

# Keyed by the INTERNAL (unprefixed) command name.
VERB_CATALOG = {
    "readiness": {
        "summary": "Read-only: is Grok Bot.exe installed / running? Cursor is last-resort fallback.",
        "long": False,
        "hint": "Zero side effects, never downloads. Branch on hostAppInstalled / hostAppRunning / hostAppFallback / errorCode host_app_not_installed. When running, also computerState / computerReachable (false = Grok Bot shows Couldn't Reach Grok Bot's Computer).",
        "related": ["grok_prewarm", "grok_open", "grok_status"],
        "pitfalls": [
            "ready=false is NOT a user-download. Offer grok_open.",
            "hostAppFallback:Cursor means Grok Bot.exe is missing — do not kill Cursor.",
            "Do not treat a yellow /status chip as a crash — the bridge is up, Grok Bot isn't.",
        ],
    },
    "prewarm": {
        "summary": "No-op re-detect. {wait:true} just re-detects; nothing to fetch.",
        "long": False,
        "hint": "Grok Bot is a host app, not a runtime asset. Cheap enough to call anytime.",
        "related": ["grok_readiness", "grok_open"],
        "pitfalls": ["wait:true does not install Grok Bot or steal focus"],
    },
    "describe": {
        "summary": "This catalog — every grok_* verb with hint/related/pitfalls.",
        "long": False,
        "hint": "Completeness escape hatch. In practice read the per-call _hint.",
        "related": ["grok_readiness", "grok_status"],
        "pitfalls": ["the catalog is static; live state is grok_readiness / grok_status"],
    },
    "status": {
        "summary": "Detect + last grok_send + inbox path + webhook bound (host only, never the key).",
        "long": False,
        "hint": "Use this after a send to confirm the durable inbox got the line.",
        "related": ["grok_send", "grok_readiness", "grok_webhook_status"],
        "pitfalls": ["lastSend is in-process; a bridge restart clears it (inbox file survives)"],
    },
    "open": {
        "summary": "Launch Grok Bot in the BACKGROUND if installed. Polls process/window; Start-Process minimized fallback.",
        "long": True,
        "hint": "Returns launched, opened, alreadyOpen, hwnd. Polls process_list/window after launch/desktop_open; Start-Process -WindowStyle Minimized if those do not take. Never steals focus. Every launch carries --force-renderer-accessibility and the no-occlusion switches so a background window paints and is readable. {restart:true, reason} relaunches a Grok Bot that was started without them (launchedWithAccessibility:false, e.g. after its own auto-update).",
        "related": ["grok_list_windows", "grok_screenshot", "grok_close", "grok_readiness"],
        "pitfalls": [
            "success:true requires a live process/window after poll — not just callback ok.",
            "Background by default. foreground:true needs foregroundReason >= 10 chars.",
            "Host is Grok Bot.exe, not Cursor. Cursor is last-resort fallback only.",
            "cliRequired verbs (shell_execute, pull_file, send_files) are never used here.",
        ],
    },
    "screenshot": {
        "summary": "Screenshot a Grok Bot window via ab desktop_screenshot_window.",
        "long": False,
        "hint": "Captures the picked MAIN window by hwnd (largest visible Grok Bot window, never one under 400x300); explicit titleContains only when you pass it. main_window_hidden when the main window is hidden (a hidden window cannot be captured; showing it takes focus, so only grok_open {foreground:true, foregroundReason}). grok_chat_read still reads it hidden. Does not raise the window.",
        "related": ["grok_list_windows", "grok_open", "grok_record_start"],
        "pitfalls": [
            "Needs a live window — grok_open first if hostAppRunning is false.",
            "The PNG lands on the desktop; pull it with the CLI, not from this HTTP callback.",
        ],
    },
    "list_windows": {
        "summary": "List Grok Bot windows (desktop_find_window preferred, then list_windows + process_list).",
        "long": False,
        "hint": "titleContains exact Grok Bot (not bare Grok / Chrome ai-grok-bot). Read-only; does not focus or click.",
        "related": ["grok_screenshot", "grok_open", "grok_chat_status", "grok_close", "grok_resize"],
        "pitfalls": [
            "empty list usually means Grok Bot isn't running — grok_open, don't panic",
            "Do not treat Google Chrome / ai-grok-bot as the host.",
        ],
    },
    "chat_status": {
        "summary": "Read-only: is the focused Grok Bot thread thinking or idle? Never steals focus.",
        "long": False,
        "hint": "thinking=true when UIA exposes a name like \"<agent> is working\" (proven: \"New Bot is working\"). idle when not thinking. Carries computerState / computerReachable. errorCode no_transcript (window has no page yet) or computer_unreachable answer at once.",
        "related": ["grok_chat_read", "grok_list_windows", "grok_screenshot"],
        "pitfalls": [
            "focusedThread null + focusedThreadReason when the page has no conversation heading; never a caption-button name.",
            "Needs a live Grok Bot window — grok_open first.",
            "Never steal focus. UIA first, not OCR, never CDP 9222.",
            "Do not treat Google Chrome / ai-grok-bot as the host.",
        ],
    },
    "chat_read": {
        "summary": "Read the visible transcript of the focused Grok Bot thread (UIA Document). Returns focusedThread.",
        "long": False,
        "hint": "desktop_ui_text {hwnd, role:Document, range:all}. Optional thread/agent clicks that sidebar row then re-reads. Optional limit is CHARACTERS from the END (not messages; values under 500 are coerced). Returns focusedThread + agent + computerState. errorCode no_transcript (blank window, answered at once with visibleText) or computer_unreachable. Never returns webhook keys.",
        "related": ["grok_chat_status", "grok_list_windows", "grok_send", "grok_open"],
        "pitfalls": [
            "After cold launch the focused thread may not be grok-bridge — pass thread/agent or wait after grok_open.",
            "Visible transcript only, not a full history API.",
            "limit is CHARACTERS from the end, not messages. limit:20 will miss a nonce. Use 4000+.",
            "Do not treat Prompt compose-box garbage as a message.",
            "Never echo webhook keys. Never OCR-first. Never CDP 9222.",
        ],
    },
    "send": {
        "summary": "Queue a message to Grok Bot: toast + durable inbox. Typing is opt-in.",
        "long": False,
        "hint": "grok_send {\"message\":\"...\", \"agent\"?: \"...\"}. notify_user toast AND ~/.adom/grok-inbox.jsonl. Does NOT type into the chat unless the caller opts in — don't steal the keyboard.",
        "related": ["grok_status", "grok_list_windows", "grok_open"],
        "pitfalls": [
            "queued:true is notify+inbox, not proof the chat widget received it.",
            "Desktop typing is opt-in. Default is toast + inbox.",
            "The Anywhere character polls the inbox; cloud AIs should still --ai-thread.",
        ],
    },
    "uninstall": {
        "summary": "Tell the caller to bridge_uninstall {name:grok}. Does not kill Grok Bot or Cursor.",
        "long": False,
        "hint": "adom-bridge bridge_uninstall '{\"name\":\"grok\"}'. Skills: ./uninstall.sh.",
        "related": ["grok_status"],
        "pitfalls": ["never kills Grok Bot.exe or Cursor.exe — those are the user's apps"],
    },
    "close": {
        "summary": "Hide the Grok Bot window (desktop_set_window_state hide). Never process_kill Cursor.",
        "long": False,
        "hint": "Prefer hide over kill. Returns closed:true. grok_open to show it again.",
        "related": ["grok_open", "grok_list_windows", "grok_resize"],
        "pitfalls": [
            "Does not kill Grok Bot.exe or Cursor.exe.",
            "Will not hide a generic Cursor IDE window even on fallback.",
        ],
    },
    "resize": {
        "summary": "desktop_set_window_bounds on the Grok Bot hwnd. Physical px (same space as ab).",
        "long": False,
        "hint": "grok_resize {w, h, x?, y?}. Restores first if maximized. Omitting x,y keeps the current origin.",
        "related": ["grok_list_windows", "grok_screenshot", "grok_open"],
        "pitfalls": [
            "w and h are required. Units are ab physical pixels, not DIPs.",
            "Background — does not steal focus.",
        ],
    },
    "record_start": {
        "summary": "desktop_record_window_start on the Grok Bot hwnd. reason required.",
        "long": True,
        "hint": "grok_record_start {\"reason\":\"...\"}. Background WGC. Stop with grok_record_stop.",
        "related": ["grok_record_stop", "grok_record_status", "grok_screenshot"],
        "pitfalls": [
            "reason is required and is passed through.",
            "The mp4 lands on the desktop; pull it with the CLI, never pull_file from this HTTP callback.",
        ],
    },
    "record_stop": {
        "summary": "desktop_record_window_stop wrap.",
        "long": False,
        "hint": "Optional recordingId. File is on the desktop.",
        "related": ["grok_record_start", "grok_record_status"],
        "pitfalls": ["does not pull the file — use the adom-bridge CLI"],
    },
    "record_status": {
        "summary": "desktop_record_window_status wrap.",
        "long": False,
        "hint": "Poll after grok_record_start.",
        "related": ["grok_record_start", "grok_record_stop"],
        "pitfalls": [],
    },
    "login": {
        "summary": "Open Grok Bot settings (grokbot://app/v1/settings?id=theme) in the BACKGROUND. No passwords.",
        "long": False,
        "hint": "Human clicks Sign In with Cursor on the account card. This verb never types credentials.",
        "related": ["grok_logout", "grok_open"],
        "pitfalls": [
            "Do not type passwords or 2FA.",
            "Only grokbot:// settings?id=* and plugin/add?id= routes exist in v1.2.",
        ],
    },
    "logout": {
        "summary": "Open settings; try background UIA Sign Out. Else needsHuman:true.",
        "long": False,
        "hint": "If Sign Out is not background-actionable, the human clicks Sign Out on the account card.",
        "related": ["grok_login", "grok_open"],
        "pitfalls": [
            "Never invent UI paths beyond sidebar account button / Sign In with Cursor / Sign Out.",
            "needsHuman:true is success, not a crash — surface _hint.",
        ],
    },
    "plugin_install": {
        "summary": "Open grokbot://app/v1/plugin/add?id=<id>. Does not confirm install (the app UI does).",
        "long": False,
        "hint": "id required. Launches Grok Bot.exe with grokbot://app/v1/plugin/add?id=<id> as an arg (desktop_open_url refuses grokbot://).",
        "related": ["grok_open", "grok_login"],
        "pitfalls": ["does not confirm or wait for install", "id is required"],
    },
    "webhook_status": {
        "summary": "Whether ~/.adom/grok-webhook.json has a url. Returns bound + urlHost (never the key).",
        "long": False,
        "hint": "Key is never logged or returned. Prefer grok_webhook_ensure to read the routine pane.",
        "related": ["grok_webhook_ensure", "grok_webhook_bind", "grok_bot_list"],
        "pitfalls": ["never echo the sender key", "bound:false → grok_webhook_ensure first"],
    },
    "webhook_ensure": {
        "summary": "Scrape the Grok Bridge webhook routine pane (UIA Edit Webhook URL / Webhook key) and bind locally. No human copy-paste.",
        "long": True,
        "hint": "grok_webhook_ensure {}. Opens Open routine Grok Bridge webhook, expands When a webhook fires, reads Edit values, writes ~/.adom/grok-webhook.json. Never returns the key. force:true re-scrapes.",
        "related": ["grok_webhook_status", "grok_webhook_fire", "grok_bot_create"],
        "pitfalls": [
            "Needs a live Grok Bot window.",
            "Never echo the sender key.",
            "Do not ask a human to copy the URL/key — this verb is the auto-setup.",
        ],
    },
    "webhook_bind": {
        "summary": "Write ~/.adom/grok-webhook.json {url, key, header, boundAt}. url must be https. key required.",
        "long": False,
        "hint": "Manual bind. Prefer grok_webhook_ensure {} which reads UIA Edits Webhook URL / Webhook key.",
        "related": ["grok_webhook_ensure", "grok_webhook_status", "grok_webhook_fire"],
        "pitfalls": [
            "url must be https.",
            "The key is stored locally and never put in _hint or logs.",
        ],
    },
    "webhook_ensure": {
        "summary": "Read the Grok Bridge webhook routine pane via UIA Edits and write ~/.adom/grok-webhook.json.",
        "long": False,
        "hint": "Clicks \"Open routine Grok Bridge webhook\", expands \"When a webhook fires\" if needed, reads Edit names Webhook URL / Webhook key / Authorization header. No OCR. Never echoes the key.",
        "related": ["grok_webhook_status", "grok_webhook_bind", "grok_bot_list"],
        "pitfalls": [
            "Settings gear is NOT the Routines list.",
            "Do not type into Prompt (clear leftover garbage; do not submit).",
            "ListItem \"When a webhook fires\" is not invokable — prefer the Button, else click its center.",
            "Never log or return the sender key.",
        ],
    },
    "chat_send": {
        "summary": "Second route to the grok-bridge agent: type a message into the user's grok-bridge chat through UIA (no focus steal) and press Send message.",
        "long": False,
        "hint": "grok_chat_send {message, reason, thread?=grok-bridge}. Use when grok_webhook_fire is refused (webhook_throttled). Every message carries '[from AI thread \"<name>\" via grok-bridge, not typed by John]'. Refuses with prompt_busy if the user has a draft in the box. Shares the webhook's per-thread / per-PC budget. Needs the main window visible.",
        "related": ["grok_webhook_fire", "grok_chat_read", "grok_wake"],
        "pitfalls": ["The message shows in the user's own chat as theirs, with the AI prefix: not a way to fake the user's approval.", "Each message starts an agent run and costs Grok usage."],
    },
    "wake": {
        "summary": "Keep Grok's agent computer running for N minutes (it pauses a few minutes after each agent run).",
        "long": False,
        "hint": "grok_wake {minutes?=10 (max 30), reason}. Starts an agent run that only sleeps, via the webhook (chat fallback when throttled). The box rejoins the mesh within about a minute; check adom-bridge targets. Costs Grok usage per wake.",
        "related": ["grok_webhook_fire", "grok_chat_send"],
        "pitfalls": ["No Grok Bot setting keeps the computer awake (checked 2026-10-15).", "Wake shortly before you need the box, not in a loop."],
    },
    "activity": {
        "summary": "Who called which grok_* verb on this PC, when, and what came back; webhook fires, throttles and the shared budget.",
        "long": False,
        "hint": "grok_activity {minutes?=60, verb?, thread?, limit?=40}. Reads ~/.adom/grok-bridge-activity.jsonl (one line per call: ts, caller thread, verb, ms, ok, errorCode, httpStatus, event, nonce, runUuid). webhookBudget shows fires in the last minute per thread and any Grok cooldown.",
        "related": ["grok_webhook_fire", "grok_webhook_status"],
        "pitfalls": ["Per PC: each PC running grok-bridge has its own log and budget.", "Never contains webhook keys or message text."],
    },
    "webhook_fire": {
        "summary": "POST {event, payload, source:grok-bridge, agent?} to the bound url. Auth is Authorization: Bearer, never a body key.",
        "long": False,
        "hint": "Optional agent (name or id) as args.agent or payload.agent — grok-bridge relays into that teammate and pastes the reply into its own thread. Header Authorization: Bearer <key>. errorCode webhook_not_bound if unbound — grok_webhook_ensure first. requireApproval:true + approvalSummary raises an Allow/Deny toast on the user's PC, fires only on Allow and stamps payload.approval (caller-supplied approval fields are stripped). The verb budget is 300 s (bridge.json timeouts). Shared mailbox: 6 fires/min per PC, 3/min per thread; errorCode webhook_throttled (throttledBy thread-budget | pc-budget | grok, retryAfterSeconds) means NOT sent: wait, never loop. After a Grok 429 the PC holds all fires for Retry-After.",
        "related": ["grok_webhook_ensure", "grok_bot_create", "grok_channel_create", "grok_bot_list"],
        "pitfalls": [
            "A JSON field named key is WRONG — auth is a header.",
            "Never log the sender key.",
            "There is still one mailbox (the Grok Bridge webhook routine). agent is a relay, not a second webhook.",
            "HTTP 200 is only a runUuid (now also a top-level runUuid field). Poll grok_chat_read for payload.nonce.",
        ],
    },
    "run_status": {
        "summary": "Echo last grok_webhook_fire runUuid (in-process). No public run HTTP API.",
        "long": False,
        "hint": "grok_run_status {runUuid?}. Returns last fire runUuid/event/httpStatus. Poll grok_chat_read for the answer.",
        "related": ["grok_webhook_fire", "grok_chat_read"],
        "pitfalls": [
            "Clears if the grok-bridge process restarts.",
            "Never logs the webhook key.",
        ],
    },
    "bot_create": {
        "summary": "Webhook event=create_bot. {name, description?}. Needs grok_webhook_bind first.",
        "long": False,
        "hint": "If unbound: errorCode webhook_not_bound — grok_webhook_ensure {} first.",
        "related": ["grok_webhook_ensure", "grok_bot_list", "grok_channel_create"],
        "pitfalls": ["name required", "does nothing local if webhook_not_bound"],
    },
    "channel_create": {
        "summary": "Webhook event=create_channel. {name, member_ids?}.",
        "long": False,
        "hint": "Same bind requirement as grok_bot_create.",
        "related": ["grok_webhook_ensure", "grok_bot_create"],
        "pitfalls": ["name required", "errorCode webhook_not_bound if unbound"],
    },
    "bot_list": {
        "summary": "Webhook event=list_bots (cloud agent lists agents).",
        "long": False,
        "hint": "If unbound: errorCode webhook_not_bound.",
        "related": ["grok_webhook_ensure", "grok_bot_create"],
        "pitfalls": ["errorCode webhook_not_bound if unbound"],
    },
    "settings": {
        "summary": "Open a Grok Bot Settings page: general / computer / usage (plan + limits) / updates. Background.",
        "long": False,
        "hint": "grok_settings {page}. usage answers 'what plan am I on / what am I paying'. Then grok_screenshot.",
        "related": ["grok_screenshot", "grok_login"],
        "pitfalls": ["The overlay covers the chat; close it before typing a prompt."],
    },
    "install": {
        "summary": "Install Grok Bot on this PC from its public update feed. Background job, per-user, no admin, no login.",
        "long": True,
        "hint": "grok_install {force?}. Resolves the current installer for this OS/arch from api2.cursor.sh/updates, downloads it (~120 MB), checks Authenticode Anysphere (Windows) or the feed sha256 (Linux), then installs silently per-user. Returns at once; poll grok_install_status.",
        "related": ["grok_install_status", "grok_readiness", "grok_open", "grok_login"],
        "pitfalls": [
            "Already installed returns alreadyInstalled:true and does nothing unless force:true.",
            "Sign-in is still a human step afterwards (grok_login); this never types credentials.",
            "Never send the user to a download page; this verb is the install path.",
        ],
    },
    "install_status": {
        "summary": "Progress of the grok_install job: resolving / downloading (percent) / installing / done / failed.",
        "long": False,
        "hint": "Poll about every 10 s. done -> grok_open {}. failed -> read error; a signature/sha256 rejection is not retryable.",
        "related": ["grok_install", "grok_readiness"],
        "pitfalls": ["Job state is in-process; a bridge restart resets it to idle (grok_readiness still tells the truth)."],
    },
    "computer_screenshot": {
        "summary": "Fire screenshot_computer, wait until the nonce is in grok-bridge, then thumbnail Grok Bot.exe. Screenshots grok-bridge's computer (the agent box), not Grok Bot.exe.",
        "long": True,
        "hint": "grok_computer_screenshot {nonce?, timeoutSeconds?}. Mints a unique BOX-SHOT-XXXX-NNNN nonce if omitted. Waits (default 180s) for that nonce in grok_chat_read. Do not bash-poll. Do not reuse a nonce.",
        "related": ["grok_webhook_fire", "grok_chat_read", "grok_screenshot"],
        "pitfalls": [
            "This is NOT grok_screenshot of Grok Bot.exe.",
            "Do not click the monitor icon — that is the same window grok_screenshot already covers.",
            "Do not poll grok_chat_read yourself — this verb waits.",
            "Never reuse a nonce; omit nonce to mint BOX-SHOT-XXXX-NNNN.",
        ],
    },
}


def handle_describe(_args: dict) -> dict:
    verbs = []
    for cmd, meta in VERB_CATALOG.items():
        entry = {
            "name": PREFIX + cmd,
            "summary": meta["summary"],
            "hint": meta.get("hint", ""),
            "related": meta.get("related", []),
            "pitfalls": meta.get("pitfalls", []),
            "longRunning": bool(meta.get("long")),
        }
        if meta.get("long"):
            entry["statusVerb"] = PREFIX + "readiness"
        verbs.append(entry)
    verbs.sort(key=lambda v: v["name"])
    names = [v["name"] for v in verbs]
    return {
        "success": True,
        "prefix": PREFIX,
        "verbCount": len(verbs),
        "commands": names,
        "commandNames": names,
        "verbs": verbs,
        "_hint": (
            "Full grok_* catalog. names are grok_* in commands / commandNames / verbs[].name (JSON — grep those keys, not adom-bridge commands unprefixed internals). "
            "Every verb ALSO returns its own rich _hint when called; that's what to read in practice. "
            "Host app is Grok Bot.exe (Cursor last-resort fallback). "
            "Drive the app: grok_open / grok_close / grok_resize / grok_screenshot / grok_record_start. "
            "Chat: grok_chat_status / grok_chat_read (UIA, no focus steal). "
            "Bots/channels: grok_webhook_ensure then grok_bot_create / grok_channel_create / grok_bot_list. Computer shot: grok_computer_screenshot {} (not grok_screenshot). "
            "CLI: adom-bridge grok_* . Always --ai-thread. Background by default."
        ),
    }