← Commit history
BRIDGE_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."