app
Adom Bridge
Public Made by Adomby adom
Adom Bridge unleashes your AI onto your full PC: full power, full safety. The AI breaks out of the container it runs in and onto your real machine, managing and running your entire laptop as you: launch apps, move files, drive any window, control KiCad, Fusion 360 or your real signed-in browser. Works with any AI, cloud or local (Claude Desktop, Claude Code, Codex), no Hydrogen required. Bridge and its bridges are an operating system for AI, with managed Node and Python runtimes, and a human-onl
← Commit history
Publish 2.0.13
5 files changed
+107−107
SKILL.md+99−99@@ -112,7 +112,7 @@ Run the **`register_command`** that `setup_desktop` returned (it's `adom-bridge and the connection appears in their list and auto-connects — **no JSON paste at all**. This is the modern path and the one to try first. (`register_container` needs an Adom cloud container — it uses the mounted account token — and returns `ok:false` on a non-Adom box; if so, use Option A/B below.) -**Already-running desktop? `server_sync` (v1.9.215+).** A signed-in AD reads the cloud profile at boot, at sign-in, and hourly — and `adom-bridge-cli server_sync` (optionally `--target <name>`) makes it re-read RIGHT NOW. The merge is cloud-wins, whole entry including `authToken`. So the flow for adding a server or **rotating a relay token** on live desktops is: update the cloud key (`register_container` writes them) → `server_sync` per desktop → verify with `server_list`. Do NOT use `server_remove` + `server_add` for rotation — removing the entry you are talking through severs your own transport (wiki #387).+**Already-running desktop? `server_sync` (v1.9.215+).** A signed-in ab reads the cloud profile at boot, at sign-in, and hourly — and `adom-bridge-cli server_sync` (optionally `--target <name>`) makes it re-read RIGHT NOW. The merge is cloud-wins, whole entry including `authToken`. So the flow for adding a server or **rotating a relay token** on live desktops is: update the cloud key (`register_container` writes them) → `server_sync` per desktop → verify with `server_list`. Do NOT use `server_remove` + `server_add` for rotation — removing the entry you are talking through severs your own transport (wiki #387). --- @@ -213,7 +213,7 @@ adom-bridge-cli --target <name> commands # + THAT box's installed bridges (ins Returns categorized JSON with every command, its required/optional args, return values, prerequisites, and workflow notes. Run this FIRST when you're unsure what a verb is called. **Two kinds of verbs (v1.9.103 — don't confuse them):**-- **BUILT-IN AD-core verbs** are always available on every Adom Bridge, no install, NOT bridges. This is where screenshots, windows, input, files, registry, shell, tunnel, update, and notify live. A whole-screen shot is `desktop_screenshot_screen`; one window (with chrome) is `desktop_screenshot_window {"hwnd":N}` or `{"titleContains":"..."}` (get hwnds from `desktop_list_windows`).+- **BUILT-IN ab-core verbs** are always available on every Adom Bridge, no install, NOT bridges. This is where screenshots, windows, input, files, registry, shell, tunnel, update, and notify live. A whole-screen shot is `desktop_screenshot_screen`; one window (with chrome) is `desktop_screenshot_window {"hwnd":N}` or `{"titleContains":"..."}` (get hwnds from `desktop_list_windows`). - **BRIDGE verbs** (`kicad_`/`fusion_`/`pup_`/`blender_`/`nbrowser_`…) need that bridge installed — see `installed_bridges_on_target` in `commands`, or run `bridge_list`. If a verb returns `errorCode:"unknown_verb"`, **read the error's `_hint`** — it carries a fuzzy "did you mean" (e.g. `desktop_screenshot` → `desktop_screenshot_screen`) and tells you whether the right verb is built-in or a bridge. Don't conclude a capability is "a missing bridge" from a guessed verb name; call `commands` and check.@@ -260,11 +260,11 @@ adom-bridge-cli server_remove '{"name":"hydrogen-workspace"}' #### Addressing multiple Adom Bridges on one relay (v1.8.133+) -One relay can have **several** Adom Bridges connected at once — e.g. a laptop and an Azure VM, eventually many. Each AD has a **name** (its `--client-name` / Settings value, defaulting to the OS hostname). The cloud CLI addresses a specific one, or fans out to all:+One relay can have **several** Adom Bridges connected at once — e.g. a laptop and an Azure VM, eventually many. Each ab has a **name** (its `--client-name` / Settings value, defaulting to the OS hostname). The cloud CLI addresses a specific one, or fans out to all: - `adom-bridge-cli targets` — list connected ADs: `{count, targets:[{name, hostname, version, os, connectedAt}]}`. Discover names here first.-- `adom-bridge-cli --target <name> <verb>` — route the verb to that AD only. `--target` is a **global** flag (goes before the verb; case-insensitive; matches `clientName`, else `hostname`). `ADOM_TARGET=<name>` sets a default (the flag wins).-- `adom-bridge-cli --target all <verb>` — fan out to **every** connected AD; returns `{broadcast:true, targets:[...], results:{<name>:<verb-result>}, summary:{total,ok,failed}}`. A per-AD timeout/failure is isolated to that AD's entry.+- `adom-bridge-cli --target <name> <verb>` — route the verb to that ab only. `--target` is a **global** flag (goes before the verb; case-insensitive; matches `clientName`, else `hostname`). `ADOM_TARGET=<name>` sets a default (the flag wins).+- `adom-bridge-cli --target all <verb>` — fan out to **every** connected ab; returns `{broadcast:true, targets:[...], results:{<name>:<verb-result>}, summary:{total,ok,failed}}`. A per-ab timeout/failure is isolated to that ab's entry. ```bash adom-bridge-cli targets@@ -272,7 +272,7 @@ adom-bridge-cli --target azurevm desktop_screenshot_screen adom-bridge-cli --target all hd_status # every box at once, results keyed by name ``` -**No-target behavior:** with exactly ONE AD connected, no `--target` is needed (back-compat). With MULTIPLE connected and no `--target`, the call FAILS with `{error:"ambiguous_target", connected:[...], _hint}` (exit 1) — it won't guess which box. A bad name → `{error:"target_not_found", requested, connected:[...]}` (exit 1). Set each AD's name with the `--client-name <name>` launch flag (Hydrogen passes this when spawning the embedded AD), the Settings field, or `config.json` `client_name`. Full design + the relay-routing internals: [`skills/MULTI_AD_ADDRESSING.md`](MULTI_AD_ADDRESSING.md).+**No-target behavior:** with exactly ONE ab connected, no `--target` is needed (back-compat). With MULTIPLE connected and no `--target`, the call FAILS with `{error:"ambiguous_target", connected:[...], _hint}` (exit 1) — it won't guess which box. A bad name → `{error:"target_not_found", requested, connected:[...]}` (exit 1). Set each ab's name with the `--client-name <name>` launch flag (Hydrogen passes this when spawning the embedded ab), the Settings field, or `config.json` `client_name`. Full design + the relay-routing internals: [`skills/MULTI_AD_ADDRESSING.md`](MULTI_AD_ADDRESSING.md). #### Direct HTTP API for sibling apps (v1.8.25+) @@ -309,17 +309,17 @@ curl -X POST http://127.0.0.1:47200/command \ Full integration guide (loopback trust model, recipes for Hydrogen startup/shutdown, retry/backoff patterns, port-discovery code in Rust + TypeScript) lives in [`skills/DIRECT_API.md`](DIRECT_API.md) in this repo, and as a wiki asset attached to `apps/adom-bridge`. -#### Embedded mode — AD bundled inside Hydrogen (v1.8.42+)+#### Embedded mode — ab bundled inside Hydrogen (v1.8.42+) -When Hydrogen bundles AD, AD enters "embedded mode" at boot. Hydrogen owns the system-tray icon (AD's is suppressed), Hydrogen owns the Adom Cloud login (Hydrogen's session token is handed to AD via Phase 4's existing channels), Hydrogen owns auto-updates (AD's wiki-poll is disabled — Hydrogen bundles a fresh AD on every Hydrogen release), and Hydrogen spawns AD hidden (its tray "Open Adom Bridge" surfaces the window on demand). Standalone AD users see zero change — none of the signals fire.+When Hydrogen bundles ab, ab enters "embedded mode" at boot. Hydrogen owns the system-tray icon (ab's is suppressed), Hydrogen owns the Adom Cloud login (Hydrogen's session token is handed to ab via Phase 4's existing channels), Hydrogen owns auto-updates (ab's wiki-poll is disabled — Hydrogen bundles a fresh ab on every Hydrogen release), and Hydrogen spawns ab hidden (its tray "Open Adom Bridge" surfaces the window on demand). Standalone ab users see zero change — none of the signals fire. **Detection** — three-signal cascade (any one triggers embedded mode): 1. `--embedded` CLI flag (what Hydrogen passes on every spawn) 2. `ADOM_EMBEDDED=1` env var (backup channel)-3. `%LOCALAPPDATA%\Adom Bridge\embedded.json` marker file (survives AD restarts; written by Hydrogen's installer; deleted by Hydrogen's uninstaller)+3. `%LOCALAPPDATA%\Adom Bridge\embedded.json` marker file (survives ab restarts; written by Hydrogen's installer; deleted by Hydrogen's uninstaller) -**CLI flags Hydrogen passes when spawning AD**:+**CLI flags Hydrogen passes when spawning ab**: ```bash adom-bridge.exe --embedded --start-hidden \@@ -328,36 +328,36 @@ adom-bridge.exe --embedded --start-hidden \ ``` - `--embedded` → enter embedded mode (also writes the marker)-- `--start-hidden` → boot with main window invisible. **v1.8.119+: honored in embedded mode** (Hydrogen owns the surface; AD stays in the tray and opens via `desktop_window_show`). v1.8.102–118 had a bug where embedded mode force-showed the window even with `--start-hidden`; that's fixed. The machine owner can still flip AD's "Always show the window at launch" Settings checkbox (config `always_show_window_on_launch`, default OFF) to override and always show — a personal-dev-box preference, not the product default.+- `--start-hidden` → boot with main window invisible. **v1.8.119+: honored in embedded mode** (Hydrogen owns the surface; ab stays in the tray and opens via `desktop_window_show`). v1.8.102–118 had a bug where embedded mode force-showed the window even with `--start-hidden`; that's fixed. The machine owner can still flip ab's "Always show the window at launch" Settings checkbox (config `always_show_window_on_launch`, default OFF) to override and always show — a personal-dev-box preference, not the product default. - `--shell-approval <prompt|1h|24h|permanent>` → Hydrogen's passthrough of its user's shell-approval choice (v1.8.116+, session-only) - `--relay-url <url>` → upsert this relay via existing `server_add` dedup; default name `hydrogen-desktop` - `--relay-name <name>` → override the default name - `--session-token <tok>` → already covered by Phase 4 handoff (env / CLI arg / file) -**Docker introspection** — `adom-bridge-cli desktop_embedded_status` returns `{embedded, owner, source, enteredVia, pendingRelayUrl, pendingRelayName, startHidden, markerPath, markerExists}` so cloud-side callers can branch on whether AD is standalone or Hydrogen-managed. **v1.8.73+:** `enteredVia` reports `"launch-flag"` (Hydrogen spawned AD as a child via `--embedded`) or `"runtime-adopt"` (AD was running standalone; Hydrogen adopted it at runtime) — see "Runtime adopt + release" below.+**Docker introspection** — `adom-bridge-cli desktop_embedded_status` returns `{embedded, owner, source, enteredVia, pendingRelayUrl, pendingRelayName, startHidden, markerPath, markerExists}` so cloud-side callers can branch on whether ab is standalone or Hydrogen-managed. **v1.8.73+:** `enteredVia` reports `"launch-flag"` (Hydrogen spawned ab as a child via `--embedded`) or `"runtime-adopt"` (ab was running standalone; Hydrogen adopted it at runtime) — see "Runtime adopt + release" below. -**New verbs Hydrogen uses to drive AD** (all also usable standalone):+**New verbs Hydrogen uses to drive ab** (all also usable standalone): | Verb | Purpose | |---|---|-| `desktop_window_show` | Bring AD's main window to foreground |-| `desktop_window_hide` | Hide AD's main window (app keeps running) |+| `desktop_window_show` | Bring ab's main window to foreground |+| `desktop_window_hide` | Hide ab's main window (app keeps running) | | `desktop_connect_all` | Spawn ws_loop for every enabled server | | `desktop_disconnect_all` | Drop every live WS connection (entries stay in config) |-| `desktop_shutdown` | Explicit AD termination (stop bridges, then exit) |+| `desktop_shutdown` | Explicit ab termination (stop bridges, then exit) | | `desktop_embedded_status` | Introspect current embedded state (incl. `enteredVia`) | | `desktop_embedded_enter` | **v1.8.73+** runtime adopt: standalone → embedded as `runtime-adopt` |-| `desktop_embedded_release` | **v1.8.73+** revert to standalone. **v1.8.77+: always reverts, never exits AD** (regardless of `entered_via`) — use `desktop_shutdown` for explicit exit |-| `desktop_logout` | Clear AD's `~/.adom/session.json` (Hydrogen's sign-out propagation) |+| `desktop_embedded_release` | **v1.8.73+** revert to standalone. **v1.8.77+: always reverts, never exits ab** (regardless of `entered_via`) — use `desktop_shutdown` for explicit exit |+| `desktop_logout` | Clear ab's `~/.adom/session.json` (Hydrogen's sign-out propagation) | #### Runtime adopt + release (v1.8.73+) — the LaunchFlag-vs-RuntimeAdopt distinction -Hydrogen has two ways to put AD into embedded mode:+Hydrogen has two ways to put ab into embedded mode: -1. **Spawn AD with `--embedded`** (LaunchFlag) — Hydrogen launches a fresh AD process as its child. AD enters embedded with `entered_via=launch-flag`. When Hydrogen subsequently dies ungracefully (without calling `/embedded/release` or `desktop_shutdown` first), AD's 60s `embedded_monitor` tick notices Hydrogen's control port is dead and **exits AD** (the "no hanging chad" rule — no tray-less orphan).-2. **Adopt at runtime via `POST /embedded/enter`** (RuntimeAdopt) — AD was already running standalone; Hydrogen calls the direct-API endpoint to flip AD into embedded mode without a respawn. AD enters embedded with `entered_via=runtime-adopt`, preserving its open state, current PID, all in-flight connections. When Hydrogen subsequently dies ungracefully, AD's monitor **reverts AD to standalone** — tray restored, footer pill hidden, marker deleted. AD process stays alive.+1. **Spawn ab with `--embedded`** (LaunchFlag) — Hydrogen launches a fresh ab process as its child. ab enters embedded with `entered_via=launch-flag`. When Hydrogen subsequently dies ungracefully (without calling `/embedded/release` or `desktop_shutdown` first), ab's 60s `embedded_monitor` tick notices Hydrogen's control port is dead and **exits ab** (the "no hanging chad" rule — no tray-less orphan).+2. **Adopt at runtime via `POST /embedded/enter`** (RuntimeAdopt) — ab was already running standalone; Hydrogen calls the direct-API endpoint to flip ab into embedded mode without a respawn. ab enters embedded with `entered_via=runtime-adopt`, preserving its open state, current PID, all in-flight connections. When Hydrogen subsequently dies ungracefully, ab's monitor **reverts ab to standalone** — tray restored, footer pill hidden, marker deleted. ab process stays alive. -The rule is simple: **whoever started AD owns AD's death**. If AD started itself (standalone → runtime adopt), Hydrogen's death doesn't take AD down with it.+The rule is simple: **whoever started ab owns ab's death**. If ab started itself (standalone → runtime adopt), Hydrogen's death doesn't take ab down with it. **Two new direct-API endpoints** (v1.8.73+, sugar over `POST /command`): @@ -366,24 +366,24 @@ The rule is simple: **whoever started AD owns AD's death**. If AD started itself curl -s -X POST http://127.0.0.1:47200/embedded/enter # Release — runs the same handler as desktop_embedded_release.-# v1.8.77+: ALWAYS reverts to standalone. AD never exits via this call.+# v1.8.77+: ALWAYS reverts to standalone. ab never exits via this call. curl -s -X POST http://127.0.0.1:47200/embedded/release ``` -**Probe target (v1.8.75+):** the 60s `embedded_monitor` checks Hydrogen's *control port* (default `47084`, discovered from `%APPDATA%\hydrogen-desktop\ports.json` key `control`), not Hydrogen's legacy `:9001` webview-eval port. Same discovery `hd_api` uses. Pre-v1.8.75 this was hardcoded to `:9001` — Hydrogen doesn't bind that port anymore, so the probe always reported Hydrogen dead and every embedded AD reverted/exited within 60s of Hydrogen starting.+**Probe target (v1.8.75+):** the 60s `embedded_monitor` checks Hydrogen's *control port* (default `47084`, discovered from `%APPDATA%\hydrogen-desktop\ports.json` key `control`), not Hydrogen's legacy `:9001` webview-eval port. Same discovery `hd_api` uses. Pre-v1.8.75 this was hardcoded to `:9001` — Hydrogen doesn't bind that port anymore, so the probe always reported Hydrogen dead and every embedded ab reverted/exited within 60s of Hydrogen starting. -**Live frontend update (v1.8.76+):** the "Embedded · Hydrogen" footer pill responds to Tauri `mode-changed` events emitted on every transition. So if AD's window is open when Hydrogen adopts it (or Hydrogen releases it), the pill flips immediately — no page reload, no manual refresh.+**Live frontend update (v1.8.76+):** the "Embedded · Hydrogen" footer pill responds to Tauri `mode-changed` events emitted on every transition. So if ab's window is open when Hydrogen adopts it (or Hydrogen releases it), the pill flips immediately — no page reload, no manual refresh. -**Stale-marker safety (v1.8.74+):** if the marker file says `entered_via=launch-flag` AND Hydrogen's control port isn't responding at boot, AD deletes the marker BEFORE exiting. Without the delete, every subsequent manual launch (no `--embedded` flag) would hit the same exit branch and AD would be unlaunchable until the user manually rm'd the marker. The delete-before-exit makes the next launch start standalone normally.+**Stale-marker safety (v1.8.74+):** if the marker file says `entered_via=launch-flag` AND Hydrogen's control port isn't responding at boot, ab deletes the marker BEFORE exiting. Without the delete, every subsequent manual launch (no `--embedded` flag) would hit the same exit branch and ab would be unlaunchable until the user manually rm'd the marker. The delete-before-exit makes the next launch start standalone normally. -**Shell auto-approve is a user preference** (v1.8.116+ — replaces the v1.8.44 embedded-permanent default). The safe default everywhere is per-command approval. Resolution order at AD boot:+**Shell auto-approve is a user preference** (v1.8.116+ — replaces the v1.8.44 embedded-permanent default). The safe default everywhere is per-command approval. Resolution order at ab boot: -1. `--shell-approval <prompt|1h|24h|permanent>` launch flag — Hydrogen's passthrough of its user's choice when spawning AD embedded. **Session-only**: never persisted by AD.+1. `--shell-approval <prompt|1h|24h|permanent>` launch flag — Hydrogen's passthrough of its user's choice when spawning ab embedded. **Session-only**: never persisted by ab. 2. The durable Settings checkbox ("Auto-approve shell commands permanently", config field `shell_auto_approve_permanent`, default OFF). Also set/cleared via `shell_auto_approve '{"permanent": true|false}'`. 3. A restored timed grant from a previous session (+1h/+24h buttons, `duration_secs` calls). 4. Otherwise: prompt per command. -In embedded mode **Hydrogen owns the preference** (Hydrogen is the parent, AD the child): AD's Settings checkbox goes read-only with "Managed by Hydrogen", and the footer shows "**Permanent** *(managed by Hydrogen)*" when armed. Hydrogen changes it at runtime with the same verb:+In embedded mode **Hydrogen owns the preference** (Hydrogen is the parent, ab the child): ab's Settings checkbox goes read-only with "Managed by Hydrogen", and the footer shows "**Permanent** *(managed by Hydrogen)*" when armed. Hydrogen changes it at runtime with the same verb: ```bash # revoke everything (timed + permanent + the saved checkbox):@@ -395,7 +395,7 @@ adom-bridge shell_auto_approve '{"permanent": true}' Migration note: pre-1.8.116 embedded boots persisted a permanent grant that leaked into standalone sessions. On first v1.8.116 boot, a permanent-class grant on disk WITHOUT the config preference is cleared — re-opt-in via the checkbox or the verb if you actually wanted it. -**⚠ v1.9.90 — a cloud/relay AI CANNOT grant shell approval itself.** The `shell_auto_approve` examples above are Hydrogen's (a trusted LOCAL caller) or the human's own. From a **relay** connection (the cloud AI), a GRANT (`shell_auto_approve {"permanent":true}` / `{"duration_secs":N>0}`) is REFUSED with `errorCode:"human_only"` and instead POPS the human approval prompt in Adom Bridge. **The AI's path: call `request_shell_approval` (or just let the gate/refusal auto-surface the prompt) → ask the user to click Approve 1 hr / 24 hr / Bypass Permissions → RETRY your original verb.** Only a human clicking in AD (or Hydrogen proxying its own user) grants. A REVOKE (`duration_secs:0`) is allowed for the AI.+**⚠ v1.9.90 — a cloud/relay AI CANNOT grant shell approval itself.** The `shell_auto_approve` examples above are Hydrogen's (a trusted LOCAL caller) or the human's own. From a **relay** connection (the cloud AI), a GRANT (`shell_auto_approve {"permanent":true}` / `{"duration_secs":N>0}`) is REFUSED with `errorCode:"human_only"` and instead POPS the human approval prompt in Adom Bridge. **The AI's path: call `request_shell_approval` (or just let the gate/refusal auto-surface the prompt) → ask the user to click Approve 1 hr / 24 hr / Bypass Permissions → RETRY your original verb.** Only a human clicking in ab (or Hydrogen proxying its own user) grants. A REVOKE (`duration_secs:0`) is allowed for the AI. #### Bridge ports are dynamic — you never need to know one (v1.8.31+) @@ -408,18 +408,18 @@ KiCad, Fusion, Browser/Puppeteer, and all third-party bridges now bind OS-assign ### Managing bridges (the AI fully controls bridges on its own) -Bridges are AD's plugins (kicad, puppeteer, fusion360 ship bundled; others install from the wiki). You — the AI — can see, update, install, remove, pause, and resume them entirely through verbs. No human step is required.+Bridges are ab's plugins (kicad, puppeteer, fusion360 ship bundled; others install from the wiki). You — the AI — can see, update, install, remove, pause, and resume them entirely through verbs. No human step is required. | Verb | What it does | |---|---|-| `bridge_list` | Every installed bridge: `name`, `displayName`, `version` (what AD runs now), `source` (bundled / cache / third_party), `paused`, spawn kind, verbPrefixes. Start here. |+| `bridge_list` | Every installed bridge: `name`, `displayName`, `version` (what ab runs now), `source` (bundled / cache / third_party), `paused`, spawn kind, verbPrefixes. Start here. | | `bridge_check_updates` | **READ-ONLY.** Per bridge: `current` vs `latest` (wiki) + `updateAvailable`. Downloads nothing. The "what's behind?" probe. | | `refresh_bridges` | Apply updates. No args = sync ALL bridges from the wiki. `{"name":"kicad"}` = update just one. |-| `bridge_install` | Install a new third-party bridge from a wiki manifest URL (`{"manifestUrl":"…/<bridge>-bridge-manifest.json"}`). On `success:true` the **capsule appears in the AD GUI immediately** (no verb call needed; it's gray/idle until first use, then green). On `success:false` **nothing is registered and no capsule appears** — read `error` (a `read zip body`/`builder error` means the manifest's zip URL couldn't be fetched). Don't treat a parsed manifest as a completed install; confirm with `bridge_list`. The manifest `url` may be relative (resolved against the manifest's dir, v1.8.169+). |+| `bridge_install` | Install a new third-party bridge from a wiki manifest URL (`{"manifestUrl":"…/<bridge>-bridge-manifest.json"}`). On `success:true` the **capsule appears in the ab GUI immediately** (no verb call needed; it's gray/idle until first use, then green). On `success:false` **nothing is registered and no capsule appears** — read `error` (a `read zip body`/`builder error` means the manifest's zip URL couldn't be fetched). Don't treat a parsed manifest as a completed install; confirm with `bridge_list`. The manifest `url` may be relative (resolved against the manifest's dir, v1.8.169+). | | `bridge_uninstall` | Remove a bridge (`{"name":"…"}`). Bundled ones revert to the bundled copy; third-party ones are fully removed. | | `bridge_pause` / `bridge_resume` | Flip a routing flag so a bridge's verbs fail-fast with `bridge_paused` (or resume). Does NOT kill the process. | -**Naming convention for published bridges (3rd-party authors, read this):** when you publish a bridge to the Adom wiki, its **page title / catalog descriptive name MUST start with `Adom Bridge - `** (spaced hyphen) — e.g. `Adom Bridge - Altium Bridge`, matching the bundled bridges (`Adom Bridge - Puppeteer Bridge`, `Adom Bridge - KiCad Bridge`). This keeps every AD bridge grouped with consistent names in app-index pages instead of scattering as bare slugs. It applies to the **wiki/catalog title only** — NOT `bridge.json` `displayName` (the in-app chip label, which stays tool-descriptive like `Altium Designer`) or `name` (the lowercase slug). Full authoring walkthrough: the `adom-bridge-bridge-sdk` skill (the `adom-bridge-sdk` wiki page) + its fill-in-the-blanks templates.+**Naming convention for published bridges (3rd-party authors, read this):** when you publish a bridge to the Adom wiki, its **page title / catalog descriptive name MUST start with `Adom Bridge - `** (spaced hyphen) — e.g. `Adom Bridge - Altium Bridge`, matching the bundled bridges (`Adom Bridge - Puppeteer Bridge`, `Adom Bridge - KiCad Bridge`). This keeps every ab bridge grouped with consistent names in app-index pages instead of scattering as bare slugs. It applies to the **wiki/catalog title only** — NOT `bridge.json` `displayName` (the in-app chip label, which stays tool-descriptive like `Altium Designer`) or `name` (the lowercase slug). Full authoring walkthrough: the `adom-bridge-bridge-sdk` skill (the `adom-bridge-sdk` wiki page) + its fill-in-the-blanks templates. **Typical "am I on the latest bridges?" flow:** ```bash@@ -430,27 +430,27 @@ adom-bridge refresh_bridges '{"name":"kicad"}' # or just one ``` **Version semantics (so the numbers make sense):**-- `current` = what AD actually runs = the newer of the writable cache and the bundled copy (version-aware, numeric semver — `1.0.10 > 1.0.9`).-- A bundled bridge that ships AHEAD of the wiki shows `updateAvailable:false` — AD correctly keeps the newer bundled copy. **puppeteer is bundled-only** (its node_modules deps exceed the wiki file limit), so it never shows a wiki update; it updates with the AD installer.+- `current` = what ab actually runs = the newer of the writable cache and the bundled copy (version-aware, numeric semver — `1.0.10 > 1.0.9`).+- A bundled bridge that ships AHEAD of the wiki shows `updateAvailable:false` — ab correctly keeps the newer bundled copy. **puppeteer is bundled-only** (its node_modules deps exceed the wiki file limit), so it never shows a wiki update; it updates with the ab installer. - After an `updated` action, the bridge SERVER PROCESS still holds the old code in memory until it respawns. `is_bridge_running` is version-aware (v1.8.142+) and forces a respawn on the next verb call, so you normally don't need to kill anything — but if you want it instant: `taskkill /f /im python.exe` (kicad/fusion) or `node.exe` (puppeteer). -**Auto-update cadence:** a running AD also auto-syncs bridges on launch + every 4 hours. `bridge_check_updates` / `refresh_bridges` (or the GUI's **"Check for updates"** button next to "+ Bridge") are the on-demand "I just published, pick it up now" path.+**Auto-update cadence:** a running ab also auto-syncs bridges on launch + every 4 hours. `bridge_check_updates` / `refresh_bridges` (or the GUI's **"Check for updates"** button next to "+ Bridge") are the on-demand "I just published, pick it up now" path. #### Bridge lifecycle — one instance, stable port (v1.8.153+) -A dynamic bridge gets **one stable port** that AD reuses across every spawn and across AD restarts (persisted in `~/.adom/bridge-ports.json`). A long-lived external client (e.g. a browser extension that dials the bridge) keeps reaching the same port — no chasing a fresh ephemeral port each cycle, no "the client is on bridge A but AD routes to bridge B" split. AD enforces a **single instance**: it tracks every spawned bridge PID, reaps a stale/wedged one before respawning on the same port, and `bridge_stop_all` reaps orphans too.+A dynamic bridge gets **one stable port** that ab reuses across every spawn and across ab restarts (persisted in `~/.adom/bridge-ports.json`). A long-lived external client (e.g. a browser extension that dials the bridge) keeps reaching the same port — no chasing a fresh ephemeral port each cycle, no "the client is on bridge A but ab routes to bridge B" split. ab enforces a **single instance**: it tracks every spawned bridge PID, reaps a stale/wedged one before respawning on the same port, and `bridge_stop_all` reaps orphans too. -`spawn.persistent` (in `bridge.json`) is now surfaced in `bridge_list` (`spawn.persistent`) and means: **AD auto-respawns this bridge on death**, on its stable port (a supervisor runs each ~15 s). So "kill the bridge to load new code" → AD brings it right back at the same port the client keeps dialing. `bridge_install` / `refresh_bridges` also no longer fail with the cache-dir lock (AD reaps the process holding the dir as its cwd, then swaps; in-place merge fallback if a handle lingers).+`spawn.persistent` (in `bridge.json`) is now surfaced in `bridge_list` (`spawn.persistent`) and means: **ab auto-respawns this bridge on death**, on its stable port (a supervisor runs each ~15 s). So "kill the bridge to load new code" → ab brings it right back at the same port the client keeps dialing. `bridge_install` / `refresh_bridges` also no longer fail with the cache-dir lock (ab reaps the process holding the dir as its cwd, then swaps; in-place merge fallback if a handle lingers). -**v1.8.186/187 — first-class bridges (panel + remote management + optional manifest fields).** The AD window has a full-height **Bridges sidebar** that shows each bridge as a mini-app (hero image, LED, rich tooltip); clicking one opens a wide pop-out with a **live console** (its `~/.adom/bridge-logs/<name>.log` + the per-bridge command history), a **ports/process debug view** (live PIDs — `>1` flags a runaway), persistent-vs-one-off + codebase-language badges, and all management actions (install/uninstall/**kill**/restart/pause/resume + "filter activity log to this bridge" + "copy contribution prompt"). The same management is available **remotely** to cloud/WSL2 threads via verbs: `bridge_info`/`bridge_detail` (rich aggregate), `bridge_log_read {name, sinceOffset?}` (**cursor-streaming remote tail -f**), `bridge_kill` (wipe one bridge's processes). As of v1.8.186 **all** bridges write their per-bridge log (bundled kicad/fusion360/puppeteer included, not just dynamic ones). New **optional** `bridge.json` fields (all back-compat, surfaced in the panel): `hero` (a relative filename on your wiki page, or a full URL — your app-icon; absent → a monogram tile), `languages` (override the auto-scan, e.g. `["Python"]`), `releasedAt` (ISO date; absent → bridge.json mtime).+**v1.8.186/187 — first-class bridges (panel + remote management + optional manifest fields).** The ab window has a full-height **Bridges sidebar** that shows each bridge as a mini-app (hero image, LED, rich tooltip); clicking one opens a wide pop-out with a **live console** (its `~/.adom/bridge-logs/<name>.log` + the per-bridge command history), a **ports/process debug view** (live PIDs — `>1` flags a runaway), persistent-vs-one-off + codebase-language badges, and all management actions (install/uninstall/**kill**/restart/pause/resume + "filter activity log to this bridge" + "copy contribution prompt"). The same management is available **remotely** to cloud/WSL2 threads via verbs: `bridge_info`/`bridge_detail` (rich aggregate), `bridge_log_read {name, sinceOffset?}` (**cursor-streaming remote tail -f**), `bridge_kill` (wipe one bridge's processes). As of v1.8.186 **all** bridges write their per-bridge log (bundled kicad/fusion360/puppeteer included, not just dynamic ones). New **optional** `bridge.json` fields (all back-compat, surfaced in the panel): `hero` (a relative filename on your wiki page, or a full URL — your app-icon; absent → a monogram tile), `languages` (override the auto-scan, e.g. `["Python"]`), `releasedAt` (ISO date; absent → bridge.json mtime). -**v1.8.156 — strays + self-documenting hints.** AD now tracks every bridge PID it spawns (persisted across AD restarts) and **reaps all stray instances** before respawning — so you don't end up with 4 copies of one bridge fighting over the routing. `bridge_list` surfaces `spawn.instanceCount` per dynamic bridge (normally 0 or 1; **>1 = strays piled up** — visible at a glance). If a bridge verb fails to reach a healthy bridge, the error `_hint` tells you to retry / check `instanceCount` / `process_list`; and a verb response right after a (re)spawn carries an `_ad_note` saying the external host reconnects within a few seconds — **retry** instead of treating a transient "not connected" as terminal. For verb discovery from a bridge, `GET <ADOM_DIRECT_API_URL>/commands` lists every reachable verb (see [`skills/DIRECT_API.md`](DIRECT_API.md)).+**v1.8.156 — strays + self-documenting hints.** ab now tracks every bridge PID it spawns (persisted across ab restarts) and **reaps all stray instances** before respawning — so you don't end up with 4 copies of one bridge fighting over the routing. `bridge_list` surfaces `spawn.instanceCount` per dynamic bridge (normally 0 or 1; **>1 = strays piled up** — visible at a glance). If a bridge verb fails to reach a healthy bridge, the error `_hint` tells you to retry / check `instanceCount` / `process_list`; and a verb response right after a (re)spawn carries an `_ad_note` saying the external host reconnects within a few seconds — **retry** instead of treating a transient "not connected" as terminal. For verb discovery from a bridge, `GET <ADOM_DIRECT_API_URL>/commands` lists every reachable verb (see [`skills/DIRECT_API.md`](DIRECT_API.md)). #### Bridge status chip — self-reported LED + tooltip (v1.8.152+) -Each bridge has a status dot in the AD GUI sidebar. **AD is a pure renderer of bridge health** — a live bridge decides its own LED color and hover text; AD just paints what the bridge reports. This replaced the old scheme where AD inferred the LED from whether it was holding a spawn handle, which left fire-and-forget (dynamic/installed) bridges stuck RED even while healthy and routing.+Each bridge has a status dot in the ab GUI sidebar. **ab is a pure renderer of bridge health** — a live bridge decides its own LED color and hover text; ab just paints what the bridge reports. This replaced the old scheme where ab inferred the LED from whether it was holding a spawn handle, which left fire-and-forget (dynamic/installed) bridges stuck RED even while healthy and routing. -**The contract (all three fields OPTIONAL).** On the endpoint AD already polls — your `bridge.json` `healthEndpoint` (default `/health`; the sample bridges use `/status`) — return a 2xx JSON body that MAY include:+**The contract (all three fields OPTIONAL).** On the endpoint ab already polls — your `bridge.json` `healthEndpoint` (default `/health`; the sample bridges use `/status`) — return a 2xx JSON body that MAY include: | Field | Type | Meaning | |---|---|---|@@ -458,12 +458,12 @@ Each bridge has a status dot in the AD GUI sidebar. **AD is a pure renderer of b | `summary` | string | Short label shown next to the dot. Keep it tiny (≤ ~24 chars) — e.g. `"2 windows"`, `"ext disconnected"`. | | `tooltip` | string | Full hover text. Newlines allowed; no hard length limit (keep it readable). | -**Rules AD enforces:**+**Rules ab enforces:** - Endpoint returns 2xx **with** `led`/`summary`/`tooltip` → rendered **verbatim**, no parsing or logic.-- Endpoint returns 2xx **without** them → AD defaults to **green** (you're reachable) + a generic tooltip. Existing bridges work unchanged — this is opt-in.-- Endpoint **unreachable** → AD owns this ONE state: a **gray/offline** dot + AD's own tooltip ("Not running" / "Bridge not reachable"). A dead bridge can't self-report, so AD never lets a bridge claim "offline" — it detects it.+- Endpoint returns 2xx **without** them → ab defaults to **green** (you're reachable) + a generic tooltip. Existing bridges work unchanged — this is opt-in.+- Endpoint **unreachable** → ab owns this ONE state: a **gray/offline** dot + ab's own tooltip ("Not running" / "Bridge not reachable"). A dead bridge can't self-report, so ab never lets a bridge claim "offline" — it detects it. -**Cadence:** AD re-polls every ~15 s, and a dynamic bridge flips green the instant it spawns. So a bridge that surfaces live counters (open windows, queue depth, ext-connection state) in `summary`/`tooltip` updates in the GUI within a tick.+**Cadence:** ab re-polls every ~15 s, and a dynamic bridge flips green the instant it spawns. So a bridge that surfaces live counters (open windows, queue depth, ext-connection state) in `summary`/`tooltip` updates in the GUI within a tick. Reference implementations: [`scripts/sample-bridges/hello-python/server.py`](../scripts/sample-bridges/hello-python/server.py) and [`hello-rust/src/main.rs`](../scripts/sample-bridges/hello-rust/src/main.rs) both self-report `led`/`summary`/`tooltip` on `/status` — fork either. @@ -486,7 +486,7 @@ Reference implementations: [`scripts/sample-bridges/hello-python/server.py`](../ ### Desktop Notifications -- `notify_user` -- Send a native Windows toast (own AUMID + Adom logo). v1.9.33: rich types — progress bar, sticky reminder, action buttons that **bring AD to the foreground** on click + round-trip via `notify_response`.+- `notify_user` -- Send a native Windows toast (own AUMID + Adom logo). v1.9.33: rich types — progress bar, sticky reminder, action buttons that **bring ab to the foreground** on click + round-trip via `notify_response`. - `title`, `body`, `level` (info/success/warning/error/emergency), `actions` (label buttons) — plus `scenario` ("reminder"=sticky), `durationLong`, `buttons` (`[{label, arguments?}]`), `progress` (`{title, value 0..1, status, valueOverride}`), `inputs`, `id` (correlation id for the round-trip). - Full guide + copy-paste examples per type: the **adom-bridge-notify** skill ([`skills/NOTIFY.md`](NOTIFY.md)). - `notify_response` -- Poll which toast button the user clicked, by correlation `id`. `{pending:true}` until they click; `{pending:false, action}` once they do (consumed on read).@@ -954,7 +954,7 @@ adom-bridge-cli watch '{"command":"fusion_walk_cloud_tree","args":{...},"interva **One-time setup (user side):** 1. Register a free app at <https://aps.autodesk.com> → enable the **Data Management API**. 2. Set the app's **Callback URL** to exactly `http://localhost:8910/aps/callback`.-3. Put the **Client ID** in AD config key `aps_client_id` (in `~/.adom/config.json`) or via Settings.+3. Put the **Client ID** in ab config key `aps_client_id` (in `~/.adom/config.json`) or via Settings. 4. Run `aps_auth` once — it opens the system browser for Autodesk sign-in. Tokens cache in `~/.adom/aps_tokens.json` and auto-refresh; you won't be prompted again. **Verbs:**@@ -1165,22 +1165,22 @@ adom-bridge-cli pup_open_window '{ **The smart flow.** CDP (the protocol pup drives) needs the browser running with `--remote-debugging-port`, which can only be set at launch. So the bridge handles each situation differently and tells you which happened: -| Situation | What AD does | Response |+| Situation | What ab does | Response | |---|---|---| | Already driving that browser+profile | Reuse the live connection | `ok` + `narrative` |-| Running **with** a debug port (power-user launched it that way, or AD did earlier) | Connect, no relaunch | `ok` + `narrative` |+| Running **with** a debug port (power-user launched it that way, or ab did earlier) | Connect, no relaunch | `ok` + `narrative` | | **Not running at all** | **Launch it fresh with the debug port — NO prompt** (nothing to disrupt) | `ok`, `launchedFresh:true` + `narrative` — just proceed |-| **Open without** a debug port | **Ask the user** — AD must close+reopen it | `errorCode:"native_relaunch_required"` + `confirmArgs` + `alternatives` |+| **Open without** a debug port | **Ask the user** — ab must close+reopen it | `errorCode:"native_relaunch_required"` + `confirmArgs` + `alternatives` | | Debug port **refuses to bind** (enterprise policy) | Won't close it again | `errorCode:"native_cdp_blocked"` + `diagnosis` + `alternatives` | **Handling the two consent/blocked codes:** - **`native_relaunch_required`** — the browser is open without the port, so driving it means closing + reopening it (with `--restore-last-session`, so all tabs/windows come back). **Ask the user's permission**, then re-call with `"nativeRelaunch":true`. The response's `alternatives` lists other installed browsers you could offer instead.-- **`native_cdp_blocked`** — the debug port won't open, almost always an **enterprise policy** (`RemoteDebuggingAllowed` disabled via Google Workspace / Chrome cloud management — affects **Chrome browser-wide, every profile**). `diagnosis` says whether it's a confirmed local-registry policy or a likely cloud-managed one. AD will **not** close that browser again. `alternatives` names other installed browsers — **Edge usually escapes Google management**, so it often drives natively when Chrome can't. Offer the user the alternative, or fall back to generic CfT.+- **`native_cdp_blocked`** — the debug port won't open, almost always an **enterprise policy** (`RemoteDebuggingAllowed` disabled via Google Workspace / Chrome cloud management — affects **Chrome browser-wide, every profile**). `diagnosis` says whether it's a confirmed local-registry policy or a likely cloud-managed one. ab will **not** close that browser again. `alternatives` names other installed browsers — **Edge usually escapes Google management**, so it often drives natively when Chrome can't. Offer the user the alternative, or fall back to generic CfT. **Always available — generic CfT.** `pup_open_window` with NO `nativeBrowser`/`nativeProfile` drives a clean Chrome-for-Testing instance: no user session, no managed policy, fully controllable. If a task needs the user's logged-in session there, have them sign in once. This is the universal fallback whenever native is blocked. -**Power-user shortcut:** if the user launches their browser at startup with `--remote-debugging-port=<N>`, AD reconnects every session with no prompt at all (a productized opt-in "keep native browsers debug-ready at boot" is on the roadmap, default OFF for security).+**Power-user shortcut:** if the user launches their browser at startup with `--remote-debugging-port=<N>`, ab reconnects every session with no prompt at all (a productized opt-in "keep native browsers debug-ready at boot" is on the roadmap, default OFF for security). **Routing rule — DON'T auto-escalate.** Treat native as opt-in: @@ -1260,12 +1260,12 @@ For a control that isn't reachable by CDP/coords (e.g. the Reload button on `edg **Visual "Adom cursor" (v1.8.171+)** — a click-through teal cursor + a label that narrates what you're driving (trust cue; byte-faithful to the browser extension's cursor). Pure visual, *not* gated: - `desktop_cursor {action:"show"|"move"|"hide", x, y, label}` — drive/narrate the cursor on its own ("watch me"). Returns the overlay `hwnd`.-- Or pass `cursor:true` + `label:"Clicking Reload"` on `desktop_click`/`desktop_double_click`/`desktop_right_click`/`desktop_hover` — AD glides the teal cursor to the target (narrating the label) *then* does the real click.+- Or pass `cursor:true` + `label:"Clicking Reload"` on `desktop_click`/`desktop_double_click`/`desktop_right_click`/`desktop_hover` — ab glides the teal cursor to the target (narrating the label) *then* does the real click. - **Auto-hide (v1.8.174+):** the cursor hides itself after `ttlMs` (default **60s**) so it never lingers on the user's screen if you forget to hide it. Each `show`/`move` refreshes the timer. Pass `ttlMs:<ms>` (≤600000) for longer, or `persist:true` to keep it until you explicitly `desktop_cursor {action:"hide"}`. If a cursor you showed was auto-hidden, the **next** cursor/click response prefixes its `_hint` with a NOTE so you learn (no chatty unsolicited message). #### Background input via UI Automation (v1.8.174+) — no foreground, no focus steal -The mouse/keyboard verbs above use `SendInput` = real OS input, which only lands on the **foreground** window — so AD foregrounds the target first, yanking it in front of the user's work. When you must NOT steal focus, use **UI Automation** instead: `InvokePattern.Invoke()` / `ValuePattern.SetValue()` are **programmatic** — the app services them via its accessibility tree with **no cursor move, no focus change, no foregrounding**. Chromium/Edge expose their a11y tree to UIA, so `chrome://` page buttons/links are reachable **by accessible name** (not coordinates).+The mouse/keyboard verbs above use `SendInput` = real OS input, which only lands on the **foreground** window — so ab foregrounds the target first, yanking it in front of the user's work. When you must NOT steal focus, use **UI Automation** instead: `InvokePattern.Invoke()` / `ValuePattern.SetValue()` are **programmatic** — the app services them via its accessibility tree with **no cursor move, no focus change, no foregrounding**. Chromium/Edge expose their a11y tree to UIA, so `chrome://` page buttons/links are reachable **by accessible name** (not coordinates). - `desktop_find_control {window|hwnd, name|contains, role?}` — **read-only** discovery (ungated). Returns `best:{name, automationId, role, rect, invokable, settable}`. Call this first to learn whether a control is UIA-reachable. - `desktop_ui_click {window|hwnd, name|contains, role?}` — UIA **Invoke** by accessible name, in the background. Gated behind shell-auto-approve.@@ -1295,7 +1295,7 @@ v1.9.0 expands UIA from ~3 patterns to a near-complete agent-drivable client. ** - **Actions (gated by shell-auto-approve; background-first via the pattern, SendInput-click fallback only when the pattern is absent → then `raised:true`):** `desktop_ui_toggle {selector, state?}`, `desktop_ui_expand {selector, action?:expand|collapse|toggle}`, `desktop_ui_select {selector, action?:select|add|remove}`, `desktop_ui_scroll {selector, dir?, amount?}`, `desktop_ui_set_range {selector, value}`, `desktop_ui_focus {selector}` (**bg:foreground** — usually raises; reports it), `desktop_ui_window {window, action:minimize|maximize|restore|close|move|resize}`. - **Events (v1.9.3 — REACT instead of poll; read-only, ungated):** - `desktop_ui_watch {event, window?|hwnd?, properties?}` → `{watchId}`. `event` ∈ `focus` (desktop-wide "what got focus") | `window-opened`/`window-closed` (new/closed top-level windows — these three can be global) | `invoke` | `selection` | `text-changed` | `text-selection` | `menu-opened` | `tooltip-opened` | `notification` | `live-region` | `structure` | `property` (these REQUIRE a `window`, else `errorCode:window_required` — avoids a desktop firehose). Cap 16 watches.- - `desktop_ui_events {sinceSeq?, watchId?, max?}` → DRAIN the buffer (poll cursor; there is NO server-push over the relay). Returns `{events:[{seq, watchId, event, ts, element:{name,role,automationId,rect,hwnd}, eventType|property|changeType}], nextSeq, highestSeq, missedEvents, activeWatches}`. Poll with `sinceSeq=nextSeq`. Events ALSO appear in the AD GUI activity-log. The 500-deep ring is shared across all watches; `missedEvents:true` = you polled too slowly.+ - `desktop_ui_events {sinceSeq?, watchId?, max?}` → DRAIN the buffer (poll cursor; there is NO server-push over the relay). Returns `{events:[{seq, watchId, event, ts, element:{name,role,automationId,rect,hwnd}, eventType|property|changeType}], nextSeq, highestSeq, missedEvents, activeWatches}`. Poll with `sinceSeq=nextSeq`. Events ALSO appear in the ab GUI activity-log. The 500-deep ring is shared across all watches; `missedEvents:true` = you polled too slowly. - `desktop_ui_unwatch {watchId|"all"}` → stop. **Canonical use:** waiting on an OTP/confirm dialog → `ui_watch {event:"window-opened"}`, poll `ui_events` until the window appears, then act on it (no busy screenshot loop). - **Introspection:** `desktop_ui_capabilities {}` — **read-only, ungated.** The foreground-hint TABLE per verb (`staticClass` + measured `{win32, chromium-pattern, chromium-fallback}`). **Consult it BEFORE a call to know if it'll steal the screen, without acting.** `desktop_ui_selftest {confirm?, window?}` writes/refreshes it (empirically measures when `confirm:true`). @@ -1316,7 +1316,7 @@ You no longer recompute scale/offset/DPI to click what you see in a screenshot. ``` Two ways to use it:-- **Let AD convert (preferred, zero math):** `desktop_click {space:"image", shotId, x, y}` where `x,y` are pixels in that screenshot. Also works on `desktop_double_click`/`right_click`/`hover`. The `shotId` comes from the capture's `coordMap.shotId` (last ~64 shots retained; re-capture if it expires).+- **Let ab convert (preferred, zero math):** `desktop_click {space:"image", shotId, x, y}` where `x,y` are pixels in that screenshot. Also works on `desktop_double_click`/`right_click`/`hover`. The `shotId` comes from the capture's `coordMap.shotId` (last ~64 shots retained; re-capture if it expires). - **Compute yourself:** `screenX = x*sx + ox`, `screenY = y*sy + oy`. `screenRect` is in the SAME physical-px space as `desktop_list_windows` rects and the `desktop_click` default (`space:"screen"`).@@ -1332,11 +1332,11 @@ Two ways to use it: **Screenshot cache is self-cleaning + self-describing (v1.9.107/108/113):** the CLI auto-pulls every screenshot to ONE user-visible, namespaced dir on the caller's machine (`~/project/screenshots/adom-bridge/`, else `~/.adom/screenshots/`; `ADOM_SHOTS_DIR` overrides) — never `/tmp` anymore. Shots are **bucketed by kind into subfolders**: `window/` (a specific window / app / owned popup) and `screen/` (a full-desktop grab). **Human-readable filenames (v1.9.113):** `<YYYY-MM-DDThh-mm-ss.mmm>[-<hwnd>].png` — a sortable ISO stamp (lexicographic == chronological), plus the `hwnd` for a window capture; the `window`/`screen` kind is NOT repeated in the name because the folder already says it. **One file when small (v1.9.114):** if the shot was already within the safe size cap (the common case) it's a single `<stem>.png` and `localFullPath == localSafePath` — no redundant re-encoded copy. Only a shot large enough to be downscaled produces a `<stem>.full.png` + `<stem>.safe.png` pair (Read the `.safe` one for image analysis; use `.full` for archive). Each shot gets a **`<stem>.json` sidecar** (shotlog-style: `source`, `timestamp`, `kind`, dims + `bytes`, `coordMap`, `rect`; a full+safe pair shares one). **Parent/child grouping (v1.9.111/113):** when `desktop_screenshot_window` returns owned popups in `screenshots[]`, the parent AND each pulled child share the same stem, and each child gets a **`.popup-NN` label** — e.g. parent `2026-07-15T01-14-16.937-986710.safe.png` and children `2026-07-15T01-14-16.937-986710.popup-01.safe.png`, `...popup-02...`. So a plain `ls` shows the whole capture grouped (shared stem) and the role (`.popup-NN`) without opening a sidecar; the sidecar also carries `parent` + `popupIndex`. (`kicad_screenshot_all` returns sibling windows with no parent, so those keep their own names.) A janitor prunes it (7 days / 2 GB / newest kept; sidecars follow their PNG, orphans swept) and drains the old `/tmp/ad-shots` + `/tmp/adom-bridge-screenshots` backlogs, running opportunistically at most once/24h on any CLI call. **You do not need to clean it up or write your own cron.** Every screenshot response carries a `_cacheHint`; verify coverage with **`adom-bridge-cli janitor`** (status: each subfolder, size, policy, last sweep) or force a sweep with **`adom-bridge-cli janitor '{"sub":"run"}'`**. It never touches other tools' files under `~/project/screenshots/` (e.g. shotlog). Tune via `ADOM_SHOTS_MAX_AGE_DAYS` / `ADOM_SHOTS_MAX_GB` / `ADOM_SHOTS_MAX_FILES`, or disable with `ADOM_JANITOR=0`. -**Background vs foreground driving (v1.8.178):** every desktop driving verb carries a structured **`drive`** field (`"background"` | `"foreground"` | `"n/a"`) — in `GET /commands`, in `adom-bridge-cli help <verb>`, and echoed in the verb's own response. **Background** (no foreground / focus steal / cursor move — UIA + capture): `desktop_ui_click`, `desktop_ui_set`, `desktop_navigate`, `desktop_find_control`, `desktop_screenshot_window/_screen`, `desktop_record_window_start`. **Foreground** (SendInput → goes to the focused window, so AD foregrounds first): `desktop_click`/`_double_click`/`_right_click`/`_hover`, `desktop_type`, `desktop_press_key`, `desktop_bring_to_front`. Run **`adom-bridge-cli help background`** for the full grouping. Rule: to act without disturbing the user's window, drive by NAME (`desktop_find_control` → `desktop_ui_click`/`ui_set`); fall back to coordinate clicks/keys only when the control isn't in the UIA tree.+**Background vs foreground driving (v1.8.178):** every desktop driving verb carries a structured **`drive`** field (`"background"` | `"foreground"` | `"n/a"`) — in `GET /commands`, in `adom-bridge-cli help <verb>`, and echoed in the verb's own response. **Background** (no foreground / focus steal / cursor move — UIA + capture): `desktop_ui_click`, `desktop_ui_set`, `desktop_navigate`, `desktop_find_control`, `desktop_screenshot_window/_screen`, `desktop_record_window_start`. **Foreground** (SendInput → goes to the focused window, so ab foregrounds first): `desktop_click`/`_double_click`/`_right_click`/`_hover`, `desktop_type`, `desktop_press_key`, `desktop_bring_to_front`. Run **`adom-bridge-cli help background`** for the full grouping. Rule: to act without disturbing the user's window, drive by NAME (`desktop_find_control` → `desktop_ui_click`/`ui_set`); fall back to coordinate clicks/keys only when the control isn't in the UIA tree. **Click a control by name → coords:** `desktop_find_control` returns `best.rect{x,y,w,h,centerX,centerY}` in screen px — `desktop_click {x:centerX, y:centerY}` to SendInput-click it (handy when `invokable`/`settable` are both false). Target by `name`, `contains`, `role`, or `automationId` (stable, locale-independent). -**Keyboard chords:** `desktop_press_key` accepts a chord as one string `["ctrl+w"]` OR separate tokens `["ctrl","w"]` — both mean Ctrl held + W. A *sequence* of distinct chords uses `+` per element: `["ctrl+l","enter"]`. The reply echoes `chords` showing how AD interpreted your `keys`.+**Keyboard chords:** `desktop_press_key` accepts a chord as one string `["ctrl+w"]` OR separate tokens `["ctrl","w"]` — both mean Ctrl held + W. A *sequence* of distinct chords uses `+` per element: `["ctrl+l","enter"]`. The reply echoes `chords` showing how ab interpreted your `keys`. - `desktop_flash_window` (v1.8.152+) -- Flash an **arbitrary** window's taskbar button orange (Win32 `FlashWindowEx`) as an "attention / ongoing activity" cue. Resolve the HWND with `desktop_list_windows` first. Args: `hwnd` (int) OR `titleContains` (string); `mode` (`"until_focused"` default — flashes until the user clicks the window; or `"count"`); `count` (int, `mode:"count"` only, default 3); `stop` (bool — clear an active flash). Returns `{ok, hwnd, title, mode}`. **Lightweight by design** (direct FlashWindowEx, no process spawn) so a bridge can call it after every verb. Distinct from the puppeteer bridge's `pup_alert_window`, which only flashes pup's OWN tracked windows — this flashes any window on the host. For progress + overlay-icon status, use `desktop_taskbar`. - Example: `adom-bridge-cli desktop_flash_window '{"hwnd":133560}'` then `adom-bridge-cli desktop_flash_window '{"hwnd":133560,"stop":true}'` to clear.@@ -1344,9 +1344,9 @@ Two ways to use it: - `hwnd` (int) OR `titleContains` (string) - `flash`: `"until_focused"` | `"stop"` - `progress`: `{state: "normal"|"paused"|"error"|"indeterminate"|"none", value?: 0..100}` — **state→color**: normal=green, **paused=yellow** (use for human-in-the-loop gates: captcha / 2FA / login), error=red, indeterminate=marquee, none=clear.- - `overlay`: `{badge: "adom"|"none", icon?: "<PNG or .ico — file path OR base64>", tooltip?: "<hover>", avatar?, profileDir?, userDataDir?, composite?}` — built-in badges are `adom` (the Adom logo — a "driven by Adom" trust tell) and `none` (clear); 3rd-party bridges pass their **own** `icon` (a **PNG** or `.ico`, file path or base64 — AD magic-sniffs + renders at 32px). AD stays generic: it paints whatever art you hand it. `tooltip` is the overlay's accessible description.- - **Compositing onto a Chrome/Edge profile avatar (v1.9.110, #154):** Windows gives ONE overlay slot per taskbar button, and Chrome/Edge use it for the **profile avatar** — so a bare `badge:"adom"` would EVICT the profile face (you'd lose "work vs personal"). To keep the avatar AND show "Adom is driving", pass the avatar and AD composites the mark onto it (bottom-right ~47%): `overlay:{badge:"adom", avatar:"<PNG path or base64>"}`, or name the profile with `overlay:{badge:"adom", profileDir:"Profile 1", userDataDir?:"<User Data root>"}` (AD reads `<UserData>/<profileDir>/Google Profile Picture.png`; `userDataDir` is derived from the window's exe if omitted). `composite:false` opts out (raw mark). If the avatar can't be resolved AD falls back to the plain mark — never errors. The CALLER (which knows its profile — e.g. the browser extension) supplies the avatar; AD does not reverse-engineer it (one browser process serves all profiles, so the process command line has no `--profile-directory`).- - `thumbnailTooltip` (v1.9.153): the taskbar **hover-preview tooltip** (`ITaskbarList3::SetThumbnailTooltip`). The ONLY uncapped text surface on the preview — the window title truncates ~25-30 chars, this string does not (and takes newlines). e.g. `"Adom Pup / Adom wiki / LOGGED-IN view / Chrome for Testing / 3 tabs"`. Pass `""` to clear it back to the title. Works on a window AD does not own.+ - `overlay`: `{badge: "adom"|"none", icon?: "<PNG or .ico — file path OR base64>", tooltip?: "<hover>", avatar?, profileDir?, userDataDir?, composite?}` — built-in badges are `adom` (the Adom logo — a "driven by Adom" trust tell) and `none` (clear); 3rd-party bridges pass their **own** `icon` (a **PNG** or `.ico`, file path or base64 — ab magic-sniffs + renders at 32px). ab stays generic: it paints whatever art you hand it. `tooltip` is the overlay's accessible description.+ - **Compositing onto a Chrome/Edge profile avatar (v1.9.110, #154):** Windows gives ONE overlay slot per taskbar button, and Chrome/Edge use it for the **profile avatar** — so a bare `badge:"adom"` would EVICT the profile face (you'd lose "work vs personal"). To keep the avatar AND show "Adom is driving", pass the avatar and ab composites the mark onto it (bottom-right ~47%): `overlay:{badge:"adom", avatar:"<PNG path or base64>"}`, or name the profile with `overlay:{badge:"adom", profileDir:"Profile 1", userDataDir?:"<User Data root>"}` (ab reads `<UserData>/<profileDir>/Google Profile Picture.png`; `userDataDir` is derived from the window's exe if omitted). `composite:false` opts out (raw mark). If the avatar can't be resolved ab falls back to the plain mark — never errors. The CALLER (which knows its profile — e.g. the browser extension) supplies the avatar; ab does not reverse-engineer it (one browser process serves all profiles, so the process command line has no `--profile-directory`).+ - `thumbnailTooltip` (v1.9.153): the taskbar **hover-preview tooltip** (`ITaskbarList3::SetThumbnailTooltip`). The ONLY uncapped text surface on the preview — the window title truncates ~25-30 chars, this string does not (and takes newlines). e.g. `"Adom Pup / Adom wiki / LOGGED-IN view / Chrome for Testing / 3 tabs"`. Pass `""` to clear it back to the title. Works on a window ab does not own. - `thumbnailClip` (v1.9.153): crop the live hover preview to a client-space region — `{x,y,w,h}`, or `null` to reset to the whole window (`SetThumbnailClip`). - Returns `{ok, hwnd, title, applied:[...]}`. Clears: `flash:"stop"`, `progress.state:"none"`, `overlay.badge:"none"`, `thumbnailTooltip:""`, `thumbnailClip:null` — call these on task-end / window-close to clean up. - Example (long task, human gate, then done):@@ -1355,7 +1355,7 @@ Two ways to use it: adom-bridge-cli desktop_taskbar '{"hwnd":133560,"progress":{"state":"paused"},"flash":"until_focused"}' # captcha — come look adom-bridge-cli desktop_taskbar '{"hwnd":133560,"progress":{"state":"none"},"overlay":{"badge":"none"},"flash":"stop"}' # done — clear all ```-- `desktop_set_window_identity` + `desktop_register_app_identity` (v1.9.134+) -- **Full window-identity takeover.** Make a window you drive present itself as YOUR app: the taskbar button, Alt-Tab entry, hover tooltip and any user pin read your name and icon instead of the process that happens to own the HWND. Built for a bridge driving a borrowed host app (a Chrome for Testing window otherwise reads "Test" with Chrome's icon, which is two brands and a confusing word). AD is generic here — it applies whatever `appId` / `iconPath` / `displayName` you pass and knows nothing about which app that is.+- `desktop_set_window_identity` + `desktop_register_app_identity` (v1.9.134+) -- **Full window-identity takeover.** Make a window you drive present itself as YOUR app: the taskbar button, Alt-Tab entry, hover tooltip and any user pin read your name and icon instead of the process that happens to own the HWND. Built for a bridge driving a borrowed host app (a Chrome for Testing window otherwise reads "Test" with Chrome's icon, which is two brands and a confusing word). ab is generic here — it applies whatever `appId` / `iconPath` / `displayName` you pass and knows nothing about which app that is. **You need BOTH halves.** `set_window_identity` stamps a LIVE window (`PKEY_AppUserModel_ID` + `WM_SETICON`); `register_app_identity` teaches Windows what that `appId` MEANS (an HKCU `AppUserModelId` key + a Start Menu shortcut carrying the same AUMID). An unregistered appId falls back to the owning exe's icon, so half one alone leaves Alt-Tab wrong. @@ -1374,10 +1374,10 @@ Two ways to use it: ``` - Window target: `hwnd` OR `titleContains` OR `cacheKey` (+ `fresh` to re-resolve). Response carries `resolvedBy` (`hwnd` | `cachedHwnd` | `title`) and `applied[]`. - **Identity is per-HWND and dies with the window** — re-apply after a window is recreated.- - AD holds the icon handles for its whole process lifetime. Icon handles are process-owned: a transient setter leaves windows pointing at dead handles, so the icon reverts moments later. AD being long-lived is exactly why it owns them.+ - ab holds the icon handles for its whole process lifetime. Icon handles are process-owned: a transient setter leaves windows pointing at dead handles, so the icon reverts moments later. ab being long-lived is exactly why it owns them. - `register_app_identity` writes HKCU only (never a UAC prompt). It is gated for RELAY callers like `write_file`; a bridge calling over the loopback API is trusted by transport and ungated.- - **Jump-list tasks (v1.9.139+): `desktop_set_window_jumplist {appId, tasks}`** puts YOUR actions on the taskbar right-click menu of every window stamped with that appId. Jump lists attach to the AUMID, so per-session appIds give each window its OWN menu. Each task: `{title (required), target? (exe, defaults to AD's bundled CLI — so `args` can simply be an AD/bridge verb + JSON), args?, iconPath?, description?}`. Tasks launch minimized (no console flash). Max 16; `tasks:[]` clears (call at session close, alongside `unregister_app_identity`). Re-set anytime to update the menu. Tier-2 gated for relay callers; loopback bridges ungated.- - **Per-session / transient appIds (v1.9.136+): register REGISTRY-ONLY with `shortcut:false`, unregister on close.** If you stamp per-session AUMIDs (e.g. `Adom.Pup.<sessionId>` so each window keeps its own taskbar button in split mode), an UNREGISTERED AUMID makes Alt-Tab fall back to the owning exe's icon. `desktop_register_app_identity {appId, displayName, iconPath, shortcut:false}` writes just the HKCU AppUserModelId key (DisplayName + IconUri) — Alt-Tab and taskbar hover resolve the name + icon with NO Start Menu entry. Pair with `desktop_unregister_app_identity {appId}` at session close (idempotent; removes the key plus any shortcut carrying that AUMID; AD's own `inc.adom.bridge` identity is protected). Keep ONE shortcut-mode registration as the pinnable launcher.+ - **Jump-list tasks (v1.9.139+): `desktop_set_window_jumplist {appId, tasks}`** puts YOUR actions on the taskbar right-click menu of every window stamped with that appId. Jump lists attach to the AUMID, so per-session appIds give each window its OWN menu. Each task: `{title (required), target? (exe, defaults to ab's bundled CLI — so `args` can simply be an ab/bridge verb + JSON), args?, iconPath?, description?}`. Tasks launch minimized (no console flash). Max 16; `tasks:[]` clears (call at session close, alongside `unregister_app_identity`). Re-set anytime to update the menu. Tier-2 gated for relay callers; loopback bridges ungated.+ - **Per-session / transient appIds (v1.9.136+): register REGISTRY-ONLY with `shortcut:false`, unregister on close.** If you stamp per-session AUMIDs (e.g. `Adom.Pup.<sessionId>` so each window keeps its own taskbar button in split mode), an UNREGISTERED AUMID makes Alt-Tab fall back to the owning exe's icon. `desktop_register_app_identity {appId, displayName, iconPath, shortcut:false}` writes just the HKCU AppUserModelId key (DisplayName + IconUri) — Alt-Tab and taskbar hover resolve the name + icon with NO Start Menu entry. Pair with `desktop_unregister_app_identity {appId}` at session close (idempotent; removes the key plus any shortcut carrying that AUMID; ab's own `inc.adom.bridge` identity is protected). Keep ONE shortcut-mode registration as the pinnable launcher. - **`desktop_set_window_jumplist`** (v1.9.139+) adds the right-click menu on the taskbar button (per-AUMID; `tasks:[]` clears) and, with `hwnd`+`headerIcon`, brands the header row (the app tile above Pin/Close). - **Icon format — read the `iconCheck` these verbs return (v1.9.152).** `desktop_set_window_identity` / `desktop_register_app_identity` / `desktop_set_window_jumplist` return an `iconCheck {kind,sizes,issues,ok,hint}` (or `headerIconCheck`) that magic-sniffs the file and explains why an icon would render as a generic white document: a **256px `.ico` size not PNG-compressed** (the Vista+ trap), **SVG** (the Win32 shell can't load it), a `.ico` with no small size, an `.exe` where an image is wanted. If `iconCheck.ok` is false, fix the file and re-stamp. Ship a multi-size `.ico` (16/24/32/48/256, the 256 PNG-compressed). **Full guide:** the `adom-bridge-window-identity` skill (AUMID model, the register+stamp two-halves rule, jump-list header, taskbar overlay + thumbnail, the live-tile trap, a recipe). - `desktop_revoke_approvals` -- Revoke all shell auto-approve permissions and deny pending approvals. The next shell command will show the approval dialog.@@ -1423,36 +1423,36 @@ Take screenshots of the user's desktop or individual windows. All screenshots us - `desktop_list_windows` -- List all visible windows (returns HWND, title, class name, position/size, and `z` = Z-order index, 0 = topmost) - `desktop_find_window` (v1.8.153+) -- Resolve a window's HWND by `titleContains` and/or `className` (case-insensitive substrings), returned in Z-order with `best` = the topmost match. Use this instead of diffing `desktop_list_windows` before/after launching something — it's race-free for "the window I just opened." Returns `{ok, count, best:{hwnd,title,className,z,rect}|null, windows[]}` (`ok` true only when ≥1 matched). Pass `best.hwnd` to `desktop_taskbar` / `desktop_flash_window` / `desktop_screenshot_window`. - Example: `adom-bridge-cli desktop_find_window '{"className":"Chrome_WidgetWin_1","titleContains":"GitHub"}'`-- `desktop_activity_filter` (v1.8.155+) -- Scope the **user's GUI activity log** so they watch the slice you care about while you work (e.g. focus it on one bridge before a debugging session). Args: `bridge` (a bridge registry name from `bridge_list` | `"none"` = only global/non-bridge AD commands | `"all"`/omit = everything), `text` (free-text substring, ANDs with bridge), `clear` (bool — reset). **Drives the human's GUI view only — it does NOT change which verbs you can call or what they return.** The GUI shows a status bar stating exactly what's shown + what's hidden.+- `desktop_activity_filter` (v1.8.155+) -- Scope the **user's GUI activity log** so they watch the slice you care about while you work (e.g. focus it on one bridge before a debugging session). Args: `bridge` (a bridge registry name from `bridge_list` | `"none"` = only global/non-bridge ab commands | `"all"`/omit = everything), `text` (free-text substring, ANDs with bridge), `clear` (bool — reset). **Drives the human's GUI view only — it does NOT change which verbs you can call or what they return.** The GUI shows a status bar stating exactly what's shown + what's hidden. - Example: `adom-bridge-cli desktop_activity_filter '{"bridge":"kicad"}'` then `adom-bridge-cli desktop_activity_filter '{"clear":true}'` - `desktop_screenshot_window` -- Capture a specific window by HWND - `desktop_screenshot_screen` -- Capture the entire desktop (all monitors)-- `desktop_ad_scroll` (v1.9.86+) -- **Scroll AD's OWN window panels.** AD is a fully AI-controllable app, so you can drive its own scrollbars — most usefully to page the **bridges sidebar** and screenshot cards that are below the fold. Args: `panel` (`bridges` default | `activity` | `connections`), `to` (`top`|`bottom`, absolute — wins over `dy`), `dy` (relative px, default 400; negative = up). Returns `{ok, panel, scrolled, state:{scrollTop,scrollHeight,clientHeight,atTop,atBottom,found}|null, _hint}` — round-tripped from the live window so you can **loop until `state.atBottom==true`**. A collapsed bridges sidebar is auto-expanded first; `state==null` means the panel didn't report (collapsed/empty).- - Capture-all-cards workflow: `desktop_ad_scroll '{"panel":"bridges","to":"top"}'` → `desktop_screenshot_window` (AD hwnd) → `desktop_ad_scroll '{"panel":"bridges"}'` → screenshot → repeat until `atBottom`.+- `desktop_ad_scroll` (v1.9.86+) -- **Scroll ab's OWN window panels.** ab is a fully AI-controllable app, so you can drive its own scrollbars — most usefully to page the **bridges sidebar** and screenshot cards that are below the fold. Args: `panel` (`bridges` default | `activity` | `connections`), `to` (`top`|`bottom`, absolute — wins over `dy`), `dy` (relative px, default 400; negative = up). Returns `{ok, panel, scrolled, state:{scrollTop,scrollHeight,clientHeight,atTop,atBottom,found}|null, _hint}` — round-tripped from the live window so you can **loop until `state.atBottom==true`**. A collapsed bridges sidebar is auto-expanded first; `state==null` means the panel didn't report (collapsed/empty).+ - Capture-all-cards workflow: `desktop_ad_scroll '{"panel":"bridges","to":"top"}'` → `desktop_screenshot_window` (ab hwnd) → `desktop_ad_scroll '{"panel":"bridges"}'` → screenshot → repeat until `atBottom`. ### App self-update (relay-driven, v1.9.87+) -AD auto-updates itself from the wiki manifest, but historically only a HUMAN clicking the footer "Install now" banner could apply it — so a headless / RDP / fleet box (a fresh Azure VM) could sit a version behind with no path forward from the relay. These verbs give the AI that path (same shape as the shell-approval surfacing verbs). Use `--target <box>`.+ab auto-updates itself from the wiki manifest, but historically only a HUMAN clicking the footer "Install now" banner could apply it — so a headless / RDP / fleet box (a fresh Azure VM) could sit a version behind with no path forward from the relay. These verbs give the AI that path (same shape as the shell-approval surfacing verbs). Use `--target <box>`. - `update_status` (read-only, ungated) -- `{currentVersion, latestVersion, updateAvailable, updateReady, state, busy, updateMode}`. `updateReady:true` = the newer version is downloaded + SHA-verified on disk, appliable with no network. - `update_check` -- force a manifest check now; if newer, download + verify (stage) it in the background. Poll `update_status` until `updateReady:true`.-- `apply_update` -- the programmatic "Install now": apply the staged update + relaunch AD. Respects `busy` (refuses if a command is in flight). Only ever applies AD's own downloaded + SHA-verified SIGNED wiki release. Returns `{applying:true}` then AD restarts (the relay drops + reconnects; poll `targets`/`ping` for the new version). If not staged yet, it kicks staging and tells you to poll + retry.-- `request_update_approval` -- surface the "Update available — Install now / Later" banner on the target + bring AD to the front (for an ATTENDED box where the human should decide). Does NOT apply; `apply_update` installs.+- `apply_update` -- the programmatic "Install now": apply the staged update + relaunch ab. Respects `busy` (refuses if a command is in flight). Only ever applies ab's own downloaded + SHA-verified SIGNED wiki release. Returns `{applying:true}` then ab restarts (the relay drops + reconnects; poll `targets`/`ping` for the new version). If not staged yet, it kicks staging and tells you to poll + retry.+- `request_update_approval` -- surface the "Update available — Install now / Later" banner on the target + bring ab to the front (for an ATTENDED box where the human should decide). Does NOT apply; `apply_update` installs. - `update_set_mode {mode: auto|prompt|off}` -- the auto-apply POLICY. `auto` = unattended stage+apply on the 4h/on-launch poll (set this ONCE on headless fleet boxes and they self-update every release with no relay call); `prompt` = show the banner (default, attended); `off` = no checks. - Fleet flow: `update_status --target <box>` → if `updateAvailable` and unattended, `apply_update --target <box>` (or set `update_set_mode {"mode":"auto"}` once). A box must already be on ≥1.9.87 to have these verbs; the one-time bootstrap of an older box still needs a human click OR `auto` mode. ### Hydrogen companion mode (v1.9.218+) -When a HUMAN opens AD on a machine where Hydrogen is detected (embedded marker, Hydrogen's ports.json, or a discoverable Hydrogen install), AD shows an identity overlay ("This is Adom Bridge" + a large Open Hydrogen button), instantly opens or foregrounds Hydrogen, and places its own window directly below Hydrogen's in z-order — most people who open AD by hand were looking for Hydrogen. Facts an AI should know:+When a HUMAN opens ab on a machine where Hydrogen is detected (embedded marker, Hydrogen's ports.json, or a discoverable Hydrogen install), ab shows an identity overlay ("This is Adom Bridge" + a large Open Hydrogen button), instantly opens or foregrounds Hydrogen, and places its own window directly below Hydrogen's in z-order — most people who open ab by hand were looking for Hydrogen. Facts an AI should know: - Human-open detection = bare argv AND parent process `explorer.exe` (double-click, Start menu, search, pins). Machine launches — `--autostart` boot, Hydrogen `--embedded` respawns, the relauncher/`restart` verb, CLI-style invocations — NEVER trigger it, so a remote `restart` cannot bounce the user's screen.-- One-shot placement: AD does not fight the user's later alt-tabbing, and it is NOT a real Win32 child window (AD's lifecycle stays independent — the recovery-channel contract holds; if Hydrogen cannot be launched the overlay says so and AD stays fully usable).+- One-shot placement: ab does not fight the user's later alt-tabbing, and it is NOT a real Win32 child window (ab's lifecycle stays independent — the recovery-channel contract holds; if Hydrogen cannot be launched the overlay says so and ab stays fully usable). - Toggle: Settings > Hydrogen > "Act as a companion to Hydrogen", or `settings_set {"settings":{"hd_child_mode":false}}`. Default ON (self-gates on Hydrogen detection, so standalone installs never see it).-- Forensics: every decision slogs `HD-COMPANION:` lines to startup.log.+- Forensics: every decision slogs `ah-COMPANION:` lines to startup.log. ### User settings over the API (`settings_get` / `settings_set`, v1.9.214+) -AD's Settings-panel preferences are readable and settable over the API. This is the sanctioned channel for an embedding app (Hydrogen) to render AD's settings in its own panel and forward the user's changes; it also lets a fleet operator read or fix a box's boot/update prefs remotely.+ab's Settings-panel preferences are readable and settable over the API. This is the sanctioned channel for an embedding app (Hydrogen) to render ab's settings in its own panel and forward the user's changes; it also lets a fleet operator read or fix a box's boot/update prefs remotely. ```bash adom-bridge-cli settings_get@@ -1462,7 +1462,7 @@ adom-bridge-cli settings_set '{"settings": {"launch_on_boot": true, "update_mode - Settable keys (whitelist): `launch_on_boot`, `always_show_window_on_launch`, `update_mode` (auto|prompt|off), `auto_restart_for_updates`, `update_idle_minutes` (1..1440), `prewarm_runtimes`, `prewarm_cft` (auto|always|never), `file_receive_directory`. Anything else lands in `rejected` — servers, shell approval (grants are human-only), and blocked peers have their own surfaces and are deliberately not settable here. - `launch_on_boot` changes apply the OS Startup artifact immediately; `settings_get` also returns `autostartArtifactPresent` so you can see intent-vs-artifact drift. - `settings_set` is Tier-2 gated for relay (cloud-AI) callers — carry a `reason` and expect the human-approval flow, like `shell_execute`. Local/loopback callers (Hydrogen, bridges, the local CLI) are trusted by transport.-- Self-heal: AD re-verifies the launch-on-boot artifact every ~60s while running (not just at launch), so anything that deletes the Startup shortcut behind AD's back gets healed within a minute — with a `RECONCILE:` forensic line in `startup.log`.+- Self-heal: ab re-verifies the launch-on-boot artifact every ~60s while running (not just at launch), so anything that deletes the Startup shortcut behind ab's back gets healed within a minute — with a `RECONCILE:` forensic line in `startup.log`. ### Browser automation (`pup_*`, Puppeteer/pup) — bridge-owned; light pointer @@ -1480,9 +1480,9 @@ adom-bridge-cli pup_fetch_url '{"url": "https://example.com/data.json"}' `pup_readiness` is the read-only probe (never spawns or downloads). Full details: the bridge's own skill via `sync_skills`. -### Screen recording — AD's desktop recorder (tab recording is pup's)+### Screen recording — ab's desktop recorder (tab recording is pup's) -AD owns DESKTOP recording: a visible HUD opens with the REASON you supply, so the user always sees that recording is on and why. Full guide: the `adom-bridge-recording` skill.+ab owns DESKTOP recording: a visible HUD opens with the REASON you supply, so the user always sees that recording is on and why. Full guide: the `adom-bridge-recording` skill. ```bash adom-bridge-cli desktop_recorder_open '{"reason": "record the DRC walkthrough for review"}'@@ -1603,7 +1603,7 @@ adom-bridge-cli hd_api '{"method":"GET","path":"/wsl/status"}' Generic HTTP pass-through to any Hydrogen endpoint. Raw JSON in `body` — no curl-through-the-relay quote mangling. Returns `{success, status, body, port, method, path}` mirroring Hydrogen's response. If Hydrogen isn't running, returns `{ok:false, error:"Hydrogen not reachable on :47084", _hint:"..."}` instead of timing out. -**Public denylist** — these paths are refused with `{ok:false, error:"not_allowed_in_public", denylist_match:"..."}` on public AD builds (use the internal `hd-dev` cargo feature to bypass):+**Public denylist** — these paths are refused with `{ok:false, error:"not_allowed_in_public", denylist_match:"..."}` on public ab builds (use the internal `hd-dev` cargo feature to bypass): ``` /wsl/unregister, /setup/virgin-reset, /container/destroy,@@ -1614,7 +1614,7 @@ Generic HTTP pass-through to any Hydrogen endpoint. Raw JSON in `body` — no cu | Verb | What | Status | |---|---|---|-| `hd_status` | Composes AD-side facts (running, repo SHA) + Hydrogen's `/workspace/health` + `/buildinfo` relayed | ✓ shipped |+| `hd_status` | Composes ab-side facts (running, repo SHA) + Hydrogen's `/workspace/health` + `/buildinfo` relayed | ✓ shipped | | `hd_api` | Generic HTTP pass-through (above) | ✓ v1.8.72+ | | `hd_log '{"tail":30}'` | Tail `%APPDATA%\hydrogen-desktop\hydrogen-desktop.log` (reads disk — works EVEN when Hydrogen is down) | ✓ shipped | | `hd_open_url '{"url":"...","browser":"chrome","profileDir":"Default"}'` | POST `/open-in-profile` — open URL in a specific browser profile | ✓ shipped |@@ -1817,11 +1817,11 @@ Edge cases: **Always pull_file the `safePath` and Read THAT** — it's already sized for Claude's image analysis. The `fullPath` is for shotlog / disk archival, NOT direct Read (may exceed Claude's per-image budget). The `_hints.pull` field gives you the exact `pull_file` command to copy-paste. -**Old workaround to drop:** Do NOT use `desktop_list_windows | filter 'Hydrogen' | desktop_screenshot_window <hwnd>` anymore for Hydrogen — `hd_shot` replaces that entire pattern. Generic `desktop_screenshot_window` still applies to non-HD windows (other apps, dialogs, etc.).+**Old workaround to drop:** Do NOT use `desktop_list_windows | filter 'Hydrogen' | desktop_screenshot_window <hwnd>` anymore for Hydrogen — `hd_shot` replaces that entire pattern. Generic `desktop_screenshot_window` still applies to non-ah windows (other apps, dialogs, etc.). ### TCP Port-forward Tunnels — `tunnel open|close|list|check|rdp` + `display` (v1.8.85+, reworked v1.8.147) -Reach a TCP service addressable only from the **cloud side** through the **laptop side**: AD binds a `127.0.0.1:<port>` listener on the laptop and pumps bytes over the existing AD↔relay WebSocket; the relay dials the upstream `to`. First use case: RDP into a Windows VM behind an NSG only the cloud container reaches — mstsc on the laptop connects to a local port and is talking through the cloud. Same path works for SSH, VNC, HTTP, postgres — any TCP.+Reach a TCP service addressable only from the **cloud side** through the **laptop side**: ab binds a `127.0.0.1:<port>` listener on the laptop and pumps bytes over the existing ab↔relay WebSocket; the relay dials the upstream `to`. First use case: RDP into a Windows VM behind an NSG only the cloud container reaches — mstsc on the laptop connects to a local port and is talking through the cloud. Same path works for SSH, VNC, HTTP, postgres — any TCP. **`tunnel` is a grouped verb** (`rdp` / `tunnel_close` / `tunnel_list` remain back-compat aliases): @@ -1841,10 +1841,10 @@ adom-bridge-cli --target <name> tunnel close '{"tunnelId":<id>}' # or {"all":t adom-bridge-cli --target <name> display # monitor geometry (for sizing) ``` -**`--target` is mandatory when >1 AD is connected.** v1.8.147 fixed a false-success bug: the bind, the upstream probe, and the mstsc launch now ALL resolve to the SAME machine. (Before, the bind could land on one AD and mstsc launch on another → "port bound" on the VM, "nothing listening" on the laptop, a bogus `ok:true`.) No target + multiple ADs → `ambiguous_target` listing the names; `--target all` is rejected (a tunnel binds one machine). `adom-bridge-cli targets` lists connected ADs.+**`--target` is mandatory when >1 ab is connected.** v1.8.147 fixed a false-success bug: the bind, the upstream probe, and the mstsc launch now ALL resolve to the SAME machine. (Before, the bind could land on one ab and mstsc launch on another → "port bound" on the VM, "nothing listening" on the laptop, a bogus `ok:true`.) No target + multiple ADs → `ambiguous_target` listing the names; `--target all` is rejected (a tunnel binds one machine). `adom-bridge-cli targets` lists connected ADs. **Active health verification — every `open`/`rdp`/`check` PROVES the path before reporting `ok`:**-- **local-bind** — AD self-probes `127.0.0.1:<boundPort>` right after binding. If it can't connect, AD tears the tunnel down and returns `ok:false` (this alone kills the false-success class).+- **local-bind** — ab self-probes `127.0.0.1:<boundPort>` right after binding. If it can't connect, ab tears the tunnel down and returns `ok:false` (this alone kills the false-success class). - **upstream-refused** — the relay dials `to` (retry/backoff ~4×/2.5s). If unreachable, the tunnel is torn down (no leaked row) and `ok:false`. - A failure names the leg: `failedLeg: "relay-to-ad" | "local-bind" | "upstream-refused"`. Health fields surfaced: `listening`, `relayConnected`, `upstreamReachable`, `connCount`, `mstscAlive`. @@ -1852,15 +1852,15 @@ adom-bridge-cli --target <name> display # monitor geometry (for si **Display-aware RDP sizing.** `tunnel rdp` resolves `size`/`monitor`/`width`/`height` against the laptop's real screen (queried via `display` — `EnumDisplayMonitors` + per-monitor DPI), writes an `.rdp` file with the geometry, and launches mstsc. e.g. `size:"90%"` on a 2560×1600 primary → a 2304×1440 window. Default 90% of primary. You never hardcode a resolution — call `display` if you want the layout first. -**`tunnel list` reconciles + reaps leaks.** It cross-checks the relay registry against every connected AD's own registry and reports `leaks:[...]` for rows only one side knows about; `tunnel close '{"all":true}'` reaps them. The relay stores the actually-bound `localPort` (so `tunnel close '{"local":<port>}'` works).+**`tunnel list` reconciles + reaps leaks.** It cross-checks the relay registry against every connected ab's own registry and reports `leaks:[...]` for rows only one side knows about; `tunnel close '{"all":true}'` reaps them. The relay stores the actually-bound `localPort` (so `tunnel close '{"local":<port>}'` works). Architectural notes for Claude: - Bytes-only — no payload parsing, so any TCP service works. TLS/cert-pinning still pins to whatever the cloud-side dial resolves (for RDP the upstream cert CN must match what mstsc expects). - Each fresh laptop-side TCP connection gets its own connId multiplexed over one tunnelId; concurrent mstsc reconnects / parallel sessions work without re-opening. Backpressure bounded (256-slot mpsc/conn, 64 KiB buf).-- Tunnels don't survive AD/relay restarts. If mstsc disconnects, `tunnel check` (names the dead leg), then re-open.+- Tunnels don't survive ab/relay restarts. If mstsc disconnects, `tunnel check` (names the dead leg), then re-open. - The classic RDP collision (Windows' own RDP owns `0.0.0.0:3389`) is dodged by `rdp`'s default `local:13389` + `SO_EXCLUSIVEADDRUSE` + ephemeral fallback (`fellBack:true` in the response; read `localPort` for the real port). -**Old workarounds to drop**: (1) NSG WAN-IP whitelisting for roaming-laptop RDP — the tunnel rides existing auth, reachable regardless of WAN IP. (2) Trusting a bare `ok:true` from the old `rdp` — it could be a dead port; now `ok` means both legs were probed. (3) Hardcoding mstsc resolution — pass `size`/`monitor`; AD resolves against the real screen.+**Old workarounds to drop**: (1) NSG WAN-IP whitelisting for roaming-laptop RDP — the tunnel rides existing auth, reachable regardless of WAN IP. (2) Trusting a bare `ok:true` from the old `rdp` — it could be a dead port; now `ok` means both legs were probed. (3) Hardcoding mstsc resolution — pass `size`/`monitor`; ab resolves against the real screen. ### Shell — `shell_execute` (escape hatch only) @@ -1938,7 +1938,7 @@ Pitfalls these verbs eliminate: Every `wsl_exec` / `run_script bash` spawns a `wsl.exe` client. If WSL's service (`LxssManager`) wedges — common after a laptop runs for a day, or under heavy parallel cloud use — those clients block in a wait that `taskkill /F` can't always finish, and retries keep spawning more. Left unchecked they pile up into the hundreds and wedge the whole machine. Three defenses: -1. **Refuse-cap.** When there are already too many `wsl.exe` (`WSL_REFUSE_AT=80`), AD refuses NEW wsl spawns with `errorCode:"wsl_wedged"` instead of adding another doomed client. If you get `wsl_wedged`, don't retry the same command — run `wsl_recover` first.+1. **Refuse-cap.** When there are already too many `wsl.exe` (`WSL_REFUSE_AT=80`), ab refuses NEW wsl spawns with `errorCode:"wsl_wedged"` instead of adding another doomed client. If you get `wsl_wedged`, don't retry the same command — run `wsl_recover` first. 2. **`wsl_recover`** — reap orphaned/hung `wsl.exe`, **DRY-RUN by default** (nothing is killed unless you pass `confirm:true`): ```bash adom-bridge-cli wsl_recover # dry-run: lists `candidates` (would-reap) + `spared`@@ -1950,13 +1950,13 @@ Every `wsl_exec` / `run_script bash` spawns a `wsl.exe` client. If WSL's service #### Long-running in-distro work + distro lifecycle — golden-image bakes (v1.8.161+) -`wsl_exec` is **synchronous** — it waits for the command and is bounded by the relay's HTTP timeout (~60-120s), so a multi-minute bake (apt, a big download) EOFs. Backgrounding inside the distro (`nohup … &`) doesn't help either: the `wsl.exe` client returns, WSL sees no active session, shuts the distro down, and kills the bg process (wiping `/tmp`). For anything long, use the **async held-session** verbs — AD holds one `wsl.exe` in the foreground for the command's whole lifetime, so the distro stays up:+`wsl_exec` is **synchronous** — it waits for the command and is bounded by the relay's HTTP timeout (~60-120s), so a multi-minute bake (apt, a big download) EOFs. Backgrounding inside the distro (`nohup … &`) doesn't help either: the `wsl.exe` client returns, WSL sees no active session, shuts the distro down, and kills the bg process (wiping `/tmp`). For anything long, use the **async held-session** verbs — ab holds one `wsl.exe` in the foreground for the command's whole lifetime, so the distro stays up: - **`wsl_exec_async`** → returns a `jobId` + `outputPath` (a live UTF-8 log) immediately; the command runs detached for as long as it takes. - **`wsl_job_status {jobId}`** → `{running, exitCode, elapsedMs, outputTail, outputPath}`. Poll until `running:false`; `pull_file` the `outputPath` for the full log. (`wsl_job_list`, `wsl_job_kill {jobId}` round it out.) ```bash-# golden-image bake: import → run (async) → poll → export → unregister, all via AD+# golden-image bake: import → run (async) → poll → export → unregister, all via ab adom-bridge-cli wsl_import '{"distro":"bake","installDir":"C:/Users/<you>/.adom/wsl/bake","tarball":"C:/.../base.tar"}' # → {jobId}; poll wsl_job_status until done (exitCode 0) B64=$(printf 'set -euo pipefail\napt-get update && apt-get install -y build-essential\n' | base64 -w0)@@ -1967,9 +1967,9 @@ adom-bridge-cli wsl_unregister '{"distro":"bake"}' # cleanup (REFUSES adom-bridge-cli wsl_list # {distros:[{name,state,version,default}]} ``` -**systemd user session — `viaRoot`.** On a `systemd=true` distro, `-u <user>` can fail "Failed to start the systemd user session for '<user>'". Pass `"viaRoot": true` to `wsl_exec_async` — AD runs `-u root -- runuser -u <user> -- bash -l`, dropping to the user WITHOUT depending on `[email protected]`.+**systemd user session — `viaRoot`.** On a `systemd=true` distro, `-u <user>` can fail "Failed to start the systemd user session for '<user>'". Pass `"viaRoot": true` to `wsl_exec_async` — ab runs `-u root -- runuser -u <user> -- bash -l`, dropping to the user WITHOUT depending on `[email protected]`. -`wsl_import` / `wsl_export` / `wsl_unregister` are async (jobId + poll) too, so big imports/exports never EOF. All are **strictly scoped to the named distro** — there is no global `wsl --shutdown` path (that would kill the live Hydrogen workspace), and `wsl_unregister` refuses `Adom-Workspace`. Job ids live in AD's memory only (not persisted across an AD restart; the log file survives). The wedge defense above still applies — an over-cap spawn returns `wsl_wedged`.+`wsl_import` / `wsl_export` / `wsl_unregister` are async (jobId + poll) too, so big imports/exports never EOF. All are **strictly scoped to the named distro** — there is no global `wsl --shutdown` path (that would kill the live Hydrogen workspace), and `wsl_unregister` refuses `Adom-Workspace`. Job ids live in ab's memory only (not persisted across an ab restart; the log file survives). The wedge defense above still applies — an over-cap spawn returns `wsl_wedged`. ## Detecting App Installation @@ -2048,7 +2048,7 @@ adom-bridge-cli desktop_decrypt_browser_credentials '{ **How to call it (this is not a normal verb — read this):** - **LOCAL / bridge callers only.** A relay (cloud-AI) call is refused unconditionally with `errorCode:"relay_forbidden"` and **pops no UAC** — a remote container must never be able to trigger a password-decrypt. pup (a loopback bridge) and the local CLI reach it.-- **Two human gates, in order:** (1) AD shows an in-window consent dialog + toast naming the requesting caller ("pup is requesting your saved browser passwords/cookies…"); decline → `errorCode:"consent_declined"`. (2) On continue, an elevated signed helper runs behind a **UAC prompt** ("Adom Industries, Inc."); cancel → `errorCode:"elevation_declined"`. The UAC elevation IS the user's permission for the decrypt — by design (John, wiki #457).+- **Two human gates, in order:** (1) ab shows an in-window consent dialog + toast naming the requesting caller ("pup is requesting your saved browser passwords/cookies…"); decline → `errorCode:"consent_declined"`. (2) On continue, an elevated signed helper runs behind a **UAC prompt** ("Adom Industries, Inc."); cancel → `errorCode:"elevation_declined"`. The UAC elevation IS the user's permission for the decrypt — by design (John, wiki #457). - **Plaintext is returned ONCE over the loopback API and is never written to disk or logged.** Don't echo the values into any other verb's args, any activity-log emit, or any file. Consume the rows, seed the profile/vault, drop them. - **errorCodes:** `relay_forbidden` (you called it over the relay — you can't), `consent_declined`, `elevation_declined`, `helper_missing` (the signed helper isn't bundled — the install is broken). Per-item decrypt failures are counted in `stats.*Failed`, never fail the whole run (a managed-profile policy can block cookies; newest-Chrome `v20` blobs are best-effort if Google rotated the elevation-service key). - **Pair it with `desktop_list_browsers`** (above) to show the user which browsers/profiles/accounts will be read before you ask.@@ -2231,7 +2231,7 @@ cd cli && cargo build --release ## Run wiki ops on the user's PC — `wiki_exec` (v1.9.116+) -The AD installer bundles the `adom-wiki` CLI next to the app, and `wiki_exec` runs it ON THE PC — so version-controlling a local project (KiCad board, docs folder) on wiki.adom.inc needs NO per-file `pull_file` round trip to the container and there is no container/PC drift.+The ab installer bundles the `adom-wiki` CLI next to the app, and `wiki_exec` runs it ON THE PC — so version-controlling a local project (KiCad board, docs folder) on wiki.adom.inc needs NO per-file `pull_file` round trip to the container and there is no container/PC drift. ```bash adom-bridge-cli wiki_exec '{"args":["repo","push","adom/my-board","--files","board.kicad_pcb","board.kicad_sch","-m","rev B"],"cwd":"C:\Users\me\boards\my-board"}'@@ -2239,8 +2239,8 @@ adom-bridge-cli wiki_exec '{"args":["repo","push","adom/my-board","--files","boa - `args` = adom-wiki argv (any pillar: repo, pkg, release, discussion). `cwd` = PC-side project dir so `--files` are project-relative. `timeoutSec` optional (default 120, max 600). - Returns `{success, exitCode, timedOut, stdout, stderr, exe, durationMs}`.-- Auth is automatic: AD injects the signed-in user's session token as `ADOM_WIKI_TOKEN` — the caller never handles credentials.-- Gated like `shell_execute` for relay callers (it writes to the wiki as the user). `errorCode:wiki_cli_missing` means the desktop AD predates 1.9.116.+- Auth is automatic: ab injects the signed-in user's session token as `ADOM_WIKI_TOKEN` — the caller never handles credentials.+- Gated like `shell_execute` for relay callers (it writes to the wiki as the user). `errorCode:wiki_cli_missing` means the desktop ab predates 1.9.116. - Container-side publishing of the CONTAINER's own files still uses your local `adom-wiki` directly; `wiki_exec` is for files that live on the PC. -Note (v1.9.117): AD's Settings panel no longer has a KiCad section — bridges own their app-specific config (AD stays generic). `kicad_library_paths` / `project_watch` keep working from `~/.adom/config.json` and the `set_project_watch` / `trigger_project_watch` verbs.+Note (v1.9.117): ab's Settings panel no longer has a KiCad section — bridges own their app-specific config (ab stays generic). `kicad_library_paths` / `project_watch` keep working from `~/.adom/config.json` and the `set_project_watch` / `trigger_project_watch` verbs.
dist/linux/adom-bridge-cli⋯ 1 unchanged line ⋯
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-bridge", - "version": "2.0.12", + "version": "2.0.13", "type": "app", "brief": "Adom Bridge unleashes your AI onto your full PC: full power, full safety. The AI breaks out of the container it runs in and onto your real machine, managing and running your entire laptop as you: launch apps, move files, drive any window, control KiCad, Fusion 360 or your real signed-in browser. Works with any AI, cloud or local (Claude Desktop, Claude Code, Codex), no Hydrogen required. Bridge and its bridges are an operating system for AI, with managed Node and Python runtimes, and a human-only permission system keeps you in charge of every side-effecting command.", "description": "Install this package in a cloud or Hydrogen Desktop container, not on the PC. It delivers two things: the Linux adom-bridge-cli (linked onto PATH) and the Claude skill set that teaches an AI every desktop verb (shell, files, screenshots, windows, notifications, KiCad, Fusion 360, browser automation). The CLI drives the Adom Bridge app running on your Windows PC over the relay; the Windows app itself is the separate installer in the Download box, not part of this package. Run adom-wiki pkg update to keep the CLI and skills current.",
page.json+1−1@@ -1,6 +1,6 @@ { "slug": "adom-bridge",- "version": "2.0.12",+ "version": "2.0.13", "type": "app", "description": "Install this package in a cloud or Hydrogen Desktop container, not on the PC. It delivers two things: the Linux adom-bridge-cli (linked onto PATH) and the Claude skill set that teaches an AI every desktop verb (shell, files, screenshots, windows, notifications, KiCad, Fusion 360, browser automation). The CLI drives the Adom Bridge app running on your Windows PC over the relay; the Windows app itself is the separate installer in the Download box, not part of this package. Run adom-wiki pkg update to keep the CLI and skills current.", "keywords": [
version.json+6−6@@ -1,16 +1,16 @@ { "manifest_version": 1, - "version": "2.0.12", + "version": "2.0.13", "released_at": "2026-08-09T00:20:00Z", "windows": { - "installer_url": "https://wiki.adom.inc/download/adom/adom-bridge/2.0.12/Adom.Bridge_2.0.12_windows_x64-setup.exe", - "installer_filename": "Adom.Bridge_2.0.12_windows_x64-setup.exe", - "installer_sha256": "852d6ea70f4285a80a83646442bd2ad28678eacfc8e13d3dda1d8491cbcdb1ad", - "installer_size": 14686952 + "installer_url": "https://wiki.adom.inc/download/adom/adom-bridge/2.0.13/Adom.Bridge_2.0.13_windows_x64-setup.exe", + "installer_filename": "Adom.Bridge_2.0.13_windows_x64-setup.exe", + "installer_sha256": "486ff9c3a6f3de52b91c73f14e46547a2383c396c14f285672920b10b50193f4", + "installer_size": 14685280 }, "changelog_url": "https://wiki.adom.inc/adom/adom-bridge", "minimum_required": "1.3.30", - "note": "Adom Bridge 2.0.12. Fixes a duplicate Hydrogen relay row after the rename (all entry-name paths normalize to 'hydrogen'; stale legacy rows are evicted regardless of tag). Approval-mirror to Hydrogen now retries across Hydrogen's control-port churn and logs delivery, so Hydrogen's approval dialog no longer intermittently fails to appear. Caller aiThread is now MANDATORY: a blank/missing thread name is hard-rejected (was silently accepted as 'unidentified').", + "note": "Adom Bridge 2.0.13. Prose cleanup only: the last 'AD'/'HD' wording in user-facing error/hint/log strings is retired in favor of ab/ah (Adom Bridge / Adom Hydrogen). No behavior, verb, or response-shape change.", "macos": { "installer_url": "https://wiki.adom.inc/download/adom/adom-bridge-macos/1.9.162/Adom.Desktop_1.9.162_macos_aarch64.dmg", "installer_filename": "Adom.Desktop_1.9.162_macos_aarch64.dmg",