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
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.
7 files changed
+184−11
BRIDGE_VERSION+1−1@@ -1 +1 @@-1.3.14+1.3.15
CLAUDE.md+7@@ -83,6 +83,13 @@ Grok 429, a shared cooldown (`~/.adom/grok-webhook-cooldown.json`, Retry-After o 300 s). `grok_activity` reads it. `probe_computer` is cached 30 s because Hydrogen polls readiness thousands of times a day. +Chat route + wake (1.3.15, wiki #8-#10): `handlers/compose.py`. `grok_chat_send` sets the Prompt+Edit (desktop_ui_set text) and invokes the "Send message" button, refuses on a user draft, always+prefixes `[from AI thread "..." via grok-bridge, not typed by John]`, shares activity.admit's budget.+`grok_wake {minutes<=30, reason}` asks the agent to `sleep`, because the agent computer suspends+between runs and Grok Bot has no keep-awake setting. A Grok rate-limit 400 parses+`rateLimitPerMinute` / `currentCount` and suggests chat_send.+ Webhook bind file: `~/.adom/grok-webhook.json` `{url, key, header, boundAt}` (never log the key). `grok_webhook_ensure` reads UIA Edits Webhook URL / Webhook key (no OCR). ## The publish loop: insiders first, public on a human's words (wiki #2)
bridge.json+6−3@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "grok", "displayName": "Grok Bot",- "version": "1.3.14",+ "version": "1.3.15", "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.",@@ -51,7 +51,9 @@ "grok_install", "grok_install_status", "grok_settings",- "grok_activity"+ "grok_activity",+ "grok_chat_send",+ "grok_wake" ], "statusVerb": "grok_readiness", "timeouts": {@@ -61,7 +63,8 @@ "computer_screenshot": 330, "open": 120, "webhook_ensure": 120,- "install": 120+ "install": 120,+ "wake": 120 } }, "dependencies": {
handlers/compose.pyadded+136@@ -0,0 +1,136 @@+"""grok_chat_send + grok_wake (1.3.15, wiki #8 / #9 / #10).++grok_chat_send: a second route into the grok-bridge agent when the webhook is refused (#8).+Sets the grok-bridge chat's Prompt box through UIA ValuePattern and invokes its "Send message"+button: programmatic, no keyboard, no focus steal (measured on AdomLapper 2026-10-15). The+message lands in the user's own chat, so it shows as the user. To keep the agent's approval+rule honest, every message carries a fixed prefix naming the AI thread; it cannot be turned off.+It refuses when the Prompt box already holds text (the user's draft is never overwritten).++grok_wake: Grok's agent computer pauses a few minutes after an agent run ends and resumes when+the next run starts (#9). There is no Grok Bot setting to keep it up (Settings > General /+Computer / Usage checked 2026-10-15). An agent run that is still working keeps it running, so+grok_wake starts a run that only sleeps for N minutes. It costs Grok usage like any agent run.+"""++from __future__ import annotations++import secrets+import time++import caller_identity+from handlers import activity+from handlers.chat import _select_sidebar_thread+from handlers.windows import _callback_ok, flatten_callback, grok_hwnd++PROMPT = {"name": "Prompt", "role": "Edit"}+SEND = {"name": "Send message", "role": "Button"}+MIN_REASON = 10+WAKE_MAX_MIN = 30+++def _ui(cmd: str, args: dict, timeout: int = 10) -> dict:+ from handlers.windows import _cb+ return _cb(cmd, args, timeout=timeout)+++def _prompt_text(hwnd: int) -> str | None:+ res = _ui("desktop_find_control", dict(PROMPT, hwnd=hwnd))+ m = flatten_callback(res)+ best = (m.get("best") or m.get("control")) if isinstance(m, dict) else None+ if not isinstance(best, dict):+ return None+ got = _ui("desktop_ui_get", dict(PROMPT, hwnd=hwnd))+ g = flatten_callback(got)+ if isinstance(g, dict):+ for k in ("value", "text", "Value"):+ if isinstance(g.get(k), str):+ return g[k]+ return ""+++def chat_send(message: str, *, reason: str, thread: str = "grok-bridge", count_budget: bool = True) -> dict:+ who = caller_identity.thread_name() or "an unnamed AI thread"+ message = (message or "").strip()+ if not message:+ return {"success": False, "errorCode": "message_required", "_hint": "Pass message."}+ if len((reason or "").strip()) < MIN_REASON:+ return {"success": False, "errorCode": "reason_required",+ "_hint": f"grok_chat_send types into the user's own Grok Bot chat; pass reason (>= {MIN_REASON} chars)."}+ if count_budget:+ refusal = activity.admit(who)+ if refusal and refusal["throttledBy"] != "grok": # Grok's webhook throttle does not apply to chat+ return {"success": False, "sent": False, "errorCode": "webhook_throttled",+ "throttledBy": refusal["throttledBy"], "retryAfterSeconds": refusal["retryAfterSeconds"],+ "_hint": "Not sent: " + refusal["why"] + " The chat and the webhook share one budget."}+ hwnd, _win = grok_hwnd(allow_cursor=False)+ if not hwnd:+ return {"success": False, "errorCode": "window_not_found",+ "_hint": "Grok Bot's main window is not visible. grok_open {} first (a hidden window cannot be typed into)."}+ _select_sidebar_thread(hwnd, thread)+ current = _prompt_text(hwnd)+ if current is None:+ return {"success": False, "errorCode": "prompt_not_found",+ "_hint": "The Prompt box is not exposed to UIA. If grok_chat_status says launchedWithAccessibility:false, "+ "grok_open {\"restart\":true,...} first."}+ if current.strip():+ return {"success": False, "errorCode": "prompt_busy",+ "_hint": "The Prompt box already holds text (likely the user's draft). Not overwritten, not sent."}+ text = f'[from AI thread "{who}" via grok-bridge, not typed by John] {message}'+ st = _ui("desktop_ui_set", dict(PROMPT, hwnd=hwnd, text=text))+ if not _callback_ok(st) or (isinstance(st, dict) and st.get("error")):+ return {"success": False, "errorCode": "prompt_set_failed", "callback": st}+ time.sleep(0.6)+ clicked = _ui("desktop_ui_click", dict(SEND, hwnd=hwnd))+ ok = _callback_ok(clicked) and not (isinstance(clicked, dict) and clicked.get("error"))+ after = _prompt_text(hwnd)+ sent = ok and (after is not None and not after.strip())+ if not sent:+ _ui("desktop_ui_set", dict(PROMPT, hwnd=hwnd, text="")) # never leave our text in the user's box+ return {+ "success": sent, "sent": sent, "via": "chat", "thread": thread, "hwnd": hwnd,+ **({} if sent else {"errorCode": "send_not_confirmed", "clickCallback": clicked}),+ "_hint": ("Typed into the user's grok-bridge chat through UIA (no focus steal) and pressed Send message. "+ "It shows as the user's message with the 'from AI thread' prefix, so the agent treats it as an "+ "AI request, not the user's approval. Read the reply with grok_chat_read."+ if sent else "Send was not confirmed; the Prompt box was cleared again."),+ }+++def handle_chat_send(args: dict) -> dict:+ return chat_send(str(args.get("message") or ""), reason=str(args.get("reason") or ""),+ thread=str(args.get("thread") or "grok-bridge"))+++def handle_wake(args: dict) -> dict:+ """Start an agent run that only sleeps, so the agent computer stays up for N minutes."""+ from handlers.webhook import handle_webhook_fire+ reason = str(args.get("reason") or "").strip()+ if len(reason) < MIN_REASON:+ return {"success": False, "errorCode": "reason_required",+ "_hint": f"grok_wake starts a Grok agent run (it costs Grok usage); pass reason (>= {MIN_REASON} chars)."}+ try:+ minutes = int(args.get("minutes") or 10)+ except (TypeError, ValueError):+ minutes = 10+ minutes = max(1, min(WAKE_MAX_MIN, minutes))+ nonce = "WAKE-" + secrets.token_hex(4)+ msg = (f"Keep your computer awake for {minutes} minutes for {caller_identity.thread_name() or 'an AI thread'} "+ f"({reason}). Run exactly `sleep {minutes * 60}` in your terminal and nothing else, then reply "+ f"with just: {nonce} awake-done.")+ fire = handle_webhook_fire({"event": "ping", "payload": {"nonce": nonce, "message": msg}})+ via = "webhook"+ if not fire.get("success") and fire.get("errorCode") in ("webhook_throttled", "webhook_http_error", "bot_unavailable"):+ fire = chat_send(f"{nonce}: {msg}", reason="wake the agent computer: " + reason)+ via = "chat"+ out = {+ "success": bool(fire.get("success")), "via": via, "nonce": nonce, "minutes": minutes,+ "awakeUntilApprox": time.strftime("%H:%M", time.localtime(time.time() + minutes * 60 + 60)),+ "result": {k: fire.get(k) for k in ("errorCode", "httpStatus", "runUuid", "throttledBy", "retryAfterSeconds")},+ "_hint": (f"Asked the agent (via {via}) to sleep {minutes} min, which keeps its computer running. It resumes "+ "within about a minute: check `adom-bridge targets` for grok-bot-vm-155244935. It pauses again a few "+ f"minutes after the run ends; the reply '{nonce} awake-done' marks the end. Each wake costs Grok usage."),+ }+ if not out["success"]:+ out["errorCode"] = fire.get("errorCode") or "wake_failed"+ return out
handlers/describe.py+14@@ -224,6 +224,20 @@ VERB_CATALOG = { "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,
handlers/webhook.py+16−6@@ -486,18 +486,28 @@ def handle_webhook_fire(args: dict) -> dict: low = resp_text.lower() if status == 429 or "rate limit" in low or "too many requests" in low or "throttl" in low: from handlers import activity+ import re as _re wait = activity.note_throttled(status, e.headers.get("Retry-After") if e.headers else None)- return {+ m_lim = _re.search(r"limit\D{0,20}?(\d+)\s*per\s*minute", low)+ m_cnt = _re.search(r"current_count\D{0,3}(\d+)", low)+ out = { "success": False, "fired": False, "event": event, "errorCode": "webhook_throttled", "throttledBy": "grok", "httpStatus": status, "retryAfterSeconds": wait, "urlHost": url_host(url), "bodyPreview": resp_text[:300],- "_hint": (f"Grok throttled the webhook (HTTP {status}). This PC now holds every fire for "- f"{wait} s (Retry-After when Grok sends one) so the throttle is not extended; all "- "AI threads share this mailbox. Do not retry before then. grok_activity shows "- "which threads have been firing."),- "_next": "grok_activity {\"minutes\":10}",+ "_hint": (f"Grok refused the webhook for its rate limit (HTTP {status}). Wait or stop; do NOT "+ f"re-run grok_webhook_ensure (the URL and key are fine). This PC holds every fire for "+ f"{wait} s so the limit is not pushed further; all AI threads share this mailbox. "+ "On 2026-10-15 Grok's automation counter sat at its cap for over an hour while no "+ "grok-bridge PC was firing, so the load can be outside grok-bridge. Urgent and cannot "+ "wait: grok_chat_send {message, reason} reaches the agent through the chat instead."),+ "_next": "grok_chat_send {\"message\":\"...\",\"reason\":\"...\"} if urgent; else wait retryAfterSeconds", }+ if m_lim:+ out["rateLimitPerMinute"] = int(m_lim.group(1))+ if m_cnt:+ out["currentCount"] = int(m_cnt.group(1))+ return out if "bot is unavailable" in low: return { "success": False,
server.py+4−1@@ -43,6 +43,7 @@ from handlers.send import handle_send 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 import activity+from handlers.compose import handle_chat_send, handle_wake from handlers.windows import handle_list_windows, handle_open, handle_screenshot DEFAULT_PORT = 8769@@ -121,7 +122,7 @@ def handle_activity(args: dict) -> dict: out["_hint"] = ( f"Last {minutes} min on this PC: {out['calls']} calls; webhook fires {w['fires']} " f"(sent {w['sent']}, throttled by Grok {w['throttledByGrok']}, refused by the local budget "- f"{w['refusedByBudget']}). byCallerVerb names the threads. Filter with verb / caller; "+ f"{w['refusedByBudget']}). byCallerVerb names the threads. Filter with verb / thread; " "limit sets how many recent rows come back." ) return out@@ -159,6 +160,8 @@ COMMAND_HANDLERS = { "channel_create": handle_channel_create, "bot_list": handle_bot_list, "activity": handle_activity,+ "chat_send": handle_chat_send,+ "wake": handle_wake, }