← Commit history
Update 5 file(s)
5 files changed
+95−11
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.1.25+0.1.26
bridge/bridge.json+3−2@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser (Chrome + Edge)",- "version": "0.1.25",+ "version": "0.1.26", "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",@@ -85,7 +85,8 @@ "nbrowser_whoami", "nbrowser_pin_session", "nbrowser_resolve_profiles",- "nbrowser_wake_profile"+ "nbrowser_wake_profile",+ "nbrowser_os_windows" ], "dependencies": { "node": ">=18"
bridge/package.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-native-browser-bridge",- "version": "0.1.25",+ "version": "0.1.26", "private": true, "description": "adom-desktop bridge for the Adom native-browser extension (Chrome + Edge)", "main": "server.js",
bridge/server.js+86−3@@ -1152,6 +1152,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_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:'] }, 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'] },@@ -1340,6 +1341,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_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: {} }), 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: {} }),@@ -1420,7 +1422,10 @@ function dispatch(verb, args) { 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 });+ const st = adomExtStateForProfile(kp.browser, kp.profileDir);+ const row = { 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 };+ if (st.checked) { row.extensionInstalled = !!st.installed; if (st.installed) row.extensionEnabled = !!st.enabled; if (!st.installed || !st.enabled) row.fix = extFixHint(kp.browser, kp.profileDir, st); }+ list.push(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. 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.' }) }); }@@ -1500,6 +1505,25 @@ function dispatch(verb, args) { })(); return; }+ // AUTHORITATIVE OS-level browser-window inventory, measured ON the laptop BY the bridge - so an AI+ // 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.+ if (verb === 'nbrowser_os_windows') {+ (async () => {+ let ws = []; let adOk = true;+ try {+ const r = await adCommand('desktop', 'desktop_list_windows', {});+ let out = r.body; if (out && typeof out.output === 'string') { try { out = JSON.parse(out.output); } catch {} }+ ws = ((out && out.data && out.data.windows) || (out && out.windows) || []);+ if (r.status === 0) adOk = false;+ } catch { adOk = false; }+ const bySess = {}; for (const [sid, h] of sessionHwnd) bySess[h] = sid;+ const browsers = ws.filter((w) => /Chrome_WidgetWin/.test(w.className || '')).map((w) => ({ hwnd: w.hwnd, title: w.title, rect: w.rect, z: w.z, kind: /for Testing/.test(w.title || '') ? 'pup/chrome-for-testing' : 'native-browser', abeSession: bySess[w.hwnd] || null }));+ resolve({ success: true, output: JSON.stringify({ ok: adOk, count: browsers.length, windows: browsers, totalOsWindows: ws.length, _hint: adOk ? 'Browser windows as the LAPTOP sees them right now (bridge-side enumeration - no --target guessing, DPI-safe). abeSession = a live ABE session owns that hwnd (NEVER force-close it; that thread cleans up its own). kind pup/chrome-for-testing = browser_open/pup litter. Edge windows appear even when Edge has no extension. To close an untracked one: nbrowser_force_close {hwnd} (two-step attestation). If your own desktop_list_windows returned 0/null: you probably targeted the WRONG desktop (multiple ADs connected - pass --target for the laptop) or AD hiccupped.' : 'AD link is DOWN from the bridge right now (desktop_list_windows unreachable) - window data unavailable, not "0 windows". Check nbrowser_activity for _ad_link entries; retry shortly.' }) });+ })();+ 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.' });@@ -1527,7 +1551,10 @@ function dispatch(verb, args) { 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.' }) });+ if (Date.now() - t0 > 12000) {+ const st = adomExtStateForProfile(kp.browser, kp.profileDir);+ return resolve({ success: true, output: JSON.stringify({ ok: false, woke: false, profile: kp.key, extensionState: st, _hint: `Launched the profile in background mode but its extension did not connect within 12s. DIAGNOSIS: ${extFixHint(kp.browser, kp.profileDir, st)}` }) });+ } setTimeout(poll, 700); }; setTimeout(poll, 1200);@@ -1707,10 +1734,23 @@ function dispatch(verb, args) { if (tgt && tgt.blocked) { return resolve({ success: false, error: `profile '${tgt.key}' is blocklisted`, _hint: 'The user blocked this profile from being driven. nbrowser_profile_unblock to allow it.' }); }+ // A named profile that exists locally deserves a DIAGNOSIS, not a mystery: is the extension not+ // installed there, installed-but-disabled, or just asleep? (Real gripe: an HD thread burned a+ // session flailing at Edge because every verb said only "not connected".)+ let diag = '';+ try {+ const want0 = args && (args.profile || args.profileEmail);+ if (want0) {+ const wantEmail0 = (String(want0).includes(':') ? String(want0).split(':').slice(1).join(':') : String(want0)).toLowerCase();+ const wb0 = String(want0).includes(':') ? String(want0).split(':')[0] : (args && args.browser) || null;+ const kp0 = knownProfiles().find((pr) => pr.email.toLowerCase() === wantEmail0 && (!wb0 || pr.browser === wb0));+ if (kp0) { const st0 = adomExtStateForProfile(kp0.browser, kp0.profileDir); diag = ` DIAGNOSIS for ${kp0.key} (${kp0.displayName}, ${kp0.profileDir}): ${extFixHint(kp0.browser, kp0.profileDir, st0)} If it IS installed+enabled, nbrowser_wake_profile {profile:"${kp0.key}"} wakes it with NO window and NO foreground.`; }+ }+ } catch {} return resolve({ success: false, error: 'native browser extension not connected',- _hint: 'No drivable profile is connected. A profile\'s extension DIES if that profile has NO OPEN WINDOWS (its service worker sleeps). Open a window in a profile where the Adom extension is loaded, then retry. nbrowser_profiles lists connected profiles; nbrowser_help for more.',+ _hint: 'No drivable profile is connected. A profile\'s extension DIES if that profile has NO OPEN WINDOWS (its service worker sleeps) - nbrowser_wake_profile wakes it without any window. nbrowser_profiles lists every profile (live + asleep) with per-profile extension state.' + diag, }); } const sock = tgt.conn.sock;@@ -2000,6 +2040,49 @@ function knownProfiles() { } return out; }+// DIAGNOSE the Adom extension's state inside a profile that is NOT connected, by reading what the+// browser itself recorded on disk (Preferences / Secure Preferences -> extensions.settings). This is+// what turns "native browser extension not connected" from a MYSTERY into an actionable answer: not+// installed at all vs installed-but-DISABLED vs enabled-but-not-dialing are three different fixes, and+// the AI deserves to know which one it is (real gripe: an HD thread flailed at Edge for a whole session).+function adomExtStateForProfile(browser, profileDir) {+ try {+ const local = process.env.LOCALAPPDATA;+ if (!local || !profileDir) return { checked: false };+ const root = browser === 'edge' ? path.join(local, 'Microsoft', 'Edge', 'User Data') : path.join(local, 'Google', 'Chrome', 'User Data');+ for (const f of ['Secure Preferences', 'Preferences']) {+ let j; try { j = JSON.parse(fs.readFileSync(path.join(root, profileDir, f), 'utf8')); } catch { continue; }+ const settings = (j.extensions && j.extensions.settings) || {};+ for (const [id, ent] of Object.entries(settings)) {+ const nm = String((ent && ent.manifest && ent.manifest.name) || '');+ if (!/adom/i.test(nm)) continue;+ return { checked: true, installed: true, id, name: nm, version: (ent.manifest && ent.manifest.version) || null, enabled: ent.state === 1, unpackedPath: ent.path || null };+ }+ }+ return { checked: true, installed: false };+ } catch { return { checked: false }; }+}+// The unpacked source dir of the extension, learned from ANY profile that has it - so a fix hint can+// say exactly what path to Load-unpacked into the broken profile.+function adomExtKnownPath() {+ for (const kp of knownProfiles()) {+ const st = adomExtStateForProfile(kp.browser, kp.profileDir);+ if (st.installed && st.unpackedPath) return st.unpackedPath;+ }+ return null;+}+// One actionable sentence for a profile whose extension is not connected. Used by wake_profile,+// not-connected refusals, and the asleep rows in nbrowser_profiles.+function extFixHint(browser, profileDir, st) {+ const scheme = browser === 'edge' ? 'edge://extensions' : 'chrome://extensions';+ if (!st || !st.checked) return 'Could not read the profile\'s extension inventory - check the profile exists (nbrowser_profiles).';+ if (!st.installed) {+ const src = adomExtKnownPath();+ return `The Adom extension is NOT INSTALLED in this ${browser} profile (${profileDir}) - extensions are per-profile. Fix: install it into THAT profile via the installing-the-extension skill (open ${scheme} in a window of that profile -> Developer mode -> Load unpacked${src ? ` -> "${src}" (the same source dir the working profile uses)` : ''}).`;+ }+ if (!st.enabled) return `The Adom extension IS installed in this ${browser} profile (${st.name} v${st.version}) but it is DISABLED. Fix: enable its toggle on ${scheme} in that profile (driving-the-desktop can do this in the background via UIA), or ask the user to flip it on.`;+ return `The Adom extension is installed AND enabled in this ${browser} profile (v${st.version}) but its service worker has not dialed the bridge. Fix: retry nbrowser_wake_profile (SWs can be slow), and if it still never connects, verify the native-messaging host manifest allows THIS browser's extension id (nbrowser_add_extension_origin {id:"${st.id}"}).`;+} // On startup, undo any STALE "Adom is driving" badge a PRIOR bridge left behind (bridges are KILLED, not // gracefully stopped, so in-memory idle timers can't clear badges across a redeploy). We must NOT blank the // overlay slot — that evicts Chrome's OWN profile avatar (the destructive regression we had). Instead, for
native-browser-bridge-manifest.json+4−4@@ -1,8 +1,8 @@ { "manifest_version": 1,- "version": "0.1.25",- "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.25/native-browser-bridge-v0.1.25.bin",- "sha256": "90e8bede041fb7a927591e5077be6a5daeeeeac6b9869cc29118fc43e374f33f",- "size": 77319,+ "version": "0.1.26",+ "url": "https://wiki.adom.inc/download/adom/adom-browser-extension/0.1.26/native-browser-bridge-v0.1.26.bin",+ "sha256": "ae62e3384da36e630e90f771033618dbbcea4101dc3b8f0ddbb15ab66a78d055",+ "size": 79784, "released_at": "2026-07-18T00:00:00Z" }