← Commit history
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.31\ No newline at end of file+0.1.32\ No newline at end of file
bridge/bridge.json+2−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Browser Extension",-  "version": "0.1.31",+  "version": "0.1.32",   "expectedExtensionVersion": "0.1.3",   "description": "Drives the user's REAL signed-in Chrome/Edge via an installed extension (native-messaging transport). Mirrors the puppeteer bridge's verb surface as nbrowser_* but runs inside the actual logged-in profile, so cookies/SSO/saved-logins/captcha human-trust all work. The bridge binds an ephemeral loopback port (PORTS.md compliant) and writes a discovery file; the native-messaging host pumps stdio <-> this bridge; the extension never touches a TCP port.",   "homepage": "https://wiki.adom.inc/adom/adom-browser-extension",@@ -87,6 +87,7 @@     "nbrowser_pin_session",     "nbrowser_resolve_profiles",     "nbrowser_wake_profile",+    "nbrowser_open_os_window",     "nbrowser_os_windows"   ],   "dependencies": {
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.1.31",+  "version": "0.1.32",   "private": true,   "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)",   "main": "server.js",
bridge/server.js+68
@@ -1161,6 +1161,7 @@ const VERB_META = {   nbrowser_profiles: { hint: 'List every BROWSER x PROFILE you can drive + which is active. Each row carries browser (chrome|edge), email, displayName (the human label the user sees on the browser profile button, e.g. "John Personal" vs "adom.inc"), and profileDir. Pick work-vs-personal from the email DOMAIN + displayName, never from which window happens to be focused. Drive several in parallel with profile:"<key>".', related: ['nbrowser_use_profile', 'nbrowser_profile_block', 'nbrowser_resolve_profiles'], pitfalls: ['active is only the default when no profile is passed', 'an entry with unresolved:true has no email yet - nbrowser_resolve_profiles first, or you may drive the wrong account', 'the same email can live in Chrome AND Edge - match browser too', 'asleep:true = the profile exists (Local State) but has no open window so its extension is not running - it cannot be driven until the user opens a window in it; never claim a profile "does not exist" when it is merely asleep'] },   nbrowser_use_profile: { hint: 'Set the default profile that verbs target.', related: ['nbrowser_profiles', 'nbrowser_pin_session'], pitfalls: ['you can still override per-call with profile:"<email>"'] },   nbrowser_pin_session: { hint: 'Pin a sessionId to a profile so EVERY verb carrying that sessionId auto-routes to it (no cross-profile mistakes when driving several accounts). open_window pins automatically to the profile it opened in; use this to re-pin explicitly.', related: ['nbrowser_profiles', 'nbrowser_resolve_profiles', 'nbrowser_open_window'], pitfalls: ['pins are per-session; a verb with an explicit profile: still overrides the pin', 'if the pinned profile goes offline, verbs fall back to the active profile'] },+  nbrowser_open_os_window: { hint: 'EXTENSION-FREE: open a URL in the user\'s REAL browser profile (Chrome or Edge) when ABE is not installed there - the graceful-degradation path for flows where the USER does the clicking, above all OAuth consent (adom-google, Autodesk APS: they are already signed in; they just click Approve). BACKGROUND by default: brief blip, then minimized + ORANGE flashing taskbar button = "come click when ready". Registration (sessionId+thread+purpose) required like every window.', related: ['nbrowser_profiles', 'nbrowser_os_windows', 'nbrowser_force_close', 'nbrowser_wake_profile'], pitfalls: ['you CANNOT read, drive, or screenshot this window - no extension in that profile; poll the outcome out-of-band (OAuth token poll) or ask the user', 'if the extension IS installed in the profile, prefer wake_profile + open_window (full control, guards, cleanup)', 'foreground:true needs foregroundReason (+ notify_user first) - background+flash is almost always right', 'close it afterwards with nbrowser_force_close {hwnd} - never leave auth litter', 'every response nudges the ABE install - relay the upsell to the user at a natural moment, never mid-auth'] },   nbrowser_os_windows: { hint: 'OS-level browser-window inventory measured ON the laptop by the bridge: every Chrome/Edge/pup window (hwnd, title, rect, z) annotated with the ABE session that owns it. THE reliable probe when your own desktop_list_windows returns 0/null (wrong --target, AD hiccup) or when a profile has no extension (Edge) so list_windows cannot see it.', related: ['nbrowser_force_close', 'nbrowser_list_windows', 'nbrowser_profiles'], pitfalls: ['a window with abeSession set belongs to a LIVE thread - never force-close it', 'ok:false = the bridge\'s AD link is down, meaning "unknown", NOT "no windows"'] },   nbrowser_wake_profile: { hint: 'Wake an ASLEEP profile (exists in nbrowser_profiles with asleep:true but not drivable) WITHOUT opening any window: launches the browser with --no-startup-window so the profile + its extension load in background mode - nothing foregrounds, no focus is stolen. When it returns woke:true, drive normally.', related: ['nbrowser_profiles', 'nbrowser_open_window', 'nbrowser_resolve_profiles'], pitfalls: ['NEVER launch a foreground browser window to wake a profile - this verb exists so waking honors background-by-default', 'woke:false usually means the Adom extension is not installed in THAT profile (extensions are per-profile)'] },   nbrowser_resolve_profiles: { hint: 'Force every connected extension to re-resolve its Google email and re-announce, so pending-N / empty-email keys eagerly become chrome:<email> / edge:<email> - call it up front so you never drive the wrong (unresolved) profile.', related: ['nbrowser_profiles', 'nbrowser_pin_session', 'nbrowser_whoami'], pitfalls: ['takes ~2.5s (waits for each re-announce)', 'a profile not signed into Google stays email-less - target it by id-key or browser:'] },@@ -1351,6 +1352,7 @@ function dispatch(verb, args) {         d('nbrowser_profiles', 'List connected BROWSER×PROFILE targets + which is active.', {}, { ok: 'bool', profiles: '[{profile,email,browser,active,blocked,live}]', active: 'str' }, 15, { example: {} }),         d('nbrowser_use_profile', 'Set the default profile action verbs target.', { profile: 'required str (key or email)' }, { ok: 'bool', active: 'str' }, 15, { example: { profile: 'chrome:[email protected]' } }),         d('nbrowser_pin_session', 'Pin a sessionId to a profile so every verb on that session auto-routes there (prevents cross-profile mistakes). open_window pins automatically.', { sessionId: 'required str', profile: 'required str (key or email)' }, { ok: 'bool', sessionId: 'str', pinnedTo: 'str' }, 15, { example: { sessionId: 'schwab', profile: 'chrome:[email protected]' } }),+        d('nbrowser_open_os_window', 'EXTENSION-FREE open of a URL in the user\'s real browser profile (OAuth consent etc). Background by default (minimize + orange taskbar flash); foreground needs foregroundReason. Returns hwnd (close later via force_close). Upsells the ABE install in every response.', { sessionId: 'required str', thread: 'required str', purpose: 'required str', profile: 'required str (key or email)', url: 'required http(s) str', browser: 'optional chrome|edge', foreground: 'optional bool', foregroundReason: 'required if foreground' }, { ok: 'bool', hwnd: 'int?', minimized: 'bool', flashed: 'bool', extensionInstalled: 'bool' }, 20, { example: { sessionId: 'gauth', thread: 'adom-google-setup', purpose: 'open Google OAuth consent for adom-google', profile: 'chrome:[email protected]', url: 'https://accounts.google.com/o/oauth2/auth?...' } }),         d('nbrowser_os_windows', 'OS-level browser-window inventory from the laptop (bridge-side): all Chrome/Edge/pup windows with hwnd/title/rect/z + which ABE session owns each. Reliable even when the caller\'s desktop_list_windows returns 0 (wrong target / AD hiccup) or a profile has no extension.', {}, { ok: 'bool (false = AD link down, data unknown)', count: 'int', windows: '[{hwnd,title,rect,z,kind,abeSession}]' }, 15, { example: {} }),         d('nbrowser_wake_profile', 'Wake an asleep browser profile with NO window (launches --no-startup-window; nothing foregrounds). Profile becomes live/drivable when its extension connects.', { profile: 'required str (key or email)', browser: 'optional chrome|edge' }, { ok: 'bool', woke: 'bool', alreadyLive: 'bool?', profile: 'str' }, 15, { example: { profile: 'chrome:[email protected]' } }),         d('nbrowser_resolve_profiles', 'Force every connected extension to re-resolve its Google email + re-announce, so pending-N/empty-email keys become chrome:<email>/edge:<email>.', {}, { ok: 'bool', profiles: '[{profile,email,browser}]' }, 15, { example: {} }),@@ -1520,6 +1522,72 @@ function dispatch(verb, args) {     // whose own desktop_list_windows returns 0 (wrong --target with two ADs connected, transient AD     // hiccup, DPI-blind PowerShell EnumWindows) still has one reliable probe. Annotates each hwnd with     // the live ABE session that owns it (never force-close those) and flags pup/Chrome-for-Testing.+    // EXTENSION-FREE OS OPEN — the graceful-degradation wedge (John, 2026-07-19). Getting a user to+    // install ABE is hard; but MANY high-value tasks only need their REAL browser OPENED at a URL in the+    // right profile - OAuth consent (adom-google, Autodesk APS) where they're already signed in and just+    // click Approve. So: open the native browser extension-free, BACKGROUND by default (brief blip, then+    // minimized + orange taskbar flash = "come click when you're ready"), registered like every window,+    // and every response UPSELLS the ABE install for the full feature set.+    if (verb === 'nbrowser_open_os_window') {+      const a = args || {};+      if (!a.sessionId || !a.thread || !a.purpose) {+        return resolve({ success: false, errorCode: 'session_unregistered', error: 'nbrowser_open_os_window requires sessionId + thread + purpose (registration - same as open_window).', _hint: `Corrected call: nbrowser_open_os_window {sessionId:"<task id>", thread:"<which AI thread>", purpose:"<why, human-readable>", profile:"chrome:[email protected]", url:"https://..."}` });+      }+      const url = String(a.url || '');+      if (!/^https?:\/\//i.test(url)) return resolve({ success: false, error: 'url must be http(s)', _hint: 'Pass the full URL to open (e.g. the OAuth consent link).' });+      const want = String(a.profile || a.email || '');+      const wantEmail = (want.includes(':') ? want.split(':').slice(1).join(':') : want).toLowerCase();+      const wantBrowser = want.includes(':') ? want.split(':')[0] : (a.browser || null);+      const kp = knownProfiles().find((pr) => pr.email.toLowerCase() === wantEmail && (!wantBrowser || pr.browser === wantBrowser));+      if (!kp) return resolve({ success: false, error: `no local browser profile matches '${want}'`, _hint: 'nbrowser_profiles lists every profile on this machine (live + asleep) with emails and displayNames - pick the identity the task needs.' });+      if (isBlocked(kp.key)) return resolve({ success: false, error: `profile '${kp.key}' is blocklisted`, _hint: 'The user blocked this profile from being driven.' });+      if (a.foreground === true && !a.foregroundReason) {+        return resolve({ success: false, errorCode: 'foreground_unjustified', error: 'foreground:true requires foregroundReason (and you should notify_user first).', _hint: 'Background is the default and is almost always right: the window opens, minimizes, and its taskbar button FLASHES orange - the user clicks it when THEY are ready. Only pass foreground:true + foregroundReason for a flow that genuinely breaks when unfocused.' });+      }+      const local = process.env.LOCALAPPDATA;+      const exes = kp.browser === 'edge'+        ? ['C:/Program Files (x86)/Microsoft/Edge/Application/msedge.exe', 'C:/Program Files/Microsoft/Edge/Application/msedge.exe']+        : ['C:/Program Files/Google/Chrome/Application/chrome.exe', 'C:/Program Files (x86)/Google/Chrome/Application/chrome.exe', local ? path.join(local, 'Google', 'Chrome', 'Application', 'chrome.exe') : ''];+      const exe = exes.find((e) => e && fs.existsSync(e));+      if (!exe) return resolve({ success: false, error: `${kp.browser} executable not found at a standard path` });+      const extSt = adomExtStateForProfile(kp.browser, kp.profileDir);+      (async () => {+        let pre = new Set();+        try { pre = new Set((await chromeWindowsDetailed()).map((w) => w.hwnd)); } catch {}+        adCommand('desktop', 'launch_process', { path: exe, args: [`--profile-directory=${kp.profileDir}`, '--new-window', url] });+        // find the new hwnd (diff, like open_window)+        let hwnd = null; const t0 = Date.now();+        while (!hwnd && Date.now() - t0 < 8000) {+          await new Promise((r) => setTimeout(r, 500));+          try { const list = await chromeWindowsDetailed(); hwnd = list.map((w) => w.hwnd).find((h) => !pre.has(h)) || null; } catch {}+        }+        let minimized = false, flashed = false;+        if (hwnd && a.foreground !== true) {+          // BACKGROUND: post-launch minimize (returns focus to whatever the user had) + orange flash so+          // the user knows exactly which taskbar button to click when they are ready to do their part.+          const psBody = [+            'Add-Type @"',+            'using System;using System.Runtime.InteropServices;',+            'public class SWM { [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int c); }',+            '"@',+            `[SWM]::ShowWindow([IntPtr]${hwnd},6)`,+          ].join(String.fromCharCode(13, 10));+          const rr = await adCommand(null, 'run_script', { interpreter: 'powershell', body: psBody, timeoutSeconds: 15 });+          minimized = !!(rr && rr.body && (rr.body.success === true || rr.body.ok === true));+          try { await taskbar(hwnd, { flash: 'until_focused' }); flashed = true; } catch {}+        }+        logActivity({ verb: 'nbrowser_open_os_window', sessionId: a.sessionId, thread: a.thread, profile: kp.key, ok: !!hwnd, errorCode: hwnd ? null : 'hwnd_not_found' });+        sessionThread.set(a.sessionId, a.thread); sessionPurpose.set(a.sessionId, a.purpose); sessionProfile.set(a.sessionId, kp.key); if (hwnd) sessionHwnd.set(a.sessionId, hwnd); persistSessions();+        const upsell = extSt && extSt.installed+          ? `NOTE: the Adom extension IS installed in this profile${extSt.enabled ? '' : ' (but DISABLED)'} - you could have FULL control instead: nbrowser_wake_profile then nbrowser_open_window gives you navigation, reading, clicking, screenshots, and ownership guards. Prefer that path.`+          : `UPSELL (tell the user when the moment is right): this window is OPEN but I cannot see or drive it - the Adom extension is not installed in this profile. With ABE installed (see installing-the-extension - it can be installed FOR the user in ~a minute) the assistant could fill the form, read the result, screenshot progress, and clean up after itself. Until then the USER must do the clicking.`;+        resolve({ success: true, output: JSON.stringify({ ok: !!hwnd, hwnd, minimized, flashed, profile: kp.key, displayName: kp.displayName, extensionInstalled: !!(extSt && extSt.installed), url,+          _hint: hwnd+            ? `Opened ${url} in the user's REAL ${kp.browser} profile '${kp.displayName}' (${kp.profileDir}) WITHOUT the extension. ${a.foreground === true ? 'Foregrounded per your foregroundReason.' : 'It briefly appeared then MINIMIZED with an ORANGE flashing taskbar button - the user clicks it when ready (perfect for OAuth consent: they are already signed in; they just approve).'} You CANNOT read/drive this window (no extension in this profile) - poll the OUTCOME out-of-band (e.g. the OAuth CLI's token poll, or ask the user). Close it afterwards via nbrowser_force_close {hwnd:${hwnd}} (two-step attestation). ${upsell}`+            : `Launch fired but the new window's hwnd was not found within 8s - it may still have opened (grouped into an existing ${kp.browser} window). Check nbrowser_os_windows. ${upsell}` }) });+      })();+      return;+    }     if (verb === 'nbrowser_os_windows') {       (async () => {         let ws = []; let adOk = true;
dev-skills/developing-the-bridge/SKILL.md+31
@@ -127,3 +127,34 @@ that until the user noticed a missing badge. Rules: (1) adCommand must re-discov AD-link failure silently - log the outage + recovery to the bridge log AND the activity trail (_ad_link entries) so a missing badge is diagnosable in one query; (3) suspect stale strays: multiple node.exe bridge processes days old = AD lost track of them - kill by PID, never by image name.++## ⭐ The graceful-degradation ladder + ABE upsell (John, 2026-07-19 - "this is gold")+Getting a user to actually INSTALL the extension is hard. Do not let that block the value: there is a+LADDER of capability, and the bridge must serve every rung while nudging users up it.++**The rungs:**+1. **No extension anywhere** - the bridge still knows every profile (Local State), can wake profiles+   with no window (`wake_profile`), can OPEN the user's real browser at a URL in the RIGHT profile in+   the BACKGROUND (`open_os_window`: brief blip -> minimize -> orange taskbar flash = "come click when+   ready"), can inventory windows (`os_windows`), and can clean up (`force_close {hwnd}`, attested).+2. **Extension in some profiles** - full driving there; rung-1 verbs for the rest, with per-profile+   extension-state diagnosis in every refusal.+3. **Extension everywhere** - the full surface: navigate, read, click, screenshot, guards, badges.++**Why rung 1 matters (the killer use case):** OAuth consent. adom-google, Autodesk APS, GitHub - the+user is ALREADY signed in in their native browser; the AI only needs the consent page OPEN in the right+profile, the USER clicks Approve, and the CLI's token poll picks up the result out-of-band. That single+capability unlocks whole product setups (adom-google!) with zero extension install. After the auth, the+user rarely needs more - so serving this WITHOUT the install is pure value.++**The upsell rule:** every rung-1/rung-2 response carries a hint that (a) states honestly what the+bridge CANNOT do right now ("I opened it but I cannot see or drive it - the user must click"), and+(b) names the upgrade + its payoff ("with ABE installed I could fill this form, read the result, and+clean up - see installing-the-extension; it installs in about a minute"). The AI relays the upsell to+the user at a NATURAL moment - never mid-auth, never nagging. Hints are how the bridge teaches; a+degraded mode that fails silently or begs constantly both lose the install.++**Background even at OS level, always:** rung-1 opens must not steal focus any more than rung-3 ones.+Post-launch minimize (ShowWindow SW_MINIMIZE returns focus to the user automatically) + a taskbar flash+is the pattern - the user opens the window when THEY are ready. foreground:true stays gated behind+foregroundReason + notify, exactly like the extension path.
native-browser-bridge-manifest.json+5−5
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "version": "0.1.31",-  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.31/native-browser-bridge-v0.1.31.zip",-  "sha256": "2f12afadcff6af4eef7a5e37f9d163094e0b5f937d9f6057be45702b6b535e72",-  "size": 81354,-  "released_at": "2026-07-18T00:00:00Z"+  "version": "0.1.32",+  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.32/native-browser-bridge-v0.1.32.zip",+  "sha256": "884f1d44801becf3eb0b21bdd84894ae7b1b1d101096a2b63c6d143c7c4f8edb",+  "size": 83965,+  "released_at": "2026-07-19T00:00:00Z" }