app
Grok Bot
Public Made by Adomby adom
Grok Bridge lets Adom talk to Grok Bot. Adom's AI environment is for electrical engineers: any provider, any desktop power tool, one place.
← Commit history
grok_install 1.3.0: install Grok Bot from its public update feed (signature-checked, per-user); sync repo runtime to the deployed 1.2.4 source
14 files changed
+934−106
BRIDGE_VERSION+1−1CLAUDE.md+5−3SKILL.md+2−1adom-bridge-grok-manifest.json+4−4bridge.json+8−3dev-skills/grok-publish/SKILL.md+1−1handlers/chat.py+103−11handlers/describe.py+55−10handlers/install.py+261handlers/readiness.py+5−5handlers/webhook.py+257−2handlers/windows.py+199−62package.json+10−2server.py+23−1BRIDGE_VERSION+1−1@@ -1 +1 @@-1.2.2+1.3.0
CLAUDE.md+5−3@@ -32,7 +32,7 @@ will seed/stream the runtime from the release zip via `updateManifestUrl` in `br `ad_callback`. Never `shell_execute` / `pull_file` / `send_files` from the HTTP path. - `handlers/*.py` — readiness, describe, windows (open/screenshot/list), chat (status/read), send, app (close/resize/record/login/logout/plugin), webhook, bots.-- `bridge.json` + `BRIDGE_VERSION` — KEEP IN LOCKSTEP (currently 1.2.2).+- `bridge.json` + `BRIDGE_VERSION` — KEEP IN LOCKSTEP (currently 1.3.0). - `adom-bridge-grok-manifest.json` — streaming manifest ab's `updateManifestUrl` points at; its `url` is the RELEASE download URL of the zip, with matching `sha256`/`size`. - `SKILL.md` (root) — core USER skill, installed as **`grok`** (not `grok-bridge`).@@ -41,7 +41,7 @@ will seed/stream the runtime from the release zip via `updateManifestUrl` in `br - `package.json` — wiki skills pkg. Description is the skills-not-runtime sentence. Runtime files and `*.zip` do **not** belong in `files[]`. -## Verbs (v1.2.2)+## Verbs (v1.3.0) Existing: `grok_readiness` `grok_prewarm` `grok_describe` `grok_status` `grok_open` `grok_screenshot` `grok_list_windows` `grok_chat_status` `grok_chat_read` `grok_send` `grok_uninstall`.@@ -51,6 +51,8 @@ App: `grok_close` `grok_resize` `grok_record_start` `grok_record_stop` `grok_rec Webhook: `grok_webhook_status` `grok_webhook_ensure` `grok_webhook_bind` `grok_webhook_fire` `grok_bot_create` `grok_channel_create` `grok_bot_list`.++Install (1.3.0): `grok_install {force?}` background job + `grok_install_status`. See `handlers/install.py`. `grok_webhook_ensure` scrapes UIA Edit **Webhook URL** / **Webhook key** (no human copy-paste). `grok_webhook_fire` optional `agent` / `payload.agent` (name or id) relays into that teammate. @@ -70,7 +72,7 @@ Wiki = canonical. **Do not publish from this clone until asked.** - **The `.zip` is a RELEASE asset, never a git file.** `*.zip` is gitignored. - **`sha256`/`size` in the manifest MUST match the served release asset** or `bridge_install` rejects it. - **Never hand-edit the bridges-cache.** Edit here, publish.-- **Never tell the user to download Grok Bot / Cursor.** `grok_open` offers to launch.+- **Never tell the user to download Grok Bot / Cursor.** `grok_install` installs it (public feed `api2.cursor.sh/updates/api/update/<platform>/sand/0.0.1/0/stable`, Authenticode Anysphere / sha256 gated); `grok_open` launches. - **Never kill Cursor.** Prefer hide (`grok_close`) over `process_kill`. - **Background by default.** Don't steal focus or the keyboard. `grok_send` is toast + inbox. - **Repo name ≠ skill name.** Install as `grok`.
SKILL.md+2−1@@ -24,7 +24,7 @@ There is no public Grok HTTP API. `ab` spawns a small Python server from the **r | Signal | What it means | What YOU do | |---|---|---|-| `grok_readiness` `hostAppInstalled:false` / `errorCode: host_app_not_installed` | Grok Bot.exe is not on this PC | **Do not send the user to a download page.** Offer `grok_open`. That verb detects, then asks ab to `launch` / `desktop_open`. |+| `grok_readiness` `hostAppInstalled:false` / `errorCode: host_app_not_installed` | Grok Bot is not on this PC | **`grok_install {}`** installs it in the background from Grok Bot's public update feed (Windows x64/arm64, macOS, Linux AppImage; signature/sha256-checked, per-user, no admin). Poll `grok_install_status` (~10 s) until `done`, then `grok_open`. Never send the user to a download page. Sign-in is still theirs (`grok_login`). | | `hostAppFallback:"Cursor"` | Grok Bot.exe missing; Cursor is last-resort host | Prefer Grok Bot. Drive Cursor only as fallback. **Never kill Cursor.** | | `hostAppInstalled:true`, `hostAppRunning:false` | Grok Bot is on disk, not running | `grok_open {}` — launches in the **background**. | | `ready:true` / host running | Grok Bot is up | `grok_list_windows` / `grok_chat_status` / `grok_chat_read` / `grok_screenshot` / `grok_send`. |@@ -42,6 +42,7 @@ adom-bridge --ai-thread "<your thread>" grok_readiness '{}' | CLI | Purpose | Key args | More | |---|---|---|---| | `grok_readiness` | Layered host-app probe. Zero side effects. | — | this skill |+| `grok_install` / `grok_install_status` | Install Grok Bot on this PC (public update feed, background job) | optional `force` | this skill | | `grok_prewarm` | No-op re-detect. `{wait:true}` just re-detects. | optional `wait` | this skill | | `grok_describe` | Catalog with hint/related/pitfalls. | — | this skill | | `grok_open` / `grok_close` / `grok_resize` / `grok_screenshot` / `grok_list_windows` / `grok_status` | Window + host | see **grok-app-control** | grok-app-control |
adom-bridge-grok-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "grok",- "version": "1.2.4",- "url": "https://wiki.adom.inc/download/adom/grok-bridge/1.2.4/adom-bridge-grok-v1.2.4.zip",- "sha256": "aaf9ca1abca68a45611e3d1a0668fed4a130c427993de8c347b7f348ef93bdf9",- "size": 43720,+ "version": "1.3.0",+ "url": "https://wiki.adom.inc/download/adom/grok-bridge/1.3.0/adom-bridge-grok-v1.3.0.zip",+ "sha256": "4a2d2172d9987987b4737fb75df0a27a8e230f635608703a65e480787e4e8049",+ "size": 48242, "verbPrefixes": [ "grok_" ],
bridge.json+8−3@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "grok", "displayName": "Grok Bot",- "version": "1.2.2",+ "version": "1.3.0", "description": "Drive Grok Bot (standalone Electron app, Grok Bot.exe) from Adom Bridge: open/close/resize/screenshot/record the app, read thinking vs idle and the visible transcript of the focused thread, queue messages, bind the Grok Bridge webhook and create bots/channels. No public Grok HTTP API \u2014 the runtime talks back to ab over desktop_* callbacks. Cursor is last-resort fallback only if Grok Bot.exe is missing.", "homepage": "https://wiki.adom.inc/adom/grok-bridge", "author": "Adom Inc.",@@ -43,13 +43,18 @@ "grok_webhook_bind", "grok_webhook_ensure", "grok_webhook_fire",+ "grok_computer_screenshot",+ "grok_run_status", "grok_bot_create", "grok_channel_create",- "grok_bot_list"+ "grok_bot_list",+ "grok_install",+ "grok_install_status" ], "statusVerb": "grok_readiness", "timeouts": {- "default": 60+ "default": 60,+ "long": 300 }, "dependencies": { "python": ">=3.10"
dev-skills/grok-publish/SKILL.md+1−1@@ -23,7 +23,7 @@ Wiki: https://wiki.adom.inc/adom/grok-bridge. Changelog / `-m` must be ≥10 cha - Skills library / README / hero: bump **`package.json` version** (pkg). Leave `BRIDGE_VERSION` + `bridge.json` version alone if the Python runtime did not change. - New/changed `grok_*` verbs: bump **all three** (`BRIDGE_VERSION`, `bridge.json`, `package.json`) in lockstep, rebuild the zip, upload release, rewrite manifest, then pkg publish. -Current runtime: **1.2.2**. Skills-only bumps `package.json` only; verb changes bump zip + manifest + package.json together.+Current runtime: **1.3.0**. Skills-only bumps `package.json` only; verb changes bump zip + manifest + package.json together. ## Skills pkg allowlist
handlers/chat.py+103−11@@ -258,7 +258,24 @@ def _read_document_text(hwnd: int) -> tuple[str, dict | None]: return "", last ++def _select_sidebar_thread(hwnd: int, name: str) -> dict:+ """Best-effort click a sidebar row by UIA name. Does not steal OS focus beyond Invoke."""+ last = None+ for args in (+ {"hwnd": hwnd, "name": name},+ {"hwnd": hwnd, "name": name, "role": "ListItem"},+ {"hwnd": hwnd, "name": name, "role": "Button"},+ {"hwnd": hwnd, "nameContains": name},+ ):+ last = _cb("desktop_ui_click", args)+ if _callback_ok(last) and not (isinstance(last, dict) and last.get("error")):+ return last+ return last if isinstance(last, dict) else {"success": False}++ def handle_chat_status(_args: dict) -> dict:+ """thinking=true when UIA exposes '<agent> is working' / working chip. Never steals focus.""" hwnd, win = grok_hwnd(allow_cursor=False) if not hwnd:@@ -274,11 +291,13 @@ def handle_chat_status(_args: dict) -> dict: "thinking": bool(thinking), "idle": (not thinking), "agentName": agent,+ "agent": agent,+ "focusedThread": agent, "hwnd": hwnd, "title": title, "windowEtiquette": "background", "_hint": (- f"Grok Bot hwnd={hwnd} agent={agent!r} "+ f"Grok Bot hwnd={hwnd} focusedThread={agent!r} " + ("thinking (UIA name like '<agent> is working')." if thinking else "idle (no working chip).") + " Read-only UIA — focus not stolen. grok_chat_read for the visible transcript." ),@@ -286,7 +305,15 @@ def handle_chat_status(_args: dict) -> dict: def handle_chat_read(args: dict) -> dict:- """Visible transcript via desktop_ui_text Document range=all. limit truncates from the END."""+ """Visible transcript via desktop_ui_text Document range=all.++ limit is CHARACTERS from the END (not messages). Values under 500 are coerced+ to max(limit*250, 4000). Pass 4000+ to search for a nonce.++ Optional args.thread / args.agent: try to click that sidebar row then re-read.+ Always returns focusedThread (heading / agent name). After cold launch the focused+ thread may not be grok-bridge — callers should pass thread/agent or wait after grok_open.+ """ hwnd, win = grok_hwnd(allow_cursor=False) if not hwnd: hwnd, win = grok_hwnd(allow_cursor=True)@@ -294,15 +321,40 @@ def handle_chat_read(args: dict) -> dict: miss = _window_missing("chat_read") miss["text"] = "" miss["length"] = 0+ miss["focusedThread"] = None+ miss["agent"] = None return miss + wanted = str(args.get("thread") or args.get("agent") or "").strip()+ select_attempt = None+ if wanted:+ select_attempt = _select_sidebar_thread(hwnd, wanted)+ thinking, working_name = _find_working(hwnd) agent = _read_agent_name(hwnd, working_name) raw, last = _read_document_text(hwnd)++ # Cold start: focused thread is often not grok-bridge. If nothing requested and+ # transcript is empty/tiny, try the grok-bridge sidebar row once.+ tiny = len((raw or "").strip()) < 40+ agent_l = (agent or "").strip().lower()+ if not wanted and tiny and "grok-bridge" not in agent_l and "grok bridge" not in agent_l:+ select_attempt = _select_sidebar_thread(hwnd, "grok-bridge")+ thinking, working_name = _find_working(hwnd)+ agent = _read_agent_name(hwnd, working_name)+ raw, last = _read_document_text(hwnd)+ tiny = len((raw or "").strip()) < 40 prompt_val = _prompt_value(hwnd) text = _strip_prompt_garbage(raw, prompt_val) text = _redact_secrets(text) + # limit is CHARACTERS from the END, not messages. Callers who pass 20/40+ # almost certainly meant "last N messages" — coerce so a nonce search+ # is not truncated to the last 20 chars ("replied ").+ LIMIT_HINT = (+ "`limit` is characters from the end, not messages. "+ "Values under 500 are coerced. Pass 4000+ to search for a nonce."+ ) limit = args.get("limit") try: limit_n = int(limit) if limit is not None and str(limit).strip() != "" else None@@ -310,15 +362,33 @@ def handle_chat_read(args: dict) -> dict: limit_n = None if limit_n is not None and limit_n < 0: limit_n = None+ limit_meta = {} if limit_n:- text = text[-limit_n:]+ limit_meta["limitRequested"] = limit_n+ if limit_n < 500:+ chars = max(limit_n * 250, 4000)+ limit_meta["limitCoerced"] = True+ else:+ chars = limit_n+ limit_meta["limitChars"] = chars+ text = text[-chars:] ok = bool(text) or _callback_ok(last)+ focused = agent+ if wanted and not ok:+ err = "thread_select_failed"+ elif not text.strip():+ err = "focused_thread_empty"+ else:+ err = None+ if not ok and not text:- return {+ out = { "success": False, "errorCode": "transcript_unavailable", "agentName": agent,+ "agent": agent,+ "focusedThread": focused, "thinking": bool(thinking), "idle": (not thinking), "text": "",@@ -328,15 +398,24 @@ def handle_chat_read(args: dict) -> dict: "windowEtiquette": "background", "_hint": ( "desktop_ui_text {hwnd, role:Document, range:all} returned no text. "- "Needs a live Grok Bot window with the chat Document (name \"Grok Bot\"). "- "Never OCR-first. Never CDP 9222. grok_chat_status {} then retry grok_chat_read."+ "After cold launch the focused thread may not be grok-bridge. "+ "Pass args.thread / args.agent to click that sidebar row, or grok_open then wait. "+ "Never OCR-first. Never CDP 9222. "+ + LIMIT_HINT+ + " Default 4000+ when polling for a nonce." ),- "_next": "grok_chat_status {} then grok_chat_read {}",+ "_next": "grok_open {} then wait, or grok_chat_read {\"thread\":\"grok-bridge\"}", }+ if select_attempt is not None:+ out["selectAttempt"] = select_attempt+ out.update(limit_meta)+ return out - return {- "success": True,+ out = {+ "success": True if text.strip() else False, "agentName": agent,+ "agent": agent,+ "focusedThread": focused, "thinking": bool(thinking), "idle": (not thinking), "text": text,@@ -346,9 +425,22 @@ def handle_chat_read(args: dict) -> dict: "windowEtiquette": "background", "_hint": ( f"Read UIA Document range=all on Grok Bot hwnd={hwnd} "- f"({len(text)} chars, agent={agent!r}, "+ f"({len(text)} chars, focusedThread={focused!r}, " + ("thinking" if thinking else "idle") + "). Latest messages are at the end. Webhook keys redacted. "- "Prompt compose box stripped when distinguishable. Never stole focus."+ "Optional thread/agent clicks a sidebar row then re-reads. "+ "If empty after cold start, grok_open then wait or pass thread:grok-bridge. "+ + LIMIT_HINT+ + (" Default 4000+ when polling for a nonce." if not limit_n else "") ), }+ out.update(limit_meta)+ if err and not text.strip():+ out["success"] = False+ out["errorCode"] = err+ out["_next"] = "grok_open {} then wait, or grok_chat_read {\"thread\":\"grok-bridge\"}"+ if wanted:+ out["requestedThread"] = wanted+ if select_attempt is not None:+ out["selectAttempt"] = select_attempt+ return out
handlers/describe.py+55−10@@ -39,12 +39,13 @@ VERB_CATALOG = { "pitfalls": ["lastSend is in-process; a bridge restart clears it (inbox file survives)"], }, "open": {- "summary": "Launch Grok Bot in the BACKGROUND if installed. Cursor only if Grok Bot.exe is missing.",+ "summary": "Launch Grok Bot in the BACKGROUND if installed. Polls process/window; Start-Process minimized fallback.", "long": True,- "hint": "Detect, then ab launch / desktop_open. If already running, lists windows. Never steals focus.",+ "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.", "related": ["grok_list_windows", "grok_screenshot", "grok_close", "grok_readiness"], "pitfalls": [- "Background by default. foreground:true without foregroundReason is refused.",+ "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.", ],@@ -81,13 +82,14 @@ VERB_CATALOG = { ], }, "chat_read": {- "summary": "Read the visible transcript of the focused Grok Bot thread (UIA Document TextPattern).",+ "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 limit truncates from the END (latest messages). Never returns webhook keys.",- "related": ["grok_chat_status", "grok_list_windows", "grok_send"],+ "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. 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.",- "Optional limit is from the end (latest), not the start.",+ "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.", ],@@ -230,7 +232,17 @@ VERB_CATALOG = { "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. Poll grok_chat_read for payload.nonce.",+ "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": {@@ -254,6 +266,36 @@ VERB_CATALOG = { "related": ["grok_webhook_ensure", "grok_bot_create"], "pitfalls": ["errorCode webhook_not_bound if unbound"], },+ "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.",+ ],+ }, } @@ -272,18 +314,21 @@ def handle_describe(_args: dict) -> dict: 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 — each entry carries hint/related/pitfalls. "+ "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. "+ "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." ), }
handlers/install.pyadded+261@@ -0,0 +1,261 @@+"""grok_install / grok_install_status — install Grok Bot on this PC, no admin, no login.++Grok Bot's own update feed is public:++ GET https://api2.cursor.sh/updates/api/update/<platform>/sand/<curVer>/<id>/stable+ -> {"version", "url", "sha256hash"?} (url on downloads.cursor.com)++platform is win32-x64-user / win32-arm64-user / darwin-arm64 / darwin-x64 / linux-x64.+This is the same request the installed app makes to update itself (see+buildUpdateRequestUrl in its app.asar), so asking it with curVer 0.0.1 returns+the current full installer.++Install targets are per-user and match grok_detect's candidate paths:+ Windows NSIS Setup.exe /S -> %LOCALAPPDATA%\\Programs\\Grok Bot+ macOS zip with Grok Bot.app -> ~/Applications/Grok Bot.app+ Linux AppImage (sha256 from feed) -> ~/.local/bin/grok-bot++The download is ~120 MB, longer than ab's 60 s verb wait, so grok_install runs+as a background job and grok_install_status reports progress. The installer is+never launched unless its signature (Windows Authenticode "Anysphere") or its+feed sha256 checks out. Sign-in stays a human step (grok_login).+"""++from __future__ import annotations++import hashlib+import json+import os+import platform+import shutil+import stat+import subprocess+import sys+import tempfile+import threading+import time+import urllib.request+import zipfile+from pathlib import Path++from grok_detect import CREATE_NO_WINDOW, IS_LINUX, IS_MACOS, IS_WINDOWS, detect_host++FEED = "https://api2.cursor.sh/updates/api/update/{platform}/sand/0.0.1/0/stable"+SIGNERS = ("Anysphere, Inc.", "Anysphere")+UA = "adom-grok-bridge"++_lock = threading.Lock()+_job: dict = {"state": "idle"}+++def _platform_key() -> str:+ arm = platform.machine().lower() in ("arm64", "aarch64")+ if IS_WINDOWS:+ return "win32-arm64-user" if arm else "win32-x64-user"+ if IS_MACOS:+ return "darwin-arm64" if arm else "darwin-x64"+ return "linux-x64"+++def _feed() -> dict:+ req = urllib.request.Request(FEED.format(platform=_platform_key()), headers={"User-Agent": UA})+ with urllib.request.urlopen(req, timeout=30) as r:+ data = json.loads(r.read().decode("utf-8"))+ if not data.get("url"):+ raise RuntimeError(f"update feed returned no url: {data}")+ return {+ "version": data.get("version") or data.get("name"),+ "url": data["url"],+ "sha256": data.get("sha256hash"),+ }+++def _set(**kw) -> None:+ with _lock:+ _job.update(kw)+++def _download(url: str, dest: Path) -> str:+ h = hashlib.sha256()+ req = urllib.request.Request(url, headers={"User-Agent": UA})+ with urllib.request.urlopen(req, timeout=60) as r, open(dest, "wb") as f:+ total = int(r.headers.get("Content-Length") or 0)+ done = 0+ _set(bytesTotal=total)+ while True:+ chunk = r.read(1 << 20)+ if not chunk:+ break+ f.write(chunk)+ h.update(chunk)+ done += len(chunk)+ _set(bytesDone=done)+ return h.hexdigest()+++def _authenticode(path: Path) -> dict:+ ps = (+ "$s = Get-AuthenticodeSignature -LiteralPath $env:GROK_SETUP; "+ "[pscustomobject]@{status=[string]$s.Status; "+ "subject=[string]$s.SignerCertificate.Subject} | ConvertTo-Json -Compress"+ )+ env = dict(os.environ, GROK_SETUP=str(path))+ out = subprocess.run(+ ["powershell.exe", "-NoProfile", "-NonInteractive", "-Command", ps],+ capture_output=True, text=True, timeout=60, env=env, creationflags=CREATE_NO_WINDOW,+ )+ try:+ return json.loads(out.stdout.strip() or "{}")+ except json.JSONDecodeError:+ return {"status": "Unknown", "subject": "", "stderr": out.stderr.strip()[:300]}+++def _install_windows(setup: Path) -> None:+ sig = _authenticode(setup)+ _set(signature=sig)+ if sig.get("status") != "Valid" or not any(f"CN={s}" in (sig.get("subject") or "") or+ f'CN="{s}"' in (sig.get("subject") or "")+ for s in SIGNERS):+ raise RuntimeError(f"installer signature rejected: {sig}")+ _set(state="installing")+ # NSIS per-user installer: /S is silent, no UAC, installs to %LOCALAPPDATA%\Programs.+ p = subprocess.run([str(setup), "/S"], timeout=600, creationflags=CREATE_NO_WINDOW)+ _set(installerExit=p.returncode)+++def _install_macos(zpath: Path) -> None:+ _set(state="installing")+ apps = Path.home() / "Applications"+ apps.mkdir(parents=True, exist_ok=True)+ tmp = Path(tempfile.mkdtemp(prefix="grokbot-"))+ # ditto keeps the code signature and symlinks that zipfile would break.+ subprocess.run(["ditto", "-x", "-k", str(zpath), str(tmp)], check=True, timeout=600)+ app = next(tmp.glob("*.app"), None)+ if app is None:+ raise RuntimeError("no .app inside the macOS zip")+ dest = apps / "Grok Bot.app"+ if dest.exists():+ shutil.rmtree(dest)+ shutil.move(str(app), str(dest))+ subprocess.run(["xattr", "-dr", "com.apple.quarantine", str(dest)], timeout=60)+++def _install_linux(appimage: Path) -> None:+ _set(state="installing")+ dest = Path.home() / ".local" / "bin" / "grok-bot"+ dest.parent.mkdir(parents=True, exist_ok=True)+ shutil.move(str(appimage), str(dest))+ dest.chmod(dest.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)+++def _run(feed: dict) -> None:+ work = Path(tempfile.mkdtemp(prefix="grokbot-dl-"))+ name = feed["url"].rsplit("/", 1)[-1] or "grokbot-setup"+ dest = work / name+ try:+ _set(state="downloading", file=name)+ digest = _download(feed["url"], dest)+ _set(sha256=digest)+ if feed.get("sha256") and digest.lower() != feed["sha256"].lower():+ raise RuntimeError(f"sha256 mismatch: got {digest}, feed says {feed['sha256']}")+ if IS_WINDOWS:+ _install_windows(dest)+ elif IS_MACOS:+ _install_macos(dest)+ else:+ if not feed.get("sha256"):+ raise RuntimeError("linux feed had no sha256; refusing to install an unverified AppImage")+ _install_linux(dest)+ # NSIS returns before every file settles on some boxes; give detection a moment.+ for _ in range(30):+ if detect_host().get("hostAppInstalled") and not detect_host().get("hostAppFallback"):+ break+ time.sleep(1)+ info = detect_host()+ ok = bool(info.get("hostAppInstalled")) and not info.get("hostAppFallback")+ _set(state="done" if ok else "failed", installed=ok, paths=info.get("paths") or [],+ finishedAt=time.time(),+ error="" if ok else "installer finished but Grok Bot was not detected")+ except Exception as e: # noqa: BLE001 - every failure must land in the job state+ _set(state="failed", error=str(e), finishedAt=time.time())+ finally:+ shutil.rmtree(work, ignore_errors=True)+++def _snapshot() -> dict:+ with _lock:+ j = dict(_job)+ total, done = j.get("bytesTotal") or 0, j.get("bytesDone") or 0+ if total:+ j["percent"] = round(100.0 * done / total, 1)+ return j+++def handle_install(args: dict) -> dict:+ info = detect_host()+ force = bool(args.get("force"))+ if info.get("hostAppInstalled") and not info.get("hostAppFallback") and not force:+ return {+ "success": True,+ "alreadyInstalled": True,+ "paths": info.get("paths") or [],+ "_hint": "Grok Bot is already installed. Next: grok_open {} (background), then grok_login if it is not signed in. Pass force:true to reinstall over it.",+ "_next": "grok_open {}",+ }+ with _lock:+ busy = _job.get("state") in ("resolving", "downloading", "installing")+ if busy:+ out = _snapshot()+ out.update(success=True, alreadyRunning=True,+ _hint="An install is already running. Poll grok_install_status about every 10 s; do not start another.",+ _next="grok_install_status {}")+ return out+ try:+ _set(state="resolving", startedAt=time.time(), error="", bytesDone=0, bytesTotal=0,+ platform=_platform_key(), signature=None, installed=False)+ feed = _feed()+ except Exception as e: # noqa: BLE001+ _set(state="failed", error=f"update feed: {e}")+ return {+ "success": False,+ "errorCode": "feed_unreachable",+ "error": str(e),+ "_hint": "Could not read Grok Bot's update feed (api2.cursor.sh). Check this PC's internet, then retry grok_install once.",+ "_next": "grok_install {}",+ }+ _set(version=feed["version"], url=feed["url"])+ threading.Thread(target=_run, args=(feed,), daemon=True, name="grok-install").start()+ return {+ "success": True,+ "started": True,+ "version": feed["version"],+ "platform": _platform_key(),+ "url": feed["url"],+ "_hint": (+ f"Installing Grok Bot {feed['version']} in the background from its public update feed "+ "(~120 MB, per-user, no admin prompt, no window). The installer only runs after its "+ "signature (Windows: Authenticode Anysphere) or sha256 (Linux) checks out. "+ "Poll grok_install_status about every 10 s until state is done or failed."+ ),+ "_next": "grok_install_status {}",+ "related": ["grok_install_status", "grok_readiness", "grok_open", "grok_login"],+ }+++def handle_install_status(_args: dict) -> dict:+ j = _snapshot()+ state = j.get("state")+ hints = {+ "idle": "No install has run since the bridge started. grok_readiness says whether Grok Bot is present; grok_install installs it.",+ "resolving": "Asking the update feed for the current installer.",+ "downloading": "Downloading. Poll again in ~10 s.",+ "installing": "Running the silent per-user installer. Poll again in ~10 s.",+ "done": "Installed. Next: grok_open {} (background), then grok_login so the user signs in with Cursor.",+ "failed": "Install failed; read error. A signature or sha256 rejection means the download was not Anysphere's: do not retry blindly, report it.",+ }+ j["success"] = state != "failed"+ j["_hint"] = hints.get(state, "Unknown install state.")+ j["_next"] = {"done": "grok_open {}", "failed": "grok_install {}"}.get(state, "grok_install_status {}")+ if state == "failed":+ j["errorCode"] = "install_failed"+ return j
handlers/readiness.py+5−5@@ -20,7 +20,7 @@ def handle_readiness(_args: dict) -> dict: """Read-only: is Grok Bot present / running? Never blocks on a download. Bridge detects host apps; it does not auto-install- them. If Grok Bot is missing, OFFER grok_open — do not send the user to a download page.+ them. If Grok Bot is missing, grok_install installs it — do not send the user to a download page. Cursor is last-resort fallback only if Grok Bot.exe is missing. """ info = detect_host()@@ -40,9 +40,9 @@ def handle_readiness(_args: dict) -> dict: ) else: state = (- "Grok Bot isn't installed — want me to open it if present? "- "Call grok_open (detect + ab launch). Cursor is last-resort fallback only if "- "Grok Bot.exe is missing. Do not send the user to download the host app themselves."+ "Grok Bot isn't installed. grok_install {} installs it for you in the background "+ "(public update feed, signature-checked, per-user, no admin prompt); poll "+ "grok_install_status. Do not send the user to download it themselves." ) out = { "success": True,@@ -62,7 +62,7 @@ def handle_readiness(_args: dict) -> dict: } if not installed: out["errorCode"] = "host_app_not_installed"- out["_next"] = "Offer grok_open to the user; on yes, call grok_open {}. Do not send the user to a download page."+ out["_next"] = "grok_install {} then poll grok_install_status {}, then grok_open {}. Do not send the user to a download page." return out
handlers/webhook.py+257−2@@ -14,6 +14,7 @@ grok_webhook_ensure reads UIA Edit controls on the Grok Bot routine pane from __future__ import annotations import json+import secrets import socket import time import urllib.error@@ -221,7 +222,101 @@ def _normalize_payload_agent(args: dict, payload): return pl, s ++_LAST_FIRE: dict = {}+++def _extract_run_uuid(parsed, raw: str | None = None) -> str | None:+ if isinstance(parsed, str):+ s = parsed.strip().strip('"').strip("'")+ if 8 <= len(s) <= 80 and " " not in s and "/" not in s:+ return s+ if isinstance(parsed, list) and parsed:+ got = _extract_run_uuid(parsed[0], raw)+ if got:+ return got+ if not isinstance(parsed, dict):+ if raw:+ for marker in ("runUuid", "run_uuid", "runId"):+ key = f'"{marker}"'+ i = raw.find(key)+ if i < 0:+ i = raw.find(marker)+ if i < 0:+ continue+ chunk = raw[i:i + 120]+ for sep in (":", "="):+ if sep in chunk:+ rest = chunk.split(sep, 1)[1].strip().strip(",} ").strip('"').strip("'")+ tok = rest.split()[0].strip(",} ") if rest else ""+ tok = tok.strip('"').strip("'")+ if 8 <= len(tok) <= 80:+ return tok+ return None+ for k in ("runUuid", "run_uuid", "runId", "run_id", "uuid", "id"):+ v = parsed.get(k)+ if v is None or v is False:+ continue+ s = str(v).strip()+ if s and s.lower() not in ("none", "null"):+ # skip obvious non-uuids like event names for generic "id"+ if k == "id" and len(s) < 8:+ continue+ return s+ for nest in ("data", "result", "response", "run"):+ inner = parsed.get(nest)+ if isinstance(inner, dict):+ got = _extract_run_uuid(inner, raw)+ if got:+ return got+ if raw:+ got = _extract_run_uuid(None, raw)+ if got:+ return got+ return None+++def handle_run_status(args: dict) -> dict:+ """Cheap last-fire echo. Optional GET of bound url is not required for runUuid surfacing."""+ want = str(args.get("runUuid") or args.get("run_uuid") or "").strip() or None+ last = dict(_LAST_FIRE) if _LAST_FIRE else {}+ run = want or last.get("runUuid")+ out = {+ "success": True if (run or last) else False,+ "runUuid": run,+ "event": last.get("event"),+ "httpStatus": last.get("httpStatus"),+ "firedAt": last.get("firedAt"),+ "urlHost": last.get("urlHost"),+ }+ if not run and not last:+ out["errorCode"] = "no_run"+ out["_hint"] = (+ "No runUuid yet. Fire with grok_webhook_fire {event} first; HTTP 200 now "+ "surfaces runUuid at the top level (and in response)."+ )+ out["_next"] = "grok_webhook_fire {\"event\":\"ping\"}"+ return out+ if want and last.get("runUuid") and want != last.get("runUuid"):+ out["success"] = True+ out["matchedLastFire"] = False+ out["_hint"] = (+ f"Requested runUuid={want}. Last fire in this process was "+ f"{last.get('runUuid')} event={last.get('event')}. "+ "There is no public run-status HTTP API; poll grok_chat_read for the nonce."+ )+ return out+ out["matchedLastFire"] = bool(last.get("runUuid") and run == last.get("runUuid"))+ out["_hint"] = (+ f"Last grok_webhook_fire in this process: runUuid={run} event={last.get('event')} "+ f"HTTP {last.get('httpStatus')}. Poll grok_chat_read for payload.nonce — "+ "the HTTP 200 is only the run id, not the answer."+ )+ return out++ def handle_webhook_fire(args: dict) -> dict:+ event = str(args.get("event") or "").strip() if not event: return {@@ -244,6 +339,17 @@ def handle_webhook_fire(args: dict) -> dict: if payload is None: payload = {} payload, agent = _normalize_payload_agent(args, payload)+ # Top-level args.nonce / args.message merge into payload. payload wins on conflict.+ if not isinstance(payload, dict):+ payload = {}+ else:+ payload = dict(payload)+ for field in ("nonce", "message"):+ if field in payload and str(payload.get(field) or "").strip():+ continue+ raw = args.get(field)+ if raw is not None and str(raw).strip():+ payload[field] = str(raw).strip() try: hostname = socket.gethostname() except Exception:@@ -253,6 +359,9 @@ def handle_webhook_fire(args: dict) -> dict: "payload": payload, "source": SOURCE, }+ nonce = str(payload.get("nonce") or "").strip()+ if nonce:+ body["nonce"] = nonce if agent: body["agent"] = agent if hostname:@@ -263,7 +372,7 @@ def handle_webhook_fire(args: dict) -> dict: headers = { "Content-Type": "application/json", "Accept": "application/json",- "User-Agent": f"{SOURCE}/1.2.2",+ "User-Agent": f"{SOURCE}/1.2.4", } key = cfg.get("key") or "" if key:@@ -318,6 +427,7 @@ def handle_webhook_fire(args: dict) -> dict: except json.JSONDecodeError: parsed = None + run_uuid = _extract_run_uuid(parsed, resp_text) out = { "success": True, "fired": True,@@ -325,24 +435,169 @@ def handle_webhook_fire(args: dict) -> dict: "httpStatus": status, "urlHost": url_host(url), "hostname": hostname,+ "runUuid": run_uuid, } if agent: out["agent"] = agent if isinstance(parsed, dict): safe = {k: v for k, v in parsed.items() if k.lower() not in ("key", "senderkey", "token", "secret")} out["response"] = safe+ if run_uuid is None:+ run_uuid = _extract_run_uuid(safe)+ out["runUuid"] = run_uuid elif resp_text: out["bodyPreview"] = resp_text[:500]+ _LAST_FIRE.clear()+ _LAST_FIRE.update({+ "runUuid": run_uuid,+ "event": event,+ "httpStatus": status,+ "urlHost": url_host(url),+ "firedAt": _now_iso(),+ }) out["_hint"] = ( f"POSTed event={event} to webhook host {url_host(url)} (HTTP {status})" + (f" for agent={agent!r}." if agent else ".")+ + (f" runUuid={run_uuid}." if run_uuid else " runUuid not present in HTTP body.") + " Auth was header Authorization: Bearer, never a body field named key. " "The HTTP response is only a runUuid; the answer lands in the grok-bridge "- "thread — poll grok_chat_read for a nonce in payload."+ "thread — poll grok_chat_read for a nonce in payload. grok_run_status {runUuid} echoes the last fire." ) return out ++SHOT_NONCE_PREFIX = "BOX-SHOT-"+SHOT_POLL_SECONDS = 4+SHOT_TIMEOUT_DEFAULT = 180+SHOT_TIMEOUT_MIN = 30+SHOT_TIMEOUT_MAX = 300+SHOT_READ_LIMIT = 8000+SHOT_SNIPPET = 600+SHOT_MESSAGE = (+ "Screenshot grok-bridge computer. Reply in grok-bridge with this nonce and attach the PNG."+)+SHOT_HINT = (+ "This screenshots grok-bridge's computer (the agent box), NOT Grok Bot.exe. "+ "HTTP 200 has no image. This verb waits and returns when the nonce is in grok-bridge. "+ "Do not bash-poll grok_chat_read. Do not reuse a nonce. "+ "Full PNG is the in-thread attachment; shotId is the Grok Bot.exe thumbnail."+)+++def _mint_shot_nonce() -> str:+ """BOX-SHOT- + 4 uppercase hex + '-' + last 4 of unix time. Never BOX-SHOT-4821."""+ hex4 = secrets.token_hex(2).upper()+ last4 = str(int(time.time()))[-4:]+ return f"{SHOT_NONCE_PREFIX}{hex4}-{last4}"+++def _snippet_around(text: str, needle: str, width: int = SHOT_SNIPPET) -> str:+ if not text:+ return ""+ i = text.find(needle)+ if i < 0:+ return text[-width:]+ half = max(width - len(needle), 0) // 2+ start = max(0, i - half)+ end = min(len(text), start + width)+ if end - start < width:+ start = max(0, end - width)+ return text[start:end]+++def handle_computer_screenshot(args: dict) -> dict:+ """Fire screenshot_computer, wait for nonce in grok-bridge, then thumbnail Grok Bot.exe.++ Runs on the laptop (AdomLapper) via Adom Bridge. Does not screenshot this Linux box.+ Never steals focus. Never types into chat. Never logs webhook keys.+ """+ from handlers.chat import handle_chat_read+ from handlers.windows import flatten_callback, handle_screenshot++ nonce = str((args or {}).get("nonce") or "").strip()+ if not nonce:+ nonce = _mint_shot_nonce()++ raw_timeout = (args or {}).get("timeoutSeconds")+ try:+ timeout = int(raw_timeout) if raw_timeout is not None and str(raw_timeout).strip() != "" else SHOT_TIMEOUT_DEFAULT+ except (TypeError, ValueError):+ timeout = SHOT_TIMEOUT_DEFAULT+ timeout = max(SHOT_TIMEOUT_MIN, min(SHOT_TIMEOUT_MAX, timeout))++ fire = handle_webhook_fire({+ "event": "screenshot_computer",+ "nonce": nonce,+ "message": SHOT_MESSAGE,+ "payload": {+ "nonce": nonce,+ "message": SHOT_MESSAGE,+ },+ })+ if not fire.get("success"):+ if "nonce" not in fire:+ fire = dict(fire)+ fire["nonce"] = nonce+ return fire++ run_uuid = fire.get("runUuid")+ t0 = time.monotonic()+ polls = 0+ last_read: dict = {}+ found = False+ snippet = ""+ deadline = t0 + timeout++ while True:+ polls += 1+ last_read = handle_chat_read({"thread": "grok-bridge", "limit": SHOT_READ_LIMIT})+ text = str(last_read.get("text") or "")+ if nonce in text:+ found = True+ snippet = _snippet_around(text, nonce)+ break+ remaining = deadline - time.monotonic()+ if remaining <= 0:+ snippet = _snippet_around(text, nonce)+ break+ time.sleep(min(SHOT_POLL_SECONDS, remaining))++ wait_ms = int((time.monotonic() - t0) * 1000)+ out = {+ "success": found,+ "found": found,+ "nonce": nonce,+ "runUuid": run_uuid,+ "waitMs": wait_ms,+ "polls": polls,+ "snippet": snippet,+ "thinking": last_read.get("thinking"),+ "idle": last_read.get("idle"),+ "_hint": SHOT_HINT,+ }+ if found:+ try:+ shot = handle_screenshot({})+ except Exception:+ shot = None+ if isinstance(shot, dict) and shot.get("success") is not False:+ merged = flatten_callback(shot)+ blob = merged if isinstance(merged, dict) else shot+ shot_id = (+ blob.get("shotId") or shot.get("shotId")+ or blob.get("screenshotId") or shot.get("screenshotId")+ or blob.get("id") or shot.get("id")+ )+ if shot_id and str(shot_id).lower() not in ("none", "null"):+ out["shotId"] = shot_id+ return out++ out["errorCode"] = "nonce_not_in_transcript"+ out["_next"] = "retry grok_computer_screenshot {} with a FRESH nonce; FIFO may be busy."+ return out++ # ── grok_webhook_ensure: UIA Edit read of the routine pane (no OCR) ──────────
handlers/windows.py+199−62@@ -7,6 +7,9 @@ HTTP callback. All driving goes through caller_identity.ad_callback. from __future__ import annotations import json+import subprocess+import sys+import time import caller_identity from grok_detect import (@@ -14,6 +17,8 @@ from grok_detect import ( detect_host, process_name_filter, window_title_needles,+ _cursor_running,+ _grok_bot_running, ) @@ -342,6 +347,85 @@ def launch_host(argv: list | None = None) -> dict: return _cb("launch", launch_args) +CREATE_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0)+_POLL_SECONDS = 12+_POLL_INTERVAL = 1.2+_FG_REASON_MIN = 10+++def _ps_quote(s: str) -> str:+ return "'" + str(s).replace("'", "''") + "'"+++def _host_alive(info: dict | None = None) -> tuple:+ """Return (hwnd, win, process_up) without stealing focus."""+ info = info or detect_host()+ allow = bool(info.get("hostAppFallback"))+ hwnd, win = grok_hwnd(allow_cursor=allow)+ if hwnd:+ return hwnd, win, True+ filt = process_name_filter(info)+ procs = _as_processes(_cb("process_list", {"nameFilter": filt}))+ if procs:+ return None, None, True+ if filt != "Grok Bot":+ extra = _as_processes(_cb("process_list", {"nameFilter": "Grok Bot"}))+ if extra:+ return None, None, True+ if info.get("hostAppFallback"):+ running = _cursor_running()+ else:+ running = _grok_bot_running()+ return None, None, bool(running)+++def _poll_host(info: dict, timeout: float = _POLL_SECONDS) -> tuple:+ deadline = time.time() + timeout+ last = (None, None, False)+ while time.time() < deadline:+ last = _host_alive(info)+ if last[0] or last[2]:+ return last+ time.sleep(_POLL_INTERVAL)+ return last+++def _start_process_minimized(exe: str, extra_args: list | None = None) -> dict:+ """Windows Start-Process -WindowStyle Minimized. Never shell_execute. Never Cursor unless that is the last-resort exe."""+ if not exe:+ return {"success": False, "error": "no exe path"}+ if sys.platform != "win32":+ return {"success": False, "via": "Start-Process", "error": "Start-Process fallback is Windows-only"}+ cmd = (+ "$p = Start-Process -FilePath " + _ps_quote(exe)+ + " -WindowStyle Minimized -PassThru"+ )+ if extra_args:+ quoted = ",".join(_ps_quote(a) for a in extra_args)+ cmd = (+ "$p = Start-Process -FilePath " + _ps_quote(exe)+ + " -ArgumentList " + quoted+ + " -WindowStyle Minimized -PassThru"+ )+ cmd += "; if ($p) { Write-Output ('pid=' + $p.Id) } else { Write-Output 'no-process' }"+ try:+ r = subprocess.run(+ ["powershell", "-NoProfile", "-NonInteractive", "-Command", cmd],+ capture_output=True, text=True, timeout=20,+ creationflags=CREATE_NO_WINDOW,+ )+ out = ((r.stdout or "") + (r.stderr or "")).strip()+ ok = r.returncode == 0 and "pid=" in out.lower()+ return {+ "success": ok or r.returncode == 0,+ "via": "Start-Process -WindowStyle Minimized",+ "returncode": r.returncode,+ "outputPreview": out[:300],+ }+ except Exception as e:+ return {"success": False, "via": "Start-Process", "error": str(e)}++ def handle_list_windows(args: dict) -> dict: info = detect_host() title = (args.get("titleContains") or "").strip()@@ -446,127 +530,180 @@ def handle_screenshot(args: dict) -> dict: def handle_open(args: dict) -> dict:- """Launch Grok Bot if installed; if running, list windows. Background by default."""+ """Launch Grok Bot if installed; if running, list windows. Background by default.++ Poll process_list / hwnd after launch. Fall back to Start-Process -WindowStyle+ Minimized when ab launch / desktop_open report success but no process appears.+ Never shell_execute. Never kill Cursor.+ """ info = detect_host() installed = bool(info.get("hostAppInstalled"))- running = bool(info.get("running")) exe = info.get("exe") fallback = info.get("hostAppFallback") want_fg = bool(args.get("foreground") or args.get("showToUser")) reason = (args.get("foregroundReason") or args.get("reason") or "").strip() - if want_fg and not reason:+ if want_fg and len(reason) < _FG_REASON_MIN: return { "success": False, "errorCode": "foreground_reason_required",+ "hwnd": None,+ "opened": False,+ "alreadyOpen": False,+ "launched": False, "windowEtiquette": "background", "openedInBackground": True, "_hint": (- "foreground requested without foregroundReason — pass "- "foreground:true AND foregroundReason:'<why the user needs to see this>'. "+ "foreground requested without a foregroundReason of at least "+ f"{_FG_REASON_MIN} characters — pass foreground:true AND "+ "foregroundReason:'<why the user needs to see this>'. " "Default is BACKGROUND so Grok Bot never steals the keyboard." ), "_next": "Re-call grok_open {\"foreground\":true,\"foregroundReason\":\"...\"} or omit foreground.", } - listed = handle_list_windows(args) if running else None fallback_note = ( " Grok Bot.exe was not found; Cursor is a LAST-RESORT fallback host. Prefer Grok Bot." if fallback else "" ) - if running:- return {- "success": True,- "alreadyRunning": True,- "launched": False,+ hwnd0, win0, proc0 = _host_alive(info)+ listed = handle_list_windows(args) if (hwnd0 or proc0) else None++ def _envelope(*, success, launched, already_open, opened, hwnd, win, via, callback, host_running, extra_hint=""):+ title = _title_of(win) if win else None+ etiq = "foreground" if (want_fg and len(reason) >= _FG_REASON_MIN) else "background"+ out = {+ "success": success,+ "alreadyRunning": bool(already_open or (proc0 and not launched)),+ "alreadyOpen": bool(already_open),+ "opened": bool(opened),+ "launched": bool(launched),+ "hwnd": hwnd,+ "title": title,+ "via": via, "hostApp": HOST_APP, "hostAppInstalled": installed,- "hostAppRunning": True,+ "hostAppRunning": bool(host_running), "hostAppFallback": fallback, "exe": exe,- "windows": (listed or {}).get("windows") or [],- "openedInBackground": True,- "windowEtiquette": "foreground" if (want_fg and reason) else "background",- "_hint": (+ "windows": (listed or handle_list_windows(args)).get("windows") or [] if (hwnd or host_running) else [],+ "openedInBackground": etiq == "background",+ "windowEtiquette": etiq,+ }+ if callback is not None:+ out["callback"] = callback+ if not success:+ out["errorCode"] = out.get("errorCode") or "launch_did_not_start"+ out["_hint"] = extra_hint + fallback_note+ out["_foregroundHint"] = (+ "Window is in the BACKGROUND. To show it: grok_open "+ "{\"foreground\":true,\"foregroundReason\":\"<why the user needs to see this>\"}."+ if etiq == "background" else+ "Raised because foregroundReason was provided."+ )+ return out++ if hwnd0 or proc0:+ if want_fg and hwnd0 and len(reason) >= _FG_REASON_MIN:+ _cb("desktop_set_window_state", {"hwnd": hwnd0, "state": "restore"})+ return _envelope(+ success=True,+ launched=False,+ already_open=bool(hwnd0),+ opened=bool(hwnd0),+ hwnd=hwnd0,+ win=win0,+ via="already-open",+ callback=None,+ host_running=True,+ extra_hint=( "Grok Bot is already running — nothing launched, focus not stolen. "- "Windows listed via ab. To show the user the window, re-call with "- "foreground:true AND foregroundReason. To queue a message: grok_send. "- "App control: grok_close / grok_resize / grok_screenshot."- + fallback_note+ "hwnd/opened/alreadyOpen populated so callers can tell launched vs already up. "+ "To queue a message: grok_send. App control: grok_close / grok_resize / grok_screenshot." ),- "_foregroundHint": (- "Window is in the BACKGROUND. To show it: grok_open "- "{\"foreground\":true,\"foregroundReason\":\"<why>\"}."- ),- }+ ) - # Not running: try ab launch / desktop_open. Never shell_execute. launch_name = fallback if fallback else HOST_APP- launch_args = {"background": True, "noActivate": True}+ launch_args = {"background": True, "noActivate": True, "detached": True} if exe: launch_args["path"] = exe else: launch_args["name"] = launch_name - launched = _cb("launch", launch_args)+ launched_cb = _cb("launch", launch_args) via = "launch"- if not _callback_ok(launched):+ hwnd, win, proc = _poll_host(info, timeout=_POLL_SECONDS)+ if not hwnd and not proc: open_args = {"background": True} if exe: open_args["path"] = exe else: open_args["target"] = launch_name- launched = _cb("desktop_open", open_args)+ launched_cb = _cb("desktop_open", open_args) via = "desktop_open"+ hwnd, win, proc = _poll_host(info, timeout=_POLL_SECONDS)++ start_fb = None+ if not hwnd and not proc:+ start_fb = _start_process_minimized(exe or launch_name)+ via = start_fb.get("via") or "Start-Process -WindowStyle Minimized"+ launched_cb = start_fb+ hwnd, win, proc = _poll_host(info, timeout=20)++ if want_fg and hwnd and len(reason) >= _FG_REASON_MIN:+ _cb("desktop_set_window_state", {"hwnd": hwnd, "state": "restore"}) - ok = _callback_ok(launched)- if not installed and not ok:+ really_up = bool(hwnd or proc)+ if not installed and not really_up: return { "success": False, "errorCode": "host_app_not_installed",+ "hwnd": None,+ "opened": False,+ "alreadyOpen": False,+ "launched": False, "hostApp": HOST_APP, "hostAppInstalled": False, "hostAppRunning": False, "hostAppFallback": fallback,- "launched": False, "via": via,- "callback": launched,+ "callback": launched_cb, "openedInBackground": True, "windowEtiquette": "background", "_hint": ( "Grok Bot isn't installed — want me to open it if present? "- "This verb already tried ab launch / desktop_open for Grok Bot.exe "- "(typical path C:\\Users\\john\\AppData\\Local\\Programs\\Grok Bot\\Grok Bot.exe). "+ "Tried ab launch / desktop_open then Start-Process minimized "+ "(typical path C:\\\\Users\\\\john\\\\AppData\\\\Local\\\\Programs\\\\Grok Bot\\\\Grok Bot.exe). " "Cursor is a last-resort fallback only if Grok Bot.exe is missing. " "Do not send the user to download the host app themselves." ), "_next": "Retry grok_open after Grok Bot exists. Cursor fallback only if Grok Bot.exe is missing.", } - return {- "success": True,- "alreadyRunning": False,- "launched": ok,- "via": via,- "hostApp": HOST_APP,- "hostAppInstalled": installed,- "hostAppRunning": False, # we asked it to start; it may not be up yet- "hostAppFallback": fallback,- "exe": exe,- "callback": launched,- "openedInBackground": not (want_fg and reason),- "windowEtiquette": "foreground" if (want_fg and reason) else "background",- "_hint": (- f"Asked ab `{via}` to start "- + ("Cursor (last-resort fallback; Grok Bot.exe missing) " if fallback else "Grok Bot ")- + (f"at {exe} " if exe else "(by name) ")- + "in the BACKGROUND — keyboard/focus not stolen. "- "Poll grok_readiness / grok_list_windows; then grok_screenshot or grok_send. "- + ("" if ok else "ab callback did not confirm success — the host may still come up; retry grok_list_windows.")- ),- "_foregroundHint": (- "Opened in the BACKGROUND. To show the user: re-call grok_open "- "{\"foreground\":true,\"foregroundReason\":\"<why the user needs to see this>\"}."- ),- }+ hint = (+ f"Asked ab then polled process_list/window (via={via}). "+ + ("Process/window appeared. " if really_up else "NO process after launch/desktop_open poll + Start-Process minimized fallback. ")+ + ("Cursor last-resort fallback. " if fallback else "Grok Bot ")+ + (f"at {exe} " if exe else "(by name) ")+ + "in the BACKGROUND. Poll grok_list_windows / grok_chat_read; then grok_screenshot or grok_send."+ )+ if start_fb is not None:+ hint += " Start-Process -WindowStyle Minimized used because launch/desktop_open did not take."++ out = _envelope(+ success=really_up,+ launched=really_up,+ already_open=False,+ opened=bool(hwnd),+ hwnd=hwnd,+ win=win,+ via=via,+ callback=launched_cb,+ host_running=really_up,+ extra_hint=hint,+ )+ if not really_up:+ out["errorCode"] = "launch_did_not_start"+ out["_next"] = "Retry grok_open {}; confirm Grok Bot.exe path via grok_readiness."+ return out
package.json+10−2@@ -1,6 +1,6 @@ { "slug": "grok-bridge",- "version": "1.2.6",+ "version": "1.3.0", "type": "app", "description": "Grok Bridge lets Adom talk to Grok Bot. Adom's AI environment is for electrical engineers: any provider (Claude, Codex, Grok, Gemini, …), any desktop power tool, through Adom Bridge. grok_* open/close/resize/screenshot/record the app, read the thread, and create bots/channels via webhook.", "keywords": [@@ -89,13 +89,21 @@ "screenshot_computer", "screenshot grok bot computer", "grok computer screenshot",- "grok_computer_screenshot"+ "grok_computer_screenshot",+ "install grok bot",+ "grok_install",+ "install grok bot on another pc",+ "download grok bot" ], "sample_prompts": [ { "label": "Is Grok Bot ready?", "prompt": "Call grok_readiness and tell me if Grok Bot.exe is installed and running" },+ {+ "label": "Install Grok Bot",+ "prompt": "Install Grok Bot on ConfRoomROG with grok_install, poll grok_install_status until done, then open it in the background"+ }, { "label": "Open Grok Bot", "prompt": "Open Grok Bot in the background (grok_open), screenshot it, then resize it"
server.py+23−1@@ -35,9 +35,10 @@ from handlers.app import ( ) from handlers.bots import handle_bot_create, handle_bot_list, handle_channel_create from handlers.describe import handle_describe+from handlers.install import handle_install, handle_install_status from handlers.readiness import handle_prewarm, handle_readiness, handle_status, handle_uninstall from handlers.send import handle_send-from handlers.webhook import handle_webhook_bind, handle_webhook_ensure, handle_webhook_fire, handle_webhook_status+from handlers.webhook import handle_computer_screenshot, handle_run_status, handle_webhook_bind, handle_webhook_ensure, handle_webhook_fire, handle_webhook_status from handlers.chat import handle_chat_read, handle_chat_status from handlers.windows import handle_list_windows, handle_open, handle_screenshot @@ -75,6 +76,10 @@ KNOWN_VERBS = [ "grok_webhook_bind", "grok_webhook_ensure", "grok_webhook_fire",+ "grok_computer_screenshot",+ "grok_install",+ "grok_install_status",+ "grok_run_status", "grok_bot_create", "grok_channel_create", "grok_bot_list",@@ -115,6 +120,10 @@ COMMAND_HANDLERS = { "webhook_bind": handle_webhook_bind, "webhook_ensure": handle_webhook_ensure, "webhook_fire": handle_webhook_fire,+ "computer_screenshot": handle_computer_screenshot,+ "install": handle_install,+ "install_status": handle_install_status,+ "run_status": handle_run_status, "bot_create": handle_bot_create, "channel_create": handle_channel_create, "bot_list": handle_bot_list,@@ -196,6 +205,19 @@ class GrokBridgeHandler(BaseHTTPRequestHandler): path = self.path.split("?", 1)[0] if path in ("/status", "/health"): self.sendJSON(200, self._status_payload())+ elif path in ("/commands", "/verbs"):+ names = list(KNOWN_VERBS)+ self.sendJSON(200, {+ "success": True,+ "prefix": "grok_",+ "commands": names,+ "verbs": names,+ "commandNames": names,+ "_hint": (+ "Grok verbs are grok_* (verbPrefixes). adom-bridge commands is JSON — "+ "grep grok_ against this list or grok_describe.verbs[].name, not unprefixed internals."+ ),+ }) else: self.sendJSON(404, { "success": False,