← Commit history
v0.16.28 (insiders): screenshots reply ab's delivery contract (nb-bridge#49): the PNG is written on the desktop and the reply carries fullPath, plus a 1568px safePath copy when the image is wider, so ab streams it to the caller instead of sending base64 through the relay; inline:true keeps base64
7 files changed
+51−14
bin/nb-dashboard+1−1@@ -20,7 +20,7 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.16.27'; // keep in lockstep with package.json (pkg version)+const VERSION = '0.16.28'; // keep in lockstep with package.json (pkg version) // SEND TO AI IS UN-PARKED (John: "yeah, we are re-enabling the send to ai. it seems to be good now"). // The ah build thread certified the crash family dead on the gate-fixed build and the delivery leg is // proven end to end. The park const is DELETED rather than flipped, so no dead branch can resurrect it.
bridge/BRIDGE_VERSION+1−1@@ -1 +1 @@-0.16.27+0.16.28
bridge/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "native-browser", "displayName": "Adom Native Browser",- "version": "0.16.27",+ "version": "0.16.28", "expectedExtensionVersion": "0.2.15", "description": "Drives the user's REAL signed-in Chrome/Edge, in two layers. Layer 1 (nb) needs NOTHING installed and works on any machine: nbrowser_open_os_window (open a URL in the user's real profile, background+flashing, the OAuth/consent path), nbrowser_os_windows, nbrowser_profiles, nbrowser_wake_profile, nbrowser_force_close. Layer 2 (nbe, the Adom Native Browser Extension) unlocks the rest: CDP driving (navigate/click/read/screenshot) inside their logged-in profile. The bridge binds an ephemeral loopback port and the native-messaging host pumps stdio <-> bridge.", "homepage": "https://wiki.adom.inc/adom/nb-bridge",
bridge/package.json+1−1@@ -1,6 +1,6 @@ { "name": "adom-native-browser-bridge",- "version": "0.16.27",+ "version": "0.16.28", "private": true, "description": "Native-browser bridge for Bridge (ab): layer 1 (nb) plus layer 2 via nbe, the Adom Native Browser Extension (Chrome + Edge).", "main": "server.js",
bridge/server.js+43−7@@ -385,7 +385,7 @@ const HELP = { pitfalls: [ 'The extension lives in ONE browser PROFILE and DIES if that profile has no open windows - if you get "not connected", ask the user to open a window in that profile (or open one yourself once reconnected). Never close your LAST window in that profile.', 'Multiple Bridges on the relay → pass `--target <name>` (e.g. AdomLapper) or you get ambiguous_target.',- 'Screenshots are large base64 and can TIME OUT over the relay - retry, or prefer ab desktop_screenshot_window for proof shots.',+ 'Screenshots reply fullPath (+ safePath for a wide image), never inline base64: ab pulls the file to your machine and adds the local path. Pass inline:true only if you truly need base64 in the reply.', 'nbrowser_eval runs in the PAGE world: chrome.* APIs are NOT available there (only DOM/window).', 'ALWAYS clean up windows you open (nbrowser_close_window) - abandoned windows annoy the user.', 'Poll nbrowser_events to learn if the USER closed your windows/tabs (window_closed {byUser:true}), or if a foregrounding attempt was auto-reverted (focus_violation {verb, autoReverted:true}) - a focus_violation means you tried to shove a window in front of the user; stop foregrounding.',@@ -402,7 +402,7 @@ const HELP = { verbs: { 'open_window/open_tab/close_window/close_tab/switch_window/switch_tab/list_windows/list_tabs/rescan': 'Sessions = a real Chrome window (durable across reloads). tabId = real chrome tab id. close what you open.', 'navigate/reload/back/forward/wait': 'wait{ms}. navigate/reload await load-complete.',- 'screenshot/screenshot_full_res/eval': 'screenshot resized <=1568 (base64); _full_res unresized. eval runs in page world.',+ 'screenshot/screenshot_full_res/eval': 'screenshot resized <=1568, _full_res unresized; both reply fullPath (+ safePath), pulled to you by ab. eval runs in page world.', 'click/double_click/right_click/hover/type/press_key/input_dispatch/cursor': 'isTrusted (CDP). selector uses modal-aware scoring. cursor:true + intent for the narrated Adom cursor. hover reveals tooltips.', 'fetch_url/download': 'fetch_url uses the user\'s REAL cookies (authed). download via chrome.downloads.', 'errors/events': 'errors = console/page errors for a tab. events = window/tab closes incl. byUser.',@@ -1749,8 +1749,8 @@ const VERB_META = { nbrowser_wait: { hint: 'Sleep server-side (let a page settle) before the next verb.', related: ['nbrowser_wait_for', 'nbrowser_screenshot'], pitfalls: ['prefer nbrowser_wait_for for a DETERMINISTIC wait (selector/url/text) instead of a fixed sleep'] }, nbrowser_wait_for: { hint: 'DETERMINISTIC wait after a click/nav: block until a CSS selector exists / the URL contains a substring / the visible text contains a substring, or timeoutMs. Use this instead of nbrowser_wait + fixed sleeps + coordinate races. Returns {matched, by, url, waitedMs}.', related: ['nbrowser_click', 'nbrowser_navigate', 'nbrowser_eval'], pitfalls: ['pass at least one of selector / urlIncludes / textIncludes', 'matched:false + timedOut:true means the condition never appeared - screenshot to see the real page', 'selector matches the top document only (not inside cross-origin iframes)'] }, nbrowser_keepalive: { hint: 'Keep a session ACTIVE to avoid vendor idle sign-outs (Gusto/Intuit ~10-15 min): replays a benign mousemove/focus on an interval (NO navigation, no side-effects). Per-session; auto-stops when the window closes. Call with {stop:true} to cancel. Set at the START of a long unattended automation.', related: ['nbrowser_open_window', 'nbrowser_login_state'], pitfalls: ['client-side activity only - a vendor that expires the SERVER session regardless may still sign out; combine with periodic real actions if so', 'default interval 4min (30s..15min); pick below the vendor idle timeout'] },- nbrowser_screenshot: { hint: 'CDP screenshot of the PAGE - captures a BACKGROUND/occluded window WITHOUT foregrounding it. Your DEFAULT way to "see" a page. It captures page CONTENT only - NOT the browser chrome (URL/tabs) and NOT native popups/dialogs (Chrome password dropdown, print dialog, alert/confirm) which live OUTSIDE the page.', related: ['nbrowser_eval', 'nbrowser_click', 'nbrowser_handle_dialog'], pitfalls: ['resized <=1568 for the model; pass toFile:true to save full-res to a laptop file (pull_file it)', 'to see the WINDOW FRAME or a native popup/dropdown/dialog, use ab desktop_screenshot_window {hwnd | titleContains} (WGC): it captures the window PLUS its OWNED CHILD windows (returns a parent/child hwnd array, so you see popups like the password dropdown), and it is background-safe (no foregrounding)', '⛔ NEVER take a fullscreen / whole-monitor screenshot to inspect a browser window - it grabs the user\'s other work, is imprecise, and often implies foregrounding. Page → nbrowser_screenshot; window/chrome/popup → desktop_screenshot_window'] },- nbrowser_screenshot_full_res: { hint: 'Full-resolution, full-PAGE screenshot via CDP (the entire scrollable page, not just the viewport). Still PAGE content only.', related: ['nbrowser_screenshot'], pitfalls: ['this is full-PAGE, NOT "fullscreen monitor" - it never captures the desktop or the user\'s other windows', 'large base64 can be slow over the relay - prefer toFile then pull_file', 'for the window frame or a native dialog/popup, use ab desktop_screenshot_window {hwnd | titleContains} (parent/child hwnds), never a monitor capture'] },+ nbrowser_screenshot: { hint: 'CDP screenshot of the PAGE - captures a BACKGROUND/occluded window WITHOUT foregrounding it. Your DEFAULT way to "see" a page. It captures page CONTENT only - NOT the browser chrome (URL/tabs) and NOT native popups/dialogs (Chrome password dropdown, print dialog, alert/confirm) which live OUTSIDE the page.', related: ['nbrowser_eval', 'nbrowser_click', 'nbrowser_handle_dialog'], pitfalls: ['resized <=1568 for the model; the reply is fullPath (a desktop PNG that ab pulls to your machine, adding the local path), not base64. toFile:"<abs path>" picks where it is written; inline:true returns base64 instead', 'to see the WINDOW FRAME or a native popup/dropdown/dialog, use ab desktop_screenshot_window {hwnd | titleContains} (WGC): it captures the window PLUS its OWNED CHILD windows (returns a parent/child hwnd array, so you see popups like the password dropdown), and it is background-safe (no foregrounding)', '⛔ NEVER take a fullscreen / whole-monitor screenshot to inspect a browser window - it grabs the user\'s other work, is imprecise, and often implies foregrounding. Page → nbrowser_screenshot; window/chrome/popup → desktop_screenshot_window'] },+ nbrowser_screenshot_full_res: { hint: 'Full-resolution, full-PAGE screenshot via CDP (the entire scrollable page, not just the viewport). Still PAGE content only.', related: ['nbrowser_screenshot'], pitfalls: ['this is full-PAGE, NOT "fullscreen monitor" - it never captures the desktop or the user\'s other windows', 'replies fullPath (full size) + safePath (a <=1568px copy to read); ab pulls both to your machine, no base64 over the relay', 'for the window frame or a native dialog/popup, use ab desktop_screenshot_window {hwnd | titleContains} (parent/child hwnds), never a monitor capture'] }, nbrowser_eval: { hint: 'Run JS in the page and get the result. Powerful - use read-only expressions to inspect state.', related: ['nbrowser_screenshot'], pitfalls: ['runs arbitrary JS on the user\'s LIVE session - never run destructive scripts'] }, nbrowser_evaluate: { hint: 'Alias of nbrowser_eval.', related: ['nbrowser_eval'], pitfalls: ['runs arbitrary JS on the user\'s LIVE session - never run destructive scripts'] }, nbrowser_input_dispatch: { hint: 'Low-level isTrusted CDP input. type: click | dblclick | rightclick | move | hover | wheel | type | key. An unknown type is REFUSED (unknown_input_type), it never becomes a click (from ext 0.2.10). wheel takes deltaX/deltaY at {x,y} (or selector/text) and is what a canvas camera zoom needs.', related: ['nbrowser_click', 'nbrowser_scroll', 'nbrowser_type'], pitfalls: ['prefer nbrowser_click (smart selector) over raw xy', 'the stale_coordinates freshness gate applies to CLICKS only: hover/move/wheel accept raw {x,y} without atScrollX/Y (ext 0.2.10)', 'there is no "mouseWheel" type; say wheel (mousewheel and scroll are accepted aliases)'] },@@ -2066,8 +2066,8 @@ function dispatch(verb, args) { d('nbrowser_wait', 'Sleep server-side for ms (or wait on the tab).', { sessionId: S, ms: 'optional int' }, { ok: 'bool' }, 125, { example: { ms: 1000 } }), d('nbrowser_wait_for', 'DETERMINISTIC wait: block until a selector exists / URL contains / visible text contains, or timeoutMs. Use instead of fixed sleeps after a click/nav.', { sessionId: S, selector: 'optional CSS selector', urlIncludes: 'optional url substring', textIncludes: 'optional visible-text substring', timeoutMs: 'optional int (default 15000, max 120000)' }, { ok: 'bool', matched: 'bool', by: '"selector"|"url"|"text"', url: 'str', waitedMs: 'int', timedOut: 'bool?' }, 125, { example: { selector: '#dashboard' } }), d('nbrowser_keepalive', 'Keep a session active (benign mousemove/focus on an interval) to avoid vendor idle sign-outs. Per-session; auto-stops on window close; {stop:true} to cancel.', { sessionId: S, intervalSec: 'optional int (30..900, default 240)', stop: 'optional bool' }, { ok: 'bool', keepalive: '"on"|"off"', intervalSec: 'int' }, 15, { example: { intervalSec: 240 } }),- d('nbrowser_screenshot', 'Screenshot the tab via CDP (captures a BACKGROUND/occluded window; resized <=1568).', { profile: P, sessionId: S, tabId: 'optional str', maxWidth: 'optional int' }, { ok: 'bool', base64: 'str', filePath: 'str?' }, 120, { example: { sessionId: 'work', maxWidth: 1500 } }),- d('nbrowser_screenshot_full_res', 'Full-resolution / full-page screenshot via CDP.', { profile: P, sessionId: S, tabId: 'optional str', fullPage: 'optional bool' }, { ok: 'bool', base64: 'str' }, 120, { example: { sessionId: 'work', fullPage: true } }),+ d('nbrowser_screenshot', 'Screenshot the tab via CDP (captures a BACKGROUND/occluded window; resized <=1568).', { profile: P, sessionId: S, tabId: 'optional str', maxWidth: 'optional int' }, { ok: 'bool', fullPath: 'str (desktop PNG; ab pulls it to the caller)', safePath: 'str? (<=1568px copy, only when the image is wider)', width: 'int', height: 'int' }, 120, { example: { sessionId: 'work', maxWidth: 1500 } }),+ d('nbrowser_screenshot_full_res', 'Full-resolution / full-page screenshot via CDP.', { profile: P, sessionId: S, tabId: 'optional str', fullPage: 'optional bool' }, { ok: 'bool', fullPath: 'str (desktop PNG; ab pulls it to the caller)', safePath: 'str? (<=1568px copy for a model to read)', width: 'int', height: 'int' }, 120, { example: { sessionId: 'work', fullPage: true } }), d('nbrowser_eval', 'Run JS in the page context and return the result.', { sessionId: S, tabId: 'optional str', expression: 'required str (JS)' }, { ok: 'bool', result: 'any' }, 45, { example: { sessionId: 'work', expression: 'document.title' } }), d('nbrowser_evaluate', 'Alias of nbrowser_eval.', { sessionId: S, tabId: 'optional str', expression: 'required str (JS)' }, { ok: 'bool', result: 'any' }, 45, { example: { sessionId: 'work', expression: '1+1' } }), d('nbrowser_input_dispatch', 'Low-level isTrusted CDP input. Unknown type is refused (unknown_input_type), never a click.', { sessionId: S, tabId: 'optional str', type: 'required "click"|"dblclick"|"rightclick"|"move"|"hover"|"wheel"|"type"|"key"', x: 'int?', y: 'int?', selector: 'str?', text: 'str? (visible text to resolve, or the text to type for type:"type")', key: 'str?', deltaX: 'int? (wheel)', deltaY: 'int? (wheel; -300 = zoom in / scroll up)', steps: 'int? (wheel, split the delta)', atScrollX: 'int? (clicks by raw xy only)', atScrollY: 'int?' }, { ok: 'bool', dispatched: 'str', errorCode: 'str? (unknown_input_type | stale_coordinates | ambiguous_target)' }, 120, { example: { sessionId: 'work', type: 'wheel', x: 400, y: 300, deltaY: -300 } }),@@ -2964,6 +2964,18 @@ function dispatch(verb, args) { // hearing it: file spill for screenshots/recordings, finder spill, taskbar/badge bookkeeping, the // activity row, auto-flash, visible-open announcements. Best-effort by contract: the caller in // onHostFrameInner wraps this in try/catch and ALWAYS resolves the reply (2026-09-03).+const SAFE_MAX = 1568; // the model-vision width ab's own screenshots use (safeWidth <= 1568)+function downscalePng(src, dst, max) {+ return new Promise((resolve) => {+ if (process.platform !== 'win32') return resolve(null);+ const q = (x) => "'" + String(x).replace(/'/g, "''") + "'";+ const ps = `Add-Type -AssemblyName System.Drawing; $i = [System.Drawing.Image]::FromFile(${q(src)}); $w = [int]${max}; $h = [int][Math]::Round($i.Height * $w / $i.Width); $b = New-Object System.Drawing.Bitmap $w, $h; $g = [System.Drawing.Graphics]::FromImage($b); $g.InterpolationMode = 'HighQualityBicubic'; $g.DrawImage($i, 0, 0, $w, $h); $b.Save(${q(dst)}, [System.Drawing.Imaging.ImageFormat]::Png); $g.Dispose(); $b.Dispose(); $i.Dispose(); "$w $h"`;+ execFile('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', ps], { timeout: 20000, windowsHide: true }, (err, out) => {+ const m = String(out || '').trim().match(/(\d+) (\d+)$/);+ resolve(!err && m && fs.existsSync(dst) ? { w: Number(m[1]), h: Number(m[2]) } : null);+ });+ });+} async function decorateReply(p, frame, payloadIn, setPayload) { let payload = payloadIn; // Screenshot file-save: huge base64 over the relay times out, so when the caller passes@@ -2971,7 +2983,31 @@ async function decorateReply(p, frame, payloadIn, setPayload) { // base64. Pull it with `adom-bridge pull_file` (binary chunked) - fast + no relay timeout. const isShot = /screenshot/.test(p.verb); const isRec = p.verb === 'nbrowser_record_stop';- if (payload.base64 && ((isShot && p.toFile) || isRec)) {+ // SCREENSHOTS -> ab's delivery contract (nb-bridge#49): write the PNG on the desktop and reply+ // fullPath (+ safePath when a <=1568px copy is needed). ab streams both to the CALLER's machine and+ // adds the local paths, so an AI in a container reads the image with no second call and no base64+ // through the relay. inline:true keeps the old base64 reply for a caller that truly wants it.+ if (payload.base64 && isShot && !p._internal && !(p.args && p.args.inline === true)) {+ try {+ const want = (typeof p.toFile === 'string' && /^[a-zA-Z]:[\\/]|^\\\\|^\//.test(p.toFile)) ? path.normalize(p.toFile) : null;+ const dir = want ? path.dirname(want) : path.join(process.env.TEMP || os.tmpdir(), 'adom-nbrowser-shots');+ fs.mkdirSync(dir, { recursive: true });+ const stem = `shot-${Date.now()}-${Math.floor(Math.random() * 1e4)}`;+ const fullPath = want || path.join(dir, `${stem}.png`);+ const bytes = Buffer.from(payload.base64, 'base64');+ fs.writeFileSync(fullPath, bytes);+ const w = Number(payload.width) || 0, h = Number(payload.height) || 0;+ const out = { ...payload, fullPath, fullWidth: w || undefined, fullHeight: h || undefined, fullBytes: bytes.length };+ delete out.base64; delete out.filePath;+ if (w > SAFE_MAX) {+ const safePath = fullPath.replace(/\.png$/i, '') + '.safe.png';+ const dims = await downscalePng(fullPath, safePath, SAFE_MAX);+ if (dims) Object.assign(out, { safePath, safeWidth: dims.w, safeHeight: dims.h, resizeMax: SAFE_MAX });+ } else if (w) Object.assign(out, { safeWidth: w, safeHeight: h, resizeMax: SAFE_MAX });+ payload = out;+ } catch (e) { payload._fileError = String((e && e.message) || e); }+ }+ if (payload.base64 && isRec) { try { const sub = isRec ? 'adom-nbrowser-recordings' : 'adom-nbrowser-shots'; // nb-bridge#41 item 3: toFile:"<absolute path>" is HONOURED (the caller's path, parent dirs created);
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "nb-bridge",- "version": "0.16.27",+ "version": "0.16.28", "type": "app", "title": "NB - Native Browser Bridge", "description": "This package installs the SKILLS into your container so your AI knows how to drive your real signed-in Chrome and Edge; Bridge loads the bridge runtime itself from the release zip.",
tools/verb-battery.mjs+3−2@@ -125,8 +125,9 @@ const T = { // reading + JS eval: () => { const r = evalIn('document.getElementById("page-title").textContent'); return A(String(r.result).includes('battery harness'), 'read page title', JSON.stringify(r).slice(0, 140)); }, evaluate: () => { const r = call('nbrowser_evaluate', { sessionId: SID, expression: '1+41' }); return A(String(r.result) === '42', 'alias works', JSON.stringify(r).slice(0, 120)); },- screenshot: () => { const r = call('nbrowser_screenshot', { sessionId: SID }); return A(r.ok === true && (r.width || 0) > 100, `${r.width}x${r.height}`, JSON.stringify(r).slice(0, 140)); },- screenshot_full_res: () => { const r = call('nbrowser_screenshot_full_res', { sessionId: SID, toFile: true }); return A(r.ok === true && !!r.path, `saved ${String(r.path).slice(-40)}`, JSON.stringify(r).slice(0, 140)); },+ // nb-bridge#49: screenshots reply ab's delivery contract (fullPath, safePath when wide), never inline base64.+ screenshot: () => { const r = call('nbrowser_screenshot', { sessionId: SID }); return A(r.ok === true && (r.width || 0) > 100 && !!r.fullPath && !r.base64, `${r.width}x${r.height} as fullPath${r.localPath || r.localFullPath ? ', pulled here' : ''}`, JSON.stringify(r).slice(0, 200)); },+ screenshot_full_res: () => { const r = call('nbrowser_screenshot_full_res', { sessionId: SID }); const wide = (r.fullWidth || r.width || 0) > 1568; return A(r.ok === true && !!r.fullPath && !r.base64 && (!wide || !!r.safePath), `${r.fullWidth || r.width}px as fullPath${r.safePath ? ' + safePath ' + r.safeWidth + 'px' : ''}`, JSON.stringify(r).slice(0, 200)); }, errors: () => { call('nbrowser_click', { sessionId: SID, selector: '#err-btn' }); call('nbrowser_wait', { sessionId: SID, ms: 500 }); const r = call('nbrowser_errors', { sessionId: SID }); return A(JSON.stringify(r).includes('battery-'), 'console error captured', JSON.stringify(r).slice(0, 160)); }, fetch_url: () => { const r = call('nbrowser_fetch_url', { sessionId: SID, url: 'https://wiki.adom.inc/blob/app/nb-bridge/demo/sample-download.txt' }); return A(JSON.stringify(r).includes('MARKER-7f3a9'), 'fetched sample content', JSON.stringify(r).slice(0, 140)); },