← Commit history
Update 6 file(s)
6 files changed
+46−10
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.21+0.1.22
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"-}+}