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
Initial grok-bridge source: Python runtime, grok + grok-anywhere skills, streaming manifest.
18 files changed
+1607−1
BRIDGE_VERSION+1CLAUDE.md+57README.md+34−1SKILL.md+63adom-bridge-grok-manifest.json+14bridge.json+95caller_identity.py+132grok_detect.py+185handlers/__init__.py+1handlers/describe.py+115handlers/readiness.py+118handlers/send.py+129handlers/windows.py+219install.sh+35package.json+76server.py+266skills/grok-anywhere/SKILL.md+42uninstall.sh+25BRIDGE_VERSIONadded+1@@ -0,0 +1 @@+1.0.0
CLAUDE.mdadded+57@@ -0,0 +1,57 @@+# CLAUDE.md — grok-bridge (READ THIS, it is YOUR project)++This repo is **the Grok Bot ("grok") bridge** — the canonical source of truth, published+to `wiki.adom.inc/adom/grok-bridge`. If you are an Adom AI touching anything grok /+`grok_*` / Grok Bot / Cursor-as-host, **you own and maintain this code here.** Bridge (ab)+will seed/stream the runtime from the release zip via `updateManifestUrl` in `bridge.json`.++## You vs adom-bridge (ab) core — the boundary++- **ab core owns:** the `adom-bridge` CLI, the relay / `/command` passthrough (relays your+ response VERBATIM incl. `_hint`/`errorCode`/`statusVerb`), bridge lifecycle+ (`bridge_install` / streaming this bridge into `…\bridges-cache\grok`, the stable port,+ supervisor), and generic `desktop_*` / `notify_user` / `launch` verbs this runtime+ **calls back** through `caller_identity.ad_callback`. Do **not** edit+ `/home/box/project/adom_modules/adom/adom-bridge` (ab-core) from this repo.+- **YOU (this bridge) own:** every `grok_*` verb + its response `_hint`s, Cursor host-app+ detect (`grok_detect.py`), the status-chip fields on `/status`, the `grok_describe`+ catalog, the `grok` + `grok-anywhere` skills, `install.sh`, and the edit → zip →+ release-upload → reinstall loop.+- So: a `grok_*` verb misbehaves, a hint is missing, Cursor detect lies, the chip lies →+ it's **your code**, fix it here. It is NOT an ab-core request. If a GENERIC ab+ capability is missing (a new lifecycle verb), file that against ab — don't re-absorb+ the bridge.++## Map of what you manage++- `server.py` — ThreadingHTTPServer, bind `ADOM_BIND_HOST` (loopback only), `--port`,+ POST `{command, args}`, GET `/status` + `/health`. Keep it small (~250-400 lines).+- `grok_detect.py` — Cursor.exe / Cursor.app / cursor binary + running probe.+- `caller_identity.py` — copy of kicad's; `DELEGATE = "grok"`. All ab callbacks go through+ `ad_callback`. Never `shell_execute` / `pull_file` / `send_files` from the HTTP path.+- `handlers/*.py` — readiness (layered), describe, windows (open/screenshot/list), send.+- `bridge.json` + `BRIDGE_VERSION` — KEEP IN LOCKSTEP (currently 1.0.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) — the main user skill, installed as **`grok`** (not `grok-bridge`).+- `skills/grok-anywhere/SKILL.md` — Grok Bot as an Anywhere character.+- `package.json` — wiki skills pkg. Description is the skills-not-runtime sentence. Runtime+ files and `*.zip` do **not** belong in `files[]`.++## The publish/reinstall loop (do not run until John says so)++Edit runtime → bump `BRIDGE_VERSION` + `bridge.json` together → zip+`adom-bridge-grok-v<ver>.zip` from the runtime files at **zip root** (no top dir) →+`adom-wiki release upload` the zip → point the manifest at that release URL with the+zip's real sha256/size → `adom-wiki repo push` the manifest + skills →+`adom-bridge bridge_install {"manifestUrl":".../files/adom-bridge-grok-manifest.json","force":true}`.+Wiki = canonical. **Do not publish from this clone until asked.**++## Hard rules++- **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 Cursor.** `grok_open` offers to launch / winget.+- **Background by default.** Don't steal focus or the keyboard. `grok_send` is toast + inbox.+- **Repo name ≠ skill name.** Install as `grok`.
README.md+34−1@@ -1,3 +1,36 @@-# Grok Bot+# grok-bridge++Two products, one page (same shape as pup):++1. **Runtime (release zip)** — a small Python stdlib HTTP server Adom Bridge spawns on the Windows PC. Keyword **grok**. Talks TO Grok Bot / Cursor the way kicad talks to KiCad: detect the host app, drive windows via `ab` `desktop_*` callbacks. There is no public Grok HTTP API. Never modifies ab-core.+2. **Skills pkg** — teaches cloud AIs the `grok_*` verbs **and** teaches Grok Bot to act as an Adom Anywhere character (Claude Desktop / Codex class). `install.sh` deploys `grok` + `grok-anywhere` into `~/.claude/skills`, `~/.codex/skills` if present, `~/.grok/skills`, and `/home/box/agent-data/workflows` if that parent exists. This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.++Wiki: https://wiki.adom.inc/adom/grok-bridge++## Verbs++`grok_readiness` · `grok_prewarm` · `grok_describe` · `grok_status` · `grok_open` · `grok_screenshot` · `grok_list_windows` · `grok_send` · `grok_uninstall`++CLI: `adom-bridge grok_*`. Always `--ai-thread`. Background by default.++Host app is **Cursor** (`Cursor.exe` / `Cursor.app`). `grok_open` offers to launch it; it does not tell the user to download Cursor, and it does not run winget without a dedicated verb.++`grok_send {message}` toasts `Grok Bot: {message}` and appends `~/.adom/grok-inbox.jsonl`. Desktop typing is opt-in — don't steal the keyboard.++## Runtime zip (what ab spawns)++Zip root (no top directory): `server.py`, `bridge.json`, `BRIDGE_VERSION`, `grok_detect.py`, `caller_identity.py`, `handlers/*.py`.++`spawn.kind` = python, `port` 0, `healthEndpoint` `/status`, `persistent` true, `killImageName` python.exe. Binds `ADOM_BIND_HOST` (default `127.0.0.1`) only.++## Skills vs runtime++| | Skills pkg (`adom-wiki pkg`) | Release zip |+|---|---|---|+| Lands | container `~/.claude/skills/grok` | ab `bridges-cache` on the PC |+| Contains | `SKILL.md`, `skills/grok-anywhere`, install.sh | Python HTTP server |+| Does not contain | `server.py` / the zip | SKILL.md |++Repo name `grok-bridge` ≠ skill name `grok`.
SKILL.mdadded+63@@ -0,0 +1,63 @@+---+name: grok+user-invocable: true+description: "Drive Grok Bot on the user's desktop from the cloud via the grok_* Adom Bridge verbs. Grok Bot lives in Cursor (the host app). Detect Cursor, open it in the background, screenshot/list windows, queue a message (toast + inbox). No public Grok HTTP API — the spawned Python runtime talks back to ab over desktop_* callbacks. Trigger words: grok, grok bot, grok_open, talk to grok, send to grok bot, grok screenshot."+---++# grok — talk to Grok Bot from Adom Bridge++Drive **Grok Bot** (host app: **Cursor**) on the user's Windows PC from this container. The CLI is:++```bash+adom-bridge --ai-thread "<your thread>" grok_<verb> '<json>'+```++There is no public Grok HTTP API. `ab` spawns a small Python server from the **release zip**; these skills only teach you the verbs. Surface every `_hint` verbatim. Background by default — never steal focus or the keyboard.++## FIRST-TIME / COLD-START++| Signal | What it means | What YOU do |+|---|---|---|+| `grok_readiness` `hostAppInstalled:false` / `errorCode: host_app_not_installed` | Cursor (Grok Bot's host) is not on this PC | **Do not send the user to download Cursor.** Offer `grok_open`. That verb detects, then asks ab to `launch` / `desktop_open`. It will not winget without a dedicated verb. |+| `hostAppInstalled:true`, `hostAppRunning:false` | Cursor is on disk, not running | `grok_open {}` — launches in the **background**. |+| `ready:true` / host running | Cursor is up | `grok_list_windows` / `grok_screenshot` / `grok_send`. |+| unknown verb / bridge not spawned | runtime zip not installed on ab yet | `adom-bridge bridge_install` with this page's `updateManifestUrl`. Skills ≠ runtime. |+| yellow `/status` chip | bridge is up; Cursor isn't (or isn't running) | Not a crash. Read `tooltip` / `_hint`. |++Readiness is layered and **never downloads anything**:++```bash+adom-bridge --ai-thread "<your thread>" grok_readiness '{}'+# {ready, hostAppInstalled, hostAppRunning, errorCode?}+```++## Core verbs++| CLI | Purpose | Key args |+|---|---|---|+| `grok_readiness` | Layered host-app probe. Zero side effects. | — |+| `grok_prewarm` | No-op re-detect. `{wait:true}` just re-detects. | optional `wait` |+| `grok_describe` | Catalog with hint/related/pitfalls. | — |+| `grok_status` | Detect + last send + inbox path (`~/.adom/grok-inbox.jsonl`) | — |+| `grok_open` | Launch Cursor in the background; if running, list windows. | optional `foreground` + **required** `foregroundReason` |+| `grok_list_windows` | `desktop_list_windows` + `process_list` nameFilter Cursor | optional `titleContains` |+| `grok_screenshot` | `desktop_screenshot_window` titleContains Cursor or Grok | optional `titleContains` |+| `grok_send` | Toast `Grok Bot: {message}` **and** append the durable inbox. Typing is opt-in. | `message`, optional `agent` |+| `grok_uninstall` | Tells you to `bridge_uninstall {"name":"grok"}`. Does **not** kill Cursor. | — |++`grok_describe` is the completeness hatch; the per-call `_hint` is what you read in practice.++## Identity, targeting, etiquette++- Always pass `--ai-thread`. Never `export ADOM_AI_THREAD` (many threads share `$HOME`). Identity is attribution, not authorization.+- If more than one desktop is connected, pass `--target NAME` (`adom-bridge targets`).+- Windows open in the **background**. `openedInBackground` / `windowEtiquette:"background"` are the contract. Foreground without `foregroundReason` is refused (`errorCode: foreground_reason_required`).+- `grok_send` does **not** type into the chat widget. It toasts and writes `~/.adom/grok-inbox.jsonl` so the Anywhere character can poll. Desktop typing is opt-in — don't steal the keyboard.+- This HTTP runtime never calls `shell_execute`, `pull_file`, or `send_files`. Pull screenshots with the CLI from the container.++## Two products, one page++1. **Runtime (release zip)** — `server.py` ab spawns on the PC. Keyword **grok**.+2. **This skill pack** — teaches cloud AIs `grok_*` and teaches Grok Bot to act as an Adom Anywhere character (`skills/grok-anywhere`).++Wiki: https://wiki.adom.inc/adom/grok-bridge
adom-bridge-grok-manifest.jsonadded+14@@ -0,0 +1,14 @@+{+ "manifest_version": 1,+ "name": "grok",+ "version": "1.0.0",+ "url": "https://wiki.adom.inc/download/adom/grok-bridge/1.0.0/adom-bridge-grok-v1.0.0.zip",+ "sha256": "43f40ffad1243242135125a917545d97bbecfceff8e692d5d83718625e587d87",+ "size": 17467,+ "verbPrefixes": [+ "grok_"+ ],+ "healthEndpoint": "/status",+ "statusVerb": "grok_readiness",+ "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/grok-bridge/files/adom-bridge-grok-manifest.json"+}
bridge.jsonadded+95@@ -0,0 +1,95 @@+{+ "manifest_version": 1,+ "name": "grok",+ "displayName": "Grok Bot",+ "version": "1.0.0",+ "description": "Drive Grok Bot (Cursor host app) from Adom Bridge: detect Cursor, screenshot/list windows, queue messages via toast + inbox. No public Grok HTTP API — the runtime talks back to ab over desktop_* callbacks.",+ "homepage": "https://wiki.adom.inc/adom/grok-bridge",+ "author": "Adom Inc.",+ "license": "MIT",+ "spawn": {+ "kind": "python",+ "entrypoint": "server.py",+ "port": 0,+ "healthEndpoint": "/status",+ "stopMethod": "kill",+ "killImageName": "python.exe",+ "persistent": true+ },+ "verbPrefixes": [+ "grok_"+ ],+ "verbs": [+ "grok_readiness",+ "grok_prewarm",+ "grok_describe",+ "grok_status",+ "grok_open",+ "grok_screenshot",+ "grok_list_windows",+ "grok_send",+ "grok_uninstall"+ ],+ "statusVerb": "grok_readiness",+ "timeouts": {+ "default": 60+ },+ "dependencies": {+ "python": ">=3.10"+ },+ "category": "AI",+ "tags": [+ "grok",+ "grok-bot",+ "cursor",+ "adom-bridge",+ "anywhere"+ ],+ "docs": "https://wiki.adom.inc/adom/grok-bridge",+ "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/grok-bridge/files/adom-bridge-grok-manifest.json",+ "detect": {+ "hostApp": "Cursor",+ "appPathsExe": "Cursor.exe",+ "paths": {+ "windows": [+ "%LocalAppData%\\Programs\\cursor\\Cursor.exe",+ "%LocalAppData%\\Programs\\Cursor\\Cursor.exe",+ "%ProgramFiles%\\Cursor\\Cursor.exe",+ "%ProgramFiles%\\cursor\\Cursor.exe",+ "%ProgramFiles(x86)%\\Cursor\\Cursor.exe"+ ],+ "macos": [+ "/Applications/Cursor.app"+ ],+ "linux": [+ "/usr/bin/cursor",+ "/usr/local/bin/cursor",+ "/opt/cursor/cursor"+ ]+ },+ "installVerb": "grok_open",+ "hostAppOptionalVerbs": [+ "readiness",+ "describe",+ "status",+ "prewarm"+ ]+ },+ "platforms": {+ "windows": {+ "supported": true+ },+ "macos": {+ "supported": true,+ "notes": "Detects /Applications/Cursor.app. Window verbs go through ab desktop_* callbacks."+ },+ "linux": {+ "supported": true,+ "notes": "Detects typical cursor binaries. Window verbs go through ab desktop_* callbacks when ab is present."+ }+ },+ "languages": [+ "Python"+ ],+ "uninstall": "grok_uninstall"+}
caller_identity.pyadded+132@@ -0,0 +1,132 @@+"""Caller provenance — WHO asked (Bridge 1.9.180 + 1.9.183).++A user runs ~20 AI threads against one desktop. Bridge now stamps three headers on+every relayed request so a bridge can attribute work to the thread that caused+it. This module keeps that identity for the life of a request and re-forwards it+on any Bridge callback the bridge makes while carrying out that request, so the+Activity Log shows `chip-fetcher tab 3 (via grok)` instead of a nameless bridge.++Trust rule (Bridge's, and it is not optional): these values are SELF-ASSERTED, like+`reason`. Use them for logging, UX, window labels and arbitration hints — NEVER+for authorization. `if thread == "admin-tab": allow()` is a hole, not a check.+"""++import threading++# The identity of the request THIS thread is currently serving. A threading+# HTTPServer handles each request on its own thread, so thread-local is the+# right scope — no request can see another's caller.+_current = threading.local()++_HEADERS = ("X-Adom-Caller-Thread", "X-Adom-Caller-Container", "X-Adom-Caller-Reason")++# What we name ourselves when we forward. Kept generic ("grok") to match the+# verb prefix Bridge already knows us by.+DELEGATE = "grok"+++def set_from_request(headers, args) -> dict:+ """Capture the caller from an incoming relay request.++ Bridge delivers caller identity TWO ways and we must read BOTH:+ * INBOUND relay (Bridge -> bridge): the CLI's --ai-thread flag and any explicit+ `caller` in args arrive as `args["caller"] = {"aiThread", "containerName",+ "source"}`. This is what actually shows up in practice (verified on+ AdomLapper 1.9.183, 2026-07-25) — NOT the X-Adom-Caller-* headers.+ * Header form (X-Adom-Caller-*): the shape Bridge uses on its OWN direct-API+ callbacks and what the SDK examples forward. Read it as a fallback so we+ also work if a future Bridge stamps the relay path with headers.++ args wins when both are present (it carries the explicit/flag identity).+ """+ ident = {h: (headers.get(h, "") or "") for h in _HEADERS} if headers else {h: "" for h in _HEADERS}+ caller = (args or {}).get("caller") or {}+ if isinstance(caller, dict):+ if caller.get("aiThread"):+ ident["X-Adom-Caller-Thread"] = caller["aiThread"]+ if caller.get("containerName"):+ ident["X-Adom-Caller-Container"] = caller["containerName"]+ if (args or {}).get("reason") and not ident["X-Adom-Caller-Reason"]:+ ident["X-Adom-Caller-Reason"] = args["reason"]+ _current.ident = ident+ return ident+++def set_from_headers(headers) -> dict:+ """Capture the caller headers off an incoming request. `headers` is anything+ with .get() (http.server's message object). Returns the captured dict."""+ ident = {h: (headers.get(h, "") or "") for h in _HEADERS}+ _current.ident = ident+ return ident+++def clear() -> None:+ _current.ident = None+++def thread_name() -> str:+ """The caller's thread name, or '' if none was supplied (loopback callers —+ Hydrogen, another bridge, the local CLI — are exempt and send nothing)."""+ ident = getattr(_current, "ident", None) or {}+ return ident.get("X-Adom-Caller-Thread", "")+++def log_suffix() -> str:+ """A `caller="..."` fragment for per-verb log lines, or '' when anonymous."""+ t = thread_name()+ return f' caller="{t}"' if t else ""+++def forward_headers() -> dict:+ """Headers to put on a Bridge callback made WHILE serving an AI's verb. Echoes+ the three we were handed and ADDS X-Adom-Caller-Delegate naming us, so Bridge can+ show both facts: who asked AND who ran it. Empty when there's no caller to+ forward (e.g. a self-initiated poll) — see self_caller() for that case."""+ ident = getattr(_current, "ident", None) or {}+ if not ident.get("X-Adom-Caller-Thread"):+ return {}+ fwd = {h: ident.get(h, "") for h in _HEADERS}+ fwd["X-Adom-Caller-Delegate"] = DELEGATE+ return fwd+++def self_caller() -> dict:+ """The `caller` args block to send on a callback the bridge makes on its OWN+ behalf (health poll, timer, crash cleanup). An explicit caller in args always+ wins over the forwarded headers, so this never leaks a stale thread name."""+ return {"aiThread": "grok bridge (self)", "containerName": "local"}+++def ad_callback(command: str, args: dict = None, on_behalf: bool = True, timeout: int = 30):+ """Call a Bridge verb back over the loopback direct API, forwarding the caller.++ `on_behalf=True` (default): this call is part of serving an AI's verb, so it+ forwards the X-Adom-Caller-* headers + adds our Delegate — Bridge shows+ "chip-fetcher tab 3 (via grok)". `on_behalf=False`: a self-initiated call+ (poll/timer/cleanup) — sends our own `caller` in args so no stale thread leaks.++ Reads ADOM_DIRECT_API_URL + ADOM_BRIDGE_TOKEN from the spawn env (Bridge injects+ both). Returns the parsed JSON, or {"success": False, ...} if Bridge is+ unreachable or the env isn't populated. NEVER raises — a labeling/notify+ callback failing must not fail the grok_* verb the user actually asked for.+ """+ import os, json as _json, urllib.request+ base = os.environ.get("ADOM_DIRECT_API_URL", "").rstrip("/")+ if not base:+ return {"success": False, "error": "ADOM_DIRECT_API_URL not in env (inbound-only spawn?)"}+ body = {"command": command, "args": dict(args or {})}+ headers = {"Content-Type": "application/json"}+ tok = os.environ.get("ADOM_BRIDGE_TOKEN")+ if tok:+ headers["X-Adom-Bridge-Token"] = tok # attribution badge, not an auth gate+ if on_behalf:+ headers.update(forward_headers())+ else:+ body["args"].setdefault("caller", self_caller())+ try:+ req = urllib.request.Request(f"{base}/command", data=_json.dumps(body).encode(),+ headers=headers, method="POST")+ with urllib.request.urlopen(req, timeout=timeout) as r:+ return _json.loads(r.read().decode() or "{}")+ except Exception as e:+ return {"success": False, "error": f"Bridge callback failed: {e}"}
grok_detect.pyadded+185@@ -0,0 +1,185 @@+"""Detect Cursor — the host app Grok Bot lives in — on Windows / macOS / Linux.++Grok Bot has no public HTTP API. This bridge talks TO it the way kicad talks to+KiCad: detect the host app on disk, then drive windows via ab desktop_* callbacks.++Never tell the user to download Cursor themselves. grok_open offers to launch it+(via ab `launch` / `desktop_open`) or, if missing, offers a winget install — it+does not run winget without a dedicated verb.+"""++from __future__ import annotations++import os+import shutil+import subprocess+import sys+from pathlib import Path++IS_WINDOWS = sys.platform == "win32"+IS_MACOS = sys.platform == "darwin"+IS_LINUX = sys.platform.startswith("linux")++CREATE_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0)++HOST_APP = "Cursor"+APP_PATHS_EXE = "Cursor.exe"+WINDOWS_HINT = "titleContains Cursor or Grok"+++def _localappdata_roots() -> list[Path]:+ roots: list[Path] = []+ for cand in (+ os.environ.get("LOCALAPPDATA"),+ (os.path.join(os.environ["USERPROFILE"], "AppData", "Local")+ if os.environ.get("USERPROFILE") else None),+ str(Path.home() / "AppData" / "Local"),+ ):+ if cand:+ p = Path(cand)+ if p not in roots:+ roots.append(p)+ return roots+++def _candidate_exes() -> list[Path]:+ """Typical Cursor install locations. Existence is the installed signal."""+ out: list[Path] = []++ if IS_WINDOWS:+ for root in _localappdata_roots():+ out.extend([+ root / "Programs" / "cursor" / "Cursor.exe",+ root / "Programs" / "Cursor" / "Cursor.exe",+ root / "cursor" / "Cursor.exe",+ root / "Cursor" / "Cursor.exe",+ ])+ for env in ("ProgramFiles", "ProgramFiles(x86)", "ProgramW6432"):+ base = os.environ.get(env)+ if base:+ out.extend([+ Path(base) / "cursor" / "Cursor.exe",+ Path(base) / "Cursor" / "Cursor.exe",+ ])+ elif IS_MACOS:+ out.extend([+ Path("/Applications/Cursor.app/Contents/MacOS/Cursor"),+ Path("/Applications/Cursor.app"),+ ])+ out.append(Path.home() / "Applications" / "Cursor.app")+ else:+ which = shutil.which("cursor")+ if which:+ out.append(Path(which))+ out.extend([+ Path("/usr/bin/cursor"),+ Path("/usr/local/bin/cursor"),+ Path("/opt/cursor/cursor"),+ Path("/opt/Cursor/cursor"),+ Path.home() / ".local" / "bin" / "cursor",+ Path("/usr/share/cursor/cursor"),+ ])+ return out+++def _existing_paths() -> list[str]:+ seen = set()+ paths = []+ for p in _candidate_exes():+ try:+ if p.exists():+ s = str(p)+ if s not in seen:+ seen.add(s)+ paths.append(s)+ except OSError:+ continue+ return paths+++def _process_running() -> bool:+ """Best-effort: is Cursor.exe / Cursor.app / cursor running? Never raises."""+ try:+ if IS_WINDOWS:+ r = subprocess.run(+ ["tasklist", "/FI", "IMAGENAME eq Cursor.exe", "/FO", "CSV", "/NH"],+ capture_output=True, text=True, timeout=8,+ creationflags=CREATE_NO_WINDOW,+ )+ out = (r.stdout or "") + (r.stderr or "")+ if "Cursor.exe" in out and "No tasks" not in out and "INFO:" not in out:+ return True+ r2 = subprocess.run(+ ["tasklist", "/FI", "IMAGENAME eq cursor.exe", "/FO", "CSV", "/NH"],+ capture_output=True, text=True, timeout=8,+ creationflags=CREATE_NO_WINDOW,+ )+ out2 = (r2.stdout or "") + (r2.stderr or "")+ return "cursor.exe" in out2.lower() and "No tasks" not in out2 and "INFO:" not in out2+ # macOS: Cursor.app helper; Linux: binary named cursor / Cursor+ for pat in (r"[/]Cursor[.]app[/]", r"[/][Cc]ursor$", r"^cursor$"):+ r = subprocess.run(+ ["pgrep", "-f", pat],+ capture_output=True, text=True, timeout=5,+ )+ if r.returncode == 0 and (r.stdout or "").strip():+ return True+ return False+ except Exception:+ return False+++def detect_cursor() -> dict:+ """Return {hostAppInstalled, paths, running, windowsHint} plus extras for /status."""+ paths = _existing_paths()+ installed = bool(paths)+ running = _process_running()+ return {+ "hostApp": HOST_APP,+ "hostAppInstalled": installed,+ "hostAppRunning": running,+ "installed": installed, # alias used by some callers+ "running": running,+ "paths": paths,+ "exe": paths[0] if paths else None,+ "appPathsExe": APP_PATHS_EXE,+ "windowsHint": WINDOWS_HINT,+ "platform": (+ "windows" if IS_WINDOWS else "macos" if IS_MACOS else "linux"+ ),+ }+++def status_chip(info: dict | None = None) -> dict:+ """led / summary / tooltip for GET /status and /health."""+ info = info or detect_cursor()+ installed = bool(info.get("hostAppInstalled"))+ running = bool(info.get("running"))+ if running:+ return {+ "led": "green",+ "summary": "Cursor ready (Grok Bot host)",+ "tooltip": (+ "Grok Bot lives in Cursor. Host app is running. "+ "Drive it with grok_list_windows / grok_screenshot / grok_send "+ "(background by default; don't steal the keyboard)."+ ),+ }+ if installed:+ return {+ "led": "yellow",+ "summary": "Cursor installed, not running",+ "tooltip": (+ "Cursor is on disk but not running. Call grok_open to launch it "+ "in the background — it will not steal focus."+ ),+ }+ return {+ "led": "yellow",+ "summary": "Cursor not detected",+ "tooltip": (+ "Cursor isn't installed — want me to open it if present, or I can try winget? "+ "Call grok_open (it detects, then offers launch). Do not send the user to "+ "download Cursor themselves."+ ),+ }
handlers/__init__.pyadded+1@@ -0,0 +1 @@+# grok-bridge handlers package
handlers/describe.pyadded+115@@ -0,0 +1,115 @@+"""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 Cursor (Grok Bot's host app) installed / running?",+ "long": False,+ "hint": "Zero side effects, never downloads. Branch on hostAppInstalled / hostAppRunning / errorCode host_app_not_installed.",+ "related": ["grok_prewarm", "grok_open", "grok_status"],+ "pitfalls": [+ "ready=false is NOT a user-download. Offer grok_open.",+ "Do not treat a yellow /status chip as a crash — the bridge is up, Cursor isn't.",+ ],+ },+ "prewarm": {+ "summary": "No-op re-detect. {wait:true} just re-detects; nothing to fetch.",+ "long": False,+ "hint": "Cursor is a host app, not a runtime asset. Cheap enough to call anytime.",+ "related": ["grok_readiness", "grok_open"],+ "pitfalls": ["wait:true does not install Cursor 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.",+ "long": False,+ "hint": "Use this after a send to confirm the durable inbox got the line.",+ "related": ["grok_send", "grok_readiness"],+ "pitfalls": ["lastSend is in-process; a bridge restart clears it (inbox file survives)"],+ },+ "open": {+ "summary": "Launch Cursor (Grok Bot host) in the BACKGROUND if installed.",+ "long": True,+ "hint": "Detect, then ab launch / desktop_open. If already running, lists windows. Never steals focus. If missing: offers launch / winget — does not winget itself.",+ "related": ["grok_list_windows", "grok_screenshot", "grok_readiness"],+ "pitfalls": [+ "Background by default. foreground:true without foregroundReason is refused.",+ "Do not tell the user to download Cursor. Offer grok_open.",+ "cliRequired verbs (shell_execute, pull_file, send_files) are never used here.",+ ],+ },+ "screenshot": {+ "summary": "Screenshot a Cursor / Grok Bot window via ab desktop_screenshot_window.",+ "long": False,+ "hint": "titleContains Cursor, then Grok. Does not raise the window.",+ "related": ["grok_list_windows", "grok_open"],+ "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 Cursor / Grok Bot windows (desktop_list_windows + process_list).",+ "long": False,+ "hint": "nameFilter Cursor. Read-only; does not focus or click.",+ "related": ["grok_screenshot", "grok_open", "grok_send"],+ "pitfalls": ["empty list usually means Cursor isn't running — grok_open, don't panic"],+ },+ "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 Cursor.",+ "long": False,+ "hint": "adom-bridge bridge_uninstall '{\"name\":\"grok\"}'. Skills: ./uninstall.sh.",+ "related": ["grok_status"],+ "pitfalls": ["never kills Cursor.exe / Cursor.app — that's the user's host app"],+ },+}+++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"])+ return {+ "success": True,+ "prefix": PREFIX,+ "verbCount": len(verbs),+ "verbs": verbs,+ "_hint": (+ "Full grok_* catalog — each entry carries hint/related/pitfalls. "+ "Every verb ALSO returns its own rich _hint when called; that's what to read in practice. "+ "Host-app probe: grok_readiness. Drive Grok Bot: grok_open / grok_send / grok_screenshot. "+ "CLI: adom-bridge grok_* . Always --ai-thread. Background by default."+ ),+ }
handlers/readiness.pyadded+118@@ -0,0 +1,118 @@+"""Layered readiness / prewarm / status / uninstall — host-app probes, zero downloads."""++from __future__ import annotations++from grok_detect import detect_cursor, HOST_APP++try:+ from handlers import send as send_mod+except Exception: # pragma: no cover - circular-import guard at import time+ send_mod = None+++def handle_readiness(_args: dict) -> dict:+ """Read-only: is Cursor (Grok Bot's host app) present / running?++ Never blocks on a download. Bridge detects host apps; it does not auto-install+ them. If Cursor is missing, OFFER grok_open — do not send the user to a download page.+ """+ info = detect_cursor()+ installed = bool(info.get("hostAppInstalled"))+ running = bool(info.get("running"))+ ready = installed # installed is enough; grok_open launches if not running+ out = {+ "success": True,+ "ready": ready,+ "hostApp": HOST_APP,+ "hostAppInstalled": installed,+ "hostAppRunning": running,+ "layers": {+ "hostAppInstalled": installed,+ "hostAppRunning": running,+ },+ "paths": info.get("paths") or [],+ "windowsHint": info.get("windowsHint"),+ "_hint": (+ "Read-only host-app detection (no download, no launch). "+ + (+ "Cursor is installed"+ + (" and running." if running else " but not running — grok_open launches it in the background.")+ if installed+ else (+ "Cursor isn't installed — want me to open it if present, or I can try winget? "+ "Call grok_open (detect + ab launch). Do not send the user to download Cursor themselves."+ )+ )+ ),+ }+ if not installed:+ out["errorCode"] = "host_app_not_installed"+ out["_next"] = "Offer grok_open to the user; on yes, call grok_open {}. Do not winget without a dedicated verb."+ return out+++def handle_prewarm(args: dict) -> dict:+ """Cheap re-detect. {wait:true} just re-detects; there is nothing to download."""+ info = detect_cursor()+ waited = bool(args.get("wait"))+ return {+ "success": True,+ "prewarmed": True,+ "waited": waited,+ "hostAppInstalled": bool(info.get("hostAppInstalled")),+ "hostAppRunning": bool(info.get("running")),+ "paths": info.get("paths") or [],+ "_hint": (+ "grok_prewarm is a no-op re-detect — Cursor is a host app, not a runtime asset. "+ "Nothing was downloaded. Use grok_readiness for the layered probe, grok_open to launch."+ ),+ }+++def handle_status(_args: dict) -> dict:+ """Detect + last grok_send + inbox path."""+ info = detect_cursor()+ last = None+ inbox = None+ if send_mod is None:+ try:+ from handlers import send as _send+ except Exception:+ _send = None+ else:+ _send = send_mod+ if _send is not None:+ last = _send.last_send()+ inbox = str(_send.inbox_path())+ return {+ "success": True,+ "hostApp": HOST_APP,+ "hostAppInstalled": bool(info.get("hostAppInstalled")),+ "hostAppRunning": bool(info.get("running")),+ "paths": info.get("paths") or [],+ "windowsHint": info.get("windowsHint"),+ "lastSend": last,+ "inboxPath": inbox,+ "_hint": (+ "Status is detect + last grok_send + the durable inbox path "+ "(~/.adom/grok-inbox.jsonl). For the verb catalog call grok_describe; "+ "to queue a message call grok_send {\"message\":\"...\"}."+ ),+ }+++def handle_uninstall(_args: dict) -> dict:+ """Tell the caller to bridge_uninstall {name:grok}. Do not kill Cursor."""+ return {+ "success": True,+ "uninstalled": False,+ "killedHostApp": False,+ "cursorLeftRunning": True,+ "_hint": (+ "grok_uninstall does NOT kill Cursor and does not delete the Grok Bot window. "+ "Ask Adom Bridge to drop this runtime with: "+ "adom-bridge bridge_uninstall '{\"name\":\"grok\"}'. "+ "Container skills come off with ./uninstall.sh (or adom-wiki pkg uninstall adom/grok-bridge)."+ ),+ "_next": "adom-bridge bridge_uninstall {\"name\":\"grok\"}",+ }
handlers/send.pyadded+129@@ -0,0 +1,129 @@+"""grok_send — toast + durable inbox. Desktop typing is opt-in (don't steal keyboard)."""++from __future__ import annotations++import json+import os+from datetime import datetime, timezone+from pathlib import Path++import caller_identity++_LAST_SEND: dict | None = None+++def inbox_path() -> Path:+ return Path.home() / ".adom" / "grok-inbox.jsonl"+++def last_send() -> dict | None:+ return _LAST_SEND+++def _append_inbox(record: dict) -> str | None:+ path = inbox_path()+ try:+ path.parent.mkdir(parents=True, exist_ok=True)+ with path.open("a", encoding="utf-8") as fh:+ fh.write(json.dumps(record, ensure_ascii=False) + "\n")+ return str(path)+ except OSError:+ return None+++def handle_send(args: dict) -> dict:+ global _LAST_SEND+ message = args.get("message")+ if message is None:+ message = args.get("text") or args.get("body") or ""+ message = str(message)+ if not message.strip():+ return {+ "success": False,+ "errorCode": "message_required",+ "error": "grok_send needs {message}.",+ "_hint": "Call grok_send {\"message\":\"hello from Adom\"}. Optional agent names the Grok character.",+ "_next": "grok_send {\"message\":\"...\"}",+ }++ agent = (args.get("agent") or "Grok Bot").strip() or "Grok Bot"+ thread = caller_identity.thread_name() or ""+ record = {+ "ts": datetime.now(timezone.utc).isoformat(),+ "message": message,+ "agent": agent,+ "caller": thread,+ "pid": os.getpid(),+ }+ path = _append_inbox(record)++ toast = caller_identity.ad_callback(+ "notify_user",+ {+ "title": "Grok Bot",+ "body": f"Grok Bot: {message}",+ "level": "info",+ },+ on_behalf=True,+ )+ toast_ok = isinstance(toast, dict) and toast.get("success") is not False+ if isinstance(toast, dict) and (toast.get("error") or "").find("not in env") >= 0:+ toast_ok = False++ # Look for a chat window so we can HINT that typing is opt-in — never type by default.+ windows = caller_identity.ad_callback(+ "desktop_list_windows", {"titleContains": "Grok"}, on_behalf=True,+ )+ found_chat = False+ if isinstance(windows, dict):+ for key in ("windows", "items", "result"):+ val = windows.get(key)+ if isinstance(val, list) and val:+ found_chat = True+ break++ _LAST_SEND = {+ "ts": record["ts"],+ "message": message,+ "agent": agent,+ "inboxPath": path,+ "queued": True,+ "via": "notify+inbox",+ "toastOk": toast_ok,+ "chatWindowSeen": found_chat,+ }++ hint_parts = [+ "Queued via notify_user toast + durable inbox "+ f"({path or '~/.adom/grok-inbox.jsonl'}).",+ "via=notify+inbox — this is NOT proof the Cursor chat widget received a keystroke.",+ ]+ if found_chat:+ hint_parts.append(+ "A Grok/Cursor chat window is open. Desktop typing is OPT-IN "+ "(don't steal the keyboard). Pass typeIntoChat:true only if the user asked "+ "you to type; default is toast + inbox the Anywhere character can poll."+ )+ else:+ hint_parts.append(+ "No Grok-titled window seen. The inbox still got the line; grok_open if you "+ "need the Cursor UI."+ )+ if args.get("typeIntoChat"):+ hint_parts.append(+ "typeIntoChat was set, but this bridge will not inject keystrokes by default "+ "(focus-steal). Ask the user, then use an explicit desktop_press_key path from "+ "the CLI — never from this HTTP callback's cliRequired-forbidden set."+ )++ return {+ "success": True,+ "queued": True,+ "via": "notify+inbox",+ "agent": agent,+ "inboxPath": path,+ "toastOk": toast_ok,+ "chatWindowSeen": found_chat,+ "record": record,+ "_hint": " ".join(hint_parts),+ }
handlers/windows.pyadded+219@@ -0,0 +1,219 @@+"""Window verbs: grok_open / grok_screenshot / grok_list_windows via ab desktop_* callbacks.++cliRequired verbs (shell_execute, pull_file, send_files) are NEVER used from this+HTTP callback. All driving goes through caller_identity.ad_callback.+"""++from __future__ import annotations++import caller_identity+from grok_detect import detect_cursor, HOST_APP+++def _cb(command: str, args: dict | None = None, timeout: int = 30) -> dict:+ """Best-effort ab callback. Never raises."""+ return caller_identity.ad_callback(command, args or {}, on_behalf=True, timeout=timeout)+++def _callback_ok(res: dict | None) -> bool:+ if not isinstance(res, dict):+ return False+ if res.get("success") is False:+ return False+ err = (res.get("error") or "")+ if "not in env" in err or "callback failed" in err.lower():+ return False+ return True+++def handle_list_windows(args: dict) -> dict:+ title = (args.get("titleContains") or "Cursor").strip() or "Cursor"+ wins = _cb("desktop_list_windows", {"titleContains": title})+ grok_wins = _cb("desktop_list_windows", {"titleContains": "Grok"})+ procs = _cb("process_list", {"nameFilter": "Cursor"})++ windows = []+ for blob in (wins, grok_wins):+ if not isinstance(blob, dict):+ continue+ for key in ("windows", "items", "result"):+ val = blob.get(key)+ if isinstance(val, list):+ windows.extend(val)+ break+ else:+ if blob.get("success") and blob.get("title"):+ windows.append(blob)++ processes = []+ if isinstance(procs, dict):+ for key in ("processes", "items", "result"):+ val = procs.get(key)+ if isinstance(val, list):+ processes = val+ break++ ab_up = _callback_ok(wins) or _callback_ok(procs)+ return {+ "success": True,+ "hostApp": HOST_APP,+ "windows": windows,+ "processes": processes,+ "count": len(windows),+ "abCallbackReached": ab_up,+ "windowEtiquette": "background",+ "_hint": (+ "Listed via ab desktop_list_windows (titleContains Cursor / Grok) and "+ "process_list nameFilter Cursor. Read-only — no focus steal. "+ + (+ "Empty? Cursor likely isn't running — grok_open launches it in the background."+ if not windows+ else "To capture one, grok_screenshot {}. To queue a message, grok_send."+ )+ + ("" if ab_up else " ab loopback (ADOM_DIRECT_API_URL) was not reachable — this process can still detect Cursor on disk.")+ ),+ }+++def handle_screenshot(args: dict) -> dict:+ title = (args.get("titleContains") or "").strip()+ needles = [title] if title else ["Cursor", "Grok"]+ last = None+ for needle in needles:+ last = _cb("desktop_screenshot_window", {"titleContains": needle})+ if _callback_ok(last) and not last.get("error"):+ shot = dict(last)+ shot.setdefault("success", True)+ shot.setdefault("titleContains", needle)+ shot.setdefault("windowEtiquette", "background")+ shot.setdefault("openedInBackground", True)+ shot["_hint"] = (+ f"Screenshot via ab desktop_screenshot_window titleContains={needle!r}. "+ "Window was NOT raised. PNG is on the desktop; use the adom-bridge CLI "+ "to pull it — this HTTP callback never calls pull_file / send_files / shell_execute. "+ + (shot.get("_hint") or "")+ )+ return shot+ return {+ "success": False,+ "errorCode": "window_not_found",+ "error": "No Cursor / Grok Bot window to screenshot.",+ "lastCallback": last,+ "_hint": (+ "desktop_screenshot_window found no titleContains Cursor or Grok. "+ "Call grok_open to launch Cursor in the background, then grok_list_windows, then retry. "+ "Do not steal focus to take the shot."+ ),+ "_next": "grok_open {} then grok_list_windows {} then grok_screenshot {}",+ }+++def handle_open(args: dict) -> dict:+ """Launch Cursor if installed; if running, list windows. Background by default."""+ info = detect_cursor()+ installed = bool(info.get("hostAppInstalled"))+ running = bool(info.get("running"))+ exe = info.get("exe")+ 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:+ return {+ "success": False,+ "errorCode": "foreground_reason_required",+ "windowEtiquette": "background",+ "openedInBackground": True,+ "_hint": (+ "foreground requested without foregroundReason — 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++ if running:+ return {+ "success": True,+ "alreadyRunning": True,+ "launched": False,+ "hostAppInstalled": installed,+ "hostAppRunning": True,+ "exe": exe,+ "windows": (listed or {}).get("windows") or [],+ "openedInBackground": True,+ "windowEtiquette": "foreground" if (want_fg and reason) else "background",+ "_hint": (+ "Cursor (Grok Bot host) 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."+ ),+ "_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_args = {"background": True, "noActivate": True}+ if exe:+ launch_args["path"] = exe+ else:+ launch_args["name"] = HOST_APP++ launched = _cb("launch", launch_args)+ via = "launch"+ if not _callback_ok(launched):+ open_args = {"background": True}+ if exe:+ open_args["path"] = exe+ else:+ open_args["target"] = HOST_APP+ launched = _cb("desktop_open", open_args)+ via = "desktop_open"++ ok = _callback_ok(launched)+ if not installed and not ok:+ return {+ "success": False,+ "errorCode": "host_app_not_installed",+ "hostApp": HOST_APP,+ "hostAppInstalled": False,+ "hostAppRunning": False,+ "launched": False,+ "via": via,+ "callback": launched,+ "openedInBackground": True,+ "windowEtiquette": "background",+ "_hint": (+ "Cursor isn't installed — want me to open it if present, or I can try winget? "+ "This verb already tried ab launch / desktop_open. It does NOT run winget "+ "(no dedicated grok_winget verb yet). Do not send the user to download Cursor themselves."+ ),+ "_next": "If the user wants winget, ask them, then use a dedicated install verb (not this one). Retry grok_open after Cursor exists.",+ }++ return {+ "success": True,+ "alreadyRunning": False,+ "launched": ok,+ "via": via,+ "hostAppInstalled": installed,+ "hostAppRunning": False, # we asked it to start; it may not be up yet+ "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 "+ + (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>\"}."+ ),+ }
install.shadded+35@@ -0,0 +1,35 @@+#!/usr/bin/env bash+# Install the grok USER skillpack: main skill `grok` + sub-skill `grok-anywhere`.+# Per the Bridge SDK a pkg ships ONLY user skills. The bridge RUNTIME is not+# here — Adom Bridge loads it from the release zip (adom-bridge-grok-v*.zip).+# Repo name (grok-bridge) != skill name (grok). Don't re-conflate (pup's lesson).+set -euo pipefail+PKG_DIR="$(cd "$(dirname "$0")" && pwd)"++deploy_into() {+ local BASE="$1"+ mkdir -p "$BASE"+ install_skill() {+ local dest="$BASE/$1"+ mkdir -p "$dest"+ cp "$PKG_DIR/$2" "$dest/SKILL.md"+ printf '%s\n' "grok-bridge" > "$dest/.adom-managed"+ }+ # Drop mis-slugged copies: page/repo slug must not become the skill dir.+ # Only drop THIS pack's mis-slug. Leave adom-bridge-grok (Anywhere MCP skill) alone.+ rm -rf "$BASE/grok-bridge"+ install_skill grok SKILL.md+ install_skill grok-anywhere skills/grok-anywhere/SKILL.md+ echo " -> $BASE (grok + grok-anywhere)"+}++echo "Installing grok-bridge skills:"+deploy_into "$HOME/.claude/skills" # Claude Code (always)+[ -d "$HOME/.codex" ] && deploy_into "$HOME/.codex/skills" # Codex (if present)+mkdir -p "$HOME/.grok/skills"+deploy_into "$HOME/.grok/skills" # Grok Bot+if [ -d /home/box/agent-data ]; then+ mkdir -p /home/box/agent-data/workflows+ deploy_into /home/box/agent-data/workflows+fi+echo "Done. This package installed SKILLS; Adom Bridge loads the grok runtime from the release zip."
package.jsonadded+76@@ -0,0 +1,76 @@+{+ "slug": "grok-bridge",+ "version": "1.0.0",+ "type": "app",+ "description": "This package installs the bridge's SKILLS into your container so your AI knows how to drive it; Adom Bridge loads the bridge runtime itself from the release zip.",+ "keywords": [+ "grok",+ "grok-bot",+ "cursor",+ "adom-bridge",+ "bridge",+ "skills",+ "grok-bridge",+ "anywhere"+ ],+ "tags": [+ "grok",+ "grok-bot",+ "cursor",+ "adom-bridge",+ "bridge",+ "skills",+ "anywhere"+ ],+ "files": [+ "SKILL.md",+ "skills/grok-anywhere/SKILL.md",+ "install.sh",+ "uninstall.sh",+ "README.md",+ "package.json",+ "CLAUDE.md"+ ],+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh"+ },+ "dependencies": {+ "adom/adom-bridge": "^2.0.0"+ },+ "author": "John Lauer <[email protected]>",+ "discovery_pitch": "Say grok to drive Grok Bot (Cursor host app) from Adom Bridge: grok_open, grok_send, grok_screenshot. Skills land in the container; the Python runtime is the release zip ab spawns on the PC.",+ "discovery_triggers": [+ "grok",+ "grok bot",+ "talk to grok",+ "send a message to grok bot",+ "grok screenshot",+ "open grok bot",+ "adom anywhere grok",+ "grok_open",+ "grok_send",+ "drive grok from adom bridge"+ ],+ "sample_prompts": [+ {+ "label": "Is Grok Bot ready?",+ "prompt": "Call grok_readiness and tell me if Cursor (Grok Bot's host) is installed and running"+ },+ {+ "label": "Open Grok Bot",+ "prompt": "Open Grok Bot in the background (grok_open) and screenshot it"+ },+ {+ "label": "Send a message",+ "prompt": "Send a message to Grok Bot: the board is ready for review"+ },+ {+ "label": "Anywhere from Grok",+ "prompt": "From Grok Bot, ping ConfRoomROG and AdomLapper with --ai-thread Grok Bot"+ }+ ],+ "hero": {+ "path": "docs/hero.png"+ }+}
server.pyadded+266@@ -0,0 +1,266 @@+#!/usr/bin/env python3+"""Adom Grok Bot Bridge Server — localhost HTTP server.++Adom Bridge (ab) spawns this on the Windows PC. There is no public Grok HTTP API;+we talk TO Grok Bot / Cursor the way kicad talks to KiCad: detect the host app,+drive it via ab desktop_* callbacks. Never modifies ab-core.++Usage:+ python server.py+ python server.py --port 8769+"""++from __future__ import annotations++import json+import os+import sys+import traceback+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer+from pathlib import Path++sys.path.insert(0, str(Path(__file__).parent))++import caller_identity+from grok_detect import detect_cursor, status_chip+from handlers.describe import handle_describe+from handlers.readiness import handle_prewarm, handle_readiness, handle_status, handle_uninstall+from handlers.send import handle_send+from handlers.windows import handle_list_windows, handle_open, handle_screenshot++DEFAULT_PORT = 8769+SERVICE = "grok-bridge"+REPORT_ISSUES = "adom-wiki issue create adom/grok-bridge"+DEFAULT_HINT = (+ "Call grok_describe for the verb catalog; grok_readiness for host-app state. "+ "CLI: adom-bridge grok_* . Always --ai-thread. Background by default. "+ f"Bugs: {REPORT_ISSUES}"+)++KNOWN_VERBS = [+ "grok_readiness",+ "grok_prewarm",+ "grok_describe",+ "grok_status",+ "grok_open",+ "grok_screenshot",+ "grok_list_windows",+ "grok_send",+ "grok_uninstall",+]+++def _read_bridge_version() -> str:+ try:+ p = Path(__file__).resolve().parent / "BRIDGE_VERSION"+ return p.read_text(encoding="utf-8").strip()+ except OSError:+ return ""+++BRIDGE_VERSION = _read_bridge_version()++COMMAND_HANDLERS = {+ "readiness": handle_readiness,+ "prewarm": handle_prewarm,+ "describe": handle_describe,+ "status": handle_status,+ "open": handle_open,+ "screenshot": handle_screenshot,+ "list_windows": handle_list_windows,+ "send": handle_send,+ "uninstall": handle_uninstall,+}+++def _strip_prefix(command: str) -> str:+ """Accept grok_open, grok_list_windows, or bare open / list_windows."""+ c = (command or "").strip()+ if c.startswith("grok_"):+ return c[len("grok_"):]+ return c+++def dispatch_command(command: str, args: dict) -> dict:+ if not isinstance(args, dict):+ args = {}+ internal = _strip_prefix(command)+ handler = COMMAND_HANDLERS.get(internal)+ if handler is None:+ return {+ "success": False,+ "errorCode": "unknown_command",+ "error": f"Unknown command: {command}",+ "_hint": (+ f"Unknown command {command!r}. Call grok_describe for the catalog. "+ f"Known verbs: {', '.join(KNOWN_VERBS)}."+ ),+ "_next": "grok_describe {}",+ }+ return handler(args)+++def _enrich(data: dict, ok_status: bool) -> dict:+ """Every JSON body gets _hint. Failures also get _next + errorCode + _reportIssues."""+ if not isinstance(data, dict):+ data = {"success": False, "error": str(data)}+ failed = (data.get("success") is False) or (not ok_status)+ data.setdefault("_hint", DEFAULT_HINT)+ if failed:+ data.setdefault("errorCode", data.get("errorCode") or "error")+ data.setdefault("_next", "Call grok_describe {} then grok_readiness {}. Surface _hint verbatim.")+ data.setdefault("_reportIssues", REPORT_ISSUES)+ return data+++class GrokBridgeHandler(BaseHTTPRequestHandler):+ """POST {command, args} + GET /status + /health. Bind loopback only (see main)."""++ def sendJSON(self, status: int, data: dict) -> None:+ body = _enrich(data if isinstance(data, dict) else {"success": False, "error": str(data)},+ ok_status=(status < 400))+ payload = json.dumps(body).encode("utf-8")+ self.send_response(status)+ self.send_header("Content-Type", "application/json")+ self.send_header("Content-Length", str(len(payload)))+ self.end_headers()+ self.wfile.write(payload)++ def _status_payload(self) -> dict:+ info = detect_cursor()+ chip = status_chip(info)+ return {+ "status": "ok",+ "led": chip["led"],+ "summary": chip["summary"],+ "tooltip": chip["tooltip"],+ "service": SERVICE,+ "version": BRIDGE_VERSION,+ "bridgeVersion": BRIDGE_VERSION,+ "hostAppInstalled": info.get("hostAppInstalled"),+ "hostAppRunning": info.get("running"),+ "paths": info.get("paths") or [],+ "windowsHint": info.get("windowsHint"),+ "_hint": chip["tooltip"],+ }++ def do_GET(self):+ path = self.path.split("?", 1)[0]+ if path in ("/status", "/health"):+ self.sendJSON(200, self._status_payload())+ else:+ self.sendJSON(404, {+ "success": False,+ "errorCode": "not_found",+ "error": "Not found",+ "_hint": "Health is GET /status or GET /health. Verbs are POST /command (or POST /) with {command, args}.",+ "_next": "GET /status",+ })++ def do_POST(self):+ path = self.path.split("?", 1)[0]+ if path not in ("/command", "/", "/command/"):+ self.sendJSON(404, {+ "success": False,+ "errorCode": "not_found",+ "error": "Not found",+ "_hint": "POST /command or POST / with body {command, args}. GET /status for the chip.",+ "_next": "POST /command {\"command\":\"grok_describe\",\"args\":{}}",+ })+ return++ length = int(self.headers.get("Content-Length", 0) or 0)+ raw = self.rfile.read(length) if length else b""+ try:+ request = json.loads(raw.decode("utf-8") or "{}")+ except json.JSONDecodeError as e:+ self.sendJSON(400, {+ "success": False,+ "errorCode": "invalid_json",+ "error": f"Invalid JSON: {e}",+ "_hint": "Body must be {\"command\":\"grok_describe\",\"args\":{}}.",+ })+ return++ if not isinstance(request, dict):+ self.sendJSON(400, {+ "success": False,+ "errorCode": "invalid_json",+ "error": "JSON body must be an object.",+ })+ return++ command = request.get("command") or ""+ args = request.get("args") if isinstance(request.get("args"), dict) else {}+ caller_identity.set_from_request(self.headers, args)+ full = command if str(command).startswith("grok_") else f"grok_{_strip_prefix(str(command))}"+ print(f"[verb] {full}{caller_identity.log_suffix()}", flush=True)+ try:+ result = dispatch_command(str(command), args)+ self.sendJSON(200, result)+ except Exception as e:+ traceback.print_exc()+ self.sendJSON(500, {+ "success": False,+ "errorCode": "internal_error",+ "error": f"Internal error: {e}",+ "_hint": f"Bridge bug, not your call. {REPORT_ISSUES} with the verb and args.",+ })+ finally:+ caller_identity.clear()++ def log_message(self, fmt, *args):+ print(f"[grok-bridge] {args[0]} {args[1]} {args[2]}", flush=True)+++def main() -> None:+ try:+ sys.stdout.reconfigure(line_buffering=True)+ sys.stderr.reconfigure(line_buffering=True)+ except Exception:+ pass++ port = DEFAULT_PORT+ if "--port" in sys.argv:+ idx = sys.argv.index("--port")+ if idx + 1 < len(sys.argv):+ port = int(sys.argv[idx + 1])++ # Bind loopback ONLY. ADOM_BIND_HOST is always 127.0.0.1 when ab spawns us+ # (Bridge SDK Runtime contract, v1.9.63+) — never 0.0.0.0, no firewall prompt.+ bind_host = os.environ.get("ADOM_BIND_HOST", "127.0.0.1")+ if bind_host not in ("127.0.0.1", "::1", "localhost"):+ print(f"[grok-bridge] refusing non-loopback ADOM_BIND_HOST={bind_host!r}; using 127.0.0.1",+ flush=True)+ bind_host = "127.0.0.1"++ class _Server(ThreadingHTTPServer):+ allow_reuse_address = False+ daemon_threads = True++ print(f"[grok-bridge] starting v{BRIDGE_VERSION} on {bind_host}:{port} (pid {os.getpid()})",+ flush=True)+ try:+ server = _Server((bind_host, port), GrokBridgeHandler)+ except OSError as e:+ print(f"[grok-bridge] Port {port} already in use ({e}). "+ "Another instance owns it — exiting cleanly (single-instance guard).",+ flush=True)+ return++ info = detect_cursor()+ print(f"[grok-bridge] detect hostAppInstalled={info.get('hostAppInstalled')} "+ f"running={info.get('running')} paths={info.get('paths')}",+ flush=True)+ print(f"[grok-bridge] Listening on http://{bind_host}:{port} — GET /status POST /command",+ flush=True)++ try:+ server.serve_forever()+ except KeyboardInterrupt:+ print("\n[grok-bridge] Shutting down.", flush=True)+ server.server_close()+++if __name__ == "__main__":+ main()
skills/grok-anywhere/SKILL.mdadded+42@@ -0,0 +1,42 @@+---+name: grok-anywhere+user-invocable: true+description: "Grok Bot is an Adom Anywhere character (Claude Desktop / Codex class). This session already has adom-bridge on PATH. Always --ai-thread \"Grok Bot\" and --target when more than one desktop is connected. Prefer ab verbs over Grok ListMachines/Shell so the activity log stays intact. Trigger words: adom anywhere grok, grok bot desktop, drive my pc from grok, adom-bridge from grok bot."+---++# grok-anywhere — Grok Bot as an Adom Anywhere character++Grok Bot is an **Adom Anywhere** character in the same class as Claude Desktop and Codex: a cloud/session AI that drives a real PC through the `adom-bridge` CLI over the relay.++This Grok session **already has `adom-bridge` on PATH** when the CLI is installed. Relay desktops are whatever `adom-bridge targets` returns (John's current pair: ConfRoomROG, AdomLapper — treat those as examples, not the only names). Do not paste the whole anywhere / local-ai map here — load **adom-bridge-anywhere** (and **local-ai** if you need the Hydrogen/loopback picture).++## Every call++```bash+adom-bridge --ai-thread "Grok Bot" --target NAME <verb> '<json>'+```++- Always `--ai-thread "Grok Bot"`. Never `export ADOM_AI_THREAD`.+- `--target` when more than one desktop is connected (`adom-bridge targets`). Ambiguous without it (`errorCode: ambiguous_target`).+- Surface `_hint` verbatim. Branch on `errorCode`, not prose.++## Prefer ab verbs++Prefer **ab verbs** (`ping`, `targets`, `desktop_screenshot_window`, `notify_user`, `grok_*`, `kicad_*`, `pup_*`, …) over Grok **ListMachines / Shell** on a registered PC. The activity log, approval toasts, and `--ai-thread` attribution only stay intact on the ab path.++**ConfRoomROG is ab-only** — do not expect a Grok-registered machine session there. Drive it with `--target ConfRoomROG`.++## Where you are++| You | Path |+|---|---|+| This box (container-class) | Relay. `adom-bridge` talks to Windows over the relay, not loopback. |+| Local Windows (ab on the same PC) | Loopback is fine; ab still wants `--ai-thread`. |+| ConfRoomROG | ab-only. `--target ConfRoomROG`. |+| AdomLapper | ab `--target AdomLapper`. |++For the full desktop map, load **adom-bridge-anywhere**. For loopback-vs-relay / local-ai, load **local-ai**. Do not duplicate those skills here.++## Talking to Grok Bot (the other direction)++If a *cloud* AI needs to reach **this** Grok session from a Windows ab, that is the **grok** skill (`grok_open` / `grok_send` / `grok_screenshot`) — the spawned runtime on the PC. This anywhere skill is the reverse: Grok Bot driving the PC.
uninstall.shadded+25@@ -0,0 +1,25 @@+#!/usr/bin/env bash+# Remove grok + grok-anywhere skills this pack installed (.adom-managed only).+# Does not kill Cursor and does not uninstall the Windows runtime zip.+set -euo pipefail++remove_from() {+ local BASE="$1"+ [ -d "$BASE" ] || return 0+ for name in grok grok-anywhere grok-bridge adom-bridge-grok; do+ local dest="$BASE/$name"+ if [ -d "$dest" ] && [ -f "$dest/.adom-managed" ]; then+ rm -rf "$dest"+ echo " <- $dest"+ elif [ -e "$dest" ]; then+ echo " leave $dest (no .adom-managed marker)"+ fi+ done+}++echo "Removing grok-bridge skills:"+remove_from "$HOME/.claude/skills"+[ -d "$HOME/.codex" ] && remove_from "$HOME/.codex/skills"+[ -d "$HOME/.grok" ] && remove_from "$HOME/.grok/skills"+[ -d /home/box/agent-data/workflows ] && remove_from /home/box/agent-data/workflows+echo "Done. Cursor / Grok Bot and the ab runtime zip were not touched."