← Commit history
README.md+32−32
@@ -68,32 +68,32 @@ banners.   ### Chip-fetcher - fill your CAD library while you keep designing-<video src="/blob/app/adom-native-browser/demo/clips/use-case-1-chip-fetcher.mp4" controls muted playsinline width="100%" style="border-radius:10px;border:1px solid #30363d;max-width:760px"></video>+<video src="/blob/app/nb-bridge/demo/clips/use-case-1-chip-fetcher.mp4" controls muted playsinline width="100%" style="border-radius:10px;border:1px solid #30363d;max-width:760px"></video> Sourcing CAD for a BOM is tedious - symbol, footprint, 3D model, and datasheet for every part. Your AI drives the chip-fetcher dashboard in your browser, working down the parts list and pulling each one's full CAD bundle into your library, so the components are ready to drop into your design instead of you hunting them down one tab at a time.  ### Adom Symbol - schematic symbols, generated and previewed live-<video src="/blob/app/adom-native-browser/demo/clips/use-case-2-adom-symbol.mp4" controls muted playsinline width="100%" style="border-radius:10px;border:1px solid #30363d;max-width:760px"></video>+<video src="/blob/app/nb-bridge/demo/clips/use-case-2-adom-symbol.mp4" controls muted playsinline width="100%" style="border-radius:10px;border:1px solid #30363d;max-width:760px"></video> You need a schematic symbol for a chip - pins grouped sensibly, the 3D package shown right on the symbol. Your AI drives Adom Symbol in the browser, generating the symbol from the datasheet pinout, arranging the pins by function, and embedding the 3D model - then previews it live so you can eyeball it before it lands in your library.  ### Web-based EDA & CAD - try a design change and see it routed-<video src="/blob/app/adom-native-browser/demo/clips/use-case-3-web-eda.mp4" controls muted playsinline width="100%" style="border-radius:10px;border:1px solid #30363d;max-width:760px"></video>+<video src="/blob/app/nb-bridge/demo/clips/use-case-3-web-eda.mp4" controls muted playsinline width="100%" style="border-radius:10px;border:1px solid #30363d;max-width:760px"></video> Mid-design you want to try a change and see it routed. Your AI opens the EDA tool in the browser you're already signed into, **fills the schematic, re-runs the autorouter, and inspects the nets** - turning "try this and tell me what breaks" into something it just does, in the same session where your part libraries already live.  ### 3D board CAD - review the layout in the browser-<video src="/blob/app/adom-native-browser/demo/clips/use-case-4-3d-board.mp4" controls muted playsinline width="100%" style="border-radius:10px;border:1px solid #30363d;max-width:760px"></video>+<video src="/blob/app/nb-bridge/demo/clips/use-case-4-3d-board.mp4" controls muted playsinline width="100%" style="border-radius:10px;border:1px solid #30363d;max-width:760px"></video> Before a board goes out, you want to look at the actual layout. Your AI opens the 3D board viewer in your browser, frames the camera, and x-rays the board to reveal the traces underneath - so reviewing a PCB is a conversation, not a context-switch into a separate desktop tool.  ### Adom Chipsmith - check a part fits its footprint before it ships-<video src="/blob/app/adom-native-browser/demo/clips/use-case-5-chipsmith.mp4" controls muted playsinline width="100%" style="border-radius:10px;border:1px solid #30363d;max-width:760px"></video>+<video src="/blob/app/nb-bridge/demo/clips/use-case-5-chipsmith.mp4" controls muted playsinline width="100%" style="border-radius:10px;border:1px solid #30363d;max-width:760px"></video> Before a part goes onto a board, its 3D model and footprint have to actually line up. Your AI drives Adom Chipsmith in the browser to drop the chip onto its pads and check the fit visually - catching a mis-seated or wrong-variant model before it ever reaches the PCB.@@ -118,7 +118,7 @@ That's the whole reason for `debugger`, `cookies`, and `<all_urls>`: a developer against authenticated third-party APIs needs **full DevTools-grade access in their real session**. Take it away and this is just a worse screenshot tool. The table below maps every capability to a concrete request. -<img src="/blob/app/adom-native-browser/screenshots/dev-debug-mouser-digikey.png" alt="The Adom AI assistant debugging an authenticated DigiKey API call inside the developer's own signed-in browser - eval in the page context, reading back the real authed JSON response" width="100%">+<img src="/blob/app/nb-bridge/screenshots/dev-debug-mouser-digikey.png" alt="The Adom AI assistant debugging an authenticated DigiKey API call inside the developer's own signed-in browser - eval in the page context, reading back the real authed JSON response" width="100%">  | Permission | What the user-requested feature needs it for | |---|---|@@ -180,7 +180,7 @@ point it at, via the debugger/scripting APIs - never by loading external scripts  ## Fork & develop your own -Third parties are welcome to build their own version. The **[Fork &amp; develop guide →](https://wiki.adom.inc/adom/adom-native-browser/files/docs/DEVELOPING.md)** walks through getting the source, loading it in **Chrome and Edge developer mode** (with screenshots), having **Bridge load it for you**, and the edit → reload dev loop.+Third parties are welcome to build their own version. The **[Fork &amp; develop guide →](https://wiki.adom.inc/adom/nb-bridge/files/docs/DEVELOPING.md)** walks through getting the source, loading it in **Chrome and Edge developer mode** (with screenshots), having **Bridge load it for you**, and the edit → reload dev loop.  ## Sub-skills @@ -208,7 +208,7 @@ reports this directly as `staleOnDisk`. > skills, so most setups want both. (Bridge v1.9.142+ already ships this bridge as a default seed, > so extension-free control works on a fresh install with no steps at all.) -`adom-wiki pkg install adom/adom-native-browser` installs this whole skill set - the core driving mechanics+`adom-wiki pkg install adom/nb-bridge` installs this whole skill set - the core driving mechanics plus a growing list of **site recipes** that teach the AI how to drive specific websites. Each links to its full readme. @@ -216,31 +216,31 @@ to its full readme.  | Skill | What it covers | |---|---|-| [`adom-native-browser`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/adom-native-browser/SKILL.md) | **The main skill / router.** When to reach for the real browser (login/forms) vs pup, the `nbrowser_readiness` cold-start check, the offer-to-install hook, the three install surfaces (skills + bridge + extension), and the map to every sub-skill. |+| [`adom-native-browser`](https://wiki.adom.inc/adom/nb-bridge/files/skills/adom-native-browser/SKILL.md) | **The main skill / router.** When to reach for the real browser (login/forms) vs pup, the `nbrowser_readiness` cold-start check, the offer-to-install hook, the three install surfaces (skills + bridge + extension), and the map to every sub-skill. |  **Core mechanics**  | Skill | What it covers | |---|---|-| [`driving-the-extension`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/driving-the-extension/SKILL.md) | The full `nbrowser_*` surface: the CDP-vs-native "debugging" bar, `isTrusted` input + xy-finding, multi-profile driving, the credential-library query + login automation, the taskbar status feature. |-| [`native-browser-safety`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/native-browser-safety/SKILL.md) | The cardinal safety rules: only drive windows YOU opened, background-by-default, the credential gating, and the blocklist. Read before driving. |-| [`installing-the-extension`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/installing-the-extension/SKILL.md) | How WE install the extension into the user's Chrome/Edge over Bridge's generic verbs (dev-mode Load-unpacked, one-click consent, verify-by-screenshot). |-| [`uninstalling-the-extension`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/uninstalling-the-extension/SKILL.md) | Clean removal - filter to the Adom card so Remove is unambiguous, the confirm-bubble beat, verify by screenshot + dropped profile key. |-| [`native-browser-recording`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/native-browser-recording/SKILL.md) | Full-framerate **background** recording of the user's real, logged-in Chrome - the occlusion-flag mode, the restart-into-background-recording flow, and the offer-to-revert-for-battery step (the unique capability). |-| [`driving-the-desktop`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/driving-the-desktop/SKILL.md) | OS-level control via `desktop_*` when CDP can't reach (extension lifecycle, `chrome://` pages, native dialogs, popups) - including the ⛔ never-drive-a-PWA-window guard and the v20 saved-password ban. |-| [`driving-the-extension-demo`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/driving-the-extension-demo/SKILL.md) | A runnable demo playbook - recipes + the recording gotchas (window-capture-vs-CDP white pages, the keep-awake guard). |-| [`cross-platform-dev`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/cross-platform-dev/SKILL.md) | The Windows-first → Kyle-ports-Mac dev flow for the bridge/host/extension. |+| [`driving-the-extension`](https://wiki.adom.inc/adom/nb-bridge/files/skills/driving-the-extension/SKILL.md) | The full `nbrowser_*` surface: the CDP-vs-native "debugging" bar, `isTrusted` input + xy-finding, multi-profile driving, the credential-library query + login automation, the taskbar status feature. |+| [`native-browser-safety`](https://wiki.adom.inc/adom/nb-bridge/files/skills/native-browser-safety/SKILL.md) | The cardinal safety rules: only drive windows YOU opened, background-by-default, the credential gating, and the blocklist. Read before driving. |+| [`installing-the-extension`](https://wiki.adom.inc/adom/nb-bridge/files/skills/installing-the-extension/SKILL.md) | How WE install the extension into the user's Chrome/Edge over Bridge's generic verbs (dev-mode Load-unpacked, one-click consent, verify-by-screenshot). |+| [`uninstalling-the-extension`](https://wiki.adom.inc/adom/nb-bridge/files/skills/uninstalling-the-extension/SKILL.md) | Clean removal - filter to the Adom card so Remove is unambiguous, the confirm-bubble beat, verify by screenshot + dropped profile key. |+| [`native-browser-recording`](https://wiki.adom.inc/adom/nb-bridge/files/skills/native-browser-recording/SKILL.md) | Full-framerate **background** recording of the user's real, logged-in Chrome - the occlusion-flag mode, the restart-into-background-recording flow, and the offer-to-revert-for-battery step (the unique capability). |+| [`driving-the-desktop`](https://wiki.adom.inc/adom/nb-bridge/files/skills/driving-the-desktop/SKILL.md) | OS-level control via `desktop_*` when CDP can't reach (extension lifecycle, `chrome://` pages, native dialogs, popups) - including the ⛔ never-drive-a-PWA-window guard and the v20 saved-password ban. |+| [`driving-the-extension-demo`](https://wiki.adom.inc/adom/nb-bridge/files/skills/driving-the-extension-demo/SKILL.md) | A runnable demo playbook - recipes + the recording gotchas (window-capture-vs-CDP white pages, the keep-awake guard). |+| [`cross-platform-dev`](https://wiki.adom.inc/adom/nb-bridge/files/skills/cross-platform-dev/SKILL.md) | The Windows-first → Kyle-ports-Mac dev flow for the bridge/host/extension. |  **Site recipes** - the extension also supports pluggable per-site playbooks that teach the AI a specific app's flow (selectors, multi-step forms, gotchas). The index and the contribution model live in-[`driving-sites`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/driving-sites/SKILL.md).+[`driving-sites`](https://wiki.adom.inc/adom/nb-bridge/files/skills/driving-sites/SKILL.md).  ### Contributing a sub-skill  The recipe set is meant to **grow** - anyone can add one, and it stays part of *this* one repo (we don't spin up a separate wiki page per bundled skill): -1. Add `skills/driving-<site>/SKILL.md` - copy the template in [`driving-sites`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/driving-sites/SKILL.md).+1. Add `skills/driving-<site>/SKILL.md` - copy the template in [`driving-sites`](https://wiki.adom.inc/adom/nb-bridge/files/skills/driving-sites/SKILL.md). 2. Register it in `install.sh`, `uninstall.sh`, `package.json` `files[]`, and the index in `driving-sites`. 3. Open a PR on this page. @@ -254,19 +254,19 @@ The Extension runs inside your own browser and acts only on the commands you - o working on your behalf - give it. The pages and screenshots it reads go to your own Adom session to carry out the task you asked for, and it works through your existing signed-in sessions so the tools you already use just work. Saved passwords stay with the browser; its own autofill enters them. Full policy:-**[Privacy Policy (Markdown) →](https://wiki.adom.inc/adom/adom-native-browser/files/PRIVACY.md)** ·-**[HTML version →](https://wiki.adom.inc/adom/adom-native-browser/files/privacy.html)**+**[Privacy Policy (Markdown) →](https://wiki.adom.inc/adom/nb-bridge/files/PRIVACY.md)** ·+**[HTML version →](https://wiki.adom.inc/adom/nb-bridge/files/privacy.html)**  ## How the install actually works (end to end)  New to this and wondering what happens the first time, from a cloud / Hydrogen container all the way-to your real browser doing something useful? The **[First-time install flow &rarr;](https://wiki.adom.inc/adom/adom-native-browser/files/docs/INSTALL-FLOW.md)**+to your real browser doing something useful? The **[First-time install flow &rarr;](https://wiki.adom.inc/adom/nb-bridge/files/docs/INSTALL-FLOW.md)** walks every step and endpoint: how the container reaches Bridge over the relay, who installs the bridge / host / extension (and who does NOT), how the bridge knows the extension connected, how skills propagate, and how third parties add sub-skills so the extension keeps getting better.  Removing it (or reinstalling to apply an updated build) is documented too:-**[Uninstall flow &rarr;](https://wiki.adom.inc/adom/adom-native-browser/files/docs/UNINSTALL-FLOW.md)**+**[Uninstall flow &rarr;](https://wiki.adom.inc/adom/nb-bridge/files/docs/UNINSTALL-FLOW.md)** - background-first, the search-filter guard so you never remove the wrong extension, and the one brief narrated foreground beat the confirm bubble forces. @@ -291,8 +291,8 @@ How the AI drives it (the flow, proven end-to-end on a fresh machine):    with no errors. 4. **Confirm + drive** - `nbrowser_readiness` flips to `ready`; the AI proceeds with your task. -The recipe, hardening, and every picker/OS-dialog trap live in the **[`installing-the-extension`](https://wiki.adom.inc/adom/adom-native-browser/files/skills/installing-the-extension/SKILL.md)**-skill; the step-by-step first-time walk-through is the **[First-time install flow](https://wiki.adom.inc/adom/adom-native-browser/files/docs/INSTALL-FLOW.md)**.+The recipe, hardening, and every picker/OS-dialog trap live in the **[`installing-the-extension`](https://wiki.adom.inc/adom/nb-bridge/files/skills/installing-the-extension/SKILL.md)**+skill; the step-by-step first-time walk-through is the **[First-time install flow](https://wiki.adom.inc/adom/nb-bridge/files/docs/INSTALL-FLOW.md)**. Cardinal rule throughout: **the AI does everything it can itself - it never makes you do a step it could do.**  ## Dependencies@@ -355,7 +355,7 @@ Driving the user's real browser, verified end-to-end:   `type`, `press_key` (via `chrome.debugger`/CDP). **Click by visible text** and the smart finder reads   the element's `getBoundingClientRect` to compute its xy - DOM-truth, not pixel-guessing. - **The gliding Adom cursor** - `cursor:true` + `intent:"Clicking the Pay button"` shows a teal arrow +-  pulsing ring that glides to the target while **narrating its intention** (trust-building, mimics HD).+  pulsing ring that glides to the target while **narrating its intention** (trust-building, mimics Hydrogen). - **Screenshots** - `screenshot {toFile:true}` → pull the PNG (avoids relay base64 timeouts). - **Authed `fetch_url`** - uses the user's REAL cookies, so it grabs files behind a login. - **Taskbar status** - `flash`/`taskbar` paint a "come look" flash, a progress bar@@ -455,7 +455,7 @@ The browser can be recorded with **either** of two engines. Pick with `method`; | **Background** | ✅ keeps capturing the chosen surface even when occluded… | ✅ true background | | **Display wake-lock** | ⚠️ holds the display awake while sharing (can't blank the screen) | ✅ none | | **Output** | WebM (base64 returned) | mp4 (file path) |-| **Where it runs** | the extension (offscreen `MediaRecorder`) | the **bridge → Bridge** (`desktop_record_window_*`, standalone - never HD) |+| **Where it runs** | the extension (offscreen `MediaRecorder`) | the **bridge → Bridge** (`desktop_record_window_*`, standalone - never Hydrogen) |  **Rule of thumb:** on Windows (Bridge), use **`wgc`** - it's picker-free and truly background, ideal for "record while the user does something else." Everywhere else (Mac/Ubuntu, or older ab), use@@ -468,17 +468,17 @@ choose and we can compare them head-to-head. forbids an extension from selecting the source programmatically (anti-spyware). There is **no API bypass**. To record **without a human click**, use one of: -1. **`wgc` (Windows + HD)** - the agent records a window *by handle*; no picker exists. ✅ best on Win.+1. **`wgc` (Windows + ah)** - the agent records a window *by handle*; no picker exists. ✅ best on Win. 2. **`getdisplaymedia` + Chrome launch flag `--auto-select-desktop-capture-source="<window title>"`** --   when *we* launch Chrome (e.g. HD launches it), the picker **auto-selects** the matching window and+   when *we* launch Chrome (e.g. Hydrogen launches it), the picker **auto-selects** the matching window and    is skipped. ✅ **cross-platform** (Mac/Ubuntu/Windows) → this is the Mac/Ubuntu answer. -On an **already-running, un-managed Chrome** (not launched by us, no HD) the pick is unavoidable -+On an **already-running, un-managed Chrome** (not launched by us, no Hydrogen) the pick is unavoidable - that's the manual/ad-hoc fallback only.  > Status: `getdisplaymedia` (with picker) implemented in-extension; non-blocking (`record_start`-> returns immediately, poll `record_status` until `recording`). **Pending:** (a) `wgc` bridge→HD-> delegation; (b) HD launching Chrome with `--auto-select-desktop-capture-source` for click-free+> returns immediately, poll `record_status` until `recording`). **Pending:** (a) `wgc` bridge→ah+> delegation; (b) Hydrogen launching Chrome with `--auto-select-desktop-capture-source` for click-free > cross-platform capture; (c) large-recording file-save (base64 return is fine for short clips only).  ## Distribution
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.2.1+0.2.2
bridge/bridge.json+4−4
@@ -2,10 +2,10 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser",-  "version": "0.2.1",+  "version": "0.2.2",   "expectedExtensionVersion": "0.2.0",   "description": "Drives the user's REAL signed-in Chrome/Edge, in two layers. Layer 1 (nb) needs NOTHING installed and works on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing, the OAuth/consent path), nbrowser_os_windows, nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. Layer 2 (nbe, the Adom Native Browser Extension) unlocks the rest: CDP driving (navigate/click/read/screenshot) inside their logged-in profile. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.",-  "homepage": "https://wiki.adom.inc/adom/adom-native-browser",+  "homepage": "https://wiki.adom.inc/adom/nb-bridge",   "author": "Adom Inc.",   "license": "MIT",   "spawn": {@@ -115,7 +115,7 @@     "nbrowser",     "pup-parity"   ],-  "docs": "https://wiki.adom.inc/adom/adom-native-browser",+  "docs": "https://wiki.adom.inc/adom/nb-bridge",   "platforms": {     "windows": {       "supported": true@@ -130,7 +130,7 @@     }   },   "statusVerb": "nbrowser_status",-  "updateManifestUrl": "https://wiki.adom.inc/api/pages/adom/adom-native-browser/files/native-browser-bridge-manifest.json",+  "updateManifestUrl": "https://wiki.adom.inc/api/pages/adom/nb-bridge/files/native-browser-bridge-manifest.json",   "timeouts": {     "default": 60,     "verbs": {
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.2.1",+  "version": "0.2.2",   "private": true,   "description": "Native-browser bridge for Bridge (ab): layer 1 (nb) plus layer 2 via nbe, the Adom Native Browser Extension (Chrome + Edge).",   "main": "server.js",
bridge/server.js+53−53
@@ -300,22 +300,22 @@ const HELP = {   }, }; -// ---- WGC recording: bridge -> ab (standalone, NO HD dependency) -----------------------------------+// ---- WGC recording: bridge -> ab (standalone, no Hydrogen dependency) ----------------------------------- // WGC per-window capture can't be done in a browser, so it lives in ab (which is always present with // this bridge). We delegate to ab's direct API. ab writes the mp4 to a WINDOWS path (so pull_file-// grabs it directly - no WSL distro). NEVER call HD here: ab must run standalone without HD.+// grabs it directly - no WSL distro). NEVER call Hydrogen here: ab must run standalone without Hydrogen. // ab 1.8.149 control-API discovery contract: env var first (ab injects it into every spawned bridge - // no file race, no stale port), then the host:port discovery file, then ab's fixed 47200..47209 range. // ⚠️ The spawn-time env values FREEZE the port ab had when it spawned us. ab SELF-UPDATES rotate the // control port while we keep running (real incident, 2026-07-18: a Jul-16 bridge process hammered a dead // port for hours - hwnd capture, badge painting, and repair ALL failed SILENTLY). So: env/override is-// only the first try; on any connection failure adCommand re-discovers from the FILE (which ab keeps+// only the first try; on any connection failure abCommand re-discovers from the FILE (which ab keeps // current), retries once, caches the working base, and LOGS the outage + recovery.-let adBaseOverride = null; // set when file-rediscovery finds a LIVE base that differs from the env one-let adDownSince = 0;+let abBaseOverride = null; // set when file-rediscovery finds a LIVE base that differs from the env one+let abDownSince = 0; function discoverAdBase(fileOnly) {   if (!fileOnly) {-    if (adBaseOverride) return adBaseOverride;+    if (abBaseOverride) return abBaseOverride;     if (process.env.ADOM_DIRECT_API_URL) return process.env.ADOM_DIRECT_API_URL.replace(/\/+$/, '');     if (process.env.ADOM_DIRECT_API_PORT) return `http://127.0.0.1:${process.env.ADOM_DIRECT_API_PORT}`;   }@@ -325,7 +325,7 @@ function discoverAdBase(fileOnly) {   } catch {}   return 'http://127.0.0.1:47200'; }-let adTokenRejected = false; // ab rotates the bridge token on self-update; a stale one 403s every call+let abTokenRejected = false; // ab rotates the bridge token on self-update; a stale one 403s every call // ── CALLER PROVENANCE (wiki #345/#351) ───────────────────────────────────────────────────────────── // ab hands us WHO asked (X-Adom-Caller-*). We (a) log it, (b) FORWARD it on every ab callback we make // while acting on that thread's behalf, adding X-Adom-Caller-Delegate so ab can show the user both@@ -337,7 +337,7 @@ function currentCaller() { return callerCtx.getStore() || null; } function callerLabel() { const c = currentCaller(); return c && c.thread ? c.thread : null; } // Headers for a Bridge call. Acting FOR a thread -> echo its identity + name ourselves as the delegate. // Acting on our OWN behalf (idle timers, startup sweeps, reapers) -> no stale identity is forwarded;-// callers of adCommand pass selfCaller:true and we send our own name instead.+// callers of abCommand pass selfCaller:true and we send our own name instead. function callerHeaders(self) {   const c = self ? null : currentCaller();   if (!c) return { 'x-adom-caller-thread': `${BRIDGE} bridge (self)`, 'x-adom-caller-container': 'local', 'x-adom-caller-delegate': BRIDGE };@@ -348,7 +348,7 @@ function callerHeaders(self) {     'x-adom-caller-delegate': BRIDGE,   }; }-function adRequest(base, payload, noToken, self) {+function abRequest(base, payload, noToken, self) {   return new Promise((resolve) => {     let u; try { u = new URL('/command', base); } catch { u = new URL('http://127.0.0.1:47200/command'); }     const data = Buffer.from(JSON.stringify(payload));@@ -356,7 +356,7 @@ function adRequest(base, payload, noToken, self) {     // env; sending it badges our outbound calls as "bridge" in the Activity Log. It is NOT an approval     // gate (we're trusted-by-install; ab binds 127.0.0.1 only). Omitting it is fine; a stale token 403s.     const headers = { 'content-type': 'application/json', 'content-length': data.length, ...callerHeaders(self) };-    if (process.env.ADOM_BRIDGE_TOKEN && !noToken && !adTokenRejected) headers['x-adom-bridge-token'] = process.env.ADOM_BRIDGE_TOKEN;+    if (process.env.ADOM_BRIDGE_TOKEN && !noToken && !abTokenRejected) headers['x-adom-bridge-token'] = process.env.ADOM_BRIDGE_TOKEN;     const req = http.request({ host: u.hostname, port: u.port || 80, path: u.pathname, method: 'POST', headers, timeout: 20000 }, (res) => {       let b = ''; res.on('data', (c) => (b += c)); res.on('end', () => { let j; try { j = JSON.parse(b); } catch { j = b; } resolve({ status: res.statusCode, body: j }); });     });@@ -365,7 +365,7 @@ function adRequest(base, payload, noToken, self) {     req.write(data); req.end();   }); }-function adCommand(app, command, args, target, self) {+function abCommand(app, command, args, target, self) {   // app is OPTIONAL on ab 1.9.84+ (inferred) - omit it for top-level verbs like notify_user, keep it   // when a caller passes one (e.g. desktop_* recording delegation).   const payload = { command, args: args || {} };@@ -375,43 +375,43 @@ function adCommand(app, command, args, target, self) {   if (target) payload.target = target;   const first = discoverAdBase();   const ok2xx = (r) => r && r.status >= 200 && r.status < 300;-  return adRequest(first, payload, false, self).then(async (r) => {+  return abRequest(first, payload, false, self).then(async (r) => {     // A STALE BRIDGE TOKEN 403s EVERY CALL (ab rotates it when it self-updates). The token is     // ATTRIBUTION-ONLY, so the correct recovery is to DROP it and retry - otherwise every ab-backed     // feature (window correlation, taskbar badge, flash) dies SILENTLY. Real incident 2026-07-21: the     // fusion-bridge maintainer saw nbrowser_os_windows report 0 windows while ab saw 21.     if (r.status === 401 || r.status === 403) {-      const retry = await adRequest(first, payload, true, self);+      const retry = await abRequest(first, payload, true, self);       if (ok2xx(retry)) {-        if (!adTokenRejected) { adTokenRejected = true; console.error('[native-browser] ab rejected our bridge token (403) - it is attribution-only, so dropping it and continuing. ab likely self-updated and rotated it.'); logActivity({ verb: '_ab_link', ok: true, errorCode: 'token_dropped', internal: true }); }-        adDownSince = 0;+        if (!abTokenRejected) { abTokenRejected = true; console.error('[native-browser] ab rejected our bridge token (403) - it is attribution-only, so dropping it and continuing. ab likely self-updated and rotated it.'); logActivity({ verb: '_ab_link', ok: true, errorCode: 'token_dropped', internal: true }); }+        abDownSince = 0;         return retry;       }       r = retry;     }     if (ok2xx(r)) {-      if (adDownSince) { console.error(`[native-browser] ab link RECOVERED after ${Math.round((Date.now() - adDownSince) / 1000)}s`); logActivity({ verb: '_ab_link', ok: true, internal: true }); adDownSince = 0; }+      if (abDownSince) { console.error(`[native-browser] ab link RECOVERED after ${Math.round((Date.now() - abDownSince) / 1000)}s`); logActivity({ verb: '_ab_link', ok: true, internal: true }); abDownSince = 0; }       return r;     }     // Any other non-2xx is a REAL failure - never treat it as success (that is what silently produced     // empty window lists). Log it so a missing badge/flash is diagnosable from nbrowser_activity.     if (r.status !== 0) {-      if (!adDownSince) { adDownSince = Date.now(); console.error(`[native-browser] ab returned HTTP ${r.status} for ${command} - ab-backed features (badge/flash/window correlation) are degraded`); logActivity({ verb: '_ab_link', ok: false, errorCode: `http_${r.status}`, internal: true }); }+      if (!abDownSince) { abDownSince = Date.now(); console.error(`[native-browser] ab returned HTTP ${r.status} for ${command} - ab-backed features (badge/flash/window correlation) are degraded`); logActivity({ verb: '_ab_link', ok: false, errorCode: `http_${r.status}`, internal: true }); }       return r;     }     // Connection-level failure -> ab may have restarted on a new port. Re-discover from the FILE and retry.     const fromFile = discoverAdBase(true);-    const retry = fromFile && fromFile !== first ? adRequest(fromFile, payload, false, self) : Promise.resolve(r);+    const retry = fromFile && fromFile !== first ? abRequest(fromFile, payload, false, self) : Promise.resolve(r);     return retry.then((r2) => {       if (ok2xx(r2)) {-        adBaseOverride = fromFile;+        abBaseOverride = fromFile;         console.error(`[native-browser] ab port rotated (${first} dead) - rediscovered ${fromFile} from file, link restored`);         logActivity({ verb: '_ab_link', ok: true, internal: true });-        adDownSince = 0;+        abDownSince = 0;         return r2;       }-      if (!adDownSince) {-        adDownSince = Date.now();+      if (!abDownSince) {+        abDownSince = Date.now();         console.error(`[native-browser] ab UNREACHABLE at ${first}${fromFile !== first ? ` and ${fromFile}` : ''} (${(r2.body && r2.body.error) || 'connect failed'}) - taskbar badge/flash + window correlation degraded until the link recovers`);         logActivity({ verb: '_ab_link', ok: false, errorCode: 'ad_unreachable', internal: true });       }@@ -449,9 +449,9 @@ async function wgcRecord(verb, args) {     nbrowser_record_status: 'desktop_record_window_status',     nbrowser_record_list: 'desktop_record_window_status',   };-  const adArgs = { ...args };-  delete adArgs.method; // 'wgc' is the bridge-side selector, not a Bridge arg-  const r = await adCommand('desktop', map[verb], adArgs);+  const abArgs = { ...args };+  delete abArgs.method; // 'wgc' is the bridge-side selector, not a Bridge arg+  const r = await abCommand('desktop', map[verb], abArgs);   let out = r.body;   // ab wraps results differently across surfaces - unwrap {output:"<json>"} and/or {data:{...}} so   // recordingId/path/fileExists are reachable no matter which shape the direct API returns.@@ -471,7 +471,7 @@ async function wgcRecord(verb, args) {     return { success: true, output: JSON.stringify({       ok: true, method: 'wgc',       recordingId: out.recordingId, path: out.path, fileExists: out.fileExists,-      ad: out,+      ab: out,       _hint: verb === 'nbrowser_record_stop' ? stopHint : startHint,     }) };   }@@ -480,7 +480,7 @@ async function wgcRecord(verb, args) {     success: false,     error: (out && out.error) || `WGC ${verb} failed`,     _hint: 'ensure titleContains/hwnd targets a real window (same logic as desktop_screenshot_window); check desktop_record_window_status captureError.',-    ad: out,+    ab: out,   }; } @@ -560,7 +560,7 @@ const b64 = (s) => Buffer.from(s, 'utf8').toString('base64'); // badge, painted on the real browser window by hwnd. (Replaces the old run_script FlashWindowEx hack.) function taskbar(hwnd, opts) {   if (!hwnd) return Promise.resolve();-  return adCommand('desktop', 'desktop_taskbar', { hwnd, ...opts }).then((r) => {+  return abCommand('desktop', 'desktop_taskbar', { hwnd, ...opts }).then((r) => {     // LOG every overlay/flash (John, 2026-07-21): "log when you place the adom icon and when you flash     // the taskbar so we can debug you better." A silently-failing badge is a broken TRUST signal, so it     // must be visible in nbrowser_activity, with the HTTP status when ab refuses.@@ -737,7 +737,7 @@ function labelWindowOwner(hwnd, sid) {   const owner = (sid && sessionCaller.get(sid)) || callerLabel();   if (!owner) return;   const purpose = (sid && sessionPurpose.get(sid)) || '';-  adCommand('desktop', 'desktop_set_window_identity', {+  abCommand('desktop', 'desktop_set_window_identity', {     hwnd,     appId: 'Adom.NativeBrowser',     displayName: `Adom · ${owner}${purpose ? ' · ' + String(purpose).slice(0, 60) : ''}`,@@ -780,7 +780,7 @@ function applyBadge(sid, hwnd, profileKey, style) { // honest to show. function announceVisibleOpen(what, why, sid) {   const reason = String(why || (sid && sessionPurpose.get(sid)) || '').trim().slice(0, 160) || 'no reason given';-  adCommand(null, 'desktop_caption', { text: `Adom ${what} - ${reason}`, id: 'adom-foreground', position: 'bottom', duration: 3000 })+  abCommand(null, 'desktop_caption', { text: `Adom ${what} - ${reason}`, id: 'adom-foreground', position: 'bottom', duration: 3000 })     .then((r) => { const ok = !!(r && r.status >= 200 && r.status < 300); logActivity({ verb: '_open_announce', sessionId: sid || null, ok, ops: `${what} | ${reason}`.slice(0, 110), errorCode: ok ? null : `http_${(r && r.status) || 0}`, internal: true }); })     .catch(() => {}); }@@ -883,7 +883,7 @@ function clearBadge(sid) { const t = badgeIdle.get(sid); if (t) clearTimeout(t); // hwnd is the one we opened). Robust vs Windows foreground-lock - no GetForegroundWindow guessing. let preOpenHwnds = new Set(); async function chromeHwnds() {-  const r = await adCommand('desktop', 'desktop_list_windows', {});+  const r = await abCommand('desktop', 'desktop_list_windows', {});   let out = r.body;   if (out && typeof out.output === 'string') { try { out = JSON.parse(out.output); } catch {} }   const ws = (out && out.data && out.data.windows) || (out && out.windows) || [];@@ -892,7 +892,7 @@ async function chromeHwnds() { // Full Chrome window list (with rects/titles) so we can correlate a SPAWNED window's bounds/title to its // OS hwnd. Spawns have their own hwnd but no sessionId, so we can't diff them at open like open_window. async function chromeWindowsDetailed() {-  const r = await adCommand('desktop', 'desktop_list_windows', {});+  const r = await abCommand('desktop', 'desktop_list_windows', {});   let out = r.body;   if (out && typeof out.output === 'string') { try { out = JSON.parse(out.output); } catch {} }   const ws = (out && out.data && out.data.windows) || (out && out.windows) || [];@@ -1039,7 +1039,7 @@ async function _notifyTarget() {   if (now - _ntCache.at < 30000) return _ntCache.val;   let target = null;   try {-    const r = await adCommand(null, 'targets', {});+    const r = await abCommand(null, 'targets', {});     const list = (r && r.body && (r.body.targets || (r.body.data && r.body.data.targets) || [])) || [];     let self = ''; try { self = os.hostname().toLowerCase(); } catch {}     const peers = list.filter((t) => String((t && (t.name || t.hostname)) || '').toLowerCase() !== self);@@ -1048,10 +1048,10 @@ async function _notifyTarget() {   _ntCache = { at: now, val: target };   return target; }-const _notify = async (args) => adCommand(null, 'notify_user', args, await _notifyTarget());-const _notifyResp = async (id) => adCommand(null, 'notify_response', { id }, await _notifyTarget());+const _notify = async (args) => abCommand(null, 'notify_user', args, await _notifyTarget());+const _notifyResp = async (id) => abCommand(null, 'notify_response', { id }, await _notifyTarget()); async function _adCanNotify() {-  const r = await adCommand(null, 'notify_user', { id: '_probe_', title: '', body: '', level: 'info', _probe: true }).catch(() => null);+  const r = await abCommand(null, 'notify_user', { id: '_probe_', title: '', body: '', level: 'info', _probe: true }).catch(() => null);   // if ab accepted (any 2xx-ish body) treat notify as available; unknown-verb → unavailable   const body = r && r.body;   if (!r || r.status === 0) return false;@@ -1260,7 +1260,7 @@ const VERB_META = {   nbrowser_close_window: { hint: 'Close a window you opened. Pass includeSpawned:true to also close everything that window\'s pages SPAWNED (composes/popups). Clean up your own sessions when done.', related: ['nbrowser_owned', 'nbrowser_cleanup', 'nbrowser_list_windows'], pitfalls: ['⛔ only close a sessionId YOU opened - closing the user\'s window can wipe their live work and other AI threads', 'if the session spawned windows (a page opened a compose/popup), they stay open unless you pass includeSpawned:true or call nbrowser_cleanup'] },   nbrowser_owned: { hint: 'List everything a session owns: opened windows + windows/tabs a page it drove SPAWNED (compose, target=_blank, OAuth popup). Calling it ACKNOWLEDGES the spawns (clears an unacknowledged_spawns block). Use it before ending a task to see what to clean up.', related: ['nbrowser_cleanup', 'nbrowser_close_window', 'nbrowser_list_windows'], next: 'nbrowser_cleanup', pitfalls: ['spawned windows belong to no session until adopted — this is how you find leaked ones (e.g. a Gmail Contacts compose)', 'after seeing spawns, KEEP the ones you need or nbrowser_cleanup the litter'] },   nbrowser_cleanup: { hint: 'Close ONLY what this session SPAWNED (leaked composes/popups) — never the user\'s windows, never your primary opened window. The fix for spawned-window leaks.', related: ['nbrowser_owned', 'nbrowser_close_window'], pitfalls: ['needs the sessionId whose spawns to close', 'your primary opened window is KEPT — close it separately with nbrowser_close_window {sessionId, includeSpawned:true}'] },-  nbrowser_force_close: { hint: 'LAST-RESORT override to close a TRUE ORPHAN window (one nbe cannot attribute to any live session) or your own - INCLUDING litter another tool left behind (pup_open/pup windows from an HD test, an abandoned launch). TWO forms, SAME two-step gate: {windowId} when the extension can see the window; {hwnd} (OS-level, from desktop_list_windows) when it cannot - e.g. an Edge window in a profile with no extension. Call once → overrideTicket + exactly what will be destroyed; call again with {…, overrideTicket, attestOrphan:true, reason} to actually close. NEVER closes another live thread\'s window.', related: ['nbrowser_owned', 'nbrowser_cleanup', 'nbrowser_list_windows'], pitfalls: ['a single call NEVER closes anything — that is the point; you must attest on a second call with a real reason', 'refused (owned_by_other) if the window belongs to another live session — cross-thread ownership is absolute', 'prefer nbrowser_cleanup for tracked spawns; force_close is only for true orphans', 'the {hwnd} form only targets REAL browser windows (Chrome_WidgetWin) and sends a graceful WM_CLOSE - unsaved pages may prompt instead of closing'] },+  nbrowser_force_close: { hint: 'LAST-RESORT override to close a TRUE ORPHAN window (one nbe cannot attribute to any live session) or your own - INCLUDING litter another tool left behind (pup_open/pup windows from an ah test, an abandoned launch). TWO forms, SAME two-step gate: {windowId} when the extension can see the window; {hwnd} (OS-level, from desktop_list_windows) when it cannot - e.g. an Edge window in a profile with no extension. Call once → overrideTicket + exactly what will be destroyed; call again with {…, overrideTicket, attestOrphan:true, reason} to actually close. NEVER closes another live thread\'s window.', related: ['nbrowser_owned', 'nbrowser_cleanup', 'nbrowser_list_windows'], pitfalls: ['a single call NEVER closes anything — that is the point; you must attest on a second call with a real reason', 'refused (owned_by_other) if the window belongs to another live session — cross-thread ownership is absolute', 'prefer nbrowser_cleanup for tracked spawns; force_close is only for true orphans', 'the {hwnd} form only targets REAL browser windows (Chrome_WidgetWin) and sends a graceful WM_CLOSE - unsaved pages may prompt instead of closing'] },   nbrowser_switch_window: { hint: 'Target a window for later verbs. Does NOT raise it. ' + NO_FG, related: ['nbrowser_list_windows'], pitfalls: ['does not foreground (by design); only userRequestedForeground:true raises it, and only when the user explicitly asked to watch live'] },   nbrowser_list_windows: { hint: 'List live windows/sessions with bounds+state + an owner label: agent-opened | agent-spawned | user. Pass mine:true for only windows an agent opened/spawned.', related: ['nbrowser_owned', 'nbrowser_switch_window', 'nbrowser_rescan'], pitfalls: ['owner:"agent-spawned" = a page you drove opened it indirectly (compose/popup) — clean it up with nbrowser_cleanup', 'owner:"user" windows are the user\'s own — NEVER drive/close them'] },   nbrowser_window_state: { hint: 'Maximize/fullscreen/normal/minimize or move+resize. ' + NO_FG, related: ['nbrowser_fullscreen'], pitfalls: ['maximize/fullscreen are USER-only foregrounding actions: they enlarge the window but it STAYS in the background (auto-reverted) unless userRequestedForeground:true - so recordings still work but you can never shove it in front of the user', 'resizing a window the USER is in disrupts them; act on your own session windows'] },@@ -1483,7 +1483,7 @@ function dispatch(verb, args) {         d('nbrowser_close_window', 'Close a window the agent opened. includeSpawned:true also closes everything that session SPAWNED (composes/popups).', { sessionId: 'required str', includeSpawned: 'optional bool (also close this session\'s spawned windows)', profile: P }, { ok: 'bool', closed: 'bool', sessionId: 'str', alsoClosedSpawned: '[int]' }, 20, { example: { sessionId: 'work', includeSpawned: true } }),         d('nbrowser_owned', 'List everything a session owns: opened windows + adopted SPAWNED windows/tabs (compose/popup/target=_blank a driven page opened). Acknowledges spawns.', { sessionId: 'optional str (all sessions if omitted)', profile: P }, { ok: 'bool', opened: '[{sessionId,windowId,url,title,purpose,ageMs}]', spawned: '[{kind,windowId,tabId,url,ageMs,spawnedBy,opener}]' }, 20, { example: { sessionId: 'irs' } }),         d('nbrowser_cleanup', 'Close ONLY what a session SPAWNED (leaked composes/popups). Never the user\'s windows, never your primary opened window.', { sessionId: 'required str', profile: P }, { ok: 'bool', closed: '[{windowId,url}]', count: 'int', keptOpenedWindow: 'int?' }, 20, { example: { sessionId: 'irs' } }),-        d('nbrowser_force_close', 'LAST-RESORT: close a TRUE ORPHAN window (unattributable to any live session) or your own - incl. pup_open/HD-test litter. TWO-STEP: call 1 {windowId} (extension-visible) OR {hwnd} (OS-level, extension-unreachable e.g. Edge sans extension) returns overrideTicket + willDestroy; call 2 adds {overrideTicket,attestOrphan:true,reason} and closes (hwnd form = graceful WM_CLOSE). NEVER closes another live thread\'s window.', { windowId: 'int (extension-visible form)', hwnd: 'int (OS-level form, from desktop_list_windows)', sessionId: 'optional str (yours, to close your own)', overrideTicket: 'str (from call 1)', attestOrphan: 'bool (call 2: must be true)', reason: 'str (call 2: why)', profile: P }, { ok: 'bool', needsAttestation: 'bool?', overrideTicket: 'str?', willDestroy: 'obj?', closed: 'bool?' }, 20, { example: { windowId: 12345 } }),+        d('nbrowser_force_close', 'LAST-RESORT: close a TRUE ORPHAN window (unattributable to any live session) or your own - incl. pup_open/ah-test litter. TWO-STEP: call 1 {windowId} (extension-visible) OR {hwnd} (OS-level, extension-unreachable e.g. Edge sans extension) returns overrideTicket + willDestroy; call 2 adds {overrideTicket,attestOrphan:true,reason} and closes (hwnd form = graceful WM_CLOSE). NEVER closes another live thread\'s window.', { windowId: 'int (extension-visible form)', hwnd: 'int (OS-level form, from desktop_list_windows)', sessionId: 'optional str (yours, to close your own)', overrideTicket: 'str (from call 1)', attestOrphan: 'bool (call 2: must be true)', reason: 'str (call 2: why)', profile: P }, { ok: 'bool', needsAttestation: 'bool?', overrideTicket: 'str?', willDestroy: 'obj?', closed: 'bool?' }, 20, { example: { windowId: 12345 } }),         d('nbrowser_switch_window', 'Target a window for subsequent verbs; does NOT raise it unless the user asked to watch live.', { sessionId: 'required str', userRequestedForeground: 'optional bool (default false = do not raise; only true, and only on explicit user request, raises it)' }, { ok: 'bool', focused: 'bool' }, 15, { example: { sessionId: 'work' } }),         d('nbrowser_list_windows', 'List the live windows/sessions, each with an owner label (agent-opened|agent-spawned|user). mine:true = only agent windows.', { profile: P, mine: 'optional bool' }, { ok: 'bool', sessions: '[{id,windowId,owner,spawnedBy,purpose,tabCount,active,state,bounds,url}]', count: 'int' }, 20, { example: { mine: true } }),         d('nbrowser_window_state', 'Set window state (fullscreen/maximized/normal/minimized) or move/resize via left/top/width/height. maximize/fullscreen enlarge the window but keep it in the BACKGROUND (auto-reverted) unless userRequestedForeground:true.', { sessionId: 'required str', state: 'optional fullscreen|maximized|normal|minimized', left: 'optional int', top: 'optional int', width: 'optional int', height: 'optional int', userRequestedForeground: 'optional bool - allow maximize/fullscreen to raise the window, ONLY when the user asked to watch live (default false)' }, { ok: 'bool', windowId: 'int', state: 'str', _warning: 'str?' }, 20, { example: { sessionId: 'work', state: 'maximized' } }),@@ -1632,7 +1632,7 @@ function dispatch(verb, args) {     }     // OS-LEVEL force_close: {hwnd} instead of {windowId} - for browser windows in a profile whose     // extension is NOT connected (e.g. Edge without the extension, or pup_open/pup litter from an-    // HD test). SAME dramatic two-step gate as the extension path: ticket + willDestroy + attestation,+    // ah test). SAME dramatic two-step gate as the extension path: ticket + willDestroy + attestation,     // and a window belonging to a live nbe session is refused absolutely. Restricted to REAL browser     // windows (Chrome_WidgetWin class) so this can never close an arbitrary app. Uses a graceful     // WM_CLOSE - a page with unsaved state may prompt instead of dying.@@ -1641,7 +1641,7 @@ function dispatch(verb, args) {       (async () => {         let ws = [];         try {-          const r = await adCommand('desktop', 'desktop_list_windows', {});+          const r = await abCommand('desktop', 'desktop_list_windows', {});           let out = r.body; if (out && typeof out.output === 'string') { try { out = JSON.parse(out.output); } catch {} }           ws = ((out && out.data && out.data.windows) || (out && out.windows) || []).filter((w) => /Chrome_WidgetWin/.test(w.className || ''));         } catch {}@@ -1665,7 +1665,7 @@ function dispatch(verb, args) {           '"@',           `[WMC]::PostMessage([IntPtr]${hwnd},0x0010,[IntPtr]::Zero,[IntPtr]::Zero)`,         ].join('\r\n');-        const rr = await adCommand(null, 'run_script', { interpreter: 'powershell', body: psBody, timeoutSeconds: 20 }); // direct API takes plain `body` (scriptB64 is a CLI-ism)+        const rr = await abCommand(null, 'run_script', { interpreter: 'powershell', body: psBody, timeoutSeconds: 20 }); // direct API takes plain `body` (scriptB64 is a CLI-ism)         const rb = rr && rr.body;         const sent = !!(rr && rr.status && rr.status < 300 && rb && (rb.success === true || rb.ok === true) && (rb.exitCode === undefined || rb.exitCode === 0));         logActivity({ verb: 'nbrowser_force_close', sessionId: (args && args.sessionId) || null, thread: (args && args.thread) || null, profile: 'os-hwnd', ok: sent, errorCode: sent ? null : 'wm_close_failed' });@@ -1771,13 +1771,13 @@ function dispatch(verb, args) {             'if ($found -ne 0) { Start-Sleep -Milliseconds 700; [WW2]::ShowWindow([IntPtr]$found, 7) | Out-Null }',             'Write-Output ("HWND=" + $found)',           ].join(String.fromCharCode(13, 10));-          const rr = await adCommand(null, 'run_script', { interpreter: 'powershell', body: psBody, timeoutSeconds: 25 });+          const rr = await abCommand(null, 'run_script', { interpreter: 'powershell', body: psBody, timeoutSeconds: 25 });           const m = String((rr && rr.body && rr.body.stdout) || '').match(/HWND=(\d+)/);           if (m && m[1] !== '0') { hwnd = Number(m[1]); minimized = true; }           if (hwnd) { try { await taskbar(hwnd, { flash: 'until_focused' }); flashed = true; } catch {} }         } else {           const pre = new Set([...curWins.map((w) => w.hwnd), ...protectedHwnds]);-          adCommand('desktop', 'launch_process', { path: exe, args: [`--profile-directory=${kp.profileDir}`, '--new-window', url] });+          abCommand('desktop', 'launch_process', { path: exe, args: [`--profile-directory=${kp.profileDir}`, '--new-window', url] });           const t0 = Date.now();           while (!hwnd && Date.now() - t0 < 8000) {             await new Promise((r) => setTimeout(r, 500));@@ -1817,16 +1817,16 @@ function dispatch(verb, args) {     }     if (verb === 'nbrowser_os_windows') {       (async () => {-        let ws = []; let adOk = true;+        let ws = []; let abOk = true;         try {-          const r = await adCommand('desktop', 'desktop_list_windows', {});+          const r = await abCommand('desktop', 'desktop_list_windows', {});           let out = r.body; if (out && typeof out.output === 'string') { try { out = JSON.parse(out.output); } catch {} }           ws = ((out && out.data && out.data.windows) || (out && out.windows) || []);-          if (r.status === 0) adOk = false;-        } catch { adOk = false; }+          if (r.status === 0) abOk = false;+        } catch { abOk = false; }         const bySess = {}; for (const [sid, h] of sessionHwnd) bySess[h] = sid;         const browsers = ws.filter((w) => /Chrome_WidgetWin/.test(w.className || '')).map((w) => ({ hwnd: w.hwnd, title: w.title, rect: w.rect, z: w.z, kind: /for Testing/.test(w.title || '') ? 'pup/chrome-for-testing' : 'native-browser', nbeSession: bySess[w.hwnd] || null }));-        resolve({ success: true, output: JSON.stringify({ ok: adOk, count: browsers.length, windows: browsers, totalOsWindows: ws.length, _hint: adOk ? 'Browser windows as the LAPTOP sees them right now (bridge-side enumeration - no --target guessing, DPI-safe). nbeSession = a live nbe session owns that hwnd (NEVER force-close it; that thread cleans up its own). kind pup/chrome-for-testing = pup_open/pup litter. Edge windows appear even when Edge has no extension. To close an untracked one: nbrowser_force_close {hwnd} (two-step attestation). If your own desktop_list_windows returned 0/null: most likely ab was mid-restart (it SELF-UPDATES and restarts, sometimes several times a day - anything in flight returns empty), or you targeted the WRONG desktop when multiple abs are connected (check `targets`; pass --target for the laptop).' : 'ab link is DOWN from the bridge right now (desktop_list_windows unreachable) - window data unavailable, not "0 windows". Check nbrowser_activity for _ab_link entries; retry shortly.' }) });+        resolve({ success: true, output: JSON.stringify({ ok: abOk, count: browsers.length, windows: browsers, totalOsWindows: ws.length, _hint: abOk ? 'Browser windows as the LAPTOP sees them right now (bridge-side enumeration - no --target guessing, DPI-safe). nbeSession = a live nbe session owns that hwnd (NEVER force-close it; that thread cleans up its own). kind pup/chrome-for-testing = pup_open/pup litter. Edge windows appear even when Edge has no extension. To close an untracked one: nbrowser_force_close {hwnd} (two-step attestation). If your own desktop_list_windows returned 0/null: most likely ab was mid-restart (it SELF-UPDATES and restarts, sometimes several times a day - anything in flight returns empty), or you targeted the WRONG desktop when multiple abs are connected (check `targets`; pass --target for the laptop).' : 'ab link is DOWN from the bridge right now (desktop_list_windows unreachable) - window data unavailable, not "0 windows". Check nbrowser_activity for _ab_link entries; retry shortly.' }) });       })();       return;     }@@ -1851,7 +1851,7 @@ function dispatch(verb, args) {       // created, NOTHING foregrounds, no focus is stolen. The extension service worker starts, dials our       // native host, and stays alive on the open port - the profile then announces and shows live. This       // honors the background-by-default rule: waking a profile never needs a foreground window.-      adCommand('desktop', 'launch_process', { path: exe, args: [`--profile-directory=${kp.profileDir}`, '--no-startup-window'] });+      abCommand('desktop', 'launch_process', { path: exe, args: [`--profile-directory=${kp.profileDir}`, '--no-startup-window'] });       const t0 = Date.now();       const poll = () => {         const k = findLive();@@ -1981,7 +1981,7 @@ function dispatch(verb, args) {       taskbar(hwnd, opts).then(() => resolve({ success: true, output: JSON.stringify({ ok: true, hwnd, applied: Object.keys(opts).length ? Object.keys(opts) : ['overlay:managed'], _hint: 'progress.state: normal(green) | paused(YELLOW - use for captcha/2FA/login human-gates) | error(red) | indeterminate(marquee) | none. overlay.badge: "adom" = the driving badge (the bridge composites the profile\'s CIRCLE avatar + the Adom favicon - never a bare square) | "none" = restore the NATIVE avatar look (never blanks the slot). The badge is AUTOMATIC while driving and idle-expires after ~3 min, so you rarely need to set it. Clear progress+flash at task-end: {progress:{state:"none"},flash:"stop"}.' }) }));       return;     }-    // WGC recording is handled HERE (bridge -> ab direct API, standalone - never HD), not in the+    // WGC recording is handled HERE (bridge -> ab direct API, standalone - never Hydrogen), not in the     // extension (a browser can't capture another window). method:"wgc".     if (verb.startsWith('nbrowser_record_') && args && args.method === 'wgc') {       return wgcRecord(verb, args).then(resolve);@@ -2041,7 +2041,7 @@ function dispatch(verb, args) {         let closed = false;         if (h) {           const psBody = ['Add-Type @"','using System;using System.Runtime.InteropServices;','public class WMC3 { [DllImport("user32.dll")] public static extern bool PostMessage(IntPtr h, uint m, IntPtr w, IntPtr l); }','"@',`[WMC3]::PostMessage([IntPtr]${h},0x0010,[IntPtr]::Zero,[IntPtr]::Zero)`].join(String.fromCharCode(13,10));-          const rr = await adCommand(null, 'run_script', { interpreter: 'powershell', body: psBody, timeoutSeconds: 15 });+          const rr = await abCommand(null, 'run_script', { interpreter: 'powershell', body: psBody, timeoutSeconds: 15 });           closed = !!(rr && rr.body && (rr.body.success === true || rr.body.ok === true));         }         sessionHwnd.delete(sid); osOpened.delete(sid); clearBadge(sid); persistSessions();@@ -2084,7 +2084,7 @@ function dispatch(verb, args) {         return resolve({ success: false, error: `profile '${tgt.key}' is blocklisted`, _hint: 'The user blocked this profile from being driven. nbrowser_profile_unblock to allow it.' });       }       // A named profile that exists locally deserves a DIAGNOSIS, not a mystery: is the extension not-      // installed there, installed-but-disabled, or just asleep? (Real gripe: an HD thread burned a+      // installed there, installed-but-disabled, or just asleep? (Real gripe: an ah thread burned a       // session flailing at Edge because every verb said only "not connected".)       let diag = '';       try {@@ -2429,7 +2429,7 @@ function knownProfiles() { // browser itself recorded on disk (Preferences / Secure Preferences -> extensions.settings). This is // what turns "native browser extension not connected" from a MYSTERY into an actionable answer: not // installed at all vs installed-but-DISABLED vs enabled-but-not-dialing are three different fixes, and-// the AI deserves to know which one it is (real gripe: an HD thread flailed at Edge for a whole session).+// the AI deserves to know which one it is (real gripe: an ah thread flailed at Edge for a whole session). function adomExtStateForProfile(browser, profileDir) {   try {     const local = process.env.LOCALAPPDATA;
dev-skills/developing-the-bridge/SKILL.md+1−1
@@ -180,7 +180,7 @@ it never minimizes.** When a working impl exists in the codebase, copy it before  **The correct recipe (shipped 0.1.41, verified live on the user's single-profile Edge — window opened minimized/background, workspace hwnd unchanged, no crash):**-1. **Launch clean, like HD:** don't pass `--profile-directory=Default` (only pass a profile dir when it+1. **Launch clean, like Hydrogen:** don't pass `--profile-directory=Default` (only pass a profile dir when it    is NOT the default). Args as a PowerShell ARRAY, never an embedded-quote string (`'--profile-directory=    "Default"'` passes LITERAL quotes to Chromium). e.g. `Start-Process $exe -ArgumentList @('--new-window',$url)`. 2. **Detect the new window READ-ONLY.** Poll `EnumWindows` for a genuinely-new `Chrome_WidgetWin` hwnd
docs/DEVELOPING.md+4−4
@@ -6,10 +6,10 @@ public, open-source repo.  ## 1. Get the source (fork it) -The extension lives as a wiki page + git repo at **[wiki.adom.inc/adom/adom-native-browser](https://wiki.adom.inc/adom/adom-native-browser)**.+The extension lives as a wiki page + git repo at **[wiki.adom.inc/adom/nb-bridge](https://wiki.adom.inc/adom/nb-bridge)**.  - **Fork / propose changes upstream** — use the page's **Pull Requests** tab to propose edits back, or-- **Clone the source** — grab the repo from the **Files** tab (or `adom-wiki pkg install adom/adom-native-browser`),+- **Clone the source** — grab the repo from the **Files** tab (or `adom-wiki pkg install adom/nb-bridge`),   then copy the **`extension/`** folder somewhere you can edit.  The folder you load into the browser is **`extension/`** — it's the one with `manifest.json`.@@ -21,7 +21,7 @@ The folder you load into the browser is **`extension/`** — it's the one with ` 3. Click **Load unpacked** (top-left) and pick your **`extension/`** folder. ② 4. The extension loads — note its **ID**. ③  After any code edit, click the **Reload (↻)** button on its card. -![Loading the extension in Chrome developer mode — Developer mode toggle, Load unpacked, and the loaded extension](/blob/app/adom-native-browser/screenshots/dev-load-unpacked-chrome.png)+![Loading the extension in Chrome developer mode — Developer mode toggle, Load unpacked, and the loaded extension](/blob/app/nb-bridge/screenshots/dev-load-unpacked-chrome.png)  ## 3. Load it in Edge (developer mode) @@ -32,7 +32,7 @@ Same idea, slightly different layout: 3. Click **Load unpacked** (top of the page) and pick your **`extension/`** folder. ② 4. The extension loads. ③ -![Loading the extension in Edge developer mode — Developer mode (bottom-left), Load unpacked, and the loaded extension](/blob/app/adom-native-browser/screenshots/dev-load-unpacked-edge.png)+![Loading the extension in Edge developer mode — Developer mode (bottom-left), Load unpacked, and the loaded extension](/blob/app/nb-bridge/screenshots/dev-load-unpacked-edge.png)  ## 4. Or — let Bridge install it for you 
docs/INSTALL-FLOW.md+7−7
@@ -17,14 +17,14 @@ on two different machines, so it is worth laying out precisely.  | Piece | Lives on | Role | |---|---|---|-| **Your AI session + skills** | the **container** (cloud or HD/WSL2) | You. The installed skills (`driving-the-extension`, `native-browser-safety`, ...) teach you the `nbrowser_*` verbs and the safety rules. |+| **Your AI session + skills** | the **container** (cloud or ah/WSL2) | You. The installed skills (`driving-the-extension`, `native-browser-safety`, ...) teach you the `nbrowser_*` verbs and the safety rules. | | **`adom-bridge-cli` (ab)** | the user's **laptop** | The hub. Receives your verbs over the relay, routes `nbrowser_*` to the bridge, ships results back. Always present. | | **The bridge** (`bridge/server.js`) | the **laptop** (in ab's `bridges-cache`) | ab-spawned Node server. Binds an ephemeral loopback port, exposes `/command` to ab, and a raw-TCP loopback the host dials. Handles `nbrowser_*` and delegates `wgc` recording to ab. | | **The native-messaging host** (`host/native-host.js`) | the **laptop** | A tiny stdio<->TCP pump. Chrome/Edge launch it when the extension calls `connectNative`; it reads the bridge's discovery file and connects the extension to the bridge. | | **The extension** (`extension/`) | the user's **Chrome/Edge** | The hands. MV3 service worker that runs CDP + native `chrome.*` calls inside the user's real, signed-in profile. |  ```- CONTAINER (cloud or HD/WSL2)                 USER'S LAPTOP+ CONTAINER (cloud or ah/WSL2)                 USER'S LAPTOP  +---------------------------+      relay      +-------------------------------------------+  |  your AI session          |  wss://...:8765 |  Bridge (ab)                         |  |  + installed skills       |  ============>  |     |  routes nbrowser_*                  |@@ -60,12 +60,12 @@ runs `Bridge`, and ab owns the rest of the chain. ### 0. Discovery (before anything is installed) The user says something like "drive my real Chrome" / "log into DigiKey for me" / "control my browser." The wiki discovery layer matches that intent to this page via its `discovery_triggers`-(`adom-wiki discover find "..."`), so you find **adom/adom-native-browser** without it being+(`adom-wiki discover find "..."`), so you find **adom/nb-bridge** without it being pre-installed.  ### 1. Install the package (skills land in the container) ```-adom-wiki pkg install adom/adom-native-browser+adom-wiki pkg install adom/nb-bridge ``` This runs `install.sh`, which copies the bundled skills into `~/.claude/skills/` **in the container**. Now your session knows the `nbrowser_*` surface and the safety doctrine. (Details: see *How skills@@ -73,7 +73,7 @@ propagate*, below.) This step is container-local; it does not touch the laptop.  ### 2. Install the bridge on the laptop (one ab call) ```-adom-bridge-cli bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/pages/adom/adom-native-browser/files/native-browser-bridge-manifest.json"}'+adom-bridge-cli bridge_install '{"manifestUrl":"https://wiki.adom.inc/api/pages/adom/nb-bridge/files/native-browser-bridge-manifest.json"}' ``` ab downloads the bridge zip the manifest points to, sha256-verifies it, and extracts it into `%LOCALAPPDATA%\Adom Bridge\bridges-cache\native-browser\`. ab's registry auto-discovers it and@@ -134,14 +134,14 @@ and (after install) the **Adom** card loaded clean: Enabled, the pinned ID, an a errors, so always read it. (Tip: after `desktop_navigate`, verify the page actually rendered - the tab title becomes "Extensions" - not just that the omnibox shows the URL.) -![chrome://extensions: Developer mode on, Load unpacked, and the Adom card loaded clean](/blob/app/adom-native-browser/screenshots/extensions-adom-card.png)+![chrome://extensions: Developer mode on, Load unpacked, and the Adom card loaded clean](/blob/app/nb-bridge/screenshots/extensions-adom-card.png)  **2. The Load-unpacked folder picker, pointed at the `extension/` folder** (`icons`, `src`, Folder = "extension"). It opens in an arbitrary last-used directory, so the agent **brings it to the front and types the path** (`bring_to_front` -> `ctrl+l` -> type -> Enter), then **screenshot-verifies the path before** clicking **Select Folder** - the wrong folder loads a broken/old build silently: -![The Load-unpacked folder picker pointed at the extension folder](/blob/app/adom-native-browser/screenshots/install-folder-picker.png)+![The Load-unpacked folder picker pointed at the extension folder](/blob/app/nb-bridge/screenshots/install-folder-picker.png)  Chrome and Edge are independent: load it in whichever the user wants, or both. The host is registered per browser (step 3), so each browser the extension runs in connects its own host.
docs/UNINSTALL-FLOW.md+2−2
@@ -27,7 +27,7 @@ Set the search box (background `desktop_ui_set {contains:"Search extensions", te Adom card remains. **Screenshot and confirm exactly one card shows** before going further - this is what makes the Remove button unambiguous: -![chrome://extensions filtered to the single Adom card, Developer mode on, no Errors](/blob/app/adom-native-browser/screenshots/extensions-adom-card.png)+![chrome://extensions filtered to the single Adom card, Developer mode on, no Errors](/blob/app/nb-bridge/screenshots/extensions-adom-card.png)  ## Step 3 - Remove + confirm (the one brief, narrated foreground beat) The confirm bubble (`Remove "Adom for Chrome & Edge"?`) is a Chromium **owned popup that dies on focus@@ -37,7 +37,7 @@ unavoidable, so caption the user ("one moment, please don't type"), then do it f role:"button", name:"Remove"}` (confirm). Do **not** insert a screenshot between trigger and confirm - the bubble auto-closes in ~1-2s. -![The Remove confirm bubble: Remove "Adom for Chrome & Edge"? with Remove and Cancel](/blob/app/adom-native-browser/screenshots/uninstall-confirm-bubble.png)+![The Remove confirm bubble: Remove "Adom for Chrome & Edge"? with Remove and Cancel](/blob/app/nb-bridge/screenshots/uninstall-confirm-bubble.png)  To photograph the bubble (as above) without removing anything, do a separate pass: trigger Remove, then `desktop_screenshot_window {hwnd:<browser>}` - the bubble rides back in the response's `screenshots[]`
extension/src/cdp.js+5−5
@@ -209,9 +209,9 @@ function focusExpr(selector) {  const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); -// ---- visible Adom cursor — EXACT match to HD's AI cursor (hd-control AI_CURSOR_JS) --------------+// ---- visible Adom cursor — EXACT match to Hydrogen's AI cursor (hd-control AI_CURSOR_JS) -------------- // Teal Adom arrow (#00b8b0) + dark-teal outline + teal glow, a pulsing teal ring, and a monospace-// "Adom" label so the user always knows it's an Adom-driven cursor (trust). Kept byte-faithful to HD+// "Adom" label so the user always knows it's an Adom-driven cursor (trust). Kept byte-faithful to Hydrogen // so the cursor looks identical across the whole product. function cursorScript(action, x, y, label) {   var id = '__adom_cursor__';@@ -225,7 +225,7 @@ function cursorScript(action, x, y, label) {     g.id = '__adom_cursor_ring__';     g.style.cssText = 'position:absolute;top:-16px;left:-16px;width:40px;height:40px;border-radius:50%;background:rgba(0,184,176,0.15);border:2px solid rgba(0,184,176,0.4);animation:adomCurP 1.5s ease-in-out infinite;';     c.appendChild(g);-    var a = document.createElement('div'); // the teal Adom arrow (HD's exact SVG)+    var a = document.createElement('div'); // the teal Adom arrow (Hydrogen's exact SVG)     a.innerHTML = '<svg width="20" height="28" viewBox="0 0 20 28" xmlns="http://www.w3.org/2000/svg"><path d="M1 1L1 22L6.5 16.5L10.5 26L14 24.5L10 15L17 15Z" fill="#00b8b0" stroke="#004d49" stroke-width="1.2" stroke-linejoin="round"/></svg>';     a.style.cssText = 'position:absolute;top:0;left:0;filter:drop-shadow(0 0 6px rgba(0,184,176,0.7));';     c.appendChild(a);@@ -259,7 +259,7 @@ export async function cursor(tabId, action, x, y, label) {   try { await chrome.scripting.executeScript({ target: { tabId }, func: cursorScript, args: [action, x == null ? null : x, y == null ? null : y, label == null ? null : String(label)] }); } catch (e) {} } -// HD-style intention narration for the cursor label. phase 'find' = while gliding there; 'act' = doing it.+// Hydrogen-style intention narration for the cursor label. phase 'find' = while gliding there; 'act' = doing it. function phaseLabel(type, tgt, phase) {   const name = tgt ? `"${tgt}"` : 'this';   if (type === 'hover') return phase === 'act' ? `Hovering ${name}` : `Moving to ${name}`;@@ -603,7 +603,7 @@ export async function inputDispatch(tabId, args) {     const finder = wantFinder ? await buildFinder(tabId, c.query || {}, c.candidates || [], c.chosenIndex == null ? 0 : c.chosenIndex, x, y) : null;     const fin = finder ? { finder } : {};     if (resolveOnly) return { ok: true, resolveOnly: true, dispatched: false, x, y, ...info, ...fin };-    // The cursor label narrates intention (HD-style). args.intent overrides; else auto from element text.+    // The cursor label narrates intention (Hydrogen-style). args.intent overrides; else auto from element text.     const intent = args.intent || args.label;     const tgt = (info.clickedText || info.matchedText || '').trim().slice(0, 40);     if (showCursor) { await cursor(tabId, 'move', x, y, intent || phaseLabel(type, tgt, 'find')); await sleep(args.cursorGlideMs || 450); }
extension/src/native.js+1−1
@@ -962,7 +962,7 @@ export async function download(args = {}) {  // ---- demo aids: on-page captions (subtitle | alert) + bring-to-front ------- // Injected into the page (self-contained; chrome.scripting serializes via toString()).-// style 'subtitle' = clean bottom narration (HD / web-hydrogen feel);+// style 'subtitle' = clean bottom narration (Hydrogen feel); // style 'alert'    = red PULSING attention banner (ab feel). pulse via Web Animations API // so page CSP can't block it. function injectCaption(o) {
extension/src/record.js+4−4
@@ -18,9 +18,9 @@ //   • 'getdisplaymedia' (desktopCapture) — CROSS-PLATFORM (Mac/Ubuntu/Windows). High-fps (the WebRTC //     pipeline Google Meet uses). Chrome REQUIRES a one-time "choose what to share" picker per //     recording (no silent variant in any browser). Records the chosen surface in the background.-//   • 'wgc' — WINDOWS-only, handled BRIDGE-SIDE via HD's Windows-Graphics-Capture recorder: no picker,+//   • 'wgc' — WINDOWS-only, handled BRIDGE-SIDE via Hydrogen's Windows-Graphics-Capture recorder: no picker, //     fully background, no wake-lock, 60fps mp4. The bridge intercepts method:'wgc' before the-//     extension; reaching here means HD/bridge WGC isn't wired on this machine.+//     extension; reaching here means Hydrogen/bridge WGC isn't wired on this machine. // // record_start is NON-BLOCKING: it shows the picker and returns immediately with a recordId in state // 'awaiting_pick'. The picker can take as long as the user needs (the old blocking version timed out@@ -39,8 +39,8 @@ async function ensureOffscreen() { export async function recordStart(tabId, args = {}) {   const method = (args.method || 'getdisplaymedia').toLowerCase();   if (method === 'wgc') {-    return { ok: false, error: 'method:wgc is handled by the bridge (HD WGC recorder), not the extension',-      _hint: 'the bridge intercepts method:"wgc"; reaching the extension means HD/bridge WGC is not wired here — use method:"getdisplaymedia"' };+    return { ok: false, error: 'method:wgc is handled by the bridge (Hydrogen WGC recorder), not the extension',+      _hint: 'the bridge intercepts method:"wgc"; reaching the extension means Hydrogen/bridge WGC is not wired here — use method:"getdisplaymedia"' };   }   // 'tabcapture' — record the CURRENT tab with NO picker. Needs activeTab, which is granted when the   // user clicks the Adom toolbar icon (that gesture). This powers the friendly "Record this tab" button.
install/README.md+1−1
@@ -1,6 +1,6 @@ # install/ -Three registrations on a managed Windows machine (golden image / HD / winvm):+Three registrations on a managed Windows machine (golden image / ah / winvm):  1. **Bridge → ab `bridges-cache`** — `bridge/` copied to `%LOCALAPPDATA%\Adom Bridge\bridges-cache\    native-browser\`; Bridge's registry auto-discovers it and routes `nbrowser_*` to it.
native-browser-bridge-manifest.json+5−5
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "version": "0.2.1",-  "url": "https://wiki.adom.inc/download/adom/adom-native-browser/0.2.1/native-browser-bridge-v0.2.1.zip",-  "sha256": "3d36956883e1aa1ac8d03f3a0ab3a9e07f2b89f73fcef3501353aa3fbf28c7ae",-  "size": 100724,-  "released_at": "2026-08-08T16:39:21Z"+  "version": "0.2.2",+  "url": "https://wiki.adom.inc/download/adom/nb-bridge/0.2.2/native-browser-bridge-v0.2.2.zip",+  "sha256": "4f7373fb47dc02297c0b034a949a8c35a44cb8f4efe638c37456ad5ddf067546",+  "size": 100725,+  "released_at": "2026-08-09T10:50:47Z" }
package.json+4−4
@@ -1,11 +1,11 @@ {-  "slug": "adom-native-browser",-  "version": "0.5.1",+  "slug": "nb-bridge",+  "version": "0.6.0",   "type": "app",   "title": "Adom Native Browser",   "description": "This package installs the SKILLS into your container so your AI knows how to drive your real signed-in Chrome and Edge; Bridge loads the bridge runtime itself from the release zip.",   "install": {-    "binary_name": "adom-native-browser"+    "binary_name": "nb-bridge"   },   "files": [     "screenshots/hero.png",@@ -91,7 +91,7 @@     "open it in my real browser",     "native browser"   ],-  "discovery_pitch": "nbe (the Adom Native Browser Extension, layer 2 of nb) drives the user's REAL signed-in Chrome/Edge via nbrowser_* verbs: use it for LOGGING IN, filling forms, and downloading behind auth with their OWN saved passwords - pup cannot (it is a fresh Chrome-for-Testing with no logins). Next moves: run 'adom-bridge-cli bridge_list' to check it is installed; if not, OFFER to install it (see the installing-the-extension skill) - never make the user do it. If this container lacks the skills, 'adom-wiki pkg install adom/adom-native-browser'. Check readiness with 'adom-bridge-cli nbrowser_readiness' (state:needs-extension means offer the install).",+  "discovery_pitch": "nbe (the Adom Native Browser Extension, layer 2 of nb) drives the user's REAL signed-in Chrome/Edge via nbrowser_* verbs: use it for LOGGING IN, filling forms, and downloading behind auth with their OWN saved passwords - pup cannot (it is a fresh Chrome-for-Testing with no logins). Next moves: run 'adom-bridge-cli bridge_list' to check it is installed; if not, OFFER to install it (see the installing-the-extension skill) - never make the user do it. If this container lacks the skills, 'adom-wiki pkg install adom/nb-bridge'. Check readiness with 'adom-bridge-cli nbrowser_readiness' (state:needs-extension means offer the install).",   "sample_prompts": [     {       "label": "Open in my browser",
page.json+2−2
@@ -68,5 +68,5 @@   },   "visibility": "public",   "brief": "Your real browser, driven by AI.",-  "discovery_pitch": "nbe (the Adom Native Browser Extension, layer 2 of nb) drives the user's REAL signed-in Chrome/Edge via nbrowser_* verbs: use it for LOGGING IN, filling forms, and downloading behind auth with their OWN saved passwords - pup cannot (it is a fresh Chrome-for-Testing with no logins). Next moves: run 'adom-bridge-cli bridge_list' to check it is installed; if not, OFFER to install it (see the installing-the-extension skill) - never make the user do it. If this container lacks the skills, 'adom-wiki pkg install adom/adom-native-browser'. Check readiness with 'adom-bridge-cli nbrowser_readiness' (state:needs-extension means offer the install)."-}\ No newline at end of file+  "discovery_pitch": "nbe (the Adom Native Browser Extension, layer 2 of nb) drives the user's REAL signed-in Chrome/Edge via nbrowser_* verbs: use it for LOGGING IN, filling forms, and downloading behind auth with their OWN saved passwords - pup cannot (it is a fresh Chrome-for-Testing with no logins). Next moves: run 'adom-bridge-cli bridge_list' to check it is installed; if not, OFFER to install it (see the installing-the-extension skill) - never make the user do it. If this container lacks the skills, 'adom-wiki pkg install adom/nb-bridge'. Check readiness with 'adom-bridge-cli nbrowser_readiness' (state:needs-extension means offer the install)."+}
publish-skills/publishing-the-bridge/SKILL.md+4−4
@@ -62,12 +62,12 @@ Deps: none (core `http`/`net`), so the payload is source-only (no `node_modules`    releases stay valid at their existing URLs). 3. **The runtime is a RELEASE ASSET, not a `/files` blob** (SDK "two-artifact" rule — fusion + pup do    exactly this). Cut a Release for the bridge version + upload the zip:-   - `adom-wiki release create adom/adom-native-browser <ver> --title "native-browser bridge runtime v<ver>"`-   - `adom-wiki release upload adom/adom-native-browser <ver> native-browser-bridge-v<ver>.zip --platform any`-   The download then serves anon at `https://wiki.adom.inc/download/adom/adom-native-browser/<ver>/native-browser-bridge-v<ver>.zip` (verify: `curl -sI` → 200). (The pkg's own release stays 0.3.x; the bridge release is 0.0.x — separate version streams on the same page, that's expected.)+   - `adom-wiki release create adom/nb-bridge <ver> --title "native-browser bridge runtime v<ver>"`+   - `adom-wiki release upload adom/nb-bridge <ver> native-browser-bridge-v<ver>.zip --platform any`+   The download then serves anon at `https://wiki.adom.inc/download/adom/nb-bridge/<ver>/native-browser-bridge-v<ver>.zip` (verify: `curl -sI` → 200). (The pkg's own release stays 0.3.x; the bridge release is 0.0.x — separate version streams on the same page, that's expected.) 4. **sha256 + byte size** → write `native-browser-bridge-manifest.json` with the `url` pointing at that    **`/download/` path** (NOT a `/files` blob):-   `{manifest_version:1, version, url:"https://wiki.adom.inc/download/adom/adom-native-browser/<ver>/native-browser-bridge-v<ver>.zip", sha256, size, released_at}`.+   `{manifest_version:1, version, url:"https://wiki.adom.inc/download/adom/nb-bridge/<ver>/native-browser-bridge-v<ver>.zip", sha256, size, released_at}`. 5. **Push manifest + source as TEXT** with `adom-wiki repo push adom-native-browser --files native-browser-bridge-manifest.json bridge/bridge.json bridge/package.json bridge/server.js bridge/BRIDGE_VERSION ...` (the manifest lives on `/files`; only the binary is off it). 6. **Land it:** `refresh_bridges {name:"native-browser"}` → verify `nbrowser_readiness` routes and    `bridge_check_updates`/`bridge_list` show `liveVersion==<ver>` (no `staleProcess`). *(bridge_check_updates
skills/adom-native-browser/SKILL.md+2−2
@@ -77,11 +77,11 @@ user to go from "never heard of it" to "logged in for me": 1. **This skill pack -> the user's container.** These `SKILL.md` files install into the cloud    container / WSL2 / Lima (`~/.claude/skills/`). **They surface BEFORE the extension exists** - that    is deliberate. Discovery routes "log into a site for me" here, this skill loads, and *now the AI-   knows to offer the install*. `adom-wiki pkg install adom/adom-native-browser` (or+   knows to offer the install*. `adom-wiki pkg install adom/nb-bridge` (or    `adom-bridge-cli sync_skills`) puts them there. 2. **The nb bridge -> Bridge (ab).** The `nbrowser_*` verbs come from the `native-browser`    bridge. `adom-bridge-cli bridge_list` shows if it's installed; if not,-   `bridge_install {manifestUrl:"https://wiki.adom.inc/api/pages/adom/adom-native-browser/files/native-browser-bridge-manifest.json"}`.+   `bridge_install {manifestUrl:"https://wiki.adom.inc/api/pages/adom/nb-bridge/files/native-browser-bridge-manifest.json"}`.    It auto-updates from there (per-bridge `updateManifestUrl`). 3. **The MV3 extension -> the user's real Chrome/Edge.** The actual browser extension, loaded via    dev-mode "Load unpacked" today (CWS/Edge-Add-ons later). This is **installing-the-extension**.
skills/driving-the-extension/SKILL.md+1−1
@@ -539,7 +539,7 @@ keyframe re-encode (see recording-mechanics.md).  ## Captions (two systems) - **In-page** `nbrowser_caption {text, style, position}` - `style:"subtitle"` (calm white-on-dark, the-  HD/web-hydrogen look) or `"alert"` (red **pulsing** banner). Tied to a page (cleared on navigation).+  Hydrogen look) or `"alert"` (red **pulsing** banner). Tied to a page (cleared on navigation). - **Fullscreen** ab `desktop_caption {text, position:"bottom", id}` - Win32 always-on-top over every app,   truly unmissable; auto-expires. (`position:"bottom"` - top covers the URL bar + the debugger banner.) 
skills/installing-the-extension/SKILL.md+2−2
@@ -22,7 +22,7 @@ The extension's `manifest.json` pins a `key`, so its ID is the **same on every m getting "Load unpacked" to pick the right folder; everything else is deterministic.  ## Preconditions (do these once, before driving any UI)-1. **The `extension/` folder is on the laptop** at a known path (e.g. `C:\Users\<user>\adom-native-browser\extension`). Get it there from the package/repo (`push_file` the folder, or the user clones it). Load-unpacked reads the **on-disk** build, so this path is what the picker must select.+1. **The `extension/` folder is on the laptop** at a known path (e.g. `C:\Users\<user>\nb-bridge\extension`). Get it there from the package/repo (`push_file` the folder, or the user clones it). Load-unpacked reads the **on-disk** build, so this path is what the picker must select. 2. **The native-messaging host is registered** (`install/install.ps1`, or its steps): host manifest written with `allowed_origins` = the pinned ID, and `HKCU\...\NativeMessagingHosts\inc.adom.native_browser` pointing at it (Chrome and/or Edge). No UAC. 3. **The bridge is installed** in ab's cache (`bridge_install`, once). See the install-flow doc. @@ -135,7 +135,7 @@ desktop_ui_click {hwnd:<W>, automationId:"loadUnpacked"}                    # op # ensure exactly one picker; grab its hwnd <PK> via desktop_list_windows (cancel extras) desktop_bring_to_front {hwnd:<PK>}                                          # ⭐ THE KEY: picker opens at z:1/z:2; SendInput only lands once it is z:0 desktop_press_key {hwnd:<PK>, keys:"ctrl+l"}                                # focus the picker address bar-desktop_type      {hwnd:<PK>, text:"C:\\Users\\<user>\\adom-native-browser\\extension"}   # agent types the path - fast+desktop_type      {hwnd:<PK>, text:"C:\\Users\\<user>\\nb-bridge\\extension"}   # agent types the path - fast desktop_press_key {hwnd:<PK>, keys:"enter"}                                 # navigates INTO the folder desktop_screenshot_window {hwnd:<PK>}                                       # VERIFY breadcrumb shows ...\extension BEFORE committing desktop_ui_click  {hwnd:<PK>, automationId:"1"}                             # commit (Select Folder = IDOK) -> page toasts "Extension loaded"
skills/native-browser-safety/SKILL.md+1−1
@@ -42,7 +42,7 @@ attribute, `nbrowser_force_close` is a deliberately hard, two-step last resort ( another thread's window). It has TWO forms behind the SAME gate (ticket → attest with a written reason): **`{windowId}`** for windows the extension can see, and **`{hwnd}`** (v0.1.23+, from `desktop_list_windows`) for browser windows the extension CANNOT reach — e.g. Chrome/Edge litter that a-`pup_open`/pup call or an HD test left behind, or a window in a profile with no extension. The hwnd+`pup_open`/pup call or an ah test left behind, or a window in a profile with no extension. The hwnd form only targets real browser windows and sends a graceful WM_CLOSE. So cleaning up ANOTHER tool's browser litter is allowed — but only through this gate, with a real justification, and never a window a live thread owns.
store/STORE-LISTING.md+2−2
@@ -55,6 +55,6 @@ https://adom.inc and wiki.adom.inc. 5. Tab/window management or a recorded walkthrough.  ## Support / homepage URLs-- Homepage: https://wiki.adom.inc/adom/adom-native-browser+- Homepage: https://wiki.adom.inc/adom/nb-bridge - Support: [email protected]-- Privacy policy: https://wiki.adom.inc/adom/adom-native-browser/files/privacy.html+- Privacy policy: https://wiki.adom.inc/adom/nb-bridge/files/privacy.html
store/SUBMIT.md+1−1
@@ -39,7 +39,7 @@ Two ways to publish, pick one: 4. Submit for certification. Publishing here removes the Edge "developer mode" warning for users.  ## Privacy Policy URL-`https://wiki.adom.inc/adom/adom-browser-extension/files/privacy.html`+`https://wiki.adom.inc/adom/nb-bridge/files/privacy.html`  ## What this fixes - Removes the Chrome **and** Edge "developer-mode extensions" warnings (signed store install).
wiki/page.json+3−3
@@ -1,8 +1,8 @@ {-  "slug": "adom-native-browser",+  "slug": "nb-bridge",   "org": "adom",   "title": "Adom Native Browser",-  "summary": "Drive your real, logged-in Chrome/Edge from the Adom ecosystem — the native-browser counterpart to pup. Same nbrowser_* surface as the puppeteer bridge, but inside the actual signed-in profile so cookies, SSO, saved logins, and captcha human-trust all work.",+  "summary": "Your AI's hands in your real, signed-in Chrome and Edge. Adom Native Browser comes in two layers: nb opens URLs in your own browser profile on day one with nothing installed, and nbe, the Adom Native Browser Extension, lets your AI fully drive that browser - navigate, click, read, screenshot, real logins. Build, drive, and test web software right in the browser you already work in.",   "category": "Browser Automation",   "tags": [     "ab",@@ -40,5 +40,5 @@     "path": "screenshots/hero.png"   },   "visibility": "public",-  "brief": "Control of the user's REAL signed-in browser (Chrome/Edge), in two layers. Layer 1 (nb) opens URLs into their native browser and works on day one with nothing installed, the OAuth workhorse. Layer 2 (nbe, the Adom Native Browser Extension) lets the AI fully drive that browser: navigate, click, read, screenshot, real logins."+  "brief": "Your real browser, driven by AI." }