← Commit history
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.21\ No newline at end of file+0.1.22\ No newline at end of file
bridge/bridge.json+3−2
@@ -2,7 +2,7 @@   "manifest_version": 1,   "name": "native-browser",   "displayName": "Adom Native Browser (Chrome + Edge)",-  "version": "0.1.21",+  "version": "0.1.22",   "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",@@ -84,7 +84,8 @@     "nbrowser_select",     "nbrowser_whoami",     "nbrowser_pin_session",-    "nbrowser_resolve_profiles"+    "nbrowser_resolve_profiles",+    "nbrowser_wake_profile"   ],   "dependencies": {     "node": ">=18"
bridge/package.json+1−1
@@ -1,6 +1,6 @@ {   "name": "adom-native-browser-bridge",-  "version": "0.1.21",+  "version": "0.1.22",   "private": true,   "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)",   "main": "server.js",
bridge/server.js+36−1
@@ -1151,6 +1151,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_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:'] },   nbrowser_whoami: { hint: 'Ask ONE profile to (re)resolve + report its Google email (and re-announce it to the bridge). For all profiles at once use nbrowser_resolve_profiles.', related: ['nbrowser_resolve_profiles', 'nbrowser_profiles'], pitfalls: ['signedIn:false = this browser profile has no Google account, so no email'] },   nbrowser_profile_block: { hint: 'Blocklist a profile so it is NEVER driven (user safety whitelist; persists).', related: ['nbrowser_profile_unblock', 'nbrowser_profiles'], pitfalls: ['respect the blocklist - it is the user telling you hands-off'] },@@ -1338,6 +1339,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_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: {} }),         d('nbrowser_whoami', 'Ask one profile to (re)resolve + report its Google email and re-announce it.', { profile: P }, { ok: 'bool', email: 'str', signedIn: 'bool', browser: 'str' }, 15, { example: {} }),         d('nbrowser_profile_block', 'Blocklist a profile so it is NEVER driven (persists).', { profile: 'required str' }, { ok: 'bool', blocklist: '[str]' }, 15, { example: { profile: 'edge:[email protected]' } }),@@ -1419,7 +1421,7 @@ function dispatch(verb, args) {         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.' }) });+      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 - wake it with nbrowser_wake_profile {profile:\'<key>\'} (opens NO window, foregrounds NOTHING), then it appears live here. NEVER launch a foreground browser window just to wake a profile. 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)) || '');@@ -1453,6 +1455,39 @@ function dispatch(verb, args) {       }, 2500);       return;     }+    if (verb === 'nbrowser_wake_profile') {+      const want = String((args && (args.profile || args.email)) || '');+      if (!want) return resolve({ success: false, error: 'profile required', _hint: 'Pass profile:"chrome:[email protected]" (or just the email, optionally browser:"edge"). nbrowser_profiles lists asleep profiles.' });+      const wantEmail = (want.includes(':') ? want.split(':').slice(1).join(':') : want).toLowerCase();+      const wantBrowser = want.includes(':') ? want.split(':')[0] : ((args && args.browser) || null);+      const findLive = () => { for (const [k, c] of conns) if (c.sock && !c.sock.destroyed && String(c.profile.email || '').toLowerCase() === wantEmail && (!wantBrowser || c.profile.browser === wantBrowser)) return k; return null; };+      const already = findLive();+      if (already) return resolve({ success: true, output: JSON.stringify({ ok: true, alreadyLive: true, profile: already, _hint: 'Profile is already connected - drive it now (open_window still opens in the BACKGROUND by default).' }) });+      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). Check the email/browser.' });+      if (isBlocked(kp.key)) return resolve({ success: false, error: `profile '${kp.key}' is blocklisted`, _hint: 'The user blocked this profile. nbrowser_profile_unblock first (with the user\'s consent).' });+      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` });+      // --no-startup-window loads the profile (and its extensions) in BACKGROUND mode: NO window is+      // 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'] });+      const t0 = Date.now();+      const poll = () => {+        const k = findLive();+        if (k) return resolve({ success: true, output: JSON.stringify({ ok: true, woke: true, profile: k, tookMs: Date.now() - t0, _hint: 'Profile is awake WITHOUT any window - nothing was foregrounded, no focus was stolen. Drive it normally now: nbrowser_open_window {sessionId, thread, purpose, profile} (which itself opens in the BACKGROUND, as always).' }) });+        _lastResolveNudge = 0; resolvePendingProfiles();+        if (Date.now() - t0 > 12000) return resolve({ success: true, output: JSON.stringify({ ok: false, woke: false, profile: kp.key, _hint: 'Launched the profile in background mode but its extension did not connect within 12s. Most likely the Adom extension is NOT installed/enabled in THAT profile (extensions are per-profile - see installing-the-extension). It may also just be slow: re-check nbrowser_profiles in a few seconds.' }) });+        setTimeout(poll, 700);+      };+      setTimeout(poll, 1200);+      return;+    }     if (verb === 'nbrowser_profile_block' || verb === 'nbrowser_profile_unblock') {       const want = String((args && (args.profile || args.email)) || '');       if (!want) return resolve({ success: false, error: 'profile (email) required' });
native-browser-bridge-manifest.json+4−4
@@ -1,8 +1,8 @@ {   "manifest_version": 1,-  "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,+  "version": "0.1.22",+  "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.22/native-browser-bridge-v0.1.22.bin",+  "sha256": "2122a36dc4a028a34eb3756d78101aae0c762780c67c8e6551114f6a23045366",+  "size": 75205,   "released_at": "2026-07-18T00:00:00Z" }
tools/verbs-policy.json+1−1
@@ -56,4 +56,4 @@   "screenshot_full_res": "readonly",   "eval": "mutating",   "evaluate": "alias"-}+}\ No newline at end of file