← Commit history
bridge/BRIDGE_VERSION+1−1
@@ -1 +1 @@-0.1.25\ No newline at end of file+0.1.26\ 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.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" }