← Commit history
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.19\ No newline at end of file+0.1.20\ 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.19",+  "version": "0.1.20",   "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.19",+  "version": "0.1.20",   "private": true,   "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)",   "main": "server.js",
bridge/server.js+24−3
@@ -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. Drive several in parallel with profile:"<key>".', related: ['nbrowser_use_profile', 'nbrowser_profile_block'], pitfalls: ['active is only the default when no profile is passed'] },+  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_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:'] },@@ -1387,8 +1387,29 @@ function dispatch(verb, args) {     }     // --- multi-profile management (bridge-local) ---     if (verb === 'nbrowser_profiles') {-      const list = [...conns.entries()].map(([k, c]) => ({ profile: k, label: profileLabel(c.profile), email: c.profile.email || '', browser: c.profile.browser || 'chrome', active: k === activeProfile, blocked: isBlocked(k), live: !!(c.sock && !c.sock.destroyed) }));-      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). Target any of them in PARALLEL by passing profile:"<key>" (or profile:"<email>"+browser:"edge") on any verb - different AI threads can each drive a different one at once. ▶active is only the default when no target is passed. Switch the default with nbrowser_use_profile; exclude one with nbrowser_profile_block.' }) });+      // The bridge is the BROWSER EXPERT: enrich each live connection with what the browser itself+      // knows about the profile (Local State info_cache) - the human display label the user sees in+      // Chrome/Edge ("John Personal", "adom.inc"), the profile DIRECTORY, and whether it has an avatar.+      // Without this an AI cannot reliably tell work from personal (the incident: "the extension status+      // doesn't expose the Chrome profile's label").+      const list = [...conns.entries()].map(([k, c]) => {+        const row = { profile: k, label: profileLabel(c.profile), email: c.profile.email || '', browser: c.profile.browser || 'chrome', active: k === activeProfile, blocked: isBlocked(k), live: !!(c.sock && !c.sock.destroyed) };+        const av = avatarForProfile(k);+        if (av) { row.profileDir = av.profileDir; row.hasAvatar = !!av.avatarPath; }+        try {+          if (row.email) {+            const local = process.env.LOCALAPPDATA;+            const root = row.browser === 'edge' ? path.join(local, 'Microsoft', 'Edge', 'User Data') : path.join(local, 'Google', 'Chrome', 'User Data');+            const jj = JSON.parse(fs.readFileSync(path.join(root, 'Local State'), 'utf8'));+            for (const [dir, info] of Object.entries((jj.profile && jj.profile.info_cache) || {})) {+              if (String((info && info.user_name) || '').toLowerCase() === row.email.toLowerCase()) { row.displayName = String((info && info.name) || ''); row.profileDir = dir; break; }+            }+          }+        } catch {}+        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.' }) });     }     if (verb === 'nbrowser_use_profile') {       const want = String((args && (args.profile || args.email)) || '');
native-browser-bridge-manifest.json+4−4
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "version": "0.1.19",-  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.19/native-browser-bridge-v0.1.19.bin",-  "sha256": "8d8dfd178452020e1387fee4052b9af7e58dd3d90117d18e8d9099de5a697ffd",-  "size": 72398,+  "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,   "released_at": "2026-07-18T00:00:00Z" }