← Commit history
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.20\ No newline at end of file+0.1.21\ No newline at end of file
bridge/bridge.json+1−1
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser (Chrome + Edge)",-  "version": "0.1.20",+  "version": "0.1.21",   "expectedExtensionVersion": "0.1.2",   "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",
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.1.20",+  "version": "0.1.21",   "private": true,   "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)",   "main": "server.js",
bridge/server.js+18−4
@@ -1148,7 +1148,7 @@ const VERB_META = {   nbrowser_fullscreen: { hint: 'Fullscreen the window (hides tabs/URL bar) - great for clean recordings of a BACKGROUND window. ' + NO_FG, related: ['nbrowser_window_state'], pitfalls: ['F11-via-key does NOT work; use this verb', 'stays in the background (auto-reverted) unless userRequestedForeground:true - it does not raise the window over the user\'s work'] },   nbrowser_login_state: { hint: 'Is the profile signed in to a URL right now (via chrome.cookies)?', related: ['nbrowser_credentials'], pitfalls: ['cookie-based inference, not definitive - a page may still challenge for 2FA even when auth cookies exist', 'never returns the password; Chrome autofills it when you focus the field'] },   nbrowser_credentials: { hint: 'Read-only QUERY of the browser\'s own saved-login store (which vendors the user can auto-login to). Never returns passwords.', related: ['nbrowser_login_state', 'nbrowser_navigate'], pitfalls: ['if a vendor is not listed, the user has no saved login - ask them to save one once', 'never exposes the password; Chrome autofills it when you focus the field'] },-  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'] },+  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_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:'] },@@ -1409,7 +1409,17 @@ function dispatch(verb, args) {         if (!row.email) row.unresolved = true;         return row;       });-      return resolve({ success: true, output: JSON.stringify({ ok: true, profiles: list, active: activeProfile, blocklist: [...blocklist], _hint: 'Each entry is one BROWSER×PROFILE the AI can drive - `profile` is the addressable key like "edge:[email protected]" (browser:email). displayName = the human label the USER sees on the browser\'s own profile button ("John Personal", "adom.inc") - use it together with the email DOMAIN to pick work vs personal (corporate domain = work; gmail.com etc = personal). NEVER assume "the" browser: the same email can exist in Chrome AND Edge - match `browser` too. An entry with unresolved:true has not announced its email yet - call nbrowser_resolve_profiles before driving so you never hit the wrong account. Target profiles in PARALLEL by passing profile:"<key>" on any verb. ▶active is only the default when no target is passed. Switch with nbrowser_use_profile; exclude with nbrowser_profile_block.' }) });+      // COMPLETENESS: also report every profile the BROWSER knows about (Local State) whose extension is+      // NOT connected right now - a profile with no open window has a SLEEPING service worker, so it+      // never announces, but it absolutely exists and the user may use it daily (real gripe: media@+      // missing from the list read as "the bridge is lazy"). asleep:true = exists, not drivable until a+      // window opens in it.+      const liveEmails = new Set(list.map((r) => `${r.browser}:${(r.email || '').toLowerCase()}`));+      for (const kp of knownProfiles()) {+        if (liveEmails.has(`${kp.browser}:${kp.email.toLowerCase()}`)) continue;+        list.push({ profile: kp.key, label: kp.email, email: kp.email, browser: kp.browser, displayName: kp.displayName, profileDir: kp.profileDir, hasAvatar: kp.hasAvatar, active: false, blocked: isBlocked(kp.key), live: false, asleep: true });+      }+      return resolve({ success: true, output: JSON.stringify({ ok: true, profiles: list, active: activeProfile, blocklist: [...blocklist], _hint: 'Each entry is one BROWSER×PROFILE the AI can drive - `profile` is the addressable key like "edge:[email protected]" (browser:email). displayName = the human label the USER sees on the browser\'s own profile button ("John Personal", "adom.inc") - use it together with the email DOMAIN to pick work vs personal (corporate domain = work; gmail.com etc = personal). NEVER assume "the" browser: the same email can exist in Chrome AND Edge - match `browser` too. unresolved:true = connected but email not announced yet (nbrowser_resolve_profiles first, or you may drive the wrong account). asleep:true = the profile EXISTS on this machine but has NO OPEN WINDOW, so its extension service worker is asleep and it CANNOT be driven yet - ask the user to open a window in that profile (do NOT launch one yourself without the notify+foregroundReason flow); once open it appears live here. Target profiles in PARALLEL by passing profile:"<key>" on any verb. ▶active is only the default when no target is passed. Switch with nbrowser_use_profile; exclude with nbrowser_profile_block.' }) });     }     if (verb === 'nbrowser_use_profile') {       const want = String((args && (args.profile || args.email)) || '');@@ -1898,9 +1908,13 @@ function knownProfiles() {     try {       const j = JSON.parse(fs.readFileSync(path.join(root, 'Local State'), 'utf8'));       const cache = (j.profile && j.profile.info_cache) || {};-      for (const info of Object.values(cache)) {+      for (const [dir, info] of Object.entries(cache)) {         const email = String((info && info.user_name) || '');-        if (email.includes('@')) out.push({ email, key: `${browser}:${email}` });+        if (!email.includes('@')) continue;+        const names = browser === 'edge' ? ['Edge Profile Picture.png', 'Google Profile Picture.png'] : ['Google Profile Picture.png', 'Edge Profile Picture.png'];+        let hasAvatar = false;+        for (const n of names) if (fs.existsSync(path.join(root, dir, n))) { hasAvatar = true; break; }+        out.push({ email, key: `${browser}:${email}`, browser, displayName: String((info && info.name) || ''), profileDir: dir, hasAvatar });       }     } catch {}   }
native-browser-bridge-manifest.json+4−4
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "version": "0.1.20",-  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.20/native-browser-bridge-v0.1.20.bin",-  "sha256": "b4783120ea6ccb8e174c210439b23a1ba19cd985e4cb5e96c4a4e76107beeea1",-  "size": 73263,+  "version": "0.1.21",+  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.21/native-browser-bridge-v0.1.21.bin",+  "sha256": "9f45fd1a016126decbf0d319857c1efe53482073be2034d14432be0c71d8eab6",+  "size": 73882,   "released_at": "2026-07-18T00:00:00Z" }