app
Pup - the AI's own browser
Public Made by Adomby adom
pup gives your AI a real browser on your desktop that it drives completely, in the background, signed in as you: windows that never jump in front of your work, one shared profile that learns your logins, and every window labeled on the taskbar with its AI thread's name.
← Commit history
BREAKING clean break: renamed the entire verb family browser_* to pup_* with NO aliases. All 62 verbs move one-for-one (browser_open_window becomes pup_open_window, browser_screenshot becomes pup_screenshot, etc.); the old browser_* names are removed and now error by design so every caller gets migrated to pup_*. Also rebranded Adom Desktop to Bridge across the docs. Part of the adom/pup-bridge rename.
6 files changed
+485−485
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.9.350",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.9.350/adom-bridge-puppeteer-v1.9.350.zip",- "sha256": "3cff610e93b706d1dc20ddc10e0bf712a05f7466b4014f49488506e2c4f17db9",- "size": 637658,+ "version": "2.0.0",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/2.0.0/adom-bridge-puppeteer-v2.0.0.zip",+ "sha256": "37fbb68f28a2fd6ff6d18d4ded650e280e205d370705cd0ece7e68cd052f869f",+ "size": 637484, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.9.350+2.0.0
src/bridge.json+47−47@@ -2,8 +2,8 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.9.350",- "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. browser_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. browser_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, browser_alert_window posts a native toast whose click focuses the window, browser_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.",+ "version": "2.0.0",+ "description": "Drives a browser ALREADY on the machine \u2014 installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) \u2014 launched with a FRESH isolated profile, so the common case has NO ~150 MB Chrome-for-Testing download and it never touches the user's real tabs/logins. Binds its local server to LOOPBACK (127.0.0.1) only, so no Windows Firewall 'allow Node through the firewall' prompt ever pops. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. SPAWN-VERIFIED fallback: pup tries each detected browser (Chrome \u2192 Edge \u2192 CfT) and actually launches it, falling through to the next if one won't start (and handling browsers like Edge that relaunch onto another PID), so a corrupt/partial install never hard-fails an open. The browser that launches is cached as the default (persisted across restarts) so it isn't re-probed each time. On spawn the bridge SELF-WARMS: detects browsers + disk, and only if the box has no Chrome/Edge and no cached CfT does it background-fetch CfT so the first open is instant. pup_use pins a specific browser (chrome | edge | cft | auto); {browser:'chrome',install:true} downloads & installs REAL Google Chrome (user-scoped, no admin) on an Edge-only box. pup_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the default + free disk. Every failure returns a specific errorCode + _hint (chrome_install_no_disk, chrome_download_network, chrome_install_permission, chrome_stable_not_detected, \u2026). Launches with FULL browser permissions by default for full debug (strictPermissions:true opts out). Headful automation: open/close windows+tabs, navigate, screenshot, eval JS, record. pup is a GENERIC, non-profile browser only \u2014 every session is a fresh isolated Chrome/Edge process, never the user's real signed-in browser; profile defaults to sessionId. To drive the user's REAL Chrome/Edge (their profile, cookies, SSO), use the Adom browser EXTENSION's nbrowser_* verbs \u2014 a separate bridge, not pup's job. macOS (v1.9.74+): full parity with mac-native mechanics \u2014 background-by-default is the app-level hide (windows stay drivable/screenshottable while hidden), raise/lower map to unhide+activate/hide, pup_alert_window posts a native toast whose click focuses the window, pup_use {browser:'chrome',install:true} installs Chrome from the official dmg, and taskbar identity/badging remains Windows-only.", "homepage": "https://wiki.adom.inc/adom/adom-desktop", "author": "Adom Inc.", "license": "MIT",@@ -16,61 +16,61 @@ "killImageName": "node.exe" }, "verbPrefixes": [- "browser_",+ "pup_", "desktop_recorder_", "desktop_record_" ], "verbs": [- "browser_open_window",- "browser_open_tab",- "browser_close_tab",- "browser_close_window",- "browser_navigate",- "browser_back",- "browser_forward",- "browser_reload",- "browser_click",- "browser_type",- "browser_press_key",- "browser_screenshot",- "browser_evaluate",- "browser_fetch_url",- "browser_input_dispatch",- "browser_readiness",- "browser_prewarm",- "browser_use",- "browser_record_start",- "browser_record_stop",- "browser_record_status",- "browser_record_list",+ "pup_open_window",+ "pup_open_tab",+ "pup_close_tab",+ "pup_close_window",+ "pup_navigate",+ "pup_back",+ "pup_forward",+ "pup_reload",+ "pup_click",+ "pup_type",+ "pup_press_key",+ "pup_screenshot",+ "pup_evaluate",+ "pup_fetch_url",+ "pup_input_dispatch",+ "pup_readiness",+ "pup_prewarm",+ "pup_use",+ "pup_record_start",+ "pup_record_stop",+ "pup_record_status",+ "pup_record_list", "desktop_recorder_open", "desktop_record_start", "desktop_record_stop",- "browser_describe",- "browser_lower_os_window",- "browser_raise_os_window",- "browser_focus_window",- "browser_alert_window",- "browser_status",- "browser_list_windows",- "browser_switch_window",- "browser_list_tabs",- "browser_switch_tab",- "browser_screenshot_full_res",- "browser_errors",- "browser_wait",- "browser_rescan",- "browser_close",- "browser_eval",+ "pup_describe",+ "pup_lower_os_window",+ "pup_raise_os_window",+ "pup_focus_window",+ "pup_alert_window",+ "pup_status",+ "pup_list_windows",+ "pup_switch_window",+ "pup_list_tabs",+ "pup_switch_tab",+ "pup_screenshot_full_res",+ "pup_errors",+ "pup_wait",+ "pup_rescan",+ "pup_close",+ "pup_eval", "desktop_record_status", "desktop_record_list", "desktop_recorder_close",- "browser_scroll",- "browser_set_viewport",- "browser_screenshot_element",- "browser_render_html"+ "pup_scroll",+ "pup_set_viewport",+ "pup_screenshot_element",+ "pup_render_html" ],- "statusVerb": "browser_readiness",+ "statusVerb": "pup_readiness", "updateManifestUrl": "https://wiki.adom.inc/api/v1/pages/adom-desktop-puppeteer-bridge/files/adom-bridge-puppeteer-manifest.json", "dependencies": { "node": ">=20",@@ -97,7 +97,7 @@ "macos": { "supported": true, "since": "1.9.74",- "notes": "macOS is supported and shipping. Chrome for Testing path resolution, the .dmg Chrome install path, and process teardown are implemented for darwin. Windows-only surfaces (taskbar buttons/badges, jump lists, window-identity stamping) are self-gated and simply inert on macOS; browser_alert_window uses a toast-and-unhide equivalent there. Verbs report platform + taskbarSupported so a caller branches on those, never on prose."+ "notes": "macOS is supported and shipping. Chrome for Testing path resolution, the .dmg Chrome install path, and process teardown are implemented for darwin. Windows-only surfaces (taskbar buttons/badges, jump lists, window-identity stamping) are self-gated and simply inert on macOS; pup_alert_window uses a toast-and-unhide equivalent there. Verbs report platform + taskbarSupported so a caller branches on those, never on prose." }, "linux": { "supported": false,
src/chrome.js+16−16@@ -20,7 +20,7 @@ // - ensureChromeReady(): the gate the bridge calls before opening a window — returns // immediately with {state:'installing'} on a cold box (non-blocking) so the first // call answers with a pollable status instead of hanging 2–3 min.-// - readiness(): the snapshot powering browser_readiness + the /status led/summary/tooltip.+// - readiness(): the snapshot powering pup_readiness + the /status led/summary/tooltip. // // Everything here is self-contained in the bridge (no adom-desktop core changes). @@ -36,7 +36,7 @@ const _puppeteerLoadError = puppeteer ? null : 'puppeteer module failed to requi // ── install-state singleton ────────────────────────────────────────────────── // Shared across every command so concurrent open calls don't double-install and-// browser_readiness / the status chip can report live progress.+// pup_readiness / the status chip can report live progress. const state = { phase: 'unknown', // 'unknown' | 'detecting' | 'installing' | 'ready' | 'failed' buildId: null,@@ -152,7 +152,7 @@ function detectSystemBrowser() { // So the bridge doesn't re-probe/re-verify on every open. Two ways it gets set: // • markVerified(): after a launch actually SUCCEEDS, we cache that exact browser // (forced:false) — the auto default. Cheap re-use next time.-// • setDefaultBrowser(): the AI PINS a browser via browser_use (forced:true) — we+// • setDefaultBrowser(): the AI PINS a browser via pup_use (forced:true) — we // honor it strictly (but still fall back if it can't spawn, so it stays airtight). // Stored as JSON next to the CfT cache so it survives bridge restarts. function defaultFile() { return path.join(cacheDir(), 'pup-browser-default.json'); }@@ -166,7 +166,7 @@ function writeDefault(obj) { function clearDefault() { try { fs.unlinkSync(defaultFile()); } catch {} } // Cache the browser that just launched successfully as the auto-default. Never-// overrides an explicit (forced) choice the AI made via browser_use.+// overrides an explicit (forced) choice the AI made via pup_use. function markVerified(cand) { if (!cand || !cand.executablePath) return; const d = readDefault();@@ -189,7 +189,7 @@ function launchCandidates() { // auto-updated build with NO 150MB download and NO fleet dep-bump to move it, and every taskbar // icon/identity override works on it identically (OS-window level, browser-agnostic). // Chrome-for-Testing is the GENUINE LAST RESORT now — a box with no installed Chromium at all- // (e.g. a Mac with no Chrome). browser_use {browser:"cft"} still pins CfT explicitly for the one+ // (e.g. a Mac with no Chrome). pup_use {browser:"cft"} still pins CfT explicitly for the one // case it earns its weight: a managed/enterprise box whose policies interfere with automation. const all = []; all.push(...listSystemBrowsers()); // installed Chrome, then Edge — FIRST (zero download, already isolated)@@ -227,14 +227,14 @@ function resolveBrowser() { }; } -// browser_use: set (or clear) the browser pup drives by default.+// pup_use: set (or clear) the browser pup drives by default. // browser: 'auto' | 'chrome' | 'edge' | 'cft' | 'chrome-for-testing' (or pass executablePath) // 'cft' installs Chrome for Testing in the background if it isn't cached, and pins it // as the default so once it lands every open uses it. Returns a pickSummary. function setDefaultBrowser({ browser, executablePath, install, elevate } = {}) { const b = String(browser || '').toLowerCase().replace(/[\s_-]/g, ''); if (executablePath) {- if (!fileExists(executablePath)) return { ok: false, errorCode: 'browser_not_found', error: `No executable at ${executablePath}.`, ...pickSummary() };+ if (!fileExists(executablePath)) return { ok: false, errorCode: 'pup_not_found', error: `No executable at ${executablePath}.`, ...pickSummary() }; writeDefault({ kind: 'custom', executablePath, source: 'explicit', forced: true, verifiedAt: Date.now() }); return { ok: true, ...pickSummary() }; }@@ -251,12 +251,12 @@ function setDefaultBrowser({ browser, executablePath, install, elevate } = {}) { // Not installed. For Chrome we can download+install the REAL browser on request. if (b === 'chrome' && install) { writeDefault({ kind: 'chrome', executablePath: null, source: 'system', forced: true, pendingInstall: true, verifiedAt: Date.now() });- installChromeStable({ elevate }); // background; browser_use polling + readiness track it+ installChromeStable({ elevate }); // background; pup_use polling + readiness track it return { ok: true, installing: true, installingChrome: true, _hint: process.platform === 'darwin'- ? "Installing Google Chrome in the background (official dmg → Applications; silent, no prompt). Poll browser_readiness: chromeStablePhase goes installing → ready. pup keeps driving its current browser meanwhile, so nothing is blocked."- : "Installing Google Chrome in the background. On a normal PC this is silent with NO prompt. On a locked-down box (a VM, managed device) it needs a Windows approval — the bridge will pop a native notification asking the user to click YES on the UAC, and if it expires it re-notifies with a one-tap 'Approve now'. Poll browser_readiness: chromeStablePhase goes installing → awaiting_uac (waiting on the user's click) → ready. pup drives Edge meanwhile, so nothing is blocked.",+ ? "Installing Google Chrome in the background (official dmg → Applications; silent, no prompt). Poll pup_readiness: chromeStablePhase goes installing → ready. pup keeps driving its current browser meanwhile, so nothing is blocked."+ : "Installing Google Chrome in the background. On a normal PC this is silent with NO prompt. On a locked-down box (a VM, managed device) it needs a Windows approval — the bridge will pop a native notification asking the user to click YES on the UAC, and if it expires it re-notifies with a one-tap 'Approve now'. Poll pup_readiness: chromeStablePhase goes installing → awaiting_uac (waiting on the user's click) → ready. pup drives Edge meanwhile, so nothing is blocked.", ...pickSummary(), }; }@@ -264,7 +264,7 @@ function setDefaultBrowser({ browser, executablePath, install, elevate } = {}) { const extra = b === 'chrome' ? " Pass {browser:'chrome', install:true} to download & install Google Chrome (~90 MB, user-scoped — silent on a normal PC; on a locked-down box it prompts the user for a Windows approval via a notification), or use browser:'cft' / browser:'edge'." : " Edge ships with every Windows PC — if it's genuinely missing, use browser:'chrome' (install:true) or browser:'cft'.";- return { ok: false, errorCode: 'browser_not_found', error: `No installed ${b} found. Installed system browsers: ${avail}.${extra}`, ...pickSummary() };+ return { ok: false, errorCode: 'pup_not_found', error: `No installed ${b} found. Installed system browsers: ${avail}.${extra}`, ...pickSummary() }; } if (b === 'cft' || b === 'chromefortesting') { const det = detectChrome();@@ -280,7 +280,7 @@ function setDefaultBrowser({ browser, executablePath, install, elevate } = {}) { return { ok: false, errorCode: 'bad_browser', error: `Unknown browser '${browser}'. Use 'chrome', 'edge', 'cft', or 'auto' (or pass an explicit executablePath).`, ...pickSummary() }; } -// Compact snapshot of the browser decision — reused by readiness + every browser_* hint.+// Compact snapshot of the browser decision — reused by readiness + every pup_* hint. function pickSummary() { const r = resolveBrowser(); const d = readDefault();@@ -420,7 +420,7 @@ function installChrome({ onProgress } = {}) { // background:true → kick the install off and return immediately (non-blocking), // so the FIRST cold-start call answers with a pollable status // instead of hanging 2–3 min while CfT downloads.-// background:false → await the install (used by browser_prewarm when the caller+// background:false → await the install (used by pup_prewarm when the caller // explicitly wants to block until ready). // Returns { ready, phase, progressPct, executablePath, errorCode, error }. async function ensureChromeReady({ background = true, onProgress } = {}) {@@ -450,7 +450,7 @@ async function ensureChromeReady({ background = true, onProgress } = {}) { }; } -// Snapshot for browser_readiness + the /status chip. nodeInstalled is implicitly+// Snapshot for pup_readiness + the /status chip. nodeInstalled is implicitly // true (this module only runs inside the node bridge process), but we surface it // so the AI's readiness check is a single source of truth. function readiness() {@@ -460,7 +460,7 @@ function readiness() { // v1.9.94 (issue #22 Finding 4) — `ready` means "a drivable browser EXISTS", nothing more. The old // `&& !installing` conflated "a background CfT download is in flight" with "nothing is drivable", so // an in-flight (or STALLED) last-resort download reported the whole subsystem not-ready even on a box- // with system Chrome AND Edge sitting right there — and browser_open_window's gate refuses on+ // with system Chrome AND Edge sitting right there — and pup_open_window's gate refuses on // `!rd.ready`, so every open failed with chrome_for_testing_installing while readiness itself // reported browserSource:"system" with a valid executable path. Self-contradictory and blocking. // `installing` is still returned as its own field for callers that care about download progress;@@ -937,7 +937,7 @@ function installChromeStable(opts = {}) { // No notify channel on this AD — we raised ONE UAC and could not tell the user. // The CALLER (AI) must relay this; do NOT silently spam more prompts. chromeStableState.errorCode = 'chrome_stable_needs_approval_uncced';- chromeStableState.error = 'Chrome needs a Windows approval (UAC) on THIS machine, but this AD build does not expose notify_user to bridges via the direct-API, so pup could not pop a notification. A UAC was raised once. RELAY TO THE USER: "Go to the machine running pup and click YES on the Google Chrome installer prompt", then re-call browser_use {browser:"chrome", install:true}. pup drives Edge meanwhile.';+ chromeStableState.error = 'Chrome needs a Windows approval (UAC) on THIS machine, but this AD build does not expose notify_user to bridges via the direct-API, so pup could not pop a notification. A UAC was raised once. RELAY TO THE USER: "Go to the machine running pup and click YES on the Google Chrome installer prompt", then re-call pup_use {browser:"chrome", install:true}. pup drives Edge meanwhile.'; } return { ok: false, errorCode: chromeStableState.errorCode, error: chromeStableState.error }; }
src/server.js+407−407@@ -132,7 +132,7 @@ function ensureDepsInstalled() { // #280: do NOT let puppeteer's postinstall block/fail on a Chrome-for-Testing download here. It // reliably fails behind npm's allow-scripts gate and on half-populated CfT cache dirs, and pup // drives system Chrome/Edge without it. Skip it so the self-install finishes fast and pup is- // immediately usable via browser_use; CfT is fetched lazily (last resort) only when genuinely no+ // immediately usable via pup_use; CfT is fetched lazily (last resort) only when genuinely no // system browser exists. execFileSync(npmCmd, ['install', '--omit=dev', '--no-audit', '--no-fund', '--loglevel=error'], { cwd: dir, stdio: 'inherit', timeout: 300000, windowsHide: true,@@ -213,8 +213,8 @@ const SHOTS_DIR = path.join(__dirname, 'screenshots'); fs.mkdirSync(SHOTS_DIR, { recursive: true }); // v1.8.21: shadow-DOM-piercing + nested-scroll helpers, installed into every page's-// window so browser_eval can use them ($deep / $$deep / deepText) AND so browser_scroll-// / browser_screenshot_element resolve elements + the ACTUAL scroll container the same+// window so pup_eval can use them ($deep / $$deep / deepText) AND so pup_scroll+// / pup_screenshot_element resolve elements + the ACTUAL scroll container the same // way. Idempotent (guarded by __pupDeepInstalled). Injected via evaluateOnNewDocument // (persists across navigations) AND ensured on-demand before each verb (covers the // already-loaded document).@@ -300,7 +300,7 @@ let sessions = new Map(); let activeSessionId = null; // v1.9.146 (John: 'different ai threads opening pup window tabs into each other's windows ... make // sure their windows only open in tabs by default in the pup window they own'). activeSessionId is-// a SINGLE GLOBAL overwritten on every open by ANY thread, so browser_open_tab with no sessionId+// a SINGLE GLOBAL overwritten on every open by ANY thread, so pup_open_tab with no sessionId // dumped the tab into whatever window was last touched — cross-thread contamination. This resolves // the CALLER THREAD'S OWN window instead: the most recently created live session this thread owns. function callerOwnWindow(callerThread) {@@ -347,7 +347,7 @@ const _overlayRefreshTimers = new Map(); // per-session debounce for the central const _sessionOwnerClaims = new Map(); // sessionId -> owner string // v1.9.119 (John's design) — CLEANUP REPORT QUEUE. pup should tidy up on its own AND tell somebody it // did. Silent janitorial work is invisible: John found four zombie sessions cluttering every thread's-// browser_list_windows and had to ask about them. So each cleanup is recorded here and attached to the+// pup_list_windows and had to ask about them. So each cleanup is recorded here and attached to the // NEXT verb response as `_cleanupReport`, then DROPPED (report-once). The thread that receives it is // whichever one happens to call next, which is usually NOT the thread whose window was cleaned — that is // accepted by design: it may be irrelevant to that caller and it can ignore it, but it reaches the user@@ -382,7 +382,7 @@ function createSession(profileName) { // v1.8.16: advisory ownership. Multiple AI threads share this bridge and // sessions are keyed only by sessionId — so one thread could silently // navigate away ("steal") a window another thread was using. `owner` is a- // caller-declared label (browser_open_window {owner:"my-task"}); it can't+ // caller-declared label (pup_open_window {owner:"my-task"}); it can't // be authenticated, but it lets us REFUSE declared cross-owner takeovers // and reprimand anonymous ones with a loud hint. owner: null,@@ -390,14 +390,14 @@ function createSession(profileName) { // v1.8.26: is this window CURRENTLY meant to be on the user's screen? false = // background (the hard default) — pup must never raise it (no bringToFront on // navigate/reload/switch_tab). Set true only when opened foreground:true or after- // browser_raise_os_window, false again after browser_lower_os_window.+ // pup_raise_os_window, false again after pup_lower_os_window. _foreground: false, // Ring buffer of recently-closed tabs (capped at 20). Used by // resolveTabOrError to enrich tab_not_found errors with `lastKnownUrl` // when the bogus tabId matches a tab that closed in the last few seconds. // The common case: a popup PDF tab auto-closes after the viewer renders, // and the caller tries to eval/fetch against the stale tabId. We can now- // surface the URL the popup was at so they can browser_fetch_url it+ // surface the URL the popup was at so they can pup_fetch_url it // directly without having to re-trigger the click. recentlyClosedTabs: [], };@@ -434,10 +434,10 @@ function getTabById(session, tabId) { } // Attach page → tab to a session, wire error collection + console forwarding.-// Called by both launchSession (opening new window) and browser_open_tab.+// Called by both launchSession (opening new window) and pup_open_tab. // // `opener` documents how the tab came to exist:-// - "user" (default): Claude requested it via browser_open_window/browser_open_tab+// - "user" (default): Claude requested it via pup_open_window/pup_open_tab // - "popup": Chrome opened it via window.open() / target=_blank — auto-tracked // by the targetcreated listener wired in wirePopupTracking(). // openerTabId names the tab that triggered the popup.@@ -539,7 +539,7 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user console.log(`[crash] session "${sessionId}" tab ${t.tabId} AUTO-recovered (renderer respawned)`); recordCleanup(`pup auto-recovered a crashed ("Aw, Snap! Out of Memory") tab in session "${sessionId}" by respawning its renderer${rurl ? ` at ${rurl}` : ''} — the tab had run out of memory (usually many heavy tabs sharing one Chrome); the window is back on its own.`); } else {- console.log(`[crash] session "${sessionId}" tab ${t.tabId} auto-recover did not take — left flagged for manual browser_reload`);+ console.log(`[crash] session "${sessionId}" tab ${t.tabId} auto-recover did not take — left flagged for manual pup_reload`); } } catch (e) {} }, 2500);@@ -556,7 +556,7 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user // the same Inspector.targetCrashed on the page's OWN session), so the extra session bought // nothing. Removed: one less permanent CDP attachment per tab, and one less thing pup does TO // Chrome that Chrome was not designed to have done to it continuously.- // v1.8.17: PERSISTENT title-suffix injector. AD's browser_raise/lower_os_window+ // v1.8.17: PERSISTENT title-suffix injector. AD's pup_raise/lower_os_window // (AD-core verbs, not this bridge — they shadow our fallback handlers) locate the // OS window by its " (session: <id>)" title suffix. Post-goto page.evaluate re-adds // the suffix, but a NEW page sets its own title BEFORE that runs → a brief window@@ -574,7 +574,7 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user // only ADD a marker when NONE is present — if it enforced an exact // " (session: <sid>)" it would fight addTabToSession's " | <tabId>" suffix: // two MutationObservers appending different strings ping-pong forever, peg the- // renderer, and flood the CDP pipe (which HUNG browser_open_tab on Edge and+ // renderer, and flood the CDP pipe (which HUNG pup_open_tab on Edge and // poisoned the whole bridge). Matching on the marker PREFIX makes it cooperate. const LBL = label || 'ai-thread'; const MARKER = ' (' + LBL + ': ' + sid; // no closing paren — matches both forms@@ -646,7 +646,7 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user // v1.9.343 (John: "put my Adom profile picture in there — show my adom identity"). Chrome's own // profile chip cannot take a custom local image (only a real Google sign-in can — tested), so we // show the user's Adom identity HERE instead: the owner chip leads with the user's Adom avatar- // (set via browser_set_identity → fetched, circle-cropped, embedded as a data URI in settings).+ // (set via pup_set_identity → fetched, circle-cropped, embedded as a data URI in settings). // Falls back to the ⬢ glyph when no identity is configured. const _avatar = pupSettings().identityAvatarB64 || ''; const _ident = pupSettings().identityName || '';@@ -677,7 +677,7 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user }, _threadDisp, _prof, _avatar, _ident).catch(() => {}); } } catch (e) {}- // v1.8.21: install the shadow-DOM + scroll helpers on every document so browser_eval+ // v1.8.21: install the shadow-DOM + scroll helpers on every document so pup_eval // can use $deep / $$deep / deepText without hand-rolling a shadow-root walk. try { page.evaluateOnNewDocument(PUP_DEEP_HELPERS).catch(() => {}); } catch (e) {} // v1.9.48: re-evaluate the window CATEGORY when this tab finishes navigating.@@ -698,7 +698,7 @@ function attachTab(session, sessionId, page, { makeActive = true, opener = 'user // v1.9.324 (John: "EVERY verb OR EVENT that could change an icon MUST trigger a recalc"). // The favicon OVERLAY badge was only recomputed by explicit MUTATING_VERBS. Any navigation // pup did not itself initiate — a JS redirect, a <meta> refresh, a form submit, an SPA route- // change, an OAuth bounce, a link click INSIDE the tab, browser_login's submit — changes the+ // change, an OAuth bounce, a link click INSIDE the tab, pup_login's submit — changes the // page favicon but fired no verb, so the taskbar kept a STALE badge. THAT is the drift John // has caught a dozen times. The navigation event is the true trigger: recompute here, for the // active/visible tab (applyAppOverlay re-resolves the visible tab), debounced ~2s so a redirect@@ -866,7 +866,7 @@ async function findTabForTarget(target) { // (window.open(), target="_blank" form post, <a target="_blank"> click, etc.), // `targetcreated` fires. We resolve the target's page, find the session that // owns its opener (via target.opener()), and attach the popup to that session-// as a tracked tab. browser_list_tabs then includes it like any other tab —+// as a tracked tab. pup_list_tabs then includes it like any other tab — // callers can switch to it, eval against it, screenshot it, close it. // // Edge cases:@@ -927,7 +927,7 @@ function wirePopupTracking(browser, profileName) { // Attach as a popup-origin tab. attachTab handles the standard error + // console + page.on('close') wiring same as user-opened tabs. const tabId = attachTab(session, ownerInfo.sessionId, popupPage, {- makeActive: false, // don't yank focus; let caller decide via browser_switch_tab+ makeActive: false, // don't yank focus; let caller decide via pup_switch_tab opener: 'popup', openerTabId: ownerInfo.tabId, });@@ -959,7 +959,7 @@ function resolveTabOrError(session, sessionId, args, { allowCrashed = false } = // v1.4.10: If the requested tabId is one we recently saw close (most // commonly a popup PDF that auto-closed after rendering), surface its // last known URL + closure metadata so the caller can immediately- // browser_fetch_url it without having to re-trigger the spawning click.+ // pup_fetch_url it without having to re-trigger the spawning click. let recentlyClosed = null; if (Array.isArray(session.recentlyClosedTabs)) { const hit = session.recentlyClosedTabs.find(c => c.tabId === args.tabId);@@ -979,9 +979,9 @@ function resolveTabOrError(session, sessionId, args, { allowCrashed = false } = `Most likely the popup auto-closed (PDF popups close after the ` + `viewer renders if Content-Disposition is attachment, or if a ` + `download fired). The right pattern with popup tabs is: ` +- `(1) browser_list_tabs IMMEDIATELY after the click that spawned ` ++ `(1) pup_list_tabs IMMEDIATELY after the click that spawned ` + `the popup, (2) read the popup's url field from that response ` +- `(don't eval against the popup's tabId), (3) browser_fetch_url ` ++ `(don't eval against the popup's tabId), (3) pup_fetch_url ` + `with that URL — never eval-against-popup-then-fetch.`, errorCode: 'tab_not_found', };@@ -991,7 +991,7 @@ function resolveTabOrError(session, sessionId, args, { allowCrashed = false } = if (recentlyClosed.lastKnownUrl) { payload._hint = `This tab closed ${Math.round(recentlyClosed.closedMsAgo / 1000)}s ago at URL ${recentlyClosed.lastKnownUrl}. ` +- `Call browser_fetch_url with that URL (and the opener tabId "${recentlyClosed.openerTabId || 'tab-1'}" for cookies) to get its bytes — ` ++ `Call pup_fetch_url with that URL (and the opener tabId "${recentlyClosed.openerTabId || 'tab-1'}" for cookies) to get its bytes — ` + `do NOT try to eval against the closed tabId.`; } }@@ -1014,11 +1014,11 @@ function resolveTabOrError(session, sessionId, args, { allowCrashed = false } = // perceives as "pup crashed". Fail FAST and tell the caller exactly how to recover. // v1.9.176: recover a CRASHED renderer IN PLACE. A crashed renderer ('Aw, Snap!') detaches the // page's main frame, so page.reload()/page.goto() throw 'Attempted to use detached Frame' — which is-// exactly why the crash hint's promised recovery ("browser_reload to recover") did NOT work. Driving+// exactly why the crash hint's promised recovery ("pup_reload to recover") did NOT work. Driving // the RAW target with Page.navigate spawns a fresh renderer in the SAME tab/window/session, which // puppeteer then re-attaches. Returns true once the fresh page answers a probe, false on failure. async function respawnCrashedRenderer(page, url) {- // Anti-race: the auto-heal (from the crash detector) and a manual browser_reload/navigate can both+ // Anti-race: the auto-heal (from the crash detector) and a manual pup_reload/navigate can both // reach here for the SAME crashed page. Two concurrent createCDPSession + Page.navigate on one // crashed target hang. So only ONE respawn runs per page; a second caller waits for it and returns // its result. Every CDP call is hard-timeout-bounded so a crashed target can never wedge the verb.@@ -1136,7 +1136,7 @@ function _throwIfCrashed(t, sessionId) { error: `Tab "${t.tabId}" in session "${sessionId}" has a CRASHED renderer (${t._crashReason || 'crash'}, ${secs}s ago).`, errorCode: 'tab_renderer_crashed', tabId: t.tabId,- _hint: `The tab's Chrome renderer process died (usually memory pressure from many heavy tabs, or a navigate racing an eval/screenshot on the same tab). The window and its OTHER tabs are fine. To recover THIS tab: browser_reload {sessionId:"${sessionId}", tabId:"${t.tabId}"} (or browser_navigate it to a URL) — that respawns the renderer and clears the crash flag. Then avoid driving a single tab with overlapping navigate/eval/screenshot calls; wait for one to return before the next.`,+ _hint: `The tab's Chrome renderer process died (usually memory pressure from many heavy tabs, or a navigate racing an eval/screenshot on the same tab). The window and its OTHER tabs are fine. To recover THIS tab: pup_reload {sessionId:"${sessionId}", tabId:"${t.tabId}"} (or pup_navigate it to a URL) — that respawns the renderer and clears the crash flag. Then avoid driving a single tab with overlapping navigate/eval/screenshot calls; wait for one to return before the next.`, }; const err = new Error(JSON.stringify(payload)); err._isStructured = true;@@ -1185,8 +1185,8 @@ async function describeTab(tab, activeTabId) { title, active: tab.tabId === activeTabId, errorCount: tab.errors.length,- // Provenance: "user" if Claude opened this tab via browser_open_window /- // browser_open_tab, "popup" if the page itself spawned it via window.open()+ // Provenance: "user" if Claude opened this tab via pup_open_window /+ // pup_open_tab, "popup" if the page itself spawned it via window.open() // / target=_blank. openerTabId names the tab that triggered the popup // (so callers can correlate "this datasheet popup came from product page X"). opener: tab.opener || 'user',@@ -1224,7 +1224,7 @@ function saveSessionFile(sessionId, profileName, url) { // by a granted raise OR by pup OBSERVING the user foreground the window themselves (taskbar click). foreground: !!((sessions.get(sessionId) || {})._foreground), // v1.9.197 BUG 4 (Bills thread): owner was NEVER written here, so every bridge restart erased it- // and browser_list_windows reported owner:null for every session — silently disabling the+ // and pup_list_windows reported owner:null for every session — silently disabling the // session_owned_by_another_thread protection that owner is supposed to back. owner: ((sessions.get(sessionId) || {}).owner) || null, // v1.9.300: persist the resolved OS window handle. Chrome windows survive a bridge respawn with@@ -1314,13 +1314,13 @@ function parseSessionIdFromTitle(title) { // session for that profile from `sessions` AND deleted the on-disk session // file. The Chrome window was still alive on the user's desktop, fully // usable, but every reference to it had been wiped — so subsequent-// browser_navigate/eval/screenshot calls hit "Session not found" and-// (worse) browser_navigate without a sessionId silently retargeted+// pup_navigate/eval/screenshot calls hit "Session not found" and+// (worse) pup_navigate without a sessionId silently retargeted // to whatever session was still in memory, hijacking unrelated windows. // Confirmed twice in chip-fetcher's session log on 2026-05-04. // // New behavior: keep the session entries in memory + on disk so they-// remain "known but currently disconnected." The next browser_rescan+// remain "known but currently disconnected." The next pup_rescan // (manual) or 30-second health check (auto) will try to reconnect via // the persisted DevToolsActivePort + verify Chrome's PID is still alive, // then walk pages and re-attach by parsing the (session: X) tag from@@ -1343,7 +1343,7 @@ function handleBrowserDisconnect(browser, profileName) { } console.log( `[disconnect] profile "${profileName}" disconnected — ${touched} session(s) marked as awaiting reconnect. ` +- `Run browser_rescan to recover; the 30s health check will also auto-attempt.`+ `Run pup_rescan to recover; the 30s health check will also auto-attempt.` ); } @@ -1658,7 +1658,7 @@ async function rescanProfile(browser, profileName, { adoptOrphans = false } = {} `${alreadyLive} pages already live` ); // v1.9.179: after re-binding, prune any dead/duplicate tab entries this or a prior reconnect left- // behind, so the tab count + overlay are correct immediately (browser_rescan triggers this).+ // behind, so the tab count + overlay are correct immediately (pup_rescan triggers this). for (const [sid, s] of sessions.entries()) { if (s.profileName === profileName) { try { await reconcileSessionTabs(s, sid); } catch (e) {} } }@@ -1695,7 +1695,7 @@ async function rescanProfile(browser, profileName, { adoptOrphans = false } = {} } // Walk every known profile (in-memory + on-disk) and try to rebuild a-// complete session map. Used by browser_rescan and by the 30s health+// complete session map. Used by pup_rescan and by the 30s health // check when it detects a disconnected browser whose Chrome PID is // still alive. Returns aggregated stats for the response payload. //@@ -2240,7 +2240,7 @@ function setupErrorCollection(pg, errorsArr) { // FIRE-AND-FORGET with an 8s timeout on a FULL Win32_Process enumeration, which TIMES OUT precisely // when the box is pegged by the runaway renderers it exists to kill (measured on AdomLapper: overall // CPU 88-98% on 16 cores, dwm starved to 38-63%, mouse frozen, forced reboot; 6 CfT procs survived a-// browser_close, 4 --type=renderer children near 100%). Three fixes: (1) push the image-name filter to+// pup_close, 4 --type=renderer children near 100%). Three fixes: (1) push the image-name filter to // WMI server-side (-Filter "Name='chrome.exe' OR Name='msedge.exe'") so ONLY browser rows are // materialized — fast even at 98% CPU; (2) AWAIT the reap so close() VERIFIES nothing survives before // returning ok; (3) return chromeProcsRemaining so the caller KNOWS the teardown completed. Still@@ -2287,7 +2287,7 @@ async function reapProfileChromes(profileName) { } // Reap ORPHANED pup chrome/msedge — any process under a pup-profiles dir whose profile has NO live // session (parent AND renderer children). Live sessions are spared by PROFILE (not just parent PID),-// so a session's own renderers are never touched. Used by browser_close (all) + boot/periodic sweeps.+// so a session's own renderers are never touched. Used by pup_close (all) + boot/periodic sweeps. async function reapOrphanPupChromes() { if (!_isWin) return { killed: 0, remaining: 0 }; const liveDirs = new Set();@@ -2552,7 +2552,7 @@ async function closeRestoredLeftoverTabs(browser, keepPage) { // tells Chrome to actually save the file when JS triggers a nav // to a binary content-type. //-// Opt-out: pass { strictPermissions: true } to browser_open_window for+// Opt-out: pass { strictPermissions: true } to pup_open_window for // sessions that genuinely need the user to gate every download (rare). // // History: v1.4.12 used Browser.grantPermissions(['automaticDownloads',@@ -2565,7 +2565,7 @@ async function closeRestoredLeftoverTabs(browser, keepPage) { // pup with full permissions for full debug). All 'granted', none denied. Applied // per-permission below so any descriptor a given Chrome build rejects (e.g. // clipboard-sanitized-write) only drops itself — the rest still land. Opt-out via-// strictPermissions:true on browser_open_window for an untrusted site you're auditing.+// strictPermissions:true on pup_open_window for an untrusted site you're auditing. const PERMISSIVE_DEFAULT_PERMISSIONS = [ // v1.8.69: window-management → getScreenDetails() works (multi-monitor enumeration from // inside the page; without the grant it hangs on a CDP-suppressed permission prompt).@@ -2656,7 +2656,7 @@ async function applyDefaultPermissions(browser, profileName, { strictPermissions // downloadPath defaults to %USERPROFILE%\Downloads on Windows / ~/Downloads // on POSIX — chip-fetcher's `desktop_watch_files` polls this exact dir, // so the two ends meet by default. Caller can override via the-// `downloadPath` arg on browser_open_window if they need somewhere else.+// `downloadPath` arg on pup_open_window if they need somewhere else. async function applyDownloadBehavior(page, downloadPath) { if (!downloadPath) { downloadPath = path.join(os.homedir(), 'Downloads');@@ -2690,7 +2690,7 @@ async function applyDownloadBehavior(page, downloadPath) { } // The browser that most recently launched (kind/source/executablePath + any fallback-// chain), so browser_open_window's response + hints report what pup ACTUALLY drove —+// chain), so pup_open_window's response + hints report what pup ACTUALLY drove — // which can differ from resolveBrowser()'s top pick if the top candidate had to fall // through. Read right after getOrLaunchBrowser() returns. let _lastChosenBrowser = null;@@ -2703,12 +2703,12 @@ function browserPickReport() { const availTxt = (sum.candidates || []).map(c => `${c.kind}(${c.source})`).join(', ') || 'none'; const def = sum.defaultBrowser; const defTxt = def- ? `Default is ${def.kind}${def.forced ? ' (pinned via browser_use)' : ' (auto — cached from a prior successful launch)'}${def.pendingInstall ? ', still downloading' : ''}.`+ ? `Default is ${def.kind}${def.forced ? ' (pinned via pup_use)' : ' (auto — cached from a prior successful launch)'}${def.pendingInstall ? ', still downloading' : ''}.` : 'Default is auto (pup drives the first browser that launches, then caches it).'; const fellBack = chosen && chosen.fallbackFrom && chosen.fallbackFrom.length ? ` NOTE: fell back past ${chosen.fallbackFrom.join(', ')} which would not spawn.` : ''; const hint = chosen- ? `Driving ${chosen.kind}${chosen.source ? ` (${chosen.source})` : ''}${chosen.executablePath ? ` — ${chosen.executablePath}` : ''}, in a FRESH isolated profile (the user's real tabs/logins are untouched).${fellBack} Available on this machine: ${availTxt}. ${defTxt} To switch the browser pup drives: browser_use {browser:"chrome"|"edge"|"cft"|"auto"}. To install & pin full Chrome for Testing: browser_use {browser:"cft"} (fetches ~150 MB once, then every open uses it) or browser_prewarm.`+ ? `Driving ${chosen.kind}${chosen.source ? ` (${chosen.source})` : ''}${chosen.executablePath ? ` — ${chosen.executablePath}` : ''}, in a FRESH isolated profile (the user's real tabs/logins are untouched).${fellBack} Available on this machine: ${availTxt}. ${defTxt} To switch the browser pup drives: pup_use {browser:"chrome"|"edge"|"cft"|"auto"}. To install & pin full Chrome for Testing: pup_use {browser:"cft"} (fetches ~150 MB once, then every open uses it) or pup_prewarm.` : `No browser resolved. Available on this machine: ${availTxt}.`; return { browser: chosen ? { kind: chosen.kind, source: chosen.source, executablePath: chosen.executablePath || null } : null,@@ -2977,7 +2977,7 @@ function pupBadgeB64() { // verbs are unknown -> _identitySupported=false -> the overlay badge fallback keeps working. // v1.8.78: AD-version awareness. Most capability gaps self-gate silently (identity -> badge, // bottom/force -> PS fallback) and need no nagging. ONE gap is a POLICY hole, not cosmetics:-// AD <1.9.124 intercepts browser_raise_os_window natively, so pup's foreground gate (reason+// AD <1.9.124 intercepts pup_raise_os_window natively, so pup's foreground gate (reason // required + audit trail) silently does not exist there. pup can't enforce through an // interceptor, but it can KNOW and SAY SO — probed once at startup, surfaced in /status and // open-response hints so users/AIs are told to update AD rather than trusting a gate that@@ -3003,7 +3003,7 @@ async function probeAdVersion() { } function adUpgradeHint() { return _adRaiseGateBypassed- ? ` ⚠ AD ${_adVersion} on this desktop is older than 1.9.124: it intercepts browser_raise_os_window natively, so pup's foreground gate (foregroundReason + audit trail) does NOT apply to raises here. Recommend the user update Adom Desktop (update_check / the footer banner) — everything else degrades gracefully, this is the one policy gap.`+ ? ` ⚠ AD ${_adVersion} on this desktop is older than 1.9.124: it intercepts pup_raise_os_window natively, so pup's foreground gate (foregroundReason + audit trail) does NOT apply to raises here. Recommend the user update Adom Desktop (update_check / the footer banner) — everything else degrades gracefully, this is the one policy gap.` : ''; } // v1.8.82: PUP SETTINGS — user-configurable behavior, persisted OUTSIDE the bridge dir (the@@ -3302,7 +3302,7 @@ function pupRelaunchCommand(sessionId) { const payload = JSON.stringify({ sessionId, foregroundReason: 'user clicked the Adom Pup taskbar header or pinned launcher', caller: { aiThread: 'user-taskbar-launcher' } }).replace(/"/g, '\\"'); // v1.9.115: same as the task commands — this runs when the user clicks the jump-list HEADER row or a // pinned pup launcher, so it needs identity or AD refuses it and the click does nothing.- return `"${cli}" browser_focus_window "${payload}"`; // v1.9.121: identity rides in the payload JSON (see pupRelaunchCommand above)+ return `"${cli}" pup_focus_window "${payload}"`; // v1.9.121: identity rides in the payload JSON (see pupRelaunchCommand above) } function _isUnknownVerb(r, raw) { return /unknown (desktop )?command|unknown verb/i.test(((r && r.error) || '') + ' ' + (raw || '')); } async function ensurePupIdentityRegistered() {@@ -3311,7 +3311,7 @@ async function ensurePupIdentityRegistered() { try { const r = await chrome.adCommand('desktop_register_app_identity', { appId: PUP_APP_ID, displayName: PUP_DISPLAY_NAME, iconPath: pupIconPath(),- args: 'browser_open_window {"sessionId":"pinned","owner":"pinned-launch","url":"https://wiki.adom.inc"}',+ args: 'pup_open_window {"sessionId":"pinned","owner":"pinned-launch","url":"https://wiki.adom.inc"}', }, { timeoutMs: 8000 }); const raw = r && (typeof r.output === 'string' ? r.output : JSON.stringify(r.output || '')); if ((r && r.success === false) || _isUnknownVerb(r, raw)) {@@ -3441,7 +3441,7 @@ async function stampPupIdentity(sessionId, opts) { // and the registry cannot brand the header at all — the header obeys the window's // Relaunch* property TRIO, and Windows IGNORES RelaunchIconResource unless RelaunchCommand // is ALSO set. So we must pass a real relaunch command. It doubles as what a header click- // or a taskbar PIN launches, so it has to genuinely refocus THIS session: browser_focus_window+ // or a taskbar PIN launches, so it has to genuinely refocus THIS session: pup_focus_window // carries a foregroundReason (the raise gate refuses without one, and a header click IS a // real user request to see the window). Quotes are backslash-escaped for Windows argv — // same trap that made jump-list clicks no-op in 1.9.9.@@ -3672,7 +3672,7 @@ async function setWikiAuthedState(session, sessionId, authed, opts = {}) { // v1.9.3 (AD #208 — desktop_set_window_jumplist, AD ≥1.9.139): give each WIKI pup window a // taskbar RIGHT-CLICK task to flip between the logged-in and public view. The jump list attaches // to the per-session AUMID (registered registry-only in v1.9.2), so it is a PER-WINDOW menu.-// Clicking a task runs AD's bundled CLI → browser_wiki_set_view, which relaunches THIS window+// Clicking a task runs AD's bundled CLI → pup_wiki_set_view, which relaunches THIS window // under the other cookie jar (profile = userDataDir is fixed at browser launch, so a view switch // = a relaunch). Only meaningful in SPLIT mode (grouped mode shares one taskbar button, so a // per-window menu would be ambiguous) and only for Adom (adom.inc) URLs; any other URL clears the@@ -3731,15 +3731,15 @@ async function updateWikiJumplist(session, sessionId, opts) { // works on every CLI version, old or new. Also space-free by construction, which dodges the // Explorer/ad-taskrun argv re-parsing hazard. const jarg = (verb, obj) => `${verb} "${JSON.stringify({ ...obj, caller: { aiThread: 'user-taskbar-menu' } }).replace(/"/g, '\\"')}"`;- const taskArgs = (v) => jarg('browser_wiki_set_view', { sessionId, view: v });+ const taskArgs = (v) => jarg('pup_wiki_set_view', { sessionId, view: v }); // v1.9.51: a CLOSE-ALL task on every pup window (John: "i have like 8 open right now"). It calls- // browser_close with no sessionId, which kills every pup Chrome + clears every session. The empty+ // pup_close with no sessionId, which kills every pup Chrome + clears every session. The empty // "{}" still goes through the same escaped-argv path so the CLI parses it as one JSON argument. const closeAllTask = { title: 'Close ALL Adom Pup windows', description: 'Close every Adom Pup browser window at once (all sessions, every tab)', iconPath: _taskIcon,- args: jarg('browser_close', {}),+ args: jarg('pup_close', {}), }; // v1.9.53: SHORT titles. John: the "Adom wiki:" prefix pushed the important word ("logged-in") // off the end where Windows truncates the task label ("...switch to logged-in vi…"). The whole@@ -3881,7 +3881,7 @@ const BUSY_IDLE_MS = 2500; // mac outright (no Dock equivalent), which is what this call passes. // So: capability real, semantics incompatible. Do not un-gate on learning "desktop_taskbar works // on mac" — it does; it just cannot address what pup needs to address. The mac equivalent of this-// cue is browser_alert_window's toast-and-unhide path, which IS per-window.+// cue is pup_alert_window's toast-and-unhide path, which IS per-window. async function setSessionProgress(sessionId, busy) { if (minimalTouch()) return; // v1.9.185 minimal-touch: no taskbar progress bars // v1.9.152 (John's audit columns): count progress-bar triggers + total ms it ran, per session.@@ -3934,7 +3934,7 @@ async function toastForeground(sessionId, reason) { } function markSessionBusy(sessionId) { if (process.platform !== 'win32' || !sessionId) return;- if (pupSettings().agentActivity === 'off') return; // user opt-out via browser_configure+ if (pupSettings().agentActivity === 'off') return; // user opt-out via pup_configure if (!sessions.has(sessionId)) return; let st = _busyState.get(sessionId); if (!st) { st = { busy: false, timer: null }; _busyState.set(sessionId, st); }@@ -3964,8 +3964,8 @@ function clearSessionBusy(sessionId) { // Verbs that are pure status/polling: they do NOT mean an agent is driving the window, and lighting // the bar for them would make every window look permanently busy (the audit loops alone poll these). const PASSIVE_VERBS = new Set([- 'browser_list_windows', 'browser_list_tabs', 'browser_readiness', 'browser_status', 'browser_maximize', 'browser_hover', 'browser_deps', 'browser_dashboard',- 'browser_describe', 'browser_configure', 'browser_prewarm', 'browser_rescan', 'browser_wait', 'browser_import_scan', 'browser_set_identity',+ 'pup_list_windows', 'pup_list_tabs', 'pup_readiness', 'pup_status', 'pup_maximize', 'pup_hover', 'pup_deps', 'pup_dashboard',+ 'pup_describe', 'pup_configure', 'pup_prewarm', 'pup_rescan', 'pup_wait', 'pup_import_scan', 'pup_set_identity', ]); // v1.9.58 — THUMBNAIL TOOLTIP (AD >= 1.9.152: desktop_taskbar { thumbnailTooltip }).@@ -4197,7 +4197,7 @@ async function brandSweep(reason) { // app's icon). Source of truth = the page's own <link rel="icon"> — the exact artifact the // adom-ui-design standard mandates every app serve (monochrome white mark; see hd-tab-icons: // the served favicon is the canonical webview identity, and pup now honors the same contract).-// Explicit overrides: browser_open_window {appIconB64|appIconUrl, appName}. Composited onto a+// Explicit overrides: pup_open_window {appIconB64|appIconUrl, appName}. Composited onto a // dark rounded plate via sharp (white marks need dark backing on the taskbar). An app-looking // URL (localhost / *.adom.cloud proxy) serving NO favicon gets a loud reprimand hint teaching // the standard. When an app icon is worn, it takes the overlay slot; the tab count moves to@@ -4318,7 +4318,7 @@ async function autoSignWikiFromDesktop(page, nextPath) { // from document.title, so the ONLY way to get the owning AI thread up there is to decorate the title. // There were already TWO title mutators (this wiki glyph prefixing the START, and the session-suffix // injector appending the END) and the code comments record why a THIRD is dangerous: two observers-// writing the same region ping-pong forever, peg the renderer and flood CDP (that HUNG browser_open_tab+// writing the same region ping-pong forever, peg the renderer and flood CDP (that HUNG pup_open_tab // on Edge). So instead of adding one, the prefix is now a SINGLE composed slot: an AI-thread tag and the // wiki label are both written by one applier under one observer, in a fixed order, and stripping only // ever removes decorations we know we wrote (never a page title that merely contains " · ").@@ -4382,7 +4382,7 @@ function wikiViewHint(url, rawView, auth) { if (!isAdomUrl(url)) return ''; if (rawView === 'authed') { if (auth && auth.authed) return ` 🔓 Wiki opened LOGGED IN BY DEFAULT (as ${auth.user || 'the user'}) — pup auto-signed you in from the desktop token, no prompt. You see private source, drafts, owner-only cards, reply/edit. To instead see the ANONYMOUS/PUBLIC view (exactly what the world sees — e.g. verify a publish didn't leak private source), reopen with noAutoLogin:true (or wikiView:"public"), an isolated logged-out jar.`;- return ` 🔑 Wiki authed view requested, but NO ONE has done the one-time login yet (or the ~30-day session expired) — so this window is currently logged-out. A thread can't self-login (it's the user's SSO). Offer it: browser_raise_os_window {foregroundReason:"sign into the Adom wiki"} → user does the hydrogen.adom.inc SSO ONCE → then EVERY thread's wikiView:"authed" is silently logged in for ~30 days (shared vault profile). Tell the user it's a one-time login that unlocks the logged-in view everywhere.`;+ return ` 🔑 Wiki authed view requested, but NO ONE has done the one-time login yet (or the ~30-day session expired) — so this window is currently logged-out. A thread can't self-login (it's the user's SSO). Offer it: pup_raise_os_window {foregroundReason:"sign into the Adom wiki"} → user does the hydrogen.adom.inc SSO ONCE → then EVERY thread's wikiView:"authed" is silently logged in for ~30 days (shared vault profile). Tell the user it's a one-time login that unlocks the logged-in view everywhere.`; } if (rawView === 'public') { // EXPLICIT public — an isolated anonymous jar, genuinely logged out. This is the one true@@ -4677,7 +4677,7 @@ async function applyAppOverlay(session, sessionId) { session.activeTabId = activeTab.tabId; // keep bookkeeping honest for everything downstream } let src = session._appIconB64 ? { b64: session._appIconB64, mime: session._appIconMime || 'image/png', href: 'app-provided' } : null;- if (src) session._overlayFav = { kind: 'app-provided', format: 'png', mode: 'default', transparent: null, via: 'browser_open_window appIconB64', href: 'app-provided' };+ if (src) session._overlayFav = { kind: 'app-provided', format: 'png', mode: 'default', transparent: null, via: 'pup_open_window appIconB64', href: 'app-provided' }; if (!src) { // v1.9.238: rich, dark-taskbar-aware resolution + full state capture. const rich = await resolveFaviconRich(activePage).catch(() => null);@@ -5010,7 +5010,7 @@ function appIconReprimand(url, hadFavicon) { if (hadFavicon) return ''; const appish = /localhost|127\.0\.0\.1|\.adom\.cloud\/proxy\//i.test(String(url || '')); if (!appish) return '';- return ' 🎨 APP ICON MISSING: this looks like an Adom app, but the page serves NO favicon, so its pup window cannot wear the app\'s icon (and its webview tab shows a blank icon too). The adom-ui-design standard requires every app to serve a favicon (<link rel="icon">, monochrome WHITE mark, clean 24x24 SVG — see the adom-ui-design + app-creator skills). Add one and reopen, or pass appIconB64/appIconUrl on browser_open_window meanwhile.';+ return ' 🎨 APP ICON MISSING: this looks like an Adom app, but the page serves NO favicon, so its pup window cannot wear the app\'s icon (and its webview tab shows a blank icon too). The adom-ui-design standard requires every app to serve a favicon (<link rel="icon">, monochrome WHITE mark, clean 24x24 SVG — see the adom-ui-design + app-creator skills). Add one and reopen, or pass appIconB64/appIconUrl on pup_open_window meanwhile.'; } // v1.9.9: on-screen CAPTION for the taskbar wiki toggle (John's ask: "why don't you show an AD@@ -5036,7 +5036,7 @@ async function wikiCaption(text, expiresInMs) { } } catch (e) {} }-// v1.9.7: reusable OS FOREGROUND raise, extracted verbatim from the proven browser_raise_os_window+// v1.9.7: reusable OS FOREGROUND raise, extracted verbatim from the proven pup_raise_os_window // path, so the wiki view-toggle callback can actually POP the relaunched window to the user's // screen. A jump-list click is a genuine USER action, so a real SetForegroundWindow raise is // warranted — the earlier CDP-only maximize left the window BEHIND whatever the user was looking@@ -5045,7 +5045,7 @@ async function wikiCaption(text, expiresInMs) { // fallback for launcher→browser PID relaunches). Best-effort throughout; never throws. async function osRaiseSessionWindow(session, sessionId, reason) { // v1.9.255 (John: "if a user asks to fg, you show a caption for 2 seconds explaining the reason.- // do you do that? cuz i asked you 100 times"): the browser_raise verb captioned, but the five+ // do you do that? cuz i asked you 100 times"): the pup_raise verb captioned, but the five // OTHER raise paths routed here and captioned NOTHING. Enforce at the choke point: every raise // through this function captions its reason for 2s. Callers that already narrate (assisted_click) // pass reason=null to avoid a double caption.@@ -5090,7 +5090,7 @@ async function osRaiseSessionWindow(session, sessionId, reason) { } } catch (e) { console.log(`[foreground] "${sessionId}" hwnd raise threw: ${e.message}`); } return;- // v1.9.30: resolve the PID EXACTLY like the proven browser_raise_os_window handler does:+ // v1.9.30: resolve the PID EXACTLY like the proven pup_raise_os_window handler does: // browser.process() ONLY. For a DETACHED launch (every Windows pup browser) process() is null, // so the PS SetForegroundWindow step is SKIPPED and the CDP place+maximize+bringToFront above // is what shows the window — and that path demonstrably works. The old code fell back to@@ -5839,7 +5839,7 @@ async function reclaimUnmanagedWindows() { try { port = parseInt(fs.readFileSync(path.join(PROFILES_DIR, s.profileName, 'DevToolsActivePort'), 'utf8').trim().split('\n')[0], 10) || null; } catch (e) {} const alive = port && await fetch(`http://127.0.0.1:${port}/json/version`).then(r => r && r.ok).catch(() => false); if (alive) continue; // Part B will reconnect + rescanProfile reattaches this same session- recordCleanup(`dropped zombie session "${sid}" (0 tabs, its Chrome was gone) and unregistered its taskbar identity — it had been showing up in browser_list_windows for every thread`);+ recordCleanup(`dropped zombie session "${sid}" (0 tabs, its Chrome was gone) and unregistered its taskbar identity — it had been showing up in pup_list_windows for every thread`); try { if (s._curAppId && s._curAppId !== PUP_APP_ID) await unregisterPupAumid(s._curAppId); } catch (e) {} sessions.delete(sid); try { deleteSessionFile(sid); } catch (e) {}@@ -6566,7 +6566,7 @@ function osBottomUnlessForeground(hwnd, force) { // session, the lease CANNOT lapse. This is the "never kill someone mid-video" // guarantee, and it is knowable (not guessed) because recording runs through our verbs. // 2. VERB ACTIVITY extends the lease — someone is demonstrably using the window.-// 3. An explicit browser_highfps_extend renews it, so an AI that knows it needs longer+// 3. An explicit pup_highfps_extend renews it, so an AI that knows it needs longer // can negotiate instead of being surprised. // // On lapse we DOWNGRADE (relaunch the same session, same tabs, without the flags) rather@@ -6644,7 +6644,7 @@ setInterval(() => { if (left <= 0) { highFpsDowngrade(sid, `lease of ${rec.minutes} min lapsed with no active recording`).catch(() => {}); } else if (left <= HIGHFPS_WARN_BEFORE_MS && !rec.warned) { rec.warned = true;- console.log(`[highfps] "${sid}" lease expires in ~${Math.ceil(left / 60000)} min — call browser_highfps_extend {sessionId,minutes} to keep it, or start a recording (a running recording holds it open indefinitely)`);+ console.log(`[highfps] "${sid}" lease expires in ~${Math.ceil(left / 60000)} min — call pup_highfps_extend {sessionId,minutes} to keep it, or start a recording (a running recording holds it open indefinitely)`); } } }, 30000);@@ -7145,7 +7145,7 @@ async function enumAllPupChromeHwnds() { // v1.8.23: verbs that MUTATE a session's visible state → auto-flash the window so the // user knows the agent changed it. evaluate counts UNLESS {readOnly:true}.-const MUTATING_VERBS = new Set(['browser_open_window', 'browser_open_tab', 'browser_navigate', 'browser_reload', 'browser_back', 'browser_forward', 'browser_switch_tab', 'browser_click', 'browser_type', 'browser_press_key', 'browser_input_dispatch', 'browser_eval', 'browser_evaluate']);+const MUTATING_VERBS = new Set(['pup_open_window', 'pup_open_tab', 'pup_navigate', 'pup_reload', 'pup_back', 'pup_forward', 'pup_switch_tab', 'pup_click', 'pup_type', 'pup_press_key', 'pup_input_dispatch', 'pup_eval', 'pup_evaluate']); // Debounced auto-flash: one flash per window per ~5s burst of verbs, re-armed after // quiet. {silent:true} on the verb OR session._flashSilent (session default) suppresses.@@ -7180,7 +7180,7 @@ function scheduleAutoFlash(session, opts) { flashViaAD(session._sessionId, {}); // fire-and-forget (async → AD desktop_flash_window) } -// Record what the agent just did to a session, for lastAgentUpdate (browser_list_tabs /+// Record what the agent just did to a session, for lastAgentUpdate (pup_list_tabs / // list_windows / status) and the "● " title marker — so the user + Session Monitor can // see WHICH window/tab, WHAT verb, and the optional human note, WHEN. function recordAgentUpdate(session, verb, args, tabId) {@@ -7209,7 +7209,7 @@ function staleWindowsHint() { if (old.length === 0 && all.length < MANY) return ''; const show = (old.length ? old : all).sort((a, b) => b.ageMin - a.ageMin).slice(0, 6) .map(w => `${w.sid}${w.owner ? `(owner:${w.owner})` : ''}=${w.ageMin}m`).join(', ');- return ` 🧹 CLEANUP: ${all.length} pup window(s) open${old.length ? `, ${old.length} ≥${OLD_MIN}m old` : ''} — close the ones you're DONE with via browser_close_window so they don't pile up on the user's desktop (pup never auto-closes; that's your job when a task ends). Oldest: [${show}].`;+ return ` 🧹 CLEANUP: ${all.length} pup window(s) open${old.length ? `, ${old.length} ≥${OLD_MIN}m old` : ''} — close the ones you're DONE with via pup_close_window so they don't pile up on the user's desktop (pup never auto-closes; that's your job when a task ends). Oldest: [${show}].`; } catch (e) { return ''; } } @@ -7369,7 +7369,7 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer // targeted cure for the bug behind John's "can't scroll", "frozen", and the Bills thread's // silently-failing typing: Chrome's Win32 occlusion detector can latch a window as OCCLUDED and // never re-evaluate it. A window in that state reports visibilityState:'hidden' even while it is- // FRONTMOST, and Chrome then withholds BOTH paint and INPUT — measured directly: browser_eval+ // FRONTMOST, and Chrome then withholds BOTH paint and INPUT — measured directly: pup_eval // DOM writes still land (main thread runs) while CDP key events vanish (compositor wedged). Only // a real OS click cleared it, which is why pup windows felt broken in a way no verb could fix. // pup windows are parked at z-bottom BY DESIGN, so they hit this detector constantly — far more@@ -7517,9 +7517,9 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer // foreground on launch (and re-grab it whenever shown), so minimizing/z-ordering // AFTER the fact loses the race and the window flashes over the user's work. The // reliable fix: launch it OFF-SCREEN (far past any monitor). CDP still renders it,- // so browser_screenshot/eval/record all work, but the user NEVER sees it pop up and+ // so pup_screenshot/eval/record all work, but the user NEVER sees it pop up and // it never covers their active window. foreground:true launches maximized on-screen- // as before. browser_raise_os_window moves it on-screen when the user asks to watch.+ // as before. pup_raise_os_window moves it on-screen when the user asks to watch. // v1.8.51: `--no-startup-window` is THE fix for the focus-steal war. Windows only grants // a process the right to take the foreground at launch. With this flag the browser // process starts with NO window at all — the window is created LATER via CDP (newPage),@@ -7735,7 +7735,7 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer // (v1.9.90) retired that doctrine — I removed the same kick from chrome.warmup() then and MISSED // this one, so a native launch still started a ~150 MB CfT download nobody asked for. Worse, it // was actively BLOCKING: while that download is in flight, chrome.readiness() reported the whole- // browser subsystem as not-ready (see the `ready` fix in chrome.js), and browser_open_window's+ // browser subsystem as not-ready (see the `ready` fix in chrome.js), and pup_open_window's // gate refuses on `!rd.ready` — so a stalled prewarm made EVERY open fail with // chrome_for_testing_installing on a machine that had a perfectly good system Chrome AND Edge. // Measured live on H2O (AD 1.9.177): readiness simultaneously reported browserSource:"system"@@ -7756,15 +7756,15 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer const cdet = chrome.detectChrome(); if (!candidates.length && !cdet.installed) { const e = new Error('No Chrome/Edge on this machine and Chrome for Testing is not installed — pup cannot launch a browser yet.');- e.errorCode = 'no_browser_available';- e._hint = 'This box has no installed Chrome or Edge and no cached Chrome for Testing. Fastest fix: have the user install Chrome or Edge (Edge ships with Windows) — pup uses it instantly, no download. Or call browser_prewarm / browser_use {browser:"cft"} to fetch Chrome for Testing (~150 MB, one time), poll browser_readiness until ready:true, then retry.';+ e.errorCode = 'no_pup_available';+ e._hint = 'This box has no installed Chrome or Edge and no cached Chrome for Testing. Fastest fix: have the user install Chrome or Edge (Edge ships with Windows) — pup uses it instantly, no download. Or call pup_prewarm / pup_use {browser:"cft"} to fetch Chrome for Testing (~150 MB, one time), poll pup_readiness until ready:true, then retry.'; throw e; } const e = new Error( `Every detected browser failed to spawn for profile "${profileName}" (${MAX_ATTEMPTS} attempts each). Tried — ${triedTxt}.` );- e.errorCode = 'browser_launch_failed';- e._hint = `pup tried every browser it found and none would launch (usually low memory, a locked/busy profile, or corrupt installs). Tried: ${triedList.map(t => `${t.kind}(${t.source})`).join(', ')}. Retry once; if it persists, pin a specific browser with browser_use {browser:"chrome"|"edge"|"cft"}, or surface the last error to the user.`;+ e.errorCode = 'pup_launch_failed';+ e._hint = `pup tried every browser it found and none would launch (usually low memory, a locked/busy profile, or corrupt installs). Tried: ${triedList.map(t => `${t.kind}(${t.source})`).join(', ')}. Retry once; if it persists, pin a specific browser with pup_use {browser:"chrome"|"edge"|"cft"}, or surface the last error to the user.`; throw e; } }@@ -7783,7 +7783,7 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer // Clean up when browser is closed externally (user closed Chrome, crashed, etc.) // v1.5.1+: see handleBrowserDisconnect for rationale. Sessions are kept- // in the in-memory Map + on disk so browser_rescan / health-check can+ // in the in-memory Map + on disk so pup_rescan / health-check can // recover the still-alive Chrome window. Old behavior (delete everything) // hijacked unrelated windows on subsequent calls. browser.on('disconnected', () => handleBrowserDisconnect(browser, profileName));@@ -8188,7 +8188,7 @@ async function launchSession(sessionId, url, { freshProfile = false, profile, st // Persist to disk for recovery after bridge restart saveSessionFile(sessionId, profileName, url); - // Stash one-shot launch metadata so the browser_open_window response can+ // Stash one-shot launch metadata so the pup_open_window response can // surface it. Cleared after first read in getSessionInfo. session.lastLaunchMeta = { restoredTabsClosed }; @@ -8201,7 +8201,7 @@ async function launchSession(sessionId, url, { freshProfile = false, profile, st async function addTabToSession(sessionId, url) { const session = sessions.get(sessionId); if (!session) {- throw new Error(`Session "${sessionId}" not found. Use browser_open_window first.`);+ throw new Error(`Session "${sessionId}" not found. Use pup_open_window first.`); } const be = browsers.get(session.profileName); if (!be || !be.browser.isConnected()) {@@ -8269,7 +8269,7 @@ async function addTabToSession(sessionId, url) { } return Promise.race([ be.browser.newPage(),- new Promise((_, rej) => setTimeout(() => rej(new Error('newPage timed out — the browser did not create the tab in 20s. Retry browser_open_tab; if it persists, browser_close_window + browser_open_window.')), 20000)),+ new Promise((_, rej) => setTimeout(() => rej(new Error('newPage timed out — the browser did not create the tab in 20s. Retry pup_open_tab; if it persists, pup_close_window + pup_open_window.')), 20000)), ]); }); _profileOpenChain.set(session.profileName, _tabCur.catch(() => {}));@@ -8281,7 +8281,7 @@ async function addTabToSession(sessionId, url) { // persistent evaluateOnNewDocument injector in attachTab (already installed above). // We deliberately do NOT install a second, per-tab MutationObserver here — two // observers appending different suffixes duel forever and hang the tab (the bug this- // release fixes). tabIds are surfaced via browser_list_tabs, not the title.+ // release fixes). tabIds are surfaced via pup_list_tabs, not the title. try { if (url) { try {@@ -8326,7 +8326,7 @@ function isSessionAlive(s) { // `_lostBrowser` guard (unlike resolveSession, which deliberately keeps disconnected entries for // rescan). So after a transient disconnect, ONE call to this function silently wiped the ENTIRE // session map — destroying the exact entries handleBrowserDisconnect had just preserved and told the-// user to recover with browser_rescan. That is the reported bug: browser_list_windows returns 0+// user to recover with pup_rescan. That is the reported bug: pup_list_windows returns 0 // windows while the windows are still open and drivable by sessionId, and rescan can't help because // the sessionId→profile records it needs are gone from memory. Mark, don't delete; removal belongs to // the close and rescan paths.@@ -8334,7 +8334,7 @@ function getActiveSession() { if (activeSessionId && sessions.has(activeSessionId)) { const s = sessions.get(activeSessionId); if (isSessionAlive(s)) return s;- s._lostBrowser = true; // keep the entry so browser_rescan can recover it+ s._lostBrowser = true; // keep the entry so pup_rescan can recover it } for (const [id, s] of sessions) { if (isSessionAlive(s)) {@@ -8356,7 +8356,7 @@ function getActiveSession() { function resolveSession(args) { // Explicit sessionId path: don't fall through to a different session if // the requested one is missing or disconnected. v1.5.0 and earlier did,- // and that's how chip-fetcher's "browser_navigate landed on the wrong+ // and that's how chip-fetcher's "pup_navigate landed on the wrong // window" bug fired (caller passed the right sessionId, the bridge // silently retargeted to whatever it had). if (args.sessionId) {@@ -8369,7 +8369,7 @@ function resolveSession(args) { // Session entry exists but its browser is gone (CDP socket dropped, // browser process died, profile crashed). Caller will see a // structured error from resolveSessionOrError pointing at- // browser_rescan.+ // pup_rescan. } return null; }@@ -8390,7 +8390,7 @@ function resolveSession(args) { // with errorCode + currentSessions + _hint) when nothing resolves. // v1.5.1+: differentiates "session not found" from "session known but // browser disconnected" so callers / Claude can give the right next--// step recipe (browser_rescan vs browser_open_window).+// step recipe (pup_rescan vs pup_open_window). function resolveSessionOrError(args) { const resolved = resolveSession(args); if (resolved) return resolved;@@ -8416,15 +8416,15 @@ function resolveSessionOrError(args) { liveSessions, disconnectedSessions, _hint:- `Run \`browser_rescan\` to attempt automatic recovery. The 30s ` ++ `Run \`pup_rescan\` to attempt automatic recovery. The 30s ` + `health check will also auto-attempt, but rescan is faster + ` + `idempotent. If rescan reports the profile as unreachable, ` +- `Chrome itself probably crashed — relaunch with browser_open_window.`,+ `Chrome itself probably crashed — relaunch with pup_open_window.`, }; } else if (explicit) { // Caller asked for a specific sessionId we have NO record of. payload = {- error: `Session "${requested}" not found in bridge. Either it was never opened, it was closed via browser_close_window, or the bridge restarted and the on-disk session file was cleaned.`,+ error: `Session "${requested}" not found in bridge. Either it was never opened, it was closed via pup_close_window, or the bridge restarted and the on-disk session file was cleaned.`, errorCode: 'session_not_found', currentSessions, liveSessions,@@ -8432,25 +8432,25 @@ function resolveSessionOrError(args) { _hint: `Live sessions right now: [${liveSessions.join(', ') || '(none)'}]. ` + (disconnectedSessions.length > 0- ? `Disconnected sessions awaiting rescan: [${disconnectedSessions.join(', ')}] — try \`browser_rescan\` first. `+ ? `Disconnected sessions awaiting rescan: [${disconnectedSessions.join(', ')}] — try \`pup_rescan\` first. ` : '') + `If the Chrome window IS still on the desktop but the bridge can't see ` +- `it, run browser_rescan (it walks every Chrome target and recovers ` ++ `it, run pup_rescan (it walks every Chrome target and recovers ` + `sessions by parsing the (session: X) tag from each page's title). ` +- `If you need to start fresh, browser_open_window with the right sessionId.`,+ `If you need to start fresh, pup_open_window with the right sessionId.`, }; } else { // No sessionId passed and nothing is alive. payload = {- error: `No live browser sessions. Use browser_open_window first.`,+ error: `No live browser sessions. Use pup_open_window first.`, errorCode: 'no_session', currentSessions, liveSessions, disconnectedSessions, _hint: disconnectedSessions.length > 0- ? `Disconnected sessions exist on disk: [${disconnectedSessions.join(', ')}]. Run \`browser_rescan\` to see if any are recoverable before opening a new window.`- : `Open a Chrome window with: browser_open_window {"sessionId":"...","profile":"...","url":"..."}`,+ ? `Disconnected sessions exist on disk: [${disconnectedSessions.join(', ')}]. Run \`pup_rescan\` to see if any are recoverable before opening a new window.`+ : `Open a Chrome window with: pup_open_window {"sessionId":"...","profile":"...","url":"..."}`, }; } const err = new Error(JSON.stringify(payload));@@ -8475,7 +8475,7 @@ function sendSessionResolveError(res, e) { // v1.6.1+: the AUTO-RECOVER path. Tries every layer of recovery before // surfacing a session-not-found error to the caller. Used by every-// browser_* dispatch site that previously called resolveSessionOrError.+// pup_* dispatch site that previously called resolveSessionOrError. // // Recovery ladder: // (a) Session is alive → return immediately.@@ -8497,7 +8497,7 @@ function sendSessionResolveError(res, e) { // session_disconnected error from resolveSessionOrError so the // caller (or its higher-level retry logic) gets a clear signal. //-// The Docker container's contract: `browser_navigate / eval / screenshot+// The Docker container's contract: `pup_navigate / eval / screenshot // / input_dispatch / errors / reload / fetch_url` should NEVER surface // "Session closed" / "detached Frame" / "Browser disconnected" — those // become internal retry signals, not user-facing errors.@@ -8577,7 +8577,7 @@ async function recoverOrRelaunchSession(args) { } // (e) Total failure — surface the structured error so the caller can- // tell the user (or escalate to browser_open_window with explicit URL).+ // tell the user (or escalate to pup_open_window with explicit URL). return resolveSessionOrError(args); } @@ -8605,7 +8605,7 @@ async function withTimeout(promise, ms, label) { // and try to recover. // // - Sessions whose browser is gone get marked _lostBrowser; their entries-// stay in the Map and on disk so browser_rescan / next health check+// stay in the Map and on disk so pup_rescan / next health check // can recover them. Only delete a session file when its underlying // Chrome PID is confirmed dead. //@@ -8658,39 +8658,39 @@ setInterval(async () => { }, 30_000); // v1.0.16: the single source of truth for per-verb HINTS — used BOTH by-// `browser_describe` (so AD's Verbs tab shows hint/related/pitfalls under each-// verb) AND by sendJSON's runtime injector below (so EVERY browser_* call+// `pup_describe` (so AD's Verbs tab shows hint/related/pitfalls under each+// verb) AND by sendJSON's runtime injector below (so EVERY pup_* call // self-documents inline, even for an AI that never read describe). This is the // reference shape the cloud-owned bridges (kicad/fusion/native-browser) were // asked to adopt: a calling AI should never have to guess and never have to // read docs first. { verb: { hint, related[], pitfalls[] } }. const VERB_META = {- browser_open_window: { hint: 'Opens a generic, fully-controllable pup window (a fresh isolated Chrome/Edge process — never your real signed-in browser) that you then drive by sessionId. To drive the user\'s REAL Chrome/Edge, use the Adom browser extension\'s nbrowser_* verbs instead.', related: ['browser_navigate', 'browser_list_windows', 'browser_close_window'], pitfalls: ['sessionId is OPTIONAL: omit it and your thread gets its OWN window (keyed on your --ai-thread name; later opens become tabs in it). Pass one only for a deliberate separate window — it is then your handle for every later call', 'profile defaults to the durable shared \'adom-you\' profile (logins/localStorage persist + are shared across all pup windows); isolated:true for a private throwaway — omit it for a plain "open in pup"', 'sessionIds are SHARED across all AI threads on this desktop — a generic id ("wiki", "test") may collide with another thread\'s window and navigating it away steals their work. Use a task-prefixed sessionId + pass owner:"<your-task>" so pup can protect it', 'reusing an existing sessionId with a new url NAVIGATES that window — check browser_status (shows owner per window) first if unsure'] },- browser_open_tab: { hint: 'Adds a tab to an existing session; returns the new tabId.', related: ['browser_switch_tab', 'browser_list_tabs', 'browser_close_tab'], pitfalls: ['the session must already exist (browser_open_window first)'] },- browser_close_tab: { hint: 'Closes one tab; the session stays open.', related: ['browser_close_window', 'browser_list_tabs'], pitfalls: ['omit tabId to close the active tab — closing the last tab may close the window'] },- browser_close_window: { hint: 'Closes the whole session and all its tabs. Destructive.', related: ['browser_list_windows'], pitfalls: ['this ends the session — its sessionId is no longer valid afterward'] },- browser_navigate: { hint: 'Drives the active tab to url and waits for load. Verify with a screenshot before asserting success.', related: ['browser_screenshot', 'browser_wait', 'browser_reload'], pitfalls: ['navigation can race SPA routing — confirm with browser_screenshot', 'some sites need browser_wait for a selector after navigate'] },- browser_back: { hint: 'History back on the active tab.', related: ['browser_forward', 'browser_navigate'], pitfalls: [] },- browser_forward: { hint: 'History forward on the active tab.', related: ['browser_back', 'browser_navigate'], pitfalls: [] },- browser_reload: { hint: 'Reloads the active tab.', related: ['browser_navigate', 'browser_wait'], pitfalls: [] },- browser_click: { hint: 'Clicks by CSS selector (preferred) or by x,y coords. Selector is more robust across layouts.', related: ['browser_type', 'browser_input_dispatch', 'browser_screenshot'], pitfalls: ['selector must match exactly one visible element — verify with browser_evaluate first', 'coords are viewport-relative; a scrolled element may be off-screen'] },- browser_type: { hint: 'Types into the focused or selector-targeted field.', related: ['browser_click', 'browser_press_key'], pitfalls: ['focus the field first (browser_click the input) or pass selector', 'does not clear existing text — select-all + delete if you need a clean field'] },- browser_press_key: { hint: 'Presses a key or chord (Enter, Ctrl+A, Tab).', related: ['browser_type', 'browser_input_dispatch'], pitfalls: ['chord syntax is "Ctrl+A" — verify the page has focus on the intended element'] },- browser_screenshot: { hint: 'PNG of the page. Your primary way to SEE state — call it after navigate/click to confirm.', related: ['browser_screenshot_full_res', 'browser_navigate'], pitfalls: ['default is the viewport; pass fullPage:true for the whole scroll height'] },- browser_evaluate: { hint: 'Runs JS in the page and returns the result — use it to read DOM state or verify selectors before acting.', related: ['browser_click', 'browser_fetch_url'], pitfalls: ['the expression must be serializable JSON in its return value', 'runs in the page context, not Node'] },- browser_fetch_url: { hint: 'Fetches a URL using the tab\'s live cookies/session — for authed APIs the bare CLI can\'t reach. Returns bodyBase64.', related: ['browser_navigate', 'browser_evaluate'], pitfalls: ['for session-scoped URLs pass the tabId whose cookies you need', 'returns base64 — decode it caller-side'] },- browser_input_dispatch: { hint: 'Low-level CDP mouse/keyboard input at coords — beats some overlays/iframes that swallow synthetic clicks.', related: ['browser_click', 'browser_press_key'], pitfalls: ['coords are viewport pixels', 'last-resort when browser_click can\'t reach the element — still cannot pass Cloudflare interactive challenges'] },- browser_record_start: { hint: 'Starts video recording of the page for this session.', related: ['browser_record_stop', 'browser_record_status'], pitfalls: ['one recording per session — stop the current one before starting another'] },- browser_record_stop: { hint: 'Stops recording and returns the saved file path.', related: ['browser_record_start', 'browser_record_list'], pitfalls: ['no-op if nothing is recording for this session'] },- browser_record_status: { hint: 'Whether a recording is active for the session.', related: ['browser_record_start', 'browser_record_stop'], pitfalls: [] },- browser_record_list: { hint: 'Lists saved page recordings.', related: ['browser_record_start'], pitfalls: [] },+ pup_open_window: { hint: 'Opens a generic, fully-controllable pup window (a fresh isolated Chrome/Edge process — never your real signed-in browser) that you then drive by sessionId. To drive the user\'s REAL Chrome/Edge, use the Adom browser extension\'s nbrowser_* verbs instead.', related: ['pup_navigate', 'pup_list_windows', 'pup_close_window'], pitfalls: ['sessionId is OPTIONAL: omit it and your thread gets its OWN window (keyed on your --ai-thread name; later opens become tabs in it). Pass one only for a deliberate separate window — it is then your handle for every later call', 'profile defaults to the durable shared \'adom-you\' profile (logins/localStorage persist + are shared across all pup windows); isolated:true for a private throwaway — omit it for a plain "open in pup"', 'sessionIds are SHARED across all AI threads on this desktop — a generic id ("wiki", "test") may collide with another thread\'s window and navigating it away steals their work. Use a task-prefixed sessionId + pass owner:"<your-task>" so pup can protect it', 'reusing an existing sessionId with a new url NAVIGATES that window — check pup_status (shows owner per window) first if unsure'] },+ pup_open_tab: { hint: 'Adds a tab to an existing session; returns the new tabId.', related: ['pup_switch_tab', 'pup_list_tabs', 'pup_close_tab'], pitfalls: ['the session must already exist (pup_open_window first)'] },+ pup_close_tab: { hint: 'Closes one tab; the session stays open.', related: ['pup_close_window', 'pup_list_tabs'], pitfalls: ['omit tabId to close the active tab — closing the last tab may close the window'] },+ pup_close_window: { hint: 'Closes the whole session and all its tabs. Destructive.', related: ['pup_list_windows'], pitfalls: ['this ends the session — its sessionId is no longer valid afterward'] },+ pup_navigate: { hint: 'Drives the active tab to url and waits for load. Verify with a screenshot before asserting success.', related: ['pup_screenshot', 'pup_wait', 'pup_reload'], pitfalls: ['navigation can race SPA routing — confirm with pup_screenshot', 'some sites need pup_wait for a selector after navigate'] },+ pup_back: { hint: 'History back on the active tab.', related: ['pup_forward', 'pup_navigate'], pitfalls: [] },+ pup_forward: { hint: 'History forward on the active tab.', related: ['pup_back', 'pup_navigate'], pitfalls: [] },+ pup_reload: { hint: 'Reloads the active tab.', related: ['pup_navigate', 'pup_wait'], pitfalls: [] },+ pup_click: { hint: 'Clicks by CSS selector (preferred) or by x,y coords. Selector is more robust across layouts.', related: ['pup_type', 'pup_input_dispatch', 'pup_screenshot'], pitfalls: ['selector must match exactly one visible element — verify with pup_evaluate first', 'coords are viewport-relative; a scrolled element may be off-screen'] },+ pup_type: { hint: 'Types into the focused or selector-targeted field.', related: ['pup_click', 'pup_press_key'], pitfalls: ['focus the field first (pup_click the input) or pass selector', 'does not clear existing text — select-all + delete if you need a clean field'] },+ pup_press_key: { hint: 'Presses a key or chord (Enter, Ctrl+A, Tab).', related: ['pup_type', 'pup_input_dispatch'], pitfalls: ['chord syntax is "Ctrl+A" — verify the page has focus on the intended element'] },+ pup_screenshot: { hint: 'PNG of the page. Your primary way to SEE state — call it after navigate/click to confirm.', related: ['pup_screenshot_full_res', 'pup_navigate'], pitfalls: ['default is the viewport; pass fullPage:true for the whole scroll height'] },+ pup_evaluate: { hint: 'Runs JS in the page and returns the result — use it to read DOM state or verify selectors before acting.', related: ['pup_click', 'pup_fetch_url'], pitfalls: ['the expression must be serializable JSON in its return value', 'runs in the page context, not Node'] },+ pup_fetch_url: { hint: 'Fetches a URL using the tab\'s live cookies/session — for authed APIs the bare CLI can\'t reach. Returns bodyBase64.', related: ['pup_navigate', 'pup_evaluate'], pitfalls: ['for session-scoped URLs pass the tabId whose cookies you need', 'returns base64 — decode it caller-side'] },+ pup_input_dispatch: { hint: 'Low-level CDP mouse/keyboard input at coords — beats some overlays/iframes that swallow synthetic clicks.', related: ['pup_click', 'pup_press_key'], pitfalls: ['coords are viewport pixels', 'last-resort when pup_click can\'t reach the element — still cannot pass Cloudflare interactive challenges'] },+ pup_record_start: { hint: 'Starts video recording of the page for this session.', related: ['pup_record_stop', 'pup_record_status'], pitfalls: ['one recording per session — stop the current one before starting another'] },+ pup_record_stop: { hint: 'Stops recording and returns the saved file path.', related: ['pup_record_start', 'pup_record_list'], pitfalls: ['no-op if nothing is recording for this session'] },+ pup_record_status: { hint: 'Whether a recording is active for the session.', related: ['pup_record_start', 'pup_record_stop'], pitfalls: [] },+ pup_record_list: { hint: 'Lists saved page recordings.', related: ['pup_record_start'], pitfalls: [] }, desktop_recorder_open: { hint: 'Opens the desktop screen-recorder UI window.', related: ['desktop_record_start', 'desktop_recorder_close'], pitfalls: [] }, desktop_record_start: { hint: 'Starts a full desktop screen recording.', related: ['desktop_record_stop', 'desktop_record_status'], pitfalls: ['only one desktop recording at a time'] }, desktop_record_stop: { hint: 'Stops the desktop recording and returns the file.', related: ['desktop_record_start', 'desktop_record_list'], pitfalls: [] },- browser_readiness: { hint: 'Cold-start readiness probe: {ready, chromeForTestingInstalled, installing, installProgressPct}. On a fresh PC, poll this until ready:true before browser_open_window — the bridge auto-installs Chrome for Testing on its own.', related: ['browser_prewarm', 'browser_open_window'], pitfalls: ['ready:false with installing:true just means Chrome for Testing is still downloading — wait + poll, do NOT treat it as an error', 'installPhase:"failed" → surface lastError to the user (usually network/proxy blocking the download, or low disk)'] },- browser_prewarm: { hint: 'Installs Chrome for Testing (the browser pup drives) WITHOUT opening a window — the warm-before-first-use path. Returns fast; poll browser_readiness for completion.', related: ['browser_readiness', 'browser_open_window'], pitfalls: ['the ~150 MB download needs network access to storage.googleapis.com', 'does not open a window — call browser_open_window once readiness reports ready:true'] },- browser_import_scan: { hint: 'READ-ONLY discovery for the comprehensive credential import (adom-you seed): enumerates every profile of every Chromium browser (Chrome/Edge/Brave) with its source account email(s), saved-password count, and ABE status. Decrypts NOTHING — actual decryption needs the AD-core elevated SYSTEM helper (adom/adom-desktop #51).', related: ['browser_open_window'], pitfalls: ['Windows-only', 'passwordCount is null if python is not on PATH', 'ABE (v20) passwords cannot be decrypted by this user process — that is the AD SYSTEM helper’s job'] },- browser_describe: { hint: 'This list — the bridge\'s verbs + schemas + hints, for AD\'s Verbs tab. Read it once to learn the surface.', related: ['browser_status'], pitfalls: [] },+ pup_readiness: { hint: 'Cold-start readiness probe: {ready, chromeForTestingInstalled, installing, installProgressPct}. On a fresh PC, poll this until ready:true before pup_open_window — the bridge auto-installs Chrome for Testing on its own.', related: ['pup_prewarm', 'pup_open_window'], pitfalls: ['ready:false with installing:true just means Chrome for Testing is still downloading — wait + poll, do NOT treat it as an error', 'installPhase:"failed" → surface lastError to the user (usually network/proxy blocking the download, or low disk)'] },+ pup_prewarm: { hint: 'Installs Chrome for Testing (the browser pup drives) WITHOUT opening a window — the warm-before-first-use path. Returns fast; poll pup_readiness for completion.', related: ['pup_readiness', 'pup_open_window'], pitfalls: ['the ~150 MB download needs network access to storage.googleapis.com', 'does not open a window — call pup_open_window once readiness reports ready:true'] },+ pup_import_scan: { hint: 'READ-ONLY discovery for the comprehensive credential import (adom-you seed): enumerates every profile of every Chromium browser (Chrome/Edge/Brave) with its source account email(s), saved-password count, and ABE status. Decrypts NOTHING — actual decryption needs the AD-core elevated SYSTEM helper (adom/adom-desktop #51).', related: ['pup_open_window'], pitfalls: ['Windows-only', 'passwordCount is null if python is not on PATH', 'ABE (v20) passwords cannot be decrypted by this user process — that is the AD SYSTEM helper’s job'] },+ pup_describe: { hint: 'This list — the bridge\'s verbs + schemas + hints, for AD\'s Verbs tab. Read it once to learn the surface.', related: ['pup_status'], pitfalls: [] }, }; // ─── Bug-report invitation (v1.9.95, John's ask) ──────────────────────────────@@ -8838,7 +8838,7 @@ function wikiUrlLint(u) { // - only on real http(s) pages, // - FILL ONLY, never auto-submit (auto-submitting on load risks MFA/lockouts/wrong-form logins), // - never throws — autofill must never block or fail a navigation,-// - reported to the caller as _autoLogin, and silenceable with browser_configure {autoLogin:"off"}.+// - reported to the caller as _autoLogin, and silenceable with pup_configure {autoLogin:"off"}. async function autoLoginOnLoad(res, page) { try { if (!res || !page) return;@@ -8852,7 +8852,7 @@ async function autoLoginOnLoad(res, page) { if (!creds) return; const r = await credentialVault.fillLoginForm(page, { username: creds.username, password: creds.password, submit: false }); if (r && r.ok && r.filledPassword) {- res._autoLoginNote = `pup AUTO-FILLED this login form for "${creds.host}" from your saved credential (username ${creds.username}) — in the BACKGROUND, NOT submitted, no Chrome dropdown, no foregrounding. To sign in: browser_login {sessionId,submit:true} or click the submit button yourself. Wrong form? browser_configure {autoLogin:"off"} turns this off.`;+ res._autoLoginNote = `pup AUTO-FILLED this login form for "${creds.host}" from your saved credential (username ${creds.username}) — in the BACKGROUND, NOT submitted, no Chrome dropdown, no foregrounding. To sign in: pup_login {sessionId,submit:true} or click the submit button yourself. Wrong form? pup_configure {autoLogin:"off"} turns this off.`; } } catch (e) { /* autofill is best-effort; never disturb the navigation */ } }@@ -8891,7 +8891,7 @@ async function probeRender(page, httpStatus) { } catch (e) { out.signals.push('probe_failed'); } return out; }-const RENDER_VERBS = new Set(['browser_open_window', 'browser_open_tab', 'browser_navigate', 'browser_reload']);+const RENDER_VERBS = new Set(['pup_open_window', 'pup_open_tab', 'pup_navigate', 'pup_reload']); // A URL that is almost certainly the caller's OWN work-in-progress (a dev server, a local file, an // Adom-hosted app) earns the strongest wording — that is exactly the case John described. function looksLikeOwnContent(u) {@@ -8915,7 +8915,7 @@ function renderVerifyHint(sessionId, url, rc) { const lint = wikiUrlLint(url); if (lint) { return 'THIS PAGE DID NOT RENDER CORRECTLY (' + sig + '). ' + lint- + ' See it yourself: `adom-desktop --ai-thread "<your thread>" browser_screenshot {"sessionId":"'+ + ' See it yourself: `adom-desktop --ai-thread "<your thread>" pup_screenshot {"sessionId":"' + sid + '"}`. Do NOT report success, and do NOT report a conclusion about permissions or login' + ' until the URL is owner-qualified and you have re-checked.'; }@@ -8923,22 +8923,22 @@ function renderVerifyHint(sessionId, url, rc) { const ex = rc.excerpt ? ' The page says: "' + String(rc.excerpt).replace(/\s+/g, ' ').slice(0, 140) + '".' : ''; return 'THIS PAGE DID NOT RENDER CORRECTLY — pup checked it, so do NOT report success. Detected: ' + sig + st + '.' + ex- + ' See it yourself: `adom-desktop --ai-thread "<your thread>" browser_screenshot {"sessionId":"' + sid + '"}`.'+ + ' See it yourself: `adom-desktop --ai-thread "<your thread>" pup_screenshot {"sessionId":"' + sid + '"}`.' + ' Then FIX THE CAUSE: a 404 or "Cannot GET" usually means the dev server is not running, is on a' + ' different port, or the path is wrong; unstyled HTML means the CSS 404d; a stack trace means the'- + ' app threw. After fixing, `browser_reload` and re-check until this response comes back clean.'+ + ' app threw. After fixing, `pup_reload` and re-check until this response comes back clean.' + ' Do not tell the user it works while this warning is present.'; } const lead = own ? 'THIS LOOKS LIKE CONTENT YOU JUST BUILT OR EDITED — SCREENSHOT IT AND LOOK BEFORE YOU CLAIM IT WORKS.' : 'VERIFY WHAT YOU JUST PUT ON SCREEN — do not assume it rendered.'; return `${lead} ok:true means the NAVIGATION succeeded; it says NOTHING about whether the page actually painted. `- + `Take a screenshot and READ the image: \`adom-desktop --ai-thread "<your thread>" browser_screenshot {"sessionId":"${sid}"}\` — it returns a real PNG path you can open. `+ + `Take a screenshot and READ the image: \`adom-desktop --ai-thread "<your thread>" pup_screenshot {"sessionId":"${sid}"}\` — it returns a real PNG path you can open. ` + `Look for: a blank or white page, an unstyled flash of plain HTML (CSS never loaded), a stack trace or framework error overlay, ` + `missing images, a broken or overlapping layout, content cut off past the fold, a dev-server "Cannot GET /" page, or the wrong build entirely. `- + `If it is wrong: fix the source, \`browser_reload\`, and screenshot AGAIN — ralph-test that loop until the render is genuinely correct, `- + `rather than reporting success after one open. If the viewport is too small to judge, browser_set_viewport first; `- + `for a long page use browser_screenshot {fullPage:true}. Showing the user a screenshot of their own working app is also the fastest way to prove it works.`;+ + `If it is wrong: fix the source, \`pup_reload\`, and screenshot AGAIN — ralph-test that loop until the render is genuinely correct, `+ + `rather than reporting success after one open. If the viewport is too small to judge, pup_set_viewport first; `+ + `for a long page use pup_screenshot {fullPage:true}. Showing the user a screenshot of their own working app is also the fastest way to prove it works.`; } let _bugInviteCalls = 0; let _bugInviteFirstDone = false;@@ -8958,7 +8958,7 @@ function maybeAttachBugInvite(obj, verb) { // feature simply never appeared): // 1. AD probes /health (+ /status for the bridge chip) every ~15s — those replies flow through // this same sendJSON chokepoint. Excluded by requiring a `verb` at all.- // 2. AD also calls pup's DECLARED statusVerb (browser_readiness) through /command on spawn, so+ // 2. AD also calls pup's DECLARED statusVerb (pup_readiness) through /command on spawn, so // it arrives as a real verb and consumed the invite anyway (measured twice: the first verb // after a restart still carried nothing). // So the orientation invite now fires on the first SUBSTANTIVE verb — an open/drive, the moment@@ -8967,8 +8967,8 @@ function maybeAttachBugInvite(obj, verb) { // whatever the verb, because that is when the evidence exists. if (!verb) return; const PROBE_VERBS = new Set([- 'browser_readiness', 'browser_status', 'browser_describe', 'browser_prewarm',- 'browser_list_windows', 'browser_list_tabs', 'browser_rescan', 'browser_configure', 'browser_import_scan',+ 'pup_readiness', 'pup_status', 'pup_describe', 'pup_prewarm',+ 'pup_list_windows', 'pup_list_tabs', 'pup_rescan', 'pup_configure', 'pup_import_scan', ]); const isProbe = PROBE_VERBS.has(verb); let mode = 'on';@@ -8999,7 +8999,7 @@ function sendJSON(res, obj, status = 200) { try { // v1.0.16: runtime _hint injection — if this response is for a documented // verb and the handler didn't already set its own _hint, attach the verb's- // canonical hint (+ related) from VERB_META. So EVERY browser_* call carries+ // canonical hint (+ related) from VERB_META. So EVERY pup_* call carries // actionable guidance inline, matching the contract AD asks all bridges for. maybeAttachBugInvite(obj, res && res._adomCommand); if (obj && typeof obj === 'object' && !Array.isArray(obj)) {@@ -9015,7 +9015,7 @@ function sendJSON(res, obj, status = 200) { // v1.9.98 — CRITICAL DELIVERY FIX. Anything attached to the OUTER envelope is DROPPED on a // SUCCESSFUL call: the conventional success shape is {success:true, output:"<json string>"}, and // AD/the CLI unwrap `output` into the top level and discard the rest of the envelope. Measured:- // a browser_navigate success came back as {durationMs, ok, sessionId, status, tabId, url} — no+ // a pup_navigate success came back as {durationMs, ok, sessionId, status, tabId, url} — no // _hint, no _next, no _reportIssues, even though sendJSON had set them. (Failures survive because // their handlers set those fields on the outer object AND AD preserves the envelope on an error.) // So the VERB_META hints and the bug-report invitation were only ever reaching callers on FAILURE@@ -9047,7 +9047,7 @@ function sendJSON(res, obj, status = 200) { } } catch (e) {} // v1.9.109: on ANY render verb, a detected failure must lead the hint the caller reads,- // not just browser_open_window's (which composes its own). reload/navigate get it here.+ // not just pup_open_window's (which composes its own). reload/navigate get it here. if (_rc2 && _rc2.ok === false && (_rc2.signals || []).length) { const _st = (typeof _rc2.httpStatus === 'number') ? ' (HTTP ' + _rc2.httpStatus + ')' : ''; const _lead = '\u26D4 THE PAGE IS STILL BROKEN: ' + _rc2.signals.join(', ') + _st@@ -9081,7 +9081,7 @@ function readBody(req) { } // v1.9.172 (jam fix): a pure IN-MEMORY snapshot of a session — NO CDP, so it can never hang.-// browser_list_windows falls back to this when a window is too busy (Chrome under load) to answer+// pup_list_windows falls back to this when a window is too busy (Chrome under load) to answer // a live CDP read in time, so the list ALWAYS returns instead of stalling to null ("pup looks // frozen"). Values come from the last successful read cached on the session. function getSessionInfoFast(sessionId, session) {@@ -9115,7 +9115,7 @@ async function getSessionInfo(sessionId, session) { try { url = session.page?.url() || null; // synchronous (puppeteer-cached frame URL) — never hangs // page.title() is a CDP round-trip that HANGS on a jammed/busy page. Cap it hard and fall back- // to the last-known title so a single busy window can never stall browser_list_windows (v1.9.172).+ // to the last-known title so a single busy window can never stall pup_list_windows (v1.9.172). if (session.page) title = await withTimeout(session.page.title(), 1200, 'title').catch(() => session._lastTitle || null); } catch (e) {} if (url) session._lastUrl = url; // cache for the degraded/fast path@@ -9205,7 +9205,7 @@ function ownershipRefusal(session, sessionId, args, verb) { errorCode: 'session_owned_by_another_thread', error: `Session "${sessionId}" belongs to another thread (owner "${session.owner}", opened ${age} min ago, currently showing ${curUrl}).`, sessionId, owner: session.owner, requestedBy: caller, currentUrl: curUrl, ageMinutes: age,- _hint: `STOP — this window belongs to AI thread "${session.owner}", not you ("${caller}"). ${verb} on it would break that thread's work. Open your OWN window instead: browser_open_window with a UNIQUE task-prefixed sessionId (e.g. "${caller}-web") and owner:"${caller}". Only pass takeover:true if the user EXPLICITLY told you to repurpose this window. browser_status lists every window + its owner.`,+ _hint: `STOP — this window belongs to AI thread "${session.owner}", not you ("${caller}"). ${verb} on it would break that thread's work. Open your OWN window instead: pup_open_window with a UNIQUE task-prefixed sessionId (e.g. "${caller}-web") and owner:"${caller}". Only pass takeover:true if the user EXPLICITLY told you to repurpose this window. pup_status lists every window + its owner.`, }; } @@ -9750,7 +9750,7 @@ const SETTINGS_DEF=[ {k:'notMineCaption',l:'"Not foregrounded by Adom" caption',d:'Low importance. When a window comes to the front on its own (JavaScript, a focus change, Chrome), briefly caption that Adom did NOT do it. It stays SILENT when YOU raised the window (detected via recent taskbar/keyboard input), so your own clicks never trigger it. Default on.'}, {k:'minimalTouch',l:'Minimal-touch mode',d:'The safety mode from the Chrome crash hunt. ON means pup keeps its hands off your windows: it only opens them and drives the pages inside, and skips the extra Windows machinery (identity stamps, window sweeps, shell churn) that was suspected of freezing Chrome. The toggles above are the real per-feature controls now and they win over this switch. Rule of thumb: leave this ON; if pup ever gets crashy, make sure it is ON and turn features back on one at a time to find the culprit.'}, ];-// Import-logins picker (the adom-you seed). Discovery runs today via browser_import_scan; the+// Import-logins picker (the adom-you seed). Discovery runs today via pup_import_scan; the // actual decrypt is AD-core (adom/adom-desktop #51) so the Import button reports honest-pending. let _importScan=null; let _importSel={}; function _idTag(a){return '<span style="display:inline-block;background:rgba(120,150,205,.22);border-radius:4px;padding:1px 6px;margin:0 4px 2px 0;font-size:11px;white-space:nowrap">'+esc(a)+'</span>';}@@ -9963,7 +9963,7 @@ const server = http.createServer(async (req, res) => { const act = a.action, sid = a.sessionId, tid = a.tabId; const done = (o2) => { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify(o2)); }; try {- // Closing = close every tab in the session (the same effect browser_close_window has); the+ // Closing = close every tab in the session (the same effect pup_close_window has); the // session is then reaped by the existing zero-tab cleanup, so there is no second code path. const closeOne = async (id) => { const sess = sessions.get(id);@@ -9994,13 +9994,13 @@ const server = http.createServer(async (req, res) => { if (act === 'import-scan') { // Dashboard "Import logins" picker → self-call the read-only discovery verb. try {- const r = await fetch(`http://127.0.0.1:${PORT}/command`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'browser_import_scan', args: {} }) });+ const r = await fetch(`http://127.0.0.1:${PORT}/command`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'pup_import_scan', args: {} }) }); const j = await r.json().catch(() => ({})); return done({ ok: !!j.success, profiles: j.profiles || [], totalPasswords: j.totalPasswords || 0, accounts: j.accounts || [], pythonAvailable: !!j.pythonAvailable }); } catch (e) { return done({ ok: false, error: e.message }); } } if (act === 'import-run') {- // AD >= 1.9.220 ships desktop_decrypt_browser_credentials (the SYSTEM-elevated ABE decrypt,+ // AD >= 1.9.220 ships desktop_decrypt_pup_credentials (the SYSTEM-elevated ABE decrypt, // adom/adom-desktop #51). It MUST be called LOCALLY: chrome.adCommand routes over AD's // direct API (127.0.0.1), which is exactly the loopback path AD requires — a relayed call is // refused with errorCode:relay_forbidden and pops no UAC. AD fires its OWN consent dialog +@@ -10013,7 +10013,7 @@ const server = http.createServer(async (req, res) => { console.log(`[import] decrypt requested for ${sel.length} profile(s) across ${browsers.join(', ')} — calling AD helper locally (consent + UAC will prompt on the desktop)`); let resp; try {- resp = await chrome.adCommand('desktop_decrypt_browser_credentials',+ resp = await chrome.adCommand('desktop_decrypt_pup_credentials', { browsers, include: ['passwords'] }, { timeoutMs: 180000 }); } catch (e) { return done({ ok: false, error: 'decrypt helper call failed: ' + (e && e.message || e) }); } const payload = adPayload(resp) || {};@@ -10080,15 +10080,15 @@ const server = http.createServer(async (req, res) => { // the reason, standard reveal geometry, badge re-assert). A dashboard click IS the user // acting, so the foreground gate's reason is exactly that. try {- if (tid) { await fetch(`http://127.0.0.1:${PORT}/command`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'browser_switch_tab', args: { sessionId: sid, tabId: tid } }) }); }- const r = await fetch(`http://127.0.0.1:${PORT}/command`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'browser_raise_os_window', args: { sessionId: sid, foregroundReason: 'you clicked "bring to fg" on this window in the pup dashboard' } }) });+ if (tid) { await fetch(`http://127.0.0.1:${PORT}/command`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'pup_switch_tab', args: { sessionId: sid, tabId: tid } }) }); }+ const r = await fetch(`http://127.0.0.1:${PORT}/command`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'pup_raise_os_window', args: { sessionId: sid, foregroundReason: 'you clicked "bring to fg" on this window in the pup dashboard' } }) }); const j = await r.json().catch(() => ({})); return done({ ok: !!j.success }); } catch (e) { return done({ ok: false, error: e.message }); } } if (act === 'bg' && sid) { try {- const r = await fetch(`http://127.0.0.1:${PORT}/command`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'browser_lower_os_window', args: { sessionId: sid } }) });+ const r = await fetch(`http://127.0.0.1:${PORT}/command`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ command: 'pup_lower_os_window', args: { sessionId: sid } }) }); const j = await r.json().catch(() => ({})); return done({ ok: !!j.success }); } catch (e) { return done({ ok: false, error: e.message }); }@@ -10358,12 +10358,12 @@ const server = http.createServer(async (req, res) => { } catch (e) {} // v1.8.23: AUTO-FLASH + agent-update tracking for state-mutating verbs on an EXISTING- // session (browser_open_window flashes from inside its own handler, once the session+ // session (pup_open_window flashes from inside its own handler, once the session // exists). evaluate is read-only iff {readOnly:true}. {silent:true} per verb or the // session default suppresses. Runs BEFORE the handler — signals "the agent is // touching this window" regardless of the verb's outcome.- if (MUTATING_VERBS.has(command) && command !== 'browser_open_window' && command !== 'browser_open_tab') {- const isReadOnlyEval = (command === 'browser_eval' || command === 'browser_evaluate') && args.readOnly;+ if (MUTATING_VERBS.has(command) && command !== 'pup_open_window' && command !== 'pup_open_tab') {+ const isReadOnlyEval = (command === 'pup_eval' || command === 'pup_evaluate') && args.readOnly; if (!isReadOnlyEval) { const sid = args.sessionId || activeSessionId; const s = sid && sessions.get(sid);@@ -10373,7 +10373,7 @@ const server = http.createServer(async (req, res) => { // v1.9.301: close_tab is NOT in MUTATING_VERBS but it changes the active tab (hence the favicon), // so it must re-trigger the overlay calc too. Centralize it here so no tab/window/favicon verb is // missed — the drift John called out.- if (command === 'browser_close_tab' || command === 'browser_close_window') { try { scheduleOverlayRefresh(args.sessionId || activeSessionId); } catch (e) {} }+ if (command === 'pup_close_tab' || command === 'pup_close_window') { try { scheduleOverlayRefresh(args.sessionId || activeSessionId); } catch (e) {} } // v1.9.70 — AUDIT EVERY INCOMING VERB. John: "do you even log all the actions against you?" // Fair — the log recorded what the bridge DID (park/identity/overlay) but never WHAT WAS ASKED@@ -10395,16 +10395,16 @@ const server = http.createServer(async (req, res) => { // MUTATING_VERBS above on purpose — a screenshot or an eval is still an agent working in that // window, and that is exactly what the user wants to see. Status/polling verbs are excluded // (see PASSIVE_VERBS) or every window would read as permanently busy.- if (command && command.startsWith('browser_') && !PASSIVE_VERBS.has(command)) {+ if (command && command.startsWith('pup_') && !PASSIVE_VERBS.has(command)) { try { markSessionBusy(args.sessionId || activeSessionId); } catch (e) {} } switch (command) {- // v1.0.15: self-describe — AD's "Verbs" tab POSTs {command:'browser_describe'}+ // v1.0.15: self-describe — AD's "Verbs" tab POSTs {command:'pup_describe'} // and renders this verbatim: one entry per documented verb with- // {name, summary, input, output, timeoutSeconds, example}. (browser_ is this- // bridge's primary prefix, so AD calls it browser_describe.)- case 'browser_describe': {+ // {name, summary, input, output, timeoutSeconds, example}. (pup_ is this+ // bridge's primary prefix, so AD calls it pup_describe.)+ case 'pup_describe': { // v1.0.16: each entry merges its VERB_META (hint/related/pitfalls) so AD's // Verbs tab renders the full self-doc — the reference shape for all bridges. const V = (name, summary, input, timeoutSeconds, example) => {@@ -10414,70 +10414,70 @@ const server = http.createServer(async (req, res) => { hint: m.hint || summary, related: m.related || [], pitfalls: m.pitfalls || [] }; }; sendJSON(res, { success: true, bridge: 'puppeteer', verbs: [- V('browser_open_window', 'Open a generic pup window — a fresh isolated Chrome/Edge process (never your real signed-in browser), fully drivable via CDP. ALWAYS opens in the BACKGROUND (does not steal focus; never disturbs the user\'s main work; still fully screenshottable/drivable) UNLESS you pass foreground:true — do that ONLY when the user explicitly wants to watch. Pass width+height (+ optional x,y, screen px) to place it at a specific size/position for screenshot framing — this NO LONGER foregrounds it (sizing stays in the background too; default unsized is maximized). profile defaults to the durable shared \'adom-you\' profile (logins/localStorage persist + are shared across all pup windows); isolated:true for a private throwaway. OWNERSHIP: sessionIds are shared across ALL AI threads — use a task-prefixed sessionId + owner:"<your-task>" so pup can refuse other threads stealing your window (reusing an existing id with a new url navigates it away; declared cross-owner reuse errors with session_owned_by_another_thread unless takeover:true). (For the user\'s REAL Chrome/Edge, use the Adom extension\'s nbrowser_* verbs.)', { sessionId: 'optional str — OMIT IT (recommended): your thread gets its OWN window keyed on your --ai-thread name; the first open creates it, later opens become TABS in it, and no other thread can touch it. Pass an explicit id only when you need a SEPARATE window (task-prefix it, e.g. "mytask-web")', url: 'optional str', profile: 'optional str — a named durable jar; DEFAULT is the shared "adom-you" profile (signed-in state shared across windows). isolated:true overrides to a throwaway', isolated: 'optional bool — private throwaway profile: fresh-login tests, acting as a different user', webSecurity: 'optional false — dev escape hatch for cross-origin eval; FORCES isolation so insecure flags never touch the credentialed profile', owner: 'optional str — your thread/task label; lets pup protect this window from other threads', takeover: 'optional bool — claim a window owned by another thread (only when the user explicitly asks)', foreground: 'optional bool — request showing the window on the user\'s screen. GATED (v1.8.70): requires foregroundReason too, else the open succeeds but stays BACKGROUND with foregroundDenied set. Only when the USER explicitly asked to watch.', foregroundReason: 'required WITH foreground:true — quote/paraphrase what the user said that justifies putting a window on their screen (min 10 chars; logged + recorded in lastAgentUpdate, auditable)', width: 'optional int px — sizes for screenshot framing, stays background', height: 'optional int px', x: 'optional int px', y: 'optional int px', wikiView: 'optional "authed"|"public" — for wiki.adom.inc URLs. THREE distinct cases: (1) OMIT = the shared adom-you profile, which is LOGGED IN as the user if they have signed into the wiki (the usual case — one shared login serves every window); this is NOT anonymous. (2) "public" = an ISOLATED anonymous jar, genuinely LOGGED OUT — exactly what the world sees; use this to verify a publish did not leak private source. (3) "authed" = the explicit logged-in vault view (30-day SSO; first use may need a one-time login — response says wikiLoginNeeded). To compare "what I see" vs "what the world sees", open one "authed" (or omit) AND one "public" — do NOT rely on omit for the public view, it shows the logged-in shared profile.', appName: 'optional str — the APP this window is driving (e.g. "shotlog"); shown in the hover tooltip', appIconB64: 'optional base64 PNG — override the app icon worn in the taskbar overlay. Usually UNNECESSARY: pup auto-fetches the page\'s own <link rel="icon"> favicon (the adom-ui-design standard artifact every app must serve) and wears it automatically. An app-looking URL with NO favicon gets a reprimand hint.', updateNote: 'optional str — a short "what I did" note, surfaced back in list_tabs/list_windows as lastAgentUpdate so the user knows what changed', autoFlash: 'optional bool (default true) — mutating verbs auto-flash this window\'s taskbar (debounced, never steals focus) so the user notices the agent touched it; pass false to make this window silent by default', silent: 'optional bool — suppress the auto-flash for just this call' }, 60, { sessionId: 'mytask-web', owner: 'mytask', url: 'https://example.com', width: 960, height: 540 }),- V('browser_open_tab', 'Open a new tab in an EXISTING session/window (returns {tabId, tabCount, activeTabId} — no need to diff list_tabs). This is the right verb for multi-URL work: ONE window with many tabs (one taskbar button), not many windows. The window stays in the background; the tab auto-flashes the taskbar (debounced) so the user notices. The USER can click the window\'s taskbar button at any time to view it — pup never pulls it back down.', { sessionId: 'required str', url: 'optional str', updateNote: 'optional str — short "what I did" note surfaced in list_tabs as lastAgentUpdate', silent: 'optional bool — suppress the auto-flash for this call' }, 30, { sessionId: 'work', url: 'https://example.com' }),- V('browser_close_tab', 'Close a tab.', { sessionId: 'required str', tabId: 'optional str (default active)' }, 15, { sessionId: 'work' }),- V('browser_close_window', 'Close a window/session (its tabs go too).', { sessionId: 'required str' }, 20, { sessionId: 'work' }),- V('browser_wiki_set_view', 'Flip a WIKI pup window between the logged-in and public view IN PLACE (relaunches the same window under the other cookie jar). This is the callback for the per-window taskbar right-click jump-list task (WINDOWS ONLY — macOS has no taskbar or jump list, so this callback never fires there; flip views with browser_open_window {wikiView} instead) — it is a USER-initiated action (the window is brought on-screen). AI threads should NOT call this; pass wikiView:"authed"|"public" on browser_open_window instead.', { sessionId: 'required str', view: 'required "authed"|"public"' }, 45, { sessionId: 'wiki-bmv080', view: 'authed' }),- V('browser_navigate', 'Navigate the active tab to a URL.', { sessionId: 'required str', url: 'required str' }, 45, { sessionId: 'work', url: 'https://example.com' }),- V('browser_back', 'History back.', { sessionId: 'required str' }, 20, { sessionId: 'work' }),- V('browser_forward', 'History forward.', { sessionId: 'required str' }, 20, { sessionId: 'work' }),- V('browser_reload', 'Reload the active tab.', { sessionId: 'required str' }, 30, { sessionId: 'work' }),- V('browser_click', 'Click an element (by selector or coords).', { sessionId: 'required str', selector: 'optional str', x: 'optional int', y: 'optional int' }, 20, { sessionId: 'work', selector: 'button#go' }),- V('browser_assisted_click', 'RESPECTFUL real-cursor click for a surface pup CDP cannot drive (a NATIVE OS dialog, or a cross-origin FedCM "Sign in with Google" account chooser that rejects every in-page click). Do NOT hand-roll raise+desktop_click for these — use THIS. It: captions the reason + toasts BEFORE touching the screen; foregrounds; then WAITS, detecting if the user alt-tabbed away and holding (captioning "Adom is waiting — click back when ready") until the user is actually on the window; then runs the TIGHT real-cursor hover->click that FedCM accepts (interleaving any screenshot/eval between raise and click makes the click silently no-op); captions each step. Times out as user_not_ready clicking NOTHING if they never return. First take a desktop_screenshot_window of the session to read the target\'s WINDOW-pixel x,y.', { sessionId: 'required str', x: 'required int — WINDOW-image pixel of the target (from desktop_screenshot_window)', y: 'required int', reason: 'required str — WHY you must click (shown to the user as a caption)', label: 'required str — WHAT you are clicking (e.g. "your Google account")', maxWaitMs: 'optional int — how long to wait for the user (default 120000)' }, 130, { sessionId: 'adsk', x: 900, y: 379, reason: 'clicking your Google account to finish signing into Autodesk', label: 'your Google account' }),- V('browser_type', 'Type text into a field. Pass clear:true to REPLACE the existing value instead of appending (this is what you want on any prefilled/React form — it selects-all then deletes with real key events, which React-controlled inputs honour, unlike setting .value).', { sessionId: 'required str', text: 'required str', selector: 'optional str', clear: 'optional bool — empty the field first (replace, not append)' }, 20, { sessionId: 'work', selector: '#user', text: 'adominc1', clear: true }),- V('browser_press_key', 'Press a key, optionally with modifier chords held (Ctrl+A, Ctrl+V, Shift+Tab).', { sessionId: 'required str', key: 'required str', modifiers: 'optional array — any of ["Control","Shift","Alt","Meta"] (aliases ctrl/cmd/command/option accepted); held down around the key press' }, 15, { sessionId: 'work', key: 'a', modifiers: ['Control'] }),- V('browser_NATIVE_INPUT_WARNING', '⛔ READ BEFORE USING AD\'s desktop_click / desktop_type / desktop_press_key ON A PUP WINDOW. Native OS input does NOT target a window — it goes to WHATEVER WINDOW HAS OS FOCUS at that instant. This user runs many AI threads and alt-tabs away from a foregrounded window almost immediately. If you fire native input without warning them, your keystrokes land in THEIR document (this happened: a username was typed into the user\'s own window). REQUIRED: notify_user toast + desktop_caption FIRST, tell them explicitly "don\'t type or click until I say done", wait until the window is genuinely foreground, re-verify immediately before EACH event, and caption when done. browser_assisted_click already does all of this — use it. And ALWAYS prefer CDP input (browser_click/browser_type/browser_hover): it needs no foreground and disturbs nobody. A native verb returning ok:true means the event was dispatched to the OS, NOT that it reached your window.', {}, 0, {}),- V('browser_set_viewport', 'Pin the page viewport (CDP device-metrics) to an exact size for deterministic screenshots — OR clear that pin. ⚠ A pin PERSISTS: it survives navigation and hard reload, and while set the window stops tracking OS resizing (a human resizing it sees no reflow, which looks exactly like an app layout bug). ALWAYS clear it when done. Reported as emulatedViewport in browser_status / browser_list_windows.', { sessionId: 'required str', width: 'int — required unless reset', height: 'int — required unless reset', deviceScaleFactor: 'optional number (default 1)', reset: 'optional bool — CLEAR the override and track the OS window again (width/height not needed)' }, 15, { sessionId: 'work', reset: true }),- V('browser_dashboard', 'Open the LIVE pup management dashboard in its OWN window (its own profile, so its own taskbar icon). Shows every window pup is driving as taskbar button -> window(s) -> tabs with each tab favicon, owning AI thread, foreground/background + the reason given to foreground it, highFps lease, uptime, and WHY each overlay icon was chosen. Live over SSE. Reuses the one window on repeat calls; backgrounded by default like any pup window.', {}, 30, {}),- V('browser_deps', 'Report pup dependency health (wiki issue #15): depsPresent, selfInstalledThisBoot, lastError. Use this to answer "is pup broken?" without reading spawn logs.', { }, 10, {}),- V('browser_hover', 'Move the pointer over a target WITHOUT clicking (reveals tooltips / hover menus). Glides the visible Adom cursor there too.', { sessionId: 'required str', selector: 'optional str', x: 'optional int', y: 'optional int', intent: 'optional str — narrated on the cursor label', steps: 'optional int (default 8)' }, 15, { sessionId: 'work', selector: '#menu' }),- V('browser_maximize', 'Maximize (or restore/fullscreen/minimize) a pup window via CDP — no AD desktop verb required. Does NOT foreground it.', { sessionId: 'required str', state: 'optional "maximized"|"normal"|"minimized"|"fullscreen" (default maximized)' }, 15, { sessionId: 'work' }),- V('browser_set_identity', 'Set the Adom identity shown in the floating owner chip on every pup window (avatar + name). Chrome\'s own profile chip cannot take a custom local image (Google sign-in only), so the user\'s Adom avatar lives here instead. Pass a pre-circle-cropped PNG as base64. Applies to live windows immediately + persists.', { avatarB64: 'optional str — base64 PNG (ideally ~48px, circle-cropped); the leading data: prefix is stripped if present', name: 'optional str — display name shown in the chip tooltip', enabled: 'optional bool — turn the whole owner chip on/off', clear: 'optional bool — remove the stored avatar + name' }, 15, { name: 'John Lauer', avatarB64: '<base64 png>' }),- V('browser_screenshot', 'Screenshot the page (PNG; fullPage optional). NOTE: fullPage only covers DOCUMENT-level scroll — on SPAs that scroll inside a nested container it returns just the viewport + a nestedScroll hint; use browser_screenshot_element or browser_scroll for those.', { sessionId: 'required str', fullPage: 'optional bool' }, 30, { sessionId: 'work' }),- V('browser_screenshot_element', 'Screenshot ONE element, resolved by CSS selector OR visible text (pierces shadow DOM), scrolled into view first. The clean way to grab a single card/widget on a component-heavy page. Text matches the TIGHTEST text node — to capture the enclosing CARD, add closest (e.g. {text:"Download…", closest:".download-card"}); with closest, an ambiguous phrase still lands on the real widget (skips prose that has no such ancestor). Returns base64 + filePath like browser_screenshot.', { sessionId: 'required str', selector: 'CSS selector (or use text)', text: 'visible text to match (or use selector)', closest: 'optional selector — climb to the nearest ancestor matching it (the card container)', padding: 'optional int px around the element', deep: 'optional bool — pierce shadow DOM (default true)' }, 30, { sessionId: 'work', selector: '.download-card' }),- V('browser_scroll', 'Scroll the ACTUAL scrolling element (nearest ancestor whose scrollHeight>clientHeight, NOT just window — window.scrollTo no-ops on nested-scroll SPAs). Scroll by delta {dy,dx} OR to an element {toSelector|toText} (pierces shadow DOM). Returns {scrollElement, scrollTop, scrollHeight, clientHeight} so you can confirm it landed.', { sessionId: 'required str', dy: 'optional int px (down+/up-)', dx: 'optional int px', toSelector: 'optional CSS selector to scroll to', toText: 'optional visible text to scroll to', block: 'optional start|center|end (for scroll-to; default center)', behavior: 'optional auto|smooth' }, 30, { sessionId: 'work', toText: 'Download' }),- V('browser_set_viewport', 'Set the page viewport (CDP device-metrics) deterministically, independent of the OS window size — fixes a too-small innerWidth so fullPage/element shots render at a known width. width+height required.', { sessionId: 'required str', width: 'required int px', height: 'required int px', deviceScaleFactor: 'optional number (default 1)' }, 15, { sessionId: 'work', width: 1280, height: 900 }),- V('browser_evaluate', 'Evaluate JS in the page; returns the result.', { sessionId: 'required str', expression: 'required str' }, 30, { sessionId: 'work', expression: 'document.title' }),- V('browser_fetch_url', 'Fetch a URL\'s content via the browser (cookies/session intact).', { sessionId: 'required str', url: 'required str' }, 45, { sessionId: 'work', url: 'https://api.example.com' }),- V('browser_input_dispatch', 'Low-level input (mouse/keyboard) at coords — beats some overlays.', { sessionId: 'required str', type: 'required str', x: 'optional int', y: 'optional int' }, 20, { sessionId: 'work', type: 'mousePressed', x: 100, y: 200 }),- V('browser_record_start', 'Start recording the page (video).', { sessionId: 'required str' }, 20, { sessionId: 'work' }),- V('browser_record_stop', 'Stop recording; returns the saved file.', { sessionId: 'required str' }, 30, { sessionId: 'work' }),- V('browser_record_status', 'Recording status for a session.', { sessionId: 'required str' }, 10, { sessionId: 'work' }),- V('browser_record_list', 'List recordings.', {}, 10, {}),+ V('pup_open_window', 'Open a generic pup window — a fresh isolated Chrome/Edge process (never your real signed-in browser), fully drivable via CDP. ALWAYS opens in the BACKGROUND (does not steal focus; never disturbs the user\'s main work; still fully screenshottable/drivable) UNLESS you pass foreground:true — do that ONLY when the user explicitly wants to watch. Pass width+height (+ optional x,y, screen px) to place it at a specific size/position for screenshot framing — this NO LONGER foregrounds it (sizing stays in the background too; default unsized is maximized). profile defaults to the durable shared \'adom-you\' profile (logins/localStorage persist + are shared across all pup windows); isolated:true for a private throwaway. OWNERSHIP: sessionIds are shared across ALL AI threads — use a task-prefixed sessionId + owner:"<your-task>" so pup can refuse other threads stealing your window (reusing an existing id with a new url navigates it away; declared cross-owner reuse errors with session_owned_by_another_thread unless takeover:true). (For the user\'s REAL Chrome/Edge, use the Adom extension\'s nbrowser_* verbs.)', { sessionId: 'optional str — OMIT IT (recommended): your thread gets its OWN window keyed on your --ai-thread name; the first open creates it, later opens become TABS in it, and no other thread can touch it. Pass an explicit id only when you need a SEPARATE window (task-prefix it, e.g. "mytask-web")', url: 'optional str', profile: 'optional str — a named durable jar; DEFAULT is the shared "adom-you" profile (signed-in state shared across windows). isolated:true overrides to a throwaway', isolated: 'optional bool — private throwaway profile: fresh-login tests, acting as a different user', webSecurity: 'optional false — dev escape hatch for cross-origin eval; FORCES isolation so insecure flags never touch the credentialed profile', owner: 'optional str — your thread/task label; lets pup protect this window from other threads', takeover: 'optional bool — claim a window owned by another thread (only when the user explicitly asks)', foreground: 'optional bool — request showing the window on the user\'s screen. GATED (v1.8.70): requires foregroundReason too, else the open succeeds but stays BACKGROUND with foregroundDenied set. Only when the USER explicitly asked to watch.', foregroundReason: 'required WITH foreground:true — quote/paraphrase what the user said that justifies putting a window on their screen (min 10 chars; logged + recorded in lastAgentUpdate, auditable)', width: 'optional int px — sizes for screenshot framing, stays background', height: 'optional int px', x: 'optional int px', y: 'optional int px', wikiView: 'optional "authed"|"public" — for wiki.adom.inc URLs. THREE distinct cases: (1) OMIT = the shared adom-you profile, which is LOGGED IN as the user if they have signed into the wiki (the usual case — one shared login serves every window); this is NOT anonymous. (2) "public" = an ISOLATED anonymous jar, genuinely LOGGED OUT — exactly what the world sees; use this to verify a publish did not leak private source. (3) "authed" = the explicit logged-in vault view (30-day SSO; first use may need a one-time login — response says wikiLoginNeeded). To compare "what I see" vs "what the world sees", open one "authed" (or omit) AND one "public" — do NOT rely on omit for the public view, it shows the logged-in shared profile.', appName: 'optional str — the APP this window is driving (e.g. "shotlog"); shown in the hover tooltip', appIconB64: 'optional base64 PNG — override the app icon worn in the taskbar overlay. Usually UNNECESSARY: pup auto-fetches the page\'s own <link rel="icon"> favicon (the adom-ui-design standard artifact every app must serve) and wears it automatically. An app-looking URL with NO favicon gets a reprimand hint.', updateNote: 'optional str — a short "what I did" note, surfaced back in list_tabs/list_windows as lastAgentUpdate so the user knows what changed', autoFlash: 'optional bool (default true) — mutating verbs auto-flash this window\'s taskbar (debounced, never steals focus) so the user notices the agent touched it; pass false to make this window silent by default', silent: 'optional bool — suppress the auto-flash for just this call' }, 60, { sessionId: 'mytask-web', owner: 'mytask', url: 'https://example.com', width: 960, height: 540 }),+ V('pup_open_tab', 'Open a new tab in an EXISTING session/window (returns {tabId, tabCount, activeTabId} — no need to diff list_tabs). This is the right verb for multi-URL work: ONE window with many tabs (one taskbar button), not many windows. The window stays in the background; the tab auto-flashes the taskbar (debounced) so the user notices. The USER can click the window\'s taskbar button at any time to view it — pup never pulls it back down.', { sessionId: 'required str', url: 'optional str', updateNote: 'optional str — short "what I did" note surfaced in list_tabs as lastAgentUpdate', silent: 'optional bool — suppress the auto-flash for this call' }, 30, { sessionId: 'work', url: 'https://example.com' }),+ V('pup_close_tab', 'Close a tab.', { sessionId: 'required str', tabId: 'optional str (default active)' }, 15, { sessionId: 'work' }),+ V('pup_close_window', 'Close a window/session (its tabs go too).', { sessionId: 'required str' }, 20, { sessionId: 'work' }),+ V('pup_wiki_set_view', 'Flip a WIKI pup window between the logged-in and public view IN PLACE (relaunches the same window under the other cookie jar). This is the callback for the per-window taskbar right-click jump-list task (WINDOWS ONLY — macOS has no taskbar or jump list, so this callback never fires there; flip views with pup_open_window {wikiView} instead) — it is a USER-initiated action (the window is brought on-screen). AI threads should NOT call this; pass wikiView:"authed"|"public" on pup_open_window instead.', { sessionId: 'required str', view: 'required "authed"|"public"' }, 45, { sessionId: 'wiki-bmv080', view: 'authed' }),+ V('pup_navigate', 'Navigate the active tab to a URL.', { sessionId: 'required str', url: 'required str' }, 45, { sessionId: 'work', url: 'https://example.com' }),+ V('pup_back', 'History back.', { sessionId: 'required str' }, 20, { sessionId: 'work' }),+ V('pup_forward', 'History forward.', { sessionId: 'required str' }, 20, { sessionId: 'work' }),+ V('pup_reload', 'Reload the active tab.', { sessionId: 'required str' }, 30, { sessionId: 'work' }),+ V('pup_click', 'Click an element (by selector or coords).', { sessionId: 'required str', selector: 'optional str', x: 'optional int', y: 'optional int' }, 20, { sessionId: 'work', selector: 'button#go' }),+ V('pup_assisted_click', 'RESPECTFUL real-cursor click for a surface pup CDP cannot drive (a NATIVE OS dialog, or a cross-origin FedCM "Sign in with Google" account chooser that rejects every in-page click). Do NOT hand-roll raise+desktop_click for these — use THIS. It: captions the reason + toasts BEFORE touching the screen; foregrounds; then WAITS, detecting if the user alt-tabbed away and holding (captioning "Adom is waiting — click back when ready") until the user is actually on the window; then runs the TIGHT real-cursor hover->click that FedCM accepts (interleaving any screenshot/eval between raise and click makes the click silently no-op); captions each step. Times out as user_not_ready clicking NOTHING if they never return. First take a desktop_screenshot_window of the session to read the target\'s WINDOW-pixel x,y.', { sessionId: 'required str', x: 'required int — WINDOW-image pixel of the target (from desktop_screenshot_window)', y: 'required int', reason: 'required str — WHY you must click (shown to the user as a caption)', label: 'required str — WHAT you are clicking (e.g. "your Google account")', maxWaitMs: 'optional int — how long to wait for the user (default 120000)' }, 130, { sessionId: 'adsk', x: 900, y: 379, reason: 'clicking your Google account to finish signing into Autodesk', label: 'your Google account' }),+ V('pup_type', 'Type text into a field. Pass clear:true to REPLACE the existing value instead of appending (this is what you want on any prefilled/React form — it selects-all then deletes with real key events, which React-controlled inputs honour, unlike setting .value).', { sessionId: 'required str', text: 'required str', selector: 'optional str', clear: 'optional bool — empty the field first (replace, not append)' }, 20, { sessionId: 'work', selector: '#user', text: 'adominc1', clear: true }),+ V('pup_press_key', 'Press a key, optionally with modifier chords held (Ctrl+A, Ctrl+V, Shift+Tab).', { sessionId: 'required str', key: 'required str', modifiers: 'optional array — any of ["Control","Shift","Alt","Meta"] (aliases ctrl/cmd/command/option accepted); held down around the key press' }, 15, { sessionId: 'work', key: 'a', modifiers: ['Control'] }),+ V('pup_NATIVE_INPUT_WARNING', '⛔ READ BEFORE USING AD\'s desktop_click / desktop_type / desktop_press_key ON A PUP WINDOW. Native OS input does NOT target a window — it goes to WHATEVER WINDOW HAS OS FOCUS at that instant. This user runs many AI threads and alt-tabs away from a foregrounded window almost immediately. If you fire native input without warning them, your keystrokes land in THEIR document (this happened: a username was typed into the user\'s own window). REQUIRED: notify_user toast + desktop_caption FIRST, tell them explicitly "don\'t type or click until I say done", wait until the window is genuinely foreground, re-verify immediately before EACH event, and caption when done. pup_assisted_click already does all of this — use it. And ALWAYS prefer CDP input (pup_click/pup_type/pup_hover): it needs no foreground and disturbs nobody. A native verb returning ok:true means the event was dispatched to the OS, NOT that it reached your window.', {}, 0, {}),+ V('pup_set_viewport', 'Pin the page viewport (CDP device-metrics) to an exact size for deterministic screenshots — OR clear that pin. ⚠ A pin PERSISTS: it survives navigation and hard reload, and while set the window stops tracking OS resizing (a human resizing it sees no reflow, which looks exactly like an app layout bug). ALWAYS clear it when done. Reported as emulatedViewport in pup_status / pup_list_windows.', { sessionId: 'required str', width: 'int — required unless reset', height: 'int — required unless reset', deviceScaleFactor: 'optional number (default 1)', reset: 'optional bool — CLEAR the override and track the OS window again (width/height not needed)' }, 15, { sessionId: 'work', reset: true }),+ V('pup_dashboard', 'Open the LIVE pup management dashboard in its OWN window (its own profile, so its own taskbar icon). Shows every window pup is driving as taskbar button -> window(s) -> tabs with each tab favicon, owning AI thread, foreground/background + the reason given to foreground it, highFps lease, uptime, and WHY each overlay icon was chosen. Live over SSE. Reuses the one window on repeat calls; backgrounded by default like any pup window.', {}, 30, {}),+ V('pup_deps', 'Report pup dependency health (wiki issue #15): depsPresent, selfInstalledThisBoot, lastError. Use this to answer "is pup broken?" without reading spawn logs.', { }, 10, {}),+ V('pup_hover', 'Move the pointer over a target WITHOUT clicking (reveals tooltips / hover menus). Glides the visible Adom cursor there too.', { sessionId: 'required str', selector: 'optional str', x: 'optional int', y: 'optional int', intent: 'optional str — narrated on the cursor label', steps: 'optional int (default 8)' }, 15, { sessionId: 'work', selector: '#menu' }),+ V('pup_maximize', 'Maximize (or restore/fullscreen/minimize) a pup window via CDP — no AD desktop verb required. Does NOT foreground it.', { sessionId: 'required str', state: 'optional "maximized"|"normal"|"minimized"|"fullscreen" (default maximized)' }, 15, { sessionId: 'work' }),+ V('pup_set_identity', 'Set the Adom identity shown in the floating owner chip on every pup window (avatar + name). Chrome\'s own profile chip cannot take a custom local image (Google sign-in only), so the user\'s Adom avatar lives here instead. Pass a pre-circle-cropped PNG as base64. Applies to live windows immediately + persists.', { avatarB64: 'optional str — base64 PNG (ideally ~48px, circle-cropped); the leading data: prefix is stripped if present', name: 'optional str — display name shown in the chip tooltip', enabled: 'optional bool — turn the whole owner chip on/off', clear: 'optional bool — remove the stored avatar + name' }, 15, { name: 'John Lauer', avatarB64: '<base64 png>' }),+ V('pup_screenshot', 'Screenshot the page (PNG; fullPage optional). NOTE: fullPage only covers DOCUMENT-level scroll — on SPAs that scroll inside a nested container it returns just the viewport + a nestedScroll hint; use pup_screenshot_element or pup_scroll for those.', { sessionId: 'required str', fullPage: 'optional bool' }, 30, { sessionId: 'work' }),+ V('pup_screenshot_element', 'Screenshot ONE element, resolved by CSS selector OR visible text (pierces shadow DOM), scrolled into view first. The clean way to grab a single card/widget on a component-heavy page. Text matches the TIGHTEST text node — to capture the enclosing CARD, add closest (e.g. {text:"Download…", closest:".download-card"}); with closest, an ambiguous phrase still lands on the real widget (skips prose that has no such ancestor). Returns base64 + filePath like pup_screenshot.', { sessionId: 'required str', selector: 'CSS selector (or use text)', text: 'visible text to match (or use selector)', closest: 'optional selector — climb to the nearest ancestor matching it (the card container)', padding: 'optional int px around the element', deep: 'optional bool — pierce shadow DOM (default true)' }, 30, { sessionId: 'work', selector: '.download-card' }),+ V('pup_scroll', 'Scroll the ACTUAL scrolling element (nearest ancestor whose scrollHeight>clientHeight, NOT just window — window.scrollTo no-ops on nested-scroll SPAs). Scroll by delta {dy,dx} OR to an element {toSelector|toText} (pierces shadow DOM). Returns {scrollElement, scrollTop, scrollHeight, clientHeight} so you can confirm it landed.', { sessionId: 'required str', dy: 'optional int px (down+/up-)', dx: 'optional int px', toSelector: 'optional CSS selector to scroll to', toText: 'optional visible text to scroll to', block: 'optional start|center|end (for scroll-to; default center)', behavior: 'optional auto|smooth' }, 30, { sessionId: 'work', toText: 'Download' }),+ V('pup_set_viewport', 'Set the page viewport (CDP device-metrics) deterministically, independent of the OS window size — fixes a too-small innerWidth so fullPage/element shots render at a known width. width+height required.', { sessionId: 'required str', width: 'required int px', height: 'required int px', deviceScaleFactor: 'optional number (default 1)' }, 15, { sessionId: 'work', width: 1280, height: 900 }),+ V('pup_evaluate', 'Evaluate JS in the page; returns the result.', { sessionId: 'required str', expression: 'required str' }, 30, { sessionId: 'work', expression: 'document.title' }),+ V('pup_fetch_url', 'Fetch a URL\'s content via the browser (cookies/session intact).', { sessionId: 'required str', url: 'required str' }, 45, { sessionId: 'work', url: 'https://api.example.com' }),+ V('pup_input_dispatch', 'Low-level input (mouse/keyboard) at coords — beats some overlays.', { sessionId: 'required str', type: 'required str', x: 'optional int', y: 'optional int' }, 20, { sessionId: 'work', type: 'mousePressed', x: 100, y: 200 }),+ V('pup_record_start', 'Start recording the page (video).', { sessionId: 'required str' }, 20, { sessionId: 'work' }),+ V('pup_record_stop', 'Stop recording; returns the saved file.', { sessionId: 'required str' }, 30, { sessionId: 'work' }),+ V('pup_record_status', 'Recording status for a session.', { sessionId: 'required str' }, 10, { sessionId: 'work' }),+ V('pup_record_list', 'List recordings.', {}, 10, {}), V('desktop_recorder_open', 'Open the desktop screen-recorder UI.', {}, 20, {}), V('desktop_record_start', 'Start a desktop screen recording.', { outPath: 'optional str' }, 20, {}), V('desktop_record_stop', 'Stop the desktop screen recording; returns the file.', {}, 30, {}),- V('browser_readiness', 'Readiness + which browser pup will drive (installed Chrome/Edge, or cached Chrome for Testing) + the full candidate list and current default. READ-ONLY; never downloads.', {}, 10, {}),- V('browser_prewarm', 'Install Chrome for Testing WITHOUT opening a window, and pin it as the default. Use when you specifically want CfT rather than the machine\'s installed Chrome/Edge.', { wait: 'optional bool — block until installed', setDefault: 'optional bool (default true) — pin CfT as the default' }, 240, {}),- V('browser_use', 'Pin (or clear) the browser pup drives by default: chrome | edge | cft | auto (or an explicit executablePath). "cft" fetches Chrome for Testing then pins it. With {browser:"chrome", install:true} it DOWNLOADS & INSTALLS real Google Chrome on an Edge-only box, then pins it — SILENT on a normal PC; on a locked-down box (VM/managed device) it needs a Windows approval, so the bridge pops a native "click YES on the UAC" notification and, if it expires, re-notifies with a one-tap "Approve now" (all autonomous — no AI turn). Poll browser_readiness.chromeStablePhase: installing → awaiting_uac → ready. Persists across restarts; launch still spawn-verifies + falls back.', { browser: '"chrome" | "edge" | "cft" | "auto"', install: 'optional bool — with browser:"chrome", install real Chrome if missing', elevate: 'optional "auto"(default)|true|false — auto: quiet install, prompt only if it needs admin; false: never prompt; true: go straight to the prompt', executablePath: 'optional — pin an exact browser binary' }, 240, {}),- V('browser_lower_os_window', 'Send a pup window to the BACKGROUND (behind the user\'s windows) without stealing focus — still fully drivable. The default for open; use this to re-hide a window you foregrounded.', { sessionId: 'optional str' }, 15, {}),- V('browser_raise_os_window', 'Bring a pup window to the FOREGROUND (user\'s screen). GATED (v1.8.70): REFUSED with foreground_reason_required unless you pass foregroundReason — a quote/paraphrase of what the user said that justifies it (min 10 chars; logged, recorded in lastAgentUpdate, auditable). Allowed ONLY when the user EXPLICITLY must SEE it (a demo, recording, "show me"). It STEALS focus — to just signal "I updated this window", use browser_alert_window (or the auto-flash), NOT raise. After the show-me: browser_lower_os_window to return it to the background. Alias: browser_focus_window. IMPORTANT: if you are foregrounding in order to CLICK a native dialog or a cross-origin FedCM/Google account chooser, do NOT raise-then-click yourself — use browser_assisted_click, which waits for the user to actually be on the window (alt-tab aware) and narrates via captions, so you never act while they are not ready.', { sessionId: 'optional str', foregroundReason: 'REQUIRED — why the user asked to see this window' }, 15, { foregroundReason: 'user said "show me the wiki page"' }),- V('browser_login', 'Fill (and optionally submit) an HTML login form in the CURRENT tab, in the BACKGROUND — no Chrome autofill dropdown, no foregrounding, React/Vue-safe. Creds from the vault (matched on the tab URL; store via credential_set or save:true) or passed explicitly. Passwords are NEVER echoed back. Vault-matched login pages also AUTO-FILL on load (_autoLogin on the response; browser_configure {autoLogin:"off"} disables). Pairs with the durable shared profile: sign in once, every window stays signed in.', { sessionId: 'required str', username: 'optional str', password: 'optional str — with username, fills these instead of the vault', submit: 'optional bool — click the submit button after filling (default false)', save: 'optional bool — with explicit creds, store them (OS keychain) so next time is automatic', host: 'optional str — override the vault host key when saving' }, 20, { sessionId: 'work', submit: true }),- V('browser_alert_window', 'Nudge the user about a pup window WITHOUT stealing focus — on Windows it flashes the taskbar button; on macOS (no taskbar, and no API to bounce another app\'s Dock tile) it unhides the window at its z-position and posts an AD toast whose click focuses it. Same contract on both, so call it identically; just never tell a Mac user to look for a flashing icon. NOTE (v1.8.23): mutating verbs already AUTO-FLASH (debounced, one per ~5s burst per window) so the user always knows the agent touched a window — use this for an explicit end-of-task "it\'s ready" nudge on top of that. STRONGLY prefer this (or the auto-flash) over browser_raise_os_window, which steals focus and can leak a mid-typed keystroke into the page.', { sessionId: 'optional str', stop: 'optional bool — CLEAR the highlight instead of flashing' }, 15, {}),- V('browser_render_html', 'Render a local HTML string in the tab via page.setContent — UTF-8-correct (a data: URL mojibakes accented chars without an explicit charset). Great for previewing generated markup without a temp file. Auto-flashes the window.', { sessionId: 'required str', html: 'required HTML string', waitUntil: 'optional (domcontentloaded default)', updateNote: 'optional note for lastAgentUpdate', silent: 'optional bool — suppress the auto-flash' }, 30, { sessionId: 'work', html: '<h1>Café</h1>' }),+ V('pup_readiness', 'Readiness + which browser pup will drive (installed Chrome/Edge, or cached Chrome for Testing) + the full candidate list and current default. READ-ONLY; never downloads.', {}, 10, {}),+ V('pup_prewarm', 'Install Chrome for Testing WITHOUT opening a window, and pin it as the default. Use when you specifically want CfT rather than the machine\'s installed Chrome/Edge.', { wait: 'optional bool — block until installed', setDefault: 'optional bool (default true) — pin CfT as the default' }, 240, {}),+ V('pup_use', 'Pin (or clear) the browser pup drives by default: chrome | edge | cft | auto (or an explicit executablePath). "cft" fetches Chrome for Testing then pins it. With {browser:"chrome", install:true} it DOWNLOADS & INSTALLS real Google Chrome on an Edge-only box, then pins it — SILENT on a normal PC; on a locked-down box (VM/managed device) it needs a Windows approval, so the bridge pops a native "click YES on the UAC" notification and, if it expires, re-notifies with a one-tap "Approve now" (all autonomous — no AI turn). Poll pup_readiness.chromeStablePhase: installing → awaiting_uac → ready. Persists across restarts; launch still spawn-verifies + falls back.', { browser: '"chrome" | "edge" | "cft" | "auto"', install: 'optional bool — with browser:"chrome", install real Chrome if missing', elevate: 'optional "auto"(default)|true|false — auto: quiet install, prompt only if it needs admin; false: never prompt; true: go straight to the prompt', executablePath: 'optional — pin an exact browser binary' }, 240, {}),+ V('pup_lower_os_window', 'Send a pup window to the BACKGROUND (behind the user\'s windows) without stealing focus — still fully drivable. The default for open; use this to re-hide a window you foregrounded.', { sessionId: 'optional str' }, 15, {}),+ V('pup_raise_os_window', 'Bring a pup window to the FOREGROUND (user\'s screen). GATED (v1.8.70): REFUSED with foreground_reason_required unless you pass foregroundReason — a quote/paraphrase of what the user said that justifies it (min 10 chars; logged, recorded in lastAgentUpdate, auditable). Allowed ONLY when the user EXPLICITLY must SEE it (a demo, recording, "show me"). It STEALS focus — to just signal "I updated this window", use pup_alert_window (or the auto-flash), NOT raise. After the show-me: pup_lower_os_window to return it to the background. Alias: pup_focus_window. IMPORTANT: if you are foregrounding in order to CLICK a native dialog or a cross-origin FedCM/Google account chooser, do NOT raise-then-click yourself — use pup_assisted_click, which waits for the user to actually be on the window (alt-tab aware) and narrates via captions, so you never act while they are not ready.', { sessionId: 'optional str', foregroundReason: 'REQUIRED — why the user asked to see this window' }, 15, { foregroundReason: 'user said "show me the wiki page"' }),+ V('pup_login', 'Fill (and optionally submit) an HTML login form in the CURRENT tab, in the BACKGROUND — no Chrome autofill dropdown, no foregrounding, React/Vue-safe. Creds from the vault (matched on the tab URL; store via credential_set or save:true) or passed explicitly. Passwords are NEVER echoed back. Vault-matched login pages also AUTO-FILL on load (_autoLogin on the response; pup_configure {autoLogin:"off"} disables). Pairs with the durable shared profile: sign in once, every window stays signed in.', { sessionId: 'required str', username: 'optional str', password: 'optional str — with username, fills these instead of the vault', submit: 'optional bool — click the submit button after filling (default false)', save: 'optional bool — with explicit creds, store them (OS keychain) so next time is automatic', host: 'optional str — override the vault host key when saving' }, 20, { sessionId: 'work', submit: true }),+ V('pup_alert_window', 'Nudge the user about a pup window WITHOUT stealing focus — on Windows it flashes the taskbar button; on macOS (no taskbar, and no API to bounce another app\'s Dock tile) it unhides the window at its z-position and posts an AD toast whose click focuses it. Same contract on both, so call it identically; just never tell a Mac user to look for a flashing icon. NOTE (v1.8.23): mutating verbs already AUTO-FLASH (debounced, one per ~5s burst per window) so the user always knows the agent touched a window — use this for an explicit end-of-task "it\'s ready" nudge on top of that. STRONGLY prefer this (or the auto-flash) over pup_raise_os_window, which steals focus and can leak a mid-typed keystroke into the page.', { sessionId: 'optional str', stop: 'optional bool — CLEAR the highlight instead of flashing' }, 15, {}),+ V('pup_render_html', 'Render a local HTML string in the tab via page.setContent — UTF-8-correct (a data: URL mojibakes accented chars without an explicit charset). Great for previewing generated markup without a temp file. Auto-flashes the window.', { sessionId: 'required str', html: 'required HTML string', waitUntil: 'optional (domcontentloaded default)', updateNote: 'optional note for lastAgentUpdate', silent: 'optional bool — suppress the auto-flash' }, 30, { sessionId: 'work', html: '<h1>Café</h1>' }), // Session/tab/window management + probes (were routed but undocumented).- V('browser_status', 'List all open pup sessions + their tabs (NOT a readiness check — use browser_readiness for that).', {}, 10, {}),- V('browser_list_windows', 'List every open session/window with its tabs.', {}, 10, {}),- V('browser_eval_frame_note', 'browser_eval accepts frame:"<name|url-substring|index>" to run inside an IFRAME (login forms usually live in one). Cross-origin frames work. Omit for the top document; a bad value returns frame_not_found WITH the list of frames in the tab.', { sessionId: 'required str', expr: 'required str', frame: 'optional str' }, 20, { sessionId: 'work', expr: 'document.querySelector("#userId").value', frame: 'logonbox' }),- V('browser_switch_window', 'Make a session the active one (for verbs that default to active).', { sessionId: 'required str' }, 10, { sessionId: 'work' }),- V('browser_list_tabs', 'List the tabs in a session.', { sessionId: 'required str' }, 10, { sessionId: 'work' }),- V('browser_switch_tab', 'Activate a tab WITHIN its session/window (idempotent — safe to call when the tab is already active). Does NOT raise a background window to the user\'s screen: the tab becomes the window\'s visible tab wherever the window is, and the taskbar auto-flash tells the user something changed. Also self-heals the window (re-park if a bug ever stranded it off-screen) and refreshes the Adom taskbar badge — good citizenship for long-lived shared windows.', { sessionId: 'required str', tabId: 'required str', updateNote: 'optional str', silent: 'optional bool' }, 10, { sessionId: 'work', tabId: 'tab-2' }),- V('browser_screenshot_full_res', 'FULL-resolution page screenshot (NOT downscaled — for saving to disk, NOT for Read: it can exceed Claude\'s image limit). Use browser_screenshot for AI-readable shots.', { sessionId: 'required str', fullPage: 'optional bool' }, 30, { sessionId: 'work' }),- V('browser_errors', 'Console errors + failed network requests for a session/tab (clears by default).', { sessionId: 'required str', tabId: 'optional str', clear: 'optional bool (default true)' }, 10, { sessionId: 'work' }),- V('browser_wait', 'Wait N ms (or for a selector) before the next step.', { sessionId: 'required str', ms: 'optional int', selector: 'optional str' }, 60, { sessionId: 'work', ms: 500 }),- V('browser_rescan', 'Recover orphaned sessions: reconnect dropped CDP sockets + rebuild sessions from window titles. Pass adoptOrphans:true to also adopt untagged tabs.', { adoptOrphans: 'optional bool' }, 30, {}),- V('browser_close', 'Close ALL pup sessions + windows at once (destructive). NOTE: `{}` here does NOT mean "the active session" the way it does on other session-scoped verbs — with no args this closes EVERY window, across every AI thread on the machine, with no confirmation. To close ONE window use browser_close_window {sessionId}. Pass {sessionId} here only if you genuinely mean that one session.', {}, 20, {}),+ V('pup_status', 'List all open pup sessions + their tabs (NOT a readiness check — use pup_readiness for that).', {}, 10, {}),+ V('pup_list_windows', 'List every open session/window with its tabs.', {}, 10, {}),+ V('pup_eval_frame_note', 'pup_eval accepts frame:"<name|url-substring|index>" to run inside an IFRAME (login forms usually live in one). Cross-origin frames work. Omit for the top document; a bad value returns frame_not_found WITH the list of frames in the tab.', { sessionId: 'required str', expr: 'required str', frame: 'optional str' }, 20, { sessionId: 'work', expr: 'document.querySelector("#userId").value', frame: 'logonbox' }),+ V('pup_switch_window', 'Make a session the active one (for verbs that default to active).', { sessionId: 'required str' }, 10, { sessionId: 'work' }),+ V('pup_list_tabs', 'List the tabs in a session.', { sessionId: 'required str' }, 10, { sessionId: 'work' }),+ V('pup_switch_tab', 'Activate a tab WITHIN its session/window (idempotent — safe to call when the tab is already active). Does NOT raise a background window to the user\'s screen: the tab becomes the window\'s visible tab wherever the window is, and the taskbar auto-flash tells the user something changed. Also self-heals the window (re-park if a bug ever stranded it off-screen) and refreshes the Adom taskbar badge — good citizenship for long-lived shared windows.', { sessionId: 'required str', tabId: 'required str', updateNote: 'optional str', silent: 'optional bool' }, 10, { sessionId: 'work', tabId: 'tab-2' }),+ V('pup_screenshot_full_res', 'FULL-resolution page screenshot (NOT downscaled — for saving to disk, NOT for Read: it can exceed Claude\'s image limit). Use pup_screenshot for AI-readable shots.', { sessionId: 'required str', fullPage: 'optional bool' }, 30, { sessionId: 'work' }),+ V('pup_errors', 'Console errors + failed network requests for a session/tab (clears by default).', { sessionId: 'required str', tabId: 'optional str', clear: 'optional bool (default true)' }, 10, { sessionId: 'work' }),+ V('pup_wait', 'Wait N ms (or for a selector) before the next step.', { sessionId: 'required str', ms: 'optional int', selector: 'optional str' }, 60, { sessionId: 'work', ms: 500 }),+ V('pup_rescan', 'Recover orphaned sessions: reconnect dropped CDP sockets + rebuild sessions from window titles. Pass adoptOrphans:true to also adopt untagged tabs.', { adoptOrphans: 'optional bool' }, 30, {}),+ V('pup_close', 'Close ALL pup sessions + windows at once (destructive). NOTE: `{}` here does NOT mean "the active session" the way it does on other session-scoped verbs — with no args this closes EVERY window, across every AI thread on the machine, with no confirmation. To close ONE window use pup_close_window {sessionId}. Pass {sessionId} here only if you genuinely mean that one session.', {}, 20, {}), V('desktop_record_status', 'Status of the desktop screen recorder + any active clip.', {}, 10, {}), V('desktop_record_list', 'List completed desktop recordings on disk.', {}, 10, {}), V('desktop_recorder_close', 'Close the desktop screen-recorder UI.', {}, 15, {}),- V('browser_configure', 'Get/set user-facing pup preferences (persisted across bridge updates). taskbarGrouping: "split" (default — one taskbar button PER WINDOW, each badged with its own tab count; best for telling AI threads apart) | "grouped" (all pup windows stack under ONE "Adom Pup" button, badge shows window count; tidy taskbar). Applies to live windows immediately. Ask the USER which they prefer before changing it — this is their taskbar. WINDOWS ONLY: macOS has no taskbar, so the setting is stored but INERT there; the reply carries platform + taskbarSupported so branch on those, not on this text.', { taskbarGrouping: 'optional "split"|"grouped"', agentActivity: 'optional "on"|"off" — the taskbar progress bar marking which window a thread is driving', reportIssues: 'optional "on"|"off" — pup\'s bug-report invitation (_reportIssues), on by default', autoLogin: 'optional "on"|"off" — auto-fill a vault-matched login form on page load (fill only, never submits), on by default' }, 15, { taskbarGrouping: 'grouped' }),- V('browser_describe', 'This list — the bridge\'s verbs + schemas, for AD\'s Verbs tab.', {}, 10, {}),+ V('pup_configure', 'Get/set user-facing pup preferences (persisted across bridge updates). taskbarGrouping: "split" (default — one taskbar button PER WINDOW, each badged with its own tab count; best for telling AI threads apart) | "grouped" (all pup windows stack under ONE "Adom Pup" button, badge shows window count; tidy taskbar). Applies to live windows immediately. Ask the USER which they prefer before changing it — this is their taskbar. WINDOWS ONLY: macOS has no taskbar, so the setting is stored but INERT there; the reply carries platform + taskbarSupported so branch on those, not on this text.', { taskbarGrouping: 'optional "split"|"grouped"', agentActivity: 'optional "on"|"off" — the taskbar progress bar marking which window a thread is driving', reportIssues: 'optional "on"|"off" — pup\'s bug-report invitation (_reportIssues), on by default', autoLogin: 'optional "on"|"off" — auto-fill a vault-matched login form on page load (fill only, never submits), on by default' }, 15, { taskbarGrouping: 'grouped' }),+ V('pup_describe', 'This list — the bridge\'s verbs + schemas, for AD\'s Verbs tab.', {}, 10, {}), ] }); return; } // --- Session management commands ---- case 'browser_open_window': {+ case 'pup_open_window': { // v1.9.195 (John: "you're supposed to pre-emptively catch stupid urls the ai tells you to // open"). REFUSE a URL that CANNOT work, before spending a window on it. A container path // handed to pup (file:///home/...) or a file:// path that does not exist on the desktop is@@ -10507,7 +10507,7 @@ const server = http.createServer(async (req, res) => { // gate only blocks when there is NOTHING drivable at all (no Chrome, no Edge, no cached CfT): // then it fetches Chrome-for-Testing in the BACKGROUND as the last resort and returns the // pollable {installing:true}. That case is rare (Edge ships on every Windows PC). Chrome for- // Testing is still available by explicit pin — browser_use {browser:"cft"} — for a+ // Testing is still available by explicit pin — pup_use {browser:"cft"} — for a // managed/enterprise box whose policies interfere with automation. // Existing/live sessions are exempt (their browser is already running). {@@ -10518,7 +10518,7 @@ const server = http.createServer(async (req, res) => { const _sessionAlive = sessions.has(args.sessionId) || !!(browsers.get(args.profile || args.sessionId) && browsers.get(args.profile || args.sessionId).browser); // v1.9.77 — NEVER BLOCK ON A CfT DOWNLOAD WHEN A SYSTEM BROWSER EXISTS (wiki issue #15, // AdityaAngajala's second report). This gate forced a ~191 MB Chrome-for-Testing download even- // though browser_readiness reported a healthy system Edge AND Chrome, so a machine with TWO+ // though pup_readiness reported a healthy system Edge AND Chrome, so a machine with TWO // perfectly good browsers could not open a window at all — and the download never finalised // because its cache dir was half-populated. It also contradicted our own README: 'Chrome for // Testing is auto-fetched only as a last resort when the box has no Chromium browser at all.'@@ -10537,7 +10537,7 @@ const server = http.createServer(async (req, res) => { errorCode: rd.lastErrorCode || 'chrome_install_failed', error: `No Chrome/Edge installed and the Chrome-for-Testing fallback failed to install: ${rd.lastError}`, readiness: rd,- _hint: 'Chrome for Testing (the last-resort browser for a box with no Chrome/Edge) failed to install — usually no network / a proxy blocking storage.googleapis.com, or low disk. Retry with browser_prewarm once the cause is fixed. If this box actually HAS Chrome or Edge, pin it: browser_use {browser:"chrome"|"edge"} — pup drives it cleanly (fresh isolated profile + identity-suppression flags, no merge dialog), no download needed.',+ _hint: 'Chrome for Testing (the last-resort browser for a box with no Chrome/Edge) failed to install — usually no network / a proxy blocking storage.googleapis.com, or low disk. Retry with pup_prewarm once the cause is fixed. If this box actually HAS Chrome or Edge, pin it: pup_use {browser:"chrome"|"edge"} — pup drives it cleanly (fresh isolated profile + identity-suppression flags, no merge dialog), no download needed.', }); return; }@@ -10561,10 +10561,10 @@ const server = http.createServer(async (req, res) => { success: false, errorCode: 'chrome_for_testing_installing', installing: true,- statusVerb: 'browser_readiness',+ statusVerb: 'pup_readiness', readiness: rd, error: 'This box has no installed Chrome or Edge, so pup is fetching Chrome for Testing as the last-resort browser (~150 MB, one time).',- _hint: 'LAST-RESORT PROVISIONING (not an error): pup drives your INSTALLED Chrome or Edge by default, cleanly — a fresh isolated profile PLUS launch flags that suppress Chrome\'s sign-in / "set up a work profile" merge dialog, so there is no identity bleed. THIS box has neither Chrome nor Edge, so pup is fetching Chrome for Testing in the background as the only drivable option (~a minute or two, one time). Poll `browser_readiness` until {ready:true}, then re-issue browser_open_window. If the box actually DOES have Chrome/Edge, pin it with browser_use {browser:"chrome"|"edge"} to skip the download.',+ _hint: 'LAST-RESORT PROVISIONING (not an error): pup drives your INSTALLED Chrome or Edge by default, cleanly — a fresh isolated profile PLUS launch flags that suppress Chrome\'s sign-in / "set up a work profile" merge dialog, so there is no identity bleed. THIS box has neither Chrome nor Edge, so pup is fetching Chrome for Testing in the background as the only drivable option (~a minute or two, one time). Poll `pup_readiness` until {ready:true}, then re-issue pup_open_window. If the box actually DOES have Chrome/Edge, pin it with pup_use {browser:"chrome"|"edge"} to skip the download.', }); return; }@@ -10601,7 +10601,7 @@ const server = http.createServer(async (req, res) => { if (_own && isSessionAlive(_own) && args.url) { try { const tabId = await addTabToSession(args.sessionId, args.url);- recordAgentUpdate(_own, 'browser_open_window', args, tabId);+ recordAgentUpdate(_own, 'pup_open_window', args, tabId); scheduleAutoFlash(_own, { silent: !!args.silent }); updateTabCountBadge(_own, args.sessionId).catch(() => {}); sendJSON(res, { success: true, output: JSON.stringify({@@ -10673,12 +10673,12 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: false, errorCode: 'caller_identity_required',- error: `'browser_open_window' needs to know WHICH AI THREAD is opening this window. pup owns every session by ai-thread NAME now — the sessionId is not an identity.`,+ error: `'pup_open_window' needs to know WHICH AI THREAD is opening this window. pup owns every session by ai-thread NAME now — the sessionId is not an identity.`, supplyIdentity: {- bestWay: `adom-desktop --ai-thread "<your conversation name>" browser_open_window '<json>'`,+ bestWay: `adom-desktop --ai-thread "<your conversation name>" pup_open_window '<json>'`, orInArgs: { caller: { aiThread: '<your conversation name>', containerName: '<container>' } }, },- _hint: `Re-run with your ai-thread name so pup can own this window as YOU — e.g. adom-desktop --ai-thread "chip-fetcher tab 3" browser_open_window '{"sessionId":"chip-fetcher-web","url":"..."}'. That NAME (not the sessionId) is the window's owner and what protects it from other threads; sessionId is just the window's key, never who owns it.`,+ _hint: `Re-run with your ai-thread name so pup can own this window as YOU — e.g. adom-desktop --ai-thread "chip-fetcher tab 3" pup_open_window '{"sessionId":"chip-fetcher-web","url":"..."}'. That NAME (not the sessionId) is the window's owner and what protects it from other threads; sessionId is just the window's key, never who owns it.`, }); return; }@@ -10695,7 +10695,7 @@ const server = http.createServer(async (req, res) => { } if (!_held) _sessionOwnerClaims.set(sessionId, _wantOwner); // Stamp the resolved ai-thread as the owner for the rest of the pipeline (launchSession +- // browser_status). _wantOwner is always set here (we refused above if not).+ // pup_status). _wantOwner is always set here (we refused above if not). args.owner = _wantOwner; if (_aiThread && _wantOwner === _aiThread) args._ownerFromCallerIdentity = true; }@@ -10754,7 +10754,7 @@ const server = http.createServer(async (req, res) => { errorCode: 'session_owned_by_another_thread', error: e.message, ...d,- _hint: `STOP — do NOT take this window. Session "${d.sessionId}" was opened by another AI thread (owner "${d.owner}") and is currently showing ${d.currentUrl}. Navigating it away would destroy that thread's work. Do ONE of: (1) open your OWN window with a UNIQUE sessionId — prefix it with your task, e.g. "${d.requestedBy}-web" (RECOMMENDED); (2) if the user EXPLICITLY told you to repurpose this window, re-send with takeover:true to claim it; (3) browser_status first to see all windows + owners before picking a sessionId. Generic ids like "wiki"/"test"/"main" collide across threads — always task-prefix yours.`,+ _hint: `STOP — do NOT take this window. Session "${d.sessionId}" was opened by another AI thread (owner "${d.owner}") and is currently showing ${d.currentUrl}. Navigating it away would destroy that thread's work. Do ONE of: (1) open your OWN window with a UNIQUE sessionId — prefix it with your task, e.g. "${d.requestedBy}-web" (RECOMMENDED); (2) if the user EXPLICITLY told you to repurpose this window, re-send with takeover:true to claim it; (3) pup_status first to see all windows + owners before picking a sessionId. Generic ids like "wiki"/"test"/"main" collide across threads — always task-prefix yours.`, }); return; }@@ -10767,7 +10767,7 @@ const server = http.createServer(async (req, res) => { // tab open or popup auto-attach in this session. if (session) session._downloadPath = downloadPath; // #338: the owner came from the calling ai-thread (caller identity), not a self-asserted arg —- // mark it so browser_status/list_windows report ownerSource:"caller-identity" instead of "declared".+ // mark it so pup_status/list_windows report ownerSource:"caller-identity" instead of "declared". if (session && args._ownerFromCallerIdentity) session._ownerFromCallerIdentity = true; // v1.9.102 — STAMP THE OWNING THREAD *HERE*, not at dispatch. The pre-dispatch stamp can // only find a session that already exists, and on an OPEN the session is created INSIDE this@@ -10889,9 +10889,9 @@ const server = http.createServer(async (req, res) => { session._wikiAuthed = !!(_wikiAuth && _wikiAuth.authed); } if (session && fgGranted) {- session._lastForeground = { ts: Date.now(), verb: 'browser_open_window', reason: fgReason };+ session._lastForeground = { ts: Date.now(), verb: 'pup_open_window', reason: fgReason }; toastForeground(sessionId, fgReason).catch(() => {}); // v1.9.166- try { recordAgentUpdate(session, 'browser_open_window', { updateNote: `FOREGROUND: ${fgReason}` }, session.activeTabId); } catch (e) {}+ try { recordAgentUpdate(session, 'pup_open_window', { updateNote: `FOREGROUND: ${fgReason}` }, session.activeTabId); } catch (e) {} } // v1.8.23: session-level auto-flash default (autoFlash:false → this window never // auto-flashes, e.g. a chatty automation loop the user isn't watching).@@ -10899,7 +10899,7 @@ const server = http.createServer(async (req, res) => { // Flash on open (a NEW window appearing IS the update) — unless foregrounded // (already on screen) or suppressed. Also stamp the first agent update. if (session) {- recordAgentUpdate(session, 'browser_open_window', args, session.activeTabId);+ recordAgentUpdate(session, 'pup_open_window', args, session.activeTabId); // v1.9.140: the launch path already flashed this open — consume the marker and skip. if (!foreground && !_openFlashDone.delete(sessionId)) scheduleAutoFlash(session, { silent: !!args.silent }); }@@ -10919,17 +10919,17 @@ const server = http.createServer(async (req, res) => { } // Verbose browser report: which browser we actually drove, what else is available. const brep = browserPickReport();- const fgDenyHint = fgDenied ? ' ⛔ FOREGROUND DENIED: you passed foreground:true WITHOUT a foregroundReason, so the window opened in the BACKGROUND (the open itself succeeded). Foregrounding steals the user\'s screen+keyboard and is allowed ONLY when the USER explicitly asked to SEE this window. If they did: re-call with foreground:true + foregroundReason:"<quote/paraphrase what the user said>" (min 10 chars — it is recorded and auditable). If they did not: leave it background; browser_alert_window is the correct "it\'s ready" signal.' : '';+ const fgDenyHint = fgDenied ? ' ⛔ FOREGROUND DENIED: you passed foreground:true WITHOUT a foregroundReason, so the window opened in the BACKGROUND (the open itself succeeded). Foregrounding steals the user\'s screen+keyboard and is allowed ONLY when the USER explicitly asked to SEE this window. If they did: re-call with foreground:true + foregroundReason:"<quote/paraphrase what the user said>" (min 10 chars — it is recorded and auditable). If they did not: leave it background; pup_alert_window is the correct "it\'s ready" signal.' : ''; const bgHint = foreground- ? 'Opened in the FOREGROUND as EXPLICITLY requested (foreground/raise/show:true). Return it to the background with browser_lower_os_window once the user is done watching.'- : `Opened in the BACKGROUND (behind the user's windows, did NOT steal focus)${sized ? ' — and placed at the requested size/position WITHOUT foregrounding, so framing it for a screenshot never disturbs the user' : ''}. This is the HARD default so pup never disrupts the user's main work. It is FULLY drivable here: browser_screenshot / browser_eval / browser_navigate all work while it stays hidden. ONLY bring it to the user's screen if they EXPLICITLY want to WATCH (a demo, a recording, a "show me") — pass foreground:true on open (or call browser_raise_os_window). Sizing alone does NOT foreground anymore.`;+ ? 'Opened in the FOREGROUND as EXPLICITLY requested (foreground/raise/show:true). Return it to the background with pup_lower_os_window once the user is done watching.'+ : `Opened in the BACKGROUND (behind the user's windows, did NOT steal focus)${sized ? ' — and placed at the requested size/position WITHOUT foregrounding, so framing it for a screenshot never disturbs the user' : ''}. This is the HARD default so pup never disrupts the user's main work. It is FULLY drivable here: pup_screenshot / pup_eval / pup_navigate all work while it stays hidden. ONLY bring it to the user's screen if they EXPLICITLY want to WATCH (a demo, a recording, a "show me") — pass foreground:true on open (or call pup_raise_os_window). Sizing alone does NOT foreground anymore.`; // v1.8.16: ownership/reuse verdict — reprimand grabs, bless refreshes. let ownHint = ''; if (reuse && reuse.navigatedAway) { if (reuse.takeover) { ownHint = ` TAKEOVER: you claimed session "${sessionId}" from thread "${reuse.previousOwner}" (was showing ${reuse.previousUrl}) via takeover:true — you now own it. Only do this when the user explicitly asked.`; } else if (reuse.anonymousGrab) {- ownHint = ` ⚠ YOU JUST TOOK OVER AN EXISTING WINDOW: session "${sessionId}" belonged to thread "${reuse.previousOwner}" and was showing ${reuse.previousUrl} — you navigated it away WITHOUT declaring an owner. If that window wasn't yours, you broke another AI thread's work: re-open ITS url (${reuse.previousUrl}) on this sessionId, then use your OWN unique task-prefixed sessionId. ALWAYS pass owner:"<your-task-name>" on browser_open_window so pup can protect windows across threads.`;+ ownHint = ` ⚠ YOU JUST TOOK OVER AN EXISTING WINDOW: session "${sessionId}" belonged to thread "${reuse.previousOwner}" and was showing ${reuse.previousUrl} — you navigated it away WITHOUT declaring an owner. If that window wasn't yours, you broke another AI thread's work: re-open ITS url (${reuse.previousUrl}) on this sessionId, then use your OWN unique task-prefixed sessionId. ALWAYS pass owner:"<your-task-name>" on pup_open_window so pup can protect windows across threads.`; } else if (reuse.sameOwner) { ownHint = ` Reused your own window (owner match) — navigated ${reuse.previousUrl} → new url.`; } else {@@ -10941,15 +10941,15 @@ const server = http.createServer(async (req, res) => { // v1.8.57: teach anonymous CREATORS too (a long-lived app like adom-shotlog that // never declares owner gets zero cross-thread protection — its window can be // navigated away or closed by any other AI thread without pup refusing).- ownHint = ` TIP: you created this window WITHOUT owner — pass owner:"<your-app-or-task>" on browser_open_window so pup REFUSES other threads' attempts to navigate/close it (session_owned_by_another_thread) instead of letting them take it. Especially important for a long-lived shared window.`;+ ownHint = ` TIP: you created this window WITHOUT owner — pass owner:"<your-app-or-task>" on pup_open_window so pup REFUSES other threads' attempts to navigate/close it (session_owned_by_another_thread) instead of letting them take it. Especially important for a long-lived shared window.`; } // v1.9.76 — HIGH-FPS / POWER hint on EVERY open. An AI cannot discover this // capability otherwise, and the wrong choice stays invisible until a video comes // out at 1-2 fps or a user's battery is flat overnight (issue #14). const hf = highFpsInfo(sessionId); const fpsHint = hf- ? ` HIGH-FPS WINDOW, LEASED ${hf.minutes} MIN (expires ${hf.expiresAt}). Chrome's four power-saving mechanisms are DISABLED here, so it renders at FULL RATE even while occluded — that is what makes a background video recording usable. It also means it never idles down, so it is LEASED: when the lease lapses pup DOWNGRADES it to normal power by relaunching the same session with the same tabs (it is NOT closed — your content survives). YOU CANNOT BE CUT OFF MID-RECORDING: while a recording is actually running the lease cannot lapse. If you know you need longer WITHOUT recording the whole time, negotiate now: browser_highfps_extend {sessionId:"${sessionId}", minutes:N} (max ${HIGHFPS_MAX_MINUTES}). pup warns ~2 min before it downgrades.`- : ` POWER: NORMAL window on Chrome's own power behaviour — it throttles when occluded, so it costs nothing sitting in the background. Right default for driving, scraping and screenshots. If you need to RECORD VIDEO of this window while it is occluded, a normal window only yields ~1-2 fps: reopen with browser_open_window {highFps:true} for full-rate background frames. highFps is leased (default ${HIGHFPS_DEFAULT_MINUTES} min) and auto-downgrades when idle, so it can never flatten the user's battery.`;+ ? ` HIGH-FPS WINDOW, LEASED ${hf.minutes} MIN (expires ${hf.expiresAt}). Chrome's four power-saving mechanisms are DISABLED here, so it renders at FULL RATE even while occluded — that is what makes a background video recording usable. It also means it never idles down, so it is LEASED: when the lease lapses pup DOWNGRADES it to normal power by relaunching the same session with the same tabs (it is NOT closed — your content survives). YOU CANNOT BE CUT OFF MID-RECORDING: while a recording is actually running the lease cannot lapse. If you know you need longer WITHOUT recording the whole time, negotiate now: pup_highfps_extend {sessionId:"${sessionId}", minutes:N} (max ${HIGHFPS_MAX_MINUTES}). pup warns ~2 min before it downgrades.`+ : ` POWER: NORMAL window on Chrome's own power behaviour — it throttles when occluded, so it costs nothing sitting in the background. Right default for driving, scraping and screenshots. If you need to RECORD VIDEO of this window while it is occluded, a normal window only yields ~1-2 fps: reopen with pup_open_window {highFps:true} for full-rate background frames. highFps is leased (default ${HIGHFPS_DEFAULT_MINUTES} min) and auto-downgrades when idle, so it can never flatten the user's battery.`; sendJSON(res, { success: true, output: JSON.stringify({@@ -10963,7 +10963,7 @@ const server = http.createServer(async (req, res) => { defaultPermissionsCount: args.strictPermissions ? 0 : PERMISSIVE_DEFAULT_PERMISSIONS.length, defaultPermissionsNote: args.strictPermissions ? 'strictPermissions: nothing pre-granted'- : 'full browser permissions pre-granted (clipboard, camera, geolocation, window-management, ...) — browser_describe has the exact list',+ : 'full browser permissions pre-granted (clipboard, camera, geolocation, window-management, ...) — pup_describe has the exact list', downloadsEnabled: !args.strictPermissions, downloadPath, // v1.4.0: which browser pup drove + the full fallback picture.@@ -11006,7 +11006,7 @@ const server = http.createServer(async (req, res) => { const st = (typeof rc.httpStatus === 'number') ? ` (HTTP ${rc.httpStatus})` : ''; return `⛔ THE PAGE YOU JUST OPENED IS BROKEN: ${rc.signals.join(', ')}${st}` + `${rc.excerpt ? ` — it says "${String(rc.excerpt).replace(/\s+/g, ' ').slice(0, 120)}"` : ''}. `- + `Do NOT report success. Screenshot it (browser_screenshot {"sessionId":"${sessionId}"}), fix the cause, browser_reload, re-check. `+ + `Do NOT report success. Screenshot it (pup_screenshot {"sessionId":"${sessionId}"}), fix the cause, pup_reload, re-check. ` + `See _verifyRender for the full checklist. Everything below is secondary: ${rest}`; })(), ...info,@@ -11014,7 +11014,7 @@ const server = http.createServer(async (req, res) => { }); return; }- case 'browser_switch_window': {+ case 'pup_switch_window': { const sessionId = args.sessionId; if (!sessionId || !sessions.has(sessionId)) { const available = [...sessions.keys()];@@ -11027,7 +11027,7 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, ...info }) }); return; }- case 'browser_list_windows': {+ case 'pup_list_windows': { // v1.9.172 (jam fix): gather sessions IN PARALLEL, each with a hard per-session timeout that // falls back to a pure in-memory snapshot. Previously this awaited getSessionInfo() SERIALLY, // and getSessionInfo did uncapped CDP reads (title() + a focus eval) per window — so ONE busy@@ -11046,7 +11046,7 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify(out) }); return; }- case 'browser_configure': {+ case 'pup_configure': { // v1.8.82: user-facing pup preferences. Currently: taskbarGrouping 'split'|'grouped'. const valid = ['split', 'grouped']; const g = args.taskbarGrouping;@@ -11124,7 +11124,7 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, settings: pupSettings(), platform: process.platform, taskbarSupported: _isWin, _hint: _gHint }) }); return; }- case 'browser_rescan': {+ case 'pup_rescan': { // v1.5.1+: orphan-recovery primitive. Walk every known profile // (in-memory + on-disk session files), attempt reconnect for any // whose CDP socket dropped, then walk pages of every connected@@ -11168,10 +11168,10 @@ const server = http.createServer(async (req, res) => { disconnectedSessions, _hint: disconnectedSessions.length > 0- ? `Some sessions still disconnected: [${disconnectedSessions.join(', ')}]. Their Chrome PID may be dead — check with browser_list_windows. browser_open_window (with the original sessionId + profile) will start fresh; the on-disk session file is overwritten.`+ ? `Some sessions still disconnected: [${disconnectedSessions.join(', ')}]. Their Chrome PID may be dead — check with pup_list_windows. pup_open_window (with the original sessionId + profile) will start fresh; the on-disk session file is overwritten.` : (liveSessions.length > 0 ? `All known sessions are live: [${liveSessions.join(', ')}].`- : `No sessions known to the bridge. browser_open_window to start one.`),+ : `No sessions known to the bridge. pup_open_window to start one.`), }), }); } catch (e) {@@ -11179,7 +11179,7 @@ const server = http.createServer(async (req, res) => { } return; }- case 'browser_close_window': {+ case 'pup_close_window': { const sessionId = args.sessionId; if (!sessionId || !sessions.has(sessionId)) { sendJSON(res, { success: false, error: `Session "${sessionId}" not found` });@@ -11187,7 +11187,7 @@ const server = http.createServer(async (req, res) => { } const session = sessions.get(sessionId); // v1.8.16: refuse a declared cross-owner close (destroying another thread's window).- { const refusal = ownershipRefusal(session, sessionId, args, 'browser_close_window'); if (refusal) { sendJSON(res, refusal); return; } }+ { const refusal = ownershipRefusal(session, sessionId, args, 'pup_close_window'); if (refusal) { sendJSON(res, refusal); return; } } // v1.9.189: prune this window's AUMID key(s) AFTER the close settles. Previously keys were // only pruned at boot, so every closed window left registry state behind until the next // restart (audit found Adom.Pup.mt3.web.i3 alive long after mt3 was gone).@@ -11244,7 +11244,7 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, remaining: sessions.size, activeSession: activeSessionId, chromeProcsKilled: _reap.killed, chromeProcsRemaining: _reap.remaining,- ...(_reap.remaining ? { _hint: `WARNING: ${_reap.remaining} chrome/renderer process(es) for this profile SURVIVED teardown (should be 0) and may peg a CPU core. The box may be under heavy load — retry browser_close_window; if it persists it is a reap bug worth reporting.` } : {}),+ ...(_reap.remaining ? { _hint: `WARNING: ${_reap.remaining} chrome/renderer process(es) for this profile SURVIVED teardown (should be 0) and may peg a CPU core. The box may be under heavy load — retry pup_close_window; if it persists it is a reap bug worth reporting.` } : {}), }) }); return; }@@ -11254,8 +11254,8 @@ const server = http.createServer(async (req, res) => { // fixed at browser launch, so it's a close-and-relaunch of the same sessionId under the // other profile at the same URL. Invoked when the user clicks the per-window taskbar // right-click task (runs AD's bundled CLI → this verb); it's a USER action, so the window- // is brought on-screen. Not meant for AI threads (they pass wikiView on browser_open_window).- case 'browser_wiki_set_view': {+ // is brought on-screen. Not meant for AI threads (they pass wikiView on pup_open_window).+ case 'pup_wiki_set_view': { const sessionId = args.sessionId; const view = args.view === 'authed' ? 'authed' : 'public'; if (!sessionId || !sessions.has(sessionId)) { sendJSON(res, { success: false, error: `Session "${sessionId}" not found` }); return; }@@ -11460,7 +11460,7 @@ const server = http.createServer(async (req, res) => { // v1.9.11: OS caption (AD >=1.9.144). Fire-and-forget: the old window is torn down // moments later, so an OS-level surface is the ONLY thing that survives the switch. wikiCaption(_switchMsg, 30000).catch(() => {});- // Close the current window (mirror browser_close_window) so we can relaunch under the+ // Close the current window (mirror pup_close_window) so we can relaunch under the // other cookie jar. const oldProfile = session.profileName; const _closingAppId = session._curAppId || `${PUP_APP_ID}.${sessionId}`;@@ -11524,8 +11524,8 @@ const server = http.createServer(async (req, res) => { wikiCaption('Adom wiki: the switch finished but the window did not come back on screen. Click the Adom Pup taskbar button to raise it.', 15000).catch(() => {}); } try { flashViaAD(sessionId, {}); } catch (e) {}- s2._lastForeground = { ts: Date.now(), verb: 'browser_wiki_set_view', reason: `user clicked the taskbar "switch to ${view} view" task` };- try { recordAgentUpdate(s2, 'browser_wiki_set_view', { updateNote: `wiki view → ${view} (user clicked taskbar task)` }, s2.activeTabId); } catch (e) {}+ s2._lastForeground = { ts: Date.now(), verb: 'pup_wiki_set_view', reason: `user clicked the taskbar "switch to ${view} view" task` };+ try { recordAgentUpdate(s2, 'pup_wiki_set_view', { updateNote: `wiki view → ${view} (user clicked taskbar task)` }, s2.activeTabId); } catch (e) {} // v1.9.7: set an OPTIMISTIC glyph immediately (the window is on the authed profile, and the // common case IS logged in) so the tab reflects the switch the instant it pops, then // RESPOND — the window is already raised. Everything below runs in the BACKGROUND so a@@ -11570,14 +11570,14 @@ const server = http.createServer(async (req, res) => { } // --- Tab-level commands ---- case 'browser_open_tab': {+ case 'pup_open_tab': { // v1.9.146: explicit sessionId wins; otherwise the CALLER THREAD's own most-recent window — // NEVER the global activeSessionId (that is another thread's window as often as not). const sessionId = args.sessionId || callerOwnWindow(_callerThread); if (!sessionId || !sessions.has(sessionId)) { const mine = _callerThread ? [...sessions].filter(([,s]) => s._ownerThread === _callerThread).map(([id]) => id) : []; sendJSON(res, { success: false, errorCode: 'no_target_window',- error: `No window to add a tab to. ${args.sessionId ? `Session "${args.sessionId}" not found.` : (_callerThread ? `Your thread ("${_callerThread}") owns no open pup window yet.` : `No sessionId given and your thread sent no caller identity.`)} Open one with browser_open_window first (pass a sessionId you'll reuse), then browser_open_tab into it.`,+ error: `No window to add a tab to. ${args.sessionId ? `Session "${args.sessionId}" not found.` : (_callerThread ? `Your thread ("${_callerThread}") owns no open pup window yet.` : `No sessionId given and your thread sent no caller identity.`)} Open one with pup_open_window first (pass a sessionId you'll reuse), then pup_open_tab into it.`, yourWindows: mine }); return; }@@ -11587,7 +11587,7 @@ const server = http.createServer(async (req, res) => { const _tgt = sessions.get(sessionId); if (_tgt && _tgt._ownerThread && _callerThread && _tgt._ownerThread !== _callerThread && !args.takeover) { sendJSON(res, { success: false, errorCode: 'session_owned_by_another_thread',- error: `Window "${sessionId}" is owned by thread "${_tgt._ownerThread}", not "${_callerThread}". Opening a tab here would land in another agent's window. Open your OWN window (browser_open_window) or pass takeover:true if you truly mean to use theirs.` });+ error: `Window "${sessionId}" is owned by thread "${_tgt._ownerThread}", not "${_callerThread}". Opening a tab here would land in another agent's window. Open your OWN window (pup_open_window) or pass takeover:true if you truly mean to use theirs.` }); return; } }@@ -11595,7 +11595,7 @@ const server = http.createServer(async (req, res) => { const tabId = await addTabToSession(sessionId, args.url); const session = sessions.get(sessionId); const tab = getTabById(session, tabId);- recordAgentUpdate(session, 'browser_open_tab', args, tabId); // v1.8.23+ recordAgentUpdate(session, 'pup_open_tab', args, tabId); // v1.8.23 scheduleAutoFlash(session, { silent: !!args.silent }); updateTabCountBadge(session, sessionId).catch(() => {}); // v1.8.80 // v1.8.57: long-lived-app touchpoint (see switch_tab) — heal a stranded window,@@ -11628,7 +11628,7 @@ const server = http.createServer(async (req, res) => { } return; }- case 'browser_render_html': {+ case 'pup_render_html': { // v1.8.23: render a local HTML string in the tab via page.setContent — which // handles encoding correctly (UTF-8), unlike a data: URL that mojibakes accented // chars without an explicit charset. Pass {html}. Great for previewing generated@@ -11640,19 +11640,19 @@ const server = http.createServer(async (req, res) => { try { ({ tab, tabId } = resolveTabOrError(session, resolvedId, args)); } catch (e) { sendTabResolveError(res, e); return; } if (typeof args.html !== 'string') {- sendJSON(res, { success: false, error: 'browser_render_html requires an html string.', _hint: 'e.g. {"sessionId":"...","html":"<h1>Café</h1>"} — setContent renders it as UTF-8 (a data: URL would mojibake the é).' });+ sendJSON(res, { success: false, error: 'pup_render_html requires an html string.', _hint: 'e.g. {"sessionId":"...","html":"<h1>Café</h1>"} — setContent renders it as UTF-8 (a data: URL would mojibake the é).' }); return; } try { await withTimeout(tab.page.setContent(args.html, { waitUntil: args.waitUntil || 'domcontentloaded', timeout: 20000 }), 25_000, 'page.setContent');- recordAgentUpdate(session, 'browser_render_html', args, tabId);+ recordAgentUpdate(session, 'pup_render_html', args, tabId); scheduleAutoFlash(session, { silent: !!args.silent }); let title = null; try { title = await tab.page.title(); } catch {} sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, title, bytes: args.html.length, _hint: 'Rendered your HTML in the tab (UTF-8, no data:-URL mojibake). Screenshot it or drive it like any page.' }) });- } catch (e) { sendJSON(res, { success: false, error: `browser_render_html failed: ${e.message}` }); }+ } catch (e) { sendJSON(res, { success: false, error: `pup_render_html failed: ${e.message}` }); } return; }- case 'browser_switch_tab': {+ case 'pup_switch_tab': { const sessionId = args.sessionId || activeSessionId; if (!sessionId || !sessions.has(sessionId)) { sendJSON(res, { success: false, error: `Session "${sessionId}" not found` });@@ -11685,7 +11685,7 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId, ...info }) }); return; }- case 'browser_close_tab': {+ case 'pup_close_tab': { const sessionId = args.sessionId || activeSessionId; if (!sessionId || !sessions.has(sessionId)) { sendJSON(res, { success: false, error: `Session "${sessionId}" not found` });@@ -11693,7 +11693,7 @@ const server = http.createServer(async (req, res) => { } const session = sessions.get(sessionId); if (!args.tabId) {- sendJSON(res, { success: false, error: 'tabId is required for browser_close_tab' });+ sendJSON(res, { success: false, error: 'tabId is required for pup_close_tab' }); return; } const idx = session.tabs.findIndex(t => t.tabId === args.tabId);@@ -11715,7 +11715,7 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId, closedTab: args.tabId, remaining: session.tabs.length, activeTabId: session.activeTabId }) }); return; }- case 'browser_list_tabs': {+ case 'pup_list_tabs': { drainWindowOpenedEvents().catch(() => {}); // v1.9.32: cheap event-driven drag-out check const sessionId = args.sessionId || activeSessionId; if (!sessionId || !sessions.has(sessionId)) {@@ -11733,7 +11733,7 @@ const server = http.createServer(async (req, res) => { return; } - case 'browser_alert_window': {+ case 'pup_alert_window': { // Flash the taskbar button for a session's window WITHOUT foregrounding it. v1.8.35: // {stop:true} (alias {clear:true}) CLEARS a lingering orange highlight instead. // (Pre-1.8.35 this wrongly called bringToFront — it raised the window instead of@@ -11801,7 +11801,7 @@ const server = http.createServer(async (req, res) => { } return; }- case 'browser_lower_os_window': {+ case 'pup_lower_os_window': { // Send a pup window to the BACKGROUND (behind the user's windows) without // stealing focus. It stays fully drivable + screenshottable. This is the // "get it off the user's screen" verb — the counterpart to raise/focus.@@ -11829,8 +11829,8 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId, placement: 'background', applied: ok, _hint: 'Window sent behind the user\'s windows (still drivable/screenshottable). Keep pup in the background unless the user asked to watch.' }) }); return; }- case 'browser_raise_os_window':- case 'browser_focus_window': {+ case 'pup_raise_os_window':+ case 'pup_focus_window': { // Actually brings the Chrome window to the foreground using Win32 SetForegroundWindow const sessionId = args.sessionId || activeSessionId; if (!sessionId || !sessions.has(sessionId)) {@@ -11850,7 +11850,7 @@ const server = http.createServer(async (req, res) => { success: false, errorCode: 'foreground_reason_required', error: 'Raise refused: foregroundReason is required.',- _hint: 'Foregrounding steals the user\'s screen and keyboard focus. It is allowed ONLY when the USER explicitly asked to SEE this window (a "show me", a demo, a live recording). Call again with foregroundReason:"<quote or closely paraphrase what the user said that justifies this>" (min 10 chars) — the reason is logged, recorded in lastAgentUpdate, and auditable by the user. If the user did NOT ask to see it, do NOT raise: use browser_alert_window (taskbar nudge) or rely on the auto-flash. After a granted raise: do the one thing the user asked for, then browser_lower_os_window to return it to the background.',+ _hint: 'Foregrounding steals the user\'s screen and keyboard focus. It is allowed ONLY when the USER explicitly asked to SEE this window (a "show me", a demo, a live recording). Call again with foregroundReason:"<quote or closely paraphrase what the user said that justifies this>" (min 10 chars) — the reason is logged, recorded in lastAgentUpdate, and auditable by the user. If the user did NOT ask to see it, do NOT raise: use pup_alert_window (taskbar nudge) or rely on the auto-flash. After a granted raise: do the one thing the user asked for, then pup_lower_os_window to return it to the background.', }); return; }@@ -12029,11 +12029,11 @@ const server = http.createServer(async (req, res) => { } // --- Existing commands (operate on active session) ---- case 'browser_launch': {- sendJSON(res, { success: false, error: 'browser_launch is removed. Use browser_open_window with required sessionId and profile params.' });+ case 'pup_launch': {+ sendJSON(res, { success: false, error: 'pup_launch is removed. Use pup_open_window with required sessionId and profile params.' }); return; }- case 'browser_screenshot': {+ case 'pup_screenshot': { let session, resolvedId; // v1.6.1+: auto-recover (reconnect or relaunch) before surfacing // any error. The Docker container should never see "Session@@ -12065,7 +12065,7 @@ const server = http.createServer(async (req, res) => { const nested = !docScrolls && main !== doc && main.scrollHeight > main.clientHeight + 1; return { docScrolls, nested, scroller: window.__pupDesc(main), scrollHeight: main.scrollHeight, clientHeight: main.clientHeight }; });- if (ns.nested) nestedScrollHint = `fullPage captured only the viewport: this page scrolls inside a NESTED container (${ns.scroller}, ${ns.scrollHeight}px of content vs ${ns.clientHeight}px visible), not the document — fullPage can't stitch that. To capture the whole thing, either browser_scroll {dy:...} through it and stitch, or (for one region) browser_screenshot_element {selector|text}. To capture a specific card, browser_screenshot_element is the one-shot fix.`;+ if (ns.nested) nestedScrollHint = `fullPage captured only the viewport: this page scrolls inside a NESTED container (${ns.scroller}, ${ns.scrollHeight}px of content vs ${ns.clientHeight}px visible), not the document — fullPage can't stitch that. To capture the whole thing, either pup_scroll {dy:...} through it and stitch, or (for one region) pup_screenshot_element {selector|text}. To capture a specific card, pup_screenshot_element is the one-shot fix.`; } catch (e) {} } @@ -12092,14 +12092,14 @@ const server = http.createServer(async (req, res) => { origWidth: shot.origWidth, origHeight: shot.origHeight, resized: shot.resized,- ...(shot.sharpUsed ? {} : { sharpUnavailable: true, _hint: 'Screenshot served at CAPTURE size — the sharp resize library is not installed on this box, so the image was NOT downscaled and may exceed Claude\'s ~2000px Read limit. If a Read fails on size, capture a smaller viewport, or (rare) this box\'s native-dep install failed — see browser_readiness.' }),+ ...(shot.sharpUsed ? {} : { sharpUnavailable: true, _hint: 'Screenshot served at CAPTURE size — the sharp resize library is not installed on this box, so the image was NOT downscaled and may exceed Claude\'s ~2000px Read limit. If a Read fails on size, capture a smaller viewport, or (rare) this box\'s native-dep install failed — see pup_readiness.' }), ...(nestedScrollHint ? { nestedScroll: true, _hint: nestedScrollHint } : {}), sessionId: resolvedId, tabId, }) }); return; }- case 'browser_set_viewport': {+ case 'pup_set_viewport': { // v1.8.21: set the page viewport (CDP device-metrics) deterministically — // independent of the OS window size. Fixes "innerWidth came up ~170px" and lets a // fullPage/element shot render at a known width. width+height required.@@ -12128,23 +12128,23 @@ const server = http.createServer(async (req, res) => { } const vw = Number(args.width), vh = Number(args.height); if (!Number.isFinite(vw) || !Number.isFinite(vh)) {- sendJSON(res, { success: false, error: 'browser_set_viewport requires numeric width + height (or {reset:true} to clear an existing override).', _hint: 'Set: {"sessionId":"...","width":1280,"height":900}. Clear: {"sessionId":"...","reset":true}. Optional deviceScaleFactor (default 1).' });+ sendJSON(res, { success: false, error: 'pup_set_viewport requires numeric width + height (or {reset:true} to clear an existing override).', _hint: 'Set: {"sessionId":"...","width":1280,"height":900}. Clear: {"sessionId":"...","reset":true}. Optional deviceScaleFactor (default 1).' }); return; } const dsf = Number(args.deviceScaleFactor) || 1; try { await tab.page.setViewport({ width: Math.round(vw), height: Math.round(vh), deviceScaleFactor: dsf });- // Fix 2 of 4: RECORD the override so browser_status / browser_list_windows can report it.+ // Fix 2 of 4: RECORD the override so pup_status / pup_list_windows can report it. session._emulatedViewport = { width: Math.round(vw), height: Math.round(vh), deviceScaleFactor: dsf }; const dims = await tab.page.evaluate(() => ({ innerWidth: window.innerWidth, innerHeight: window.innerHeight, devicePixelRatio: window.devicePixelRatio })); // Fix 3 of 4: the hint must state the LIFECYCLE, not just the benefit. The old text sold // the feature and never said the effect is permanent for the life of the window. sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, applied: { width: Math.round(vw), height: Math.round(vh), deviceScaleFactor: dsf }, ...dims,- _hint: `Viewport PINNED to ${Math.round(vw)}×${Math.round(vh)} (innerWidth now ${dims.innerWidth}). ⚠ THIS PERSISTS: it survives navigation AND hard reload, and while it is set the window NO LONGER TRACKS OS RESIZING — a human resizing this window will see the page not reflow, which reads exactly like an app layout bug. CLEAR IT when you are done: browser_set_viewport {"sessionId":"${resolvedId}","reset":true}. It is reported as emulatedViewport in browser_status / browser_list_windows.` }) });+ _hint: `Viewport PINNED to ${Math.round(vw)}×${Math.round(vh)} (innerWidth now ${dims.innerWidth}). ⚠ THIS PERSISTS: it survives navigation AND hard reload, and while it is set the window NO LONGER TRACKS OS RESIZING — a human resizing this window will see the page not reflow, which reads exactly like an app layout bug. CLEAR IT when you are done: pup_set_viewport {"sessionId":"${resolvedId}","reset":true}. It is reported as emulatedViewport in pup_status / pup_list_windows.` }) }); } catch (e) { sendJSON(res, { success: false, error: `setViewport failed: ${e.message}` }); } return; }- case 'browser_scroll': {+ case 'pup_scroll': { // v1.8.21: first-class scroll. Resolves the ACTUAL scrolling element (nearest // ancestor whose scrollHeight>clientHeight, NOT just window — window.scrollTo // no-ops on nested-scroll SPAs). Supports scroll-by-delta {dy,dx} AND scroll-to@@ -12174,18 +12174,18 @@ const server = http.createServer(async (req, res) => { return { found: true, scrollElement: window.__pupDesc(scroller), scrollTop: Math.round(scroller.scrollTop), scrollLeft: Math.round(scroller.scrollLeft), scrollHeight: scroller.scrollHeight, clientHeight: scroller.clientHeight, atBottom: scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - 2 }; }, { dy: Number(args.dy) || 0, dx: Number(args.dx) || 0, toSelector: args.toSelector || null, toText: args.toText || null, block: args.block || null, behavior: args.behavior || 'auto' }); if (!r.found) {- sendJSON(res, { success: false, errorCode: 'scroll_target_not_found', error: `browser_scroll: could not resolve ${args.toSelector ? 'selector "'+args.toSelector+'"' : 'text "'+args.toText+'"'} (searched, piercing shadow roots).`, sessionId: resolvedId, tabId });+ sendJSON(res, { success: false, errorCode: 'scroll_target_not_found', error: `pup_scroll: could not resolve ${args.toSelector ? 'selector "'+args.toSelector+'"' : 'text "'+args.toText+'"'} (searched, piercing shadow roots).`, sessionId: resolvedId, tabId }); return; }- sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, ...r, _hint: `Scrolled ${args.toSelector||args.toText ? 'to the target' : 'by delta'} inside ${r.scrollElement} — scrollTop ${r.scrollTop}/${r.scrollHeight - r.clientHeight}${r.atBottom ? ' (at bottom)' : ''}. Screenshot to confirm, or browser_screenshot_element to grab just a card.` }) });- } catch (e) { sendJSON(res, { success: false, error: `browser_scroll failed: ${e.message}` }); }+ sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, ...r, _hint: `Scrolled ${args.toSelector||args.toText ? 'to the target' : 'by delta'} inside ${r.scrollElement} — scrollTop ${r.scrollTop}/${r.scrollHeight - r.clientHeight}${r.atBottom ? ' (at bottom)' : ''}. Screenshot to confirm, or pup_screenshot_element to grab just a card.` }) });+ } catch (e) { sendJSON(res, { success: false, error: `pup_scroll failed: ${e.message}` }); } return; }- case 'browser_screenshot_element': {+ case 'pup_screenshot_element': { // v1.8.21: screenshot ONE element, resolved by CSS selector OR visible text // (piercing shadow DOM), scrolled into view first. The clean fix for "capture // just this card" on component-heavy pages. Returns the same shape as- // browser_screenshot (base64 + filePath, downscaled for Read).+ // pup_screenshot (base64 + filePath, downscaled for Read). let session, resolvedId; try { ({ session, resolvedId } = await recoverOrRelaunchSession(args)); } catch (e) { sendSessionResolveError(res, e); return; }@@ -12193,7 +12193,7 @@ const server = http.createServer(async (req, res) => { try { ({ tab, tabId } = resolveTabOrError(session, resolvedId, args)); } catch (e) { sendTabResolveError(res, e); return; } if (!args.selector && !args.text) {- sendJSON(res, { success: false, error: 'browser_screenshot_element requires a selector OR text.', _hint: 'e.g. {"selector":".download-card"} or {"text":"Download for your machine"}. Pierces shadow DOM by default.' });+ sendJSON(res, { success: false, error: 'pup_screenshot_element requires a selector OR text.', _hint: 'e.g. {"selector":".download-card"} or {"text":"Download for your machine"}. Pierces shadow DOM by default.' }); return; } await ensureDeepHelpers(tab.page);@@ -12223,7 +12223,7 @@ const server = http.createServer(async (req, res) => { const el = handle.asElement(); if (!el) { try { await handle.dispose(); } catch {}- sendJSON(res, { success: false, errorCode: 'element_not_found', error: `browser_screenshot_element: no element matched ${args.selector ? 'selector "'+args.selector+'"' : 'text "'+args.text+'"'}${args.closest ? ' with a closest("'+args.closest+'") ancestor' : ''} (searched, piercing shadow roots).`, sessionId: resolvedId, tabId, _hint: 'If matching by text is ambiguous (the phrase also appears in prose), pass a `selector` for the widget, or a `closest` selector for its card container.' });+ sendJSON(res, { success: false, errorCode: 'element_not_found', error: `pup_screenshot_element: no element matched ${args.selector ? 'selector "'+args.selector+'"' : 'text "'+args.text+'"'}${args.closest ? ' with a closest("'+args.closest+'") ancestor' : ''} (searched, piercing shadow roots).`, sessionId: resolvedId, tabId, _hint: 'If matching by text is ambiguous (the phrase also appears in prose), pass a `selector` for the widget, or a `closest` selector for its card container.' }); return; } await el.evaluate((e) => e.scrollIntoView({ block: 'center', inline: 'center' }));@@ -12246,18 +12246,18 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, matchedBy: args.selector ? 'selector' : 'text', sizeKB: Math.round(shot.buf.length / 1024), filePath, base64: shot.buf.toString('base64'), width: shot.width, height: shot.height, origWidth: shot.origWidth, origHeight: shot.origHeight, resized: shot.resized, _hint: 'Captured just that element (scrolled into view first). Read the filePath to see it.' }) }); } catch (e) { try { if (handle) await handle.dispose(); } catch {}- sendJSON(res, { success: false, error: `browser_screenshot_element failed: ${e.message}` });+ sendJSON(res, { success: false, error: `pup_screenshot_element failed: ${e.message}` }); } return; }- case 'browser_screenshot_full_res': {+ case 'pup_screenshot_full_res': { // v1.8.40+: full-resolution variant for the rare "save to disk for // external use" case (export to wiki, hand to a designer, etc.). // NEVER pass the resulting image to Claude via Read — full-res // browser viewports are typically 2559x1398 (or larger on hi-DPI // monitors), which exceeds Claude's 2000px many-image limit and // crashes the session. For AI-readable screenshots, use- // browser_screenshot which forces a ≤1568px resize.+ // pup_screenshot which forces a ≤1568px resize. let session, resolvedId; try { ({ session, resolvedId } = await recoverOrRelaunchSession(args)); } catch (e) { sendSessionResolveError(res, e); return; }@@ -12289,18 +12289,18 @@ const server = http.createServer(async (req, res) => { height: meta.height, sessionId: resolvedId, tabId,- _warning: `FULL RESOLUTION (${meta.width}x${meta.height}px) — do NOT pass this image to Claude via the Read tool. Images >2000px crash many-image sessions at the Claude API limit. Use browser_screenshot instead for AI-readable screenshots (auto-resized to ≤1568px). This command is for saving to disk / wiki / export only.`,+ _warning: `FULL RESOLUTION (${meta.width}x${meta.height}px) — do NOT pass this image to Claude via the Read tool. Images >2000px crash many-image sessions at the Claude API limit. Use pup_screenshot instead for AI-readable screenshots (auto-resized to ≤1568px). This command is for saving to disk / wiki / export only.`, }), }); return; }- // browser_evaluate is the catalog/manifest name; browser_eval the historical one.+ // pup_evaluate is the catalog/manifest name; pup_eval the historical one. // Accept both, with either `expr` or `expression`.- case 'browser_evaluate':- case 'browser_hover': {- // v1.9.198 (Bills thread): browser_hover did not exist — the only way to move the pointer- // without clicking was browser_input_dispatch {type:'move'}, which is unobvious next to- // browser_click. Hovering also reveals tooltips/menus that only appear on pointer-over.+ case 'pup_evaluate':+ case 'pup_hover': {+ // v1.9.198 (Bills thread): pup_hover did not exist — the only way to move the pointer+ // without clicking was pup_input_dispatch {type:'move'}, which is unobvious next to+ // pup_click. Hovering also reveals tooltips/menus that only appear on pointer-over. let session, resolvedId; try { ({ session, resolvedId } = await recoverOrRelaunchSession(args)); } catch (e) { sendSessionResolveError(res, e); return; }@@ -12314,20 +12314,20 @@ const server = http.createServer(async (req, res) => { px = box.x; py = box.y; } if (!Number.isFinite(px) || !Number.isFinite(py)) {- sendJSON(res, { success: false, errorCode: 'hover_target_missing', error: 'browser_hover needs selector, or x + y.' });+ sendJSON(res, { success: false, errorCode: 'hover_target_missing', error: 'pup_hover needs selector, or x + y.' }); return; } const _hIntent = (typeof args.intent === 'string' && args.intent.trim()) ? args.intent.trim() : `Hovering ${args.selector ? `"${args.selector}"` : 'target'}`; if (args.showCursor !== false) { try { await showPupCursor(tab.page, px, py, { click: false, label: _hIntent }); } catch (e) {} } await withTimeout(tab.page.mouse.move(px, py, { steps: Number(args.steps) || 8 }), 10_000, 'mouse.move'); sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, hovered: args.selector || { x: px, y: py },- _hint: 'Pointer moved (isTrusted) and the visible Adom cursor glided there. Tooltips/hover menus that need a real pointer-over should now be present — read them with browser_eval.' }) });+ _hint: 'Pointer moved (isTrusted) and the visible Adom cursor glided there. Tooltips/hover menus that need a real pointer-over should now be present — read them with pup_eval.' }) }); } catch (e) {- sendJSON(res, { success: false, errorCode: 'hover_failed', error: `browser_hover failed: ${e.message}` });+ sendJSON(res, { success: false, errorCode: 'hover_failed', error: `pup_hover failed: ${e.message}` }); } return; }- case 'browser_set_identity': {+ case 'pup_set_identity': { // v1.9.343 (John: "put my Adom profile picture in there — show my adom identity"). Chrome's // own profile chip refuses a custom local image (only a real Google sign-in gets one), so we // show the user's Adom identity in the floating owner chip instead. This verb stores the@@ -12354,13 +12354,13 @@ const server = http.createServer(async (req, res) => { ownerChip: s.ownerChip, _hint: 'Adom identity set. It shows as the avatar leading the floating owner chip (bottom-left) on every pup window, applied live. Chrome\'s own profile chip cannot take a custom local image (Google sign-in only), so this is where your Adom identity lives. Turn the chip off with ownerChip:off or {enabled:false}; clear the identity with {clear:true}.' }) }); } catch (e) {- sendJSON(res, { success: false, errorCode: 'set_identity_failed', error: `browser_set_identity failed: ${e.message}` });+ sendJSON(res, { success: false, errorCode: 'set_identity_failed', error: `pup_set_identity failed: ${e.message}` }); } return; }- case 'browser_maximize': {+ case 'pup_maximize': { // v1.9.197 GAP 6 (Bills thread): honouring "pup windows must be maximized" required- // browser_eval + window.resizeTo(...) because desktop_set_window_state is not in every+ // pup_eval + window.resizeTo(...) because desktop_set_window_state is not in every // desktop's verb set. This does it properly via CDP Browser.setWindowBounds (the same // channel park uses), so it works on any host and never depends on an AD verb being present. let session, resolvedId;@@ -12377,16 +12377,16 @@ const server = http.createServer(async (req, res) => { try { ({ bounds } = await cdp.send('Browser.getWindowBounds', { windowId })); } catch (e) {} try { await cdp.detach(); } catch (e) {} sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, state: want, bounds,- _hint: 'Window state set via CDP. This does NOT foreground the window — it stays background-parked unless you explicitly raise it (browser_raise_os_window with a foregroundReason).' }) });+ _hint: 'Window state set via CDP. This does NOT foreground the window — it stays background-parked unless you explicitly raise it (pup_raise_os_window with a foregroundReason).' }) }); } catch (e) {- sendJSON(res, { success: false, errorCode: 'maximize_failed', error: `browser_maximize failed: ${e.message}` });+ sendJSON(res, { success: false, errorCode: 'maximize_failed', error: `pup_maximize failed: ${e.message}` }); } return; }- case 'browser_eval': {+ case 'pup_eval': { if (!args.expr && args.expression) args.expr = args.expression; if (!args.expr) {- sendJSON(res, { success: false, error: 'Missing required parameter "expr" (alias: "expression"). Usage: browser_eval { "sessionId": "...", "expr": "document.title" }' });+ sendJSON(res, { success: false, error: 'Missing required parameter "expr" (alias: "expression"). Usage: pup_eval { "sessionId": "...", "expr": "document.title" }' }); return; } let session, resolvedId;@@ -12402,7 +12402,7 @@ const server = http.createServer(async (req, res) => { // v1.8.21: ensure the shadow-DOM helpers are present so an expr can use // $deep(sel) / $$deep(sel) / deepText(txt) without hand-rolling a shadow walk. await ensureDeepHelpers(tab.page);- // v1.9.197 GAP 5 (Bills thread): browser_eval only ever ran in the TOP frame, so every call+ // v1.9.197 GAP 5 (Bills thread): pup_eval only ever ran in the TOP frame, so every call // against a login form inside an iframe had to be hand-wrapped in // document.getElementById('logonbox').contentDocument — which only works because that frame // happened to be SAME-ORIGIN. A cross-origin login iframe (very common) was simply@@ -12462,12 +12462,12 @@ const server = http.createServer(async (req, res) => { return; } // ── Element-level input + history nav (v1.8.8) ─────────────────────────- // These five were advertised in bridge.json + browser_describe but had no+ // These five were advertised in bridge.json + pup_describe but had no // router cases (found by the full verb self-test). Selector-based where it- // helps; same session/tab auto-recovery as browser_eval.- case 'browser_click': {+ // helps; same session/tab auto-recovery as pup_eval.+ case 'pup_click': { if (!args.selector && !(Number.isFinite(Number(args.x)) && Number.isFinite(Number(args.y)))) {- sendJSON(res, { success: false, error: 'browser_click needs "selector" OR "x"+"y". Usage: { "sessionId": "...", "selector": "#submit" } or { "sessionId": "...", "x": 100, "y": 200 }' });+ sendJSON(res, { success: false, error: 'pup_click needs "selector" OR "x"+"y". Usage: { "sessionId": "...", "selector": "#submit" } or { "sessionId": "...", "x": 100, "y": 200 }' }); return; } let session, resolvedId;@@ -12505,9 +12505,9 @@ const server = http.createServer(async (req, res) => { if (_cursor) { await showPupCursor(tab.page, Number(args.x), Number(args.y), { label: _intent }); await new Promise(r => setTimeout(r, 440)); } await withTimeout(tab.page.mouse.click(Number(args.x), Number(args.y), _clickOpts), 10_000, 'mouse.click'); }- sendJSON(res, { success: true, output: JSON.stringify({ ok: true, clicked: args.selector || { x: Number(args.x), y: Number(args.y) }, sessionId: resolvedId, tabId, _hint: 'Click dispatched (isTrusted, CDP-level). Verify the effect with browser_eval (e.g. document.title or a state probe) — a click that hit nothing still returns ok.' }) });+ sendJSON(res, { success: true, output: JSON.stringify({ ok: true, clicked: args.selector || { x: Number(args.x), y: Number(args.y) }, sessionId: resolvedId, tabId, _hint: 'Click dispatched (isTrusted, CDP-level). Verify the effect with pup_eval (e.g. document.title or a state probe) — a click that hit nothing still returns ok.' }) }); } catch (e) {- sendJSON(res, { success: false, errorCode: 'click_failed', error: `browser_click failed: ${e.message}`, _hint: args.selector ? `Selector "${args.selector}" may not exist/be visible. Probe it first: browser_eval {"expr":"!!document.querySelector('${args.selector}')"} — or click by x/y from a browser_screenshot.` : 'x/y click failed — coordinates are page-viewport pixels; take a browser_screenshot to aim.' });+ sendJSON(res, { success: false, errorCode: 'click_failed', error: `pup_click failed: ${e.message}`, _hint: args.selector ? `Selector "${args.selector}" may not exist/be visible. Probe it first: pup_eval {"expr":"!!document.querySelector('${args.selector}')"} — or click by x/y from a pup_screenshot.` : 'x/y click failed — coordinates are page-viewport pixels; take a pup_screenshot to aim.' }); } return; }@@ -12516,12 +12516,12 @@ const server = http.createServer(async (req, res) => { // foreground, WAIT until the user is actually on the window (alt-tab aware), then a TIGHT // real-cursor hover->click, captioning each step. x,y = WINDOW-image pixels from a // desktop_screenshot_window you took to find the target. reason=why, label=what.- case 'browser_assisted_click': {+ case 'pup_assisted_click': { let session, resolvedId; try { ({ session, resolvedId } = await recoverOrRelaunchSession(args)); } catch (e) { sendSessionResolveError(res, e); return; } if (!(Number.isFinite(Number(args.x)) && Number.isFinite(Number(args.y)))) {- sendJSON(res, { success: false, error: 'browser_assisted_click needs window-image x+y (from a desktop_screenshot_window of this session) plus reason+label.' }); return;+ sendJSON(res, { success: false, error: 'pup_assisted_click needs window-image x+y (from a desktop_screenshot_window of this session) plus reason+label.' }); return; } if (process.platform !== 'win32') { sendJSON(res, { success: false, error: 'assisted click is Windows-only (uses the OS cursor + foreground detection).' }); return; } const _reason = String(args.reason || 'Adom needs to click something on your screen.').slice(0, 180);@@ -12535,7 +12535,7 @@ const server = http.createServer(async (req, res) => { const ready = await waitForUserReady(resolvedId, hwnd, { maxMs: Number(args.maxWaitMs) || 120000 }); if (!ready) { await pupCaption('Adom stopped waiting: you did not return to the window. Ask me to try again when ready.', 7000);- sendJSON(res, { success: false, errorCode: 'user_not_ready', error: 'Timed out waiting for you to focus the window. Nothing was clicked.', _hint: 'The user alt-tabbed away and did not return within the wait window. Re-issue browser_assisted_click when they are ready.' });+ sendJSON(res, { success: false, errorCode: 'user_not_ready', error: 'Timed out waiting for you to focus the window. Nothing was clicked.', _hint: 'The user alt-tabbed away and did not return within the wait window. Re-issue pup_assisted_click when they are ready.' }); return; } await pupCaption(`Adom is clicking ${_label} now.`, 5000);@@ -12553,9 +12553,9 @@ const server = http.createServer(async (req, res) => { } return; }- case 'browser_type': {+ case 'pup_type': { if (typeof args.text !== 'string') {- sendJSON(res, { success: false, error: 'browser_type needs "text" (string). Optional "selector" focuses the element first. Usage: { "sessionId": "...", "selector": "#q", "text": "hello" }' });+ sendJSON(res, { success: false, error: 'pup_type needs "text" (string). Optional "selector" focuses the element first. Usage: { "sessionId": "...", "selector": "#q", "text": "hello" }' }); return; } let session, resolvedId;@@ -12565,7 +12565,7 @@ const server = http.createServer(async (req, res) => { try { ({ tab, tabId } = resolveTabOrError(session, resolvedId, args)); } catch (e) { sendTabResolveError(res, e); return; } try {- // v1.9.197 BUG 3 (Bills thread, top priority): browser_type ALWAYS APPENDED, and with+ // v1.9.197 BUG 3 (Bills thread, top priority): pup_type ALWAYS APPENDED, and with // modifiers broken (bug 1) and clickCount ignored (bug 2) there was NO way to replace a // field's value at all — the caller got "adominc1adominc1" and fell back to a bash loop of // 20 Backspaces. `clear:true` now empties the field first, using REAL key events@@ -12580,15 +12580,15 @@ const server = http.createServer(async (req, res) => { } if (args.selector) await withTimeout(tab.page.type(args.selector, args.text), 15_000, 'page.type'); else await withTimeout(tab.page.keyboard.type(args.text), 15_000, 'keyboard.type');- sendJSON(res, { success: true, output: JSON.stringify({ ok: true, typed: args.text.length + ' chars', into: args.selector || 'focused element', sessionId: resolvedId, tabId, _hint: 'Typed via CDP (isTrusted). Verify with browser_eval reading the field value.' }) });+ sendJSON(res, { success: true, output: JSON.stringify({ ok: true, typed: args.text.length + ' chars', into: args.selector || 'focused element', sessionId: resolvedId, tabId, _hint: 'Typed via CDP (isTrusted). Verify with pup_eval reading the field value.' }) }); } catch (e) {- sendJSON(res, { success: false, errorCode: 'type_failed', error: `browser_type failed: ${e.message}`, _hint: args.selector ? `Selector "${args.selector}" may not exist. Probe: browser_eval {"expr":"!!document.querySelector('${args.selector}')"}` : 'No selector given — ensure an element has focus (browser_click it first).' });+ sendJSON(res, { success: false, errorCode: 'type_failed', error: `pup_type failed: ${e.message}`, _hint: args.selector ? `Selector "${args.selector}" may not exist. Probe: pup_eval {"expr":"!!document.querySelector('${args.selector}')"}` : 'No selector given — ensure an element has focus (pup_click it first).' }); } return; }- case 'browser_press_key': {+ case 'pup_press_key': { if (!args.key) {- sendJSON(res, { success: false, error: 'browser_press_key needs "key" — a puppeteer KeyInput name like "Enter", "Tab", "Escape", "ArrowDown", "a". Usage: { "sessionId": "...", "key": "Enter" }' });+ sendJSON(res, { success: false, error: 'pup_press_key needs "key" — a puppeteer KeyInput name like "Enter", "Tab", "Escape", "ArrowDown", "a". Usage: { "sessionId": "...", "key": "Enter" }' }); return; } let session, resolvedId;@@ -12618,19 +12618,19 @@ const server = http.createServer(async (req, res) => { } sendJSON(res, { success: true, output: JSON.stringify({ ok: true, pressed: args.key, sessionId: resolvedId, tabId, _hint: 'Key pressed in the PAGE (isTrusted). For OS-level chords outside the page (alt+f4, ctrl+shift+p in browser chrome), use AD\'s desktop_press_key instead.' }) }); } catch (e) {- sendJSON(res, { success: false, errorCode: 'press_key_failed', error: `browser_press_key failed: ${e.message}`, _hint: `"${args.key}" must be a valid puppeteer KeyInput name (case-sensitive: "Enter", "ArrowLeft", "F5", single chars like "a").` });+ sendJSON(res, { success: false, errorCode: 'press_key_failed', error: `pup_press_key failed: ${e.message}`, _hint: `"${args.key}" must be a valid puppeteer KeyInput name (case-sensitive: "Enter", "ArrowLeft", "F5", single chars like "a").` }); } return; }- case 'browser_back':- case 'browser_forward': {+ case 'pup_back':+ case 'pup_forward': { let session, resolvedId; try { ({ session, resolvedId } = await recoverOrRelaunchSession(args)); } catch (e) { sendSessionResolveError(res, e); return; } let tab, tabId; try { ({ tab, tabId } = resolveTabOrError(session, resolvedId, args)); } catch (e) { sendTabResolveError(res, e); return; }- const dir = command === 'browser_back' ? 'back' : 'forward';+ const dir = command === 'pup_back' ? 'back' : 'forward'; try { let before = null; try { before = tab.page.url(); } catch {} // goBack/goForward resolve null on a CACHED history nav even though the URL@@ -12641,11 +12641,11 @@ const server = http.createServer(async (req, res) => { const moved = !!(url && before && url !== before); sendJSON(res, { success: true, output: JSON.stringify({ ok: true, direction: dir, moved, url, sessionId: resolvedId, tabId, _hint: moved ? `Now at ${url}.` : `No ${dir === 'back' ? 'earlier' : 'later'} history entry — the page did not move (moved:false, not an error).` }) }); } catch (e) {- sendJSON(res, { success: false, errorCode: dir + '_failed', error: `browser_${dir} failed: ${e.message}` });+ sendJSON(res, { success: false, errorCode: dir + '_failed', error: `pup_${dir} failed: ${e.message}` }); } return; }- case 'browser_input_dispatch': {+ case 'pup_input_dispatch': { // Trusted input via puppeteer's CDP-backed page.mouse / page.keyboard. // Events fired here have isTrusted=true (Chromium dispatches them at the // browser level via Input.dispatchMouseEvent / dispatchKeyEvent), unlike@@ -12670,7 +12670,7 @@ const server = http.createServer(async (req, res) => { const t = (args.type || '').toLowerCase(); if (!t) {- sendJSON(res, { success: false, error: 'browser_input_dispatch: "type" required (click|move|type|key)' });+ sendJSON(res, { success: false, error: 'pup_input_dispatch: "type" required (click|move|type|key)' }); return; } try {@@ -12703,7 +12703,7 @@ const server = http.createServer(async (req, res) => { // first-document-order behavior. const matches = await tab.page.$$(args.selector); if (matches.length === 0) {- sendJSON(res, { success: false, error: `selector not found: ${args.selector}`, _hint: 'Selector matches no element. Verify the page is loaded (browser_wait) and the selector is correct (try with browser_eval first).' });+ sendJSON(res, { success: false, error: `selector not found: ${args.selector}`, _hint: 'Selector matches no element. Verify the page is loaded (pup_wait) and the selector is correct (try with pup_eval first).' }); return; } @@ -12935,7 +12935,7 @@ const server = http.createServer(async (req, res) => { return; } if (typeof args.x !== 'number' || typeof args.y !== 'number') {- sendJSON(res, { success: false, error: 'browser_input_dispatch click: provide either {selector} OR {x, y}' });+ sendJSON(res, { success: false, error: 'pup_input_dispatch click: provide either {selector} OR {x, y}' }); return; } await tab.page.mouse.click(args.x, args.y, {@@ -12948,7 +12948,7 @@ const server = http.createServer(async (req, res) => { } if (t === 'move') { if (typeof args.x !== 'number' || typeof args.y !== 'number') {- sendJSON(res, { success: false, error: 'browser_input_dispatch move requires {x, y}' });+ sendJSON(res, { success: false, error: 'pup_input_dispatch move requires {x, y}' }); return; } // v1.9.198 (Bills thread): a CDP move returned ok but the DRAWN cursor never budged —@@ -12961,7 +12961,7 @@ const server = http.createServer(async (req, res) => { } if (t === 'type') { if (typeof args.text !== 'string') {- sendJSON(res, { success: false, error: 'browser_input_dispatch type requires {text}' });+ sendJSON(res, { success: false, error: 'pup_input_dispatch type requires {text}' }); return; } // Optionally focus a selector first so keystrokes land in the right input.@@ -12978,20 +12978,20 @@ const server = http.createServer(async (req, res) => { } if (t === 'key') { if (typeof args.key !== 'string') {- sendJSON(res, { success: false, error: 'browser_input_dispatch key requires {key} (Enter|Escape|Tab|ArrowUp|...)' });+ sendJSON(res, { success: false, error: 'pup_input_dispatch key requires {key} (Enter|Escape|Tab|ArrowUp|...)' }); return; } await tab.page.keyboard.press(args.key, { delay: args.delay || 0 }); sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, dispatched: 'key', key: args.key }) }); return; }- sendJSON(res, { success: false, error: `browser_input_dispatch: unknown type "${args.type}". Supported: click | move | type | key` });+ sendJSON(res, { success: false, error: `pup_input_dispatch: unknown type "${args.type}". Supported: click | move | type | key` }); } catch (e) {- sendJSON(res, { success: false, error: `browser_input_dispatch ${t} failed: ${e.message}` });+ sendJSON(res, { success: false, error: `pup_input_dispatch ${t} failed: ${e.message}` }); } return; }- case 'browser_fetch_url': {+ case 'pup_fetch_url': { // Fetch an arbitrary URL with the session's cookies and return the // raw bytes (base64 in JSON; CLI optionally writes to disk). //@@ -13009,7 +13009,7 @@ const server = http.createServer(async (req, res) => { // - This is the BRIDGE side (Windows desktop). The bridge ALWAYS // returns bodyBase64. If the user wants the file on the Docker // side (the common case), the CLI handles the write — see- // cli/src/commands.rs::browser_fetch_url. The arg name there+ // cli/src/commands.rs::pup_fetch_url. The arg name there // is `saveTo` and it's interpreted as a Docker-side path. // - For the rare case of writing directly on the DESKTOP // filesystem (e.g. dropping a CAD bundle into the user's@@ -13032,7 +13032,7 @@ const server = http.createServer(async (req, res) => { // to active tab. Cookies are per-browser-context // so any tab in the session works.) if (!args.url) {- sendJSON(res, { success: false, error: 'browser_fetch_url requires {url}' });+ sendJSON(res, { success: false, error: 'pup_fetch_url requires {url}' }); return; } let session, resolvedId;@@ -13130,11 +13130,11 @@ const server = http.createServer(async (req, res) => { const ct = (resp.headers && resp.headers()['content-type']) || ''; respondWithBytes(buf, ct, resp.status()); } catch (e) {- sendJSON(res, { success: false, error: `browser_fetch_url failed: ${e.message}`, _hint: 'If this URL requires the popup tab\'s session cookies, make sure that tab is in the same session and pass its tabId here. The bridge always returns bodyBase64; the CLI handles container-side writes via the saveTo arg.' });+ sendJSON(res, { success: false, error: `pup_fetch_url failed: ${e.message}`, _hint: 'If this URL requires the popup tab\'s session cookies, make sure that tab is in the same session and pass its tabId here. The bridge always returns bodyBase64; the CLI handles container-side writes via the saveTo arg.' }); } return; }- case 'browser_errors': {+ case 'pup_errors': { let session, resolvedId; // v1.6.1+: auto-recover (reconnect or relaunch) before surfacing // any error. The Docker container should never see "Session@@ -13150,7 +13150,7 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ errors: errs, count: errs.length, sessionId: resolvedId, tabId }) }); return; }- case 'browser_reload': {+ case 'pup_reload': { let session, resolvedId; // v1.6.1+: auto-recover (reconnect or relaunch) before surfacing // any error. The Docker container should never see "Session@@ -13173,7 +13173,7 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId, _recovered: `crashed renderer respawned${rurl ? ' at ' + rurl : ''}` }) }); } else { console.log(`[crash] session "${resolvedId}" tab ${tabId} respawn did NOT take — advising close+reopen`);- sendJSON(res, { success: false, error: `Tab "${tabId}" renderer crashed and in-place respawn did not recover it (Chrome may be too memory-pressured to spawn a fresh renderer). Close and reopen the window: browser_close_window {sessionId:"${resolvedId}"} then browser_open_window {sessionId:"${resolvedId}", url:"${rurl || ''}"}. Closing other heavy pup windows first frees memory.`, errorCode: 'tab_renderer_crashed_unrecovered', sessionId: resolvedId, tabId });+ sendJSON(res, { success: false, error: `Tab "${tabId}" renderer crashed and in-place respawn did not recover it (Chrome may be too memory-pressured to spawn a fresh renderer). Close and reopen the window: pup_close_window {sessionId:"${resolvedId}"} then pup_open_window {sessionId:"${resolvedId}", url:"${rurl || ''}"}. Closing other heavy pup windows first frees memory.`, errorCode: 'tab_renderer_crashed_unrecovered', sessionId: resolvedId, tabId }); } return; }@@ -13201,8 +13201,8 @@ const server = http.createServer(async (req, res) => { // v1.8.26: the user is signalled by the auto-flash, NOT by raising the window. // Only raise if this session is one the user is actively watching. if (session._foreground) { try { await tab.page.bringToFront(); } catch {} }- // v1.9.109 — PROBE ON RELOAD TOO. This was the hole: only browser_open_window probed, so a- // thread that hit a 404, fixed its dev server, called browser_reload and got ok:true with the+ // v1.9.109 — PROBE ON RELOAD TOO. This was the hole: only pup_open_window probed, so a+ // thread that hit a 404, fixed its dev server, called pup_reload and got ok:true with the // GENERIC "do not assume it rendered" hint concluded "Mockup is now live again" — reporting // success on a page nobody had checked. The verb used to confirm a fix is exactly the verb // that must verify it.@@ -13215,7 +13215,7 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: resolvedId, tabId }) }); return; }- case 'browser_navigate': {+ case 'pup_navigate': { { // v1.9.195: same pre-flight as open — never navigate a window to a guaranteed-broken // file:// URL (container path, or missing on the desktop) and leave the caller guessing.@@ -13234,7 +13234,7 @@ const server = http.createServer(async (req, res) => { try { ({ session, resolvedId } = await recoverOrRelaunchSession(args)); } catch (e) { sendSessionResolveError(res, e); return; } // v1.8.16: refuse a declared cross-owner navigate (window steal).- { const refusal = ownershipRefusal(session, resolvedId, args, 'browser_navigate'); if (refusal) { sendJSON(res, refusal); return; } }+ { const refusal = ownershipRefusal(session, resolvedId, args, 'pup_navigate'); if (refusal) { sendJSON(res, refusal); return; } } let tab, tabId; try { ({ tab, tabId } = resolveTabOrError(session, resolvedId, args, { allowCrashed: true })); } catch (e) { sendTabResolveError(res, e); return; }@@ -13254,7 +13254,7 @@ const server = http.createServer(async (req, res) => { const ok = await respawnCrashedRenderer(tab.page, args.url); if (!ok) { console.log(`[crash] session "${resolvedId}" tab ${tabId} navigate respawn did NOT take — advising close+reopen`);- sendJSON(res, { success: false, error: `Tab "${tabId}" renderer crashed and in-place respawn to ${args.url} did not recover it (Chrome may be too memory-pressured). Close and reopen: browser_close_window {sessionId:"${resolvedId}"} then browser_open_window {sessionId:"${resolvedId}", url:"${args.url}"}. Closing other heavy pup windows first frees memory.`, errorCode: 'tab_renderer_crashed_unrecovered', sessionId: resolvedId, tabId });+ sendJSON(res, { success: false, error: `Tab "${tabId}" renderer crashed and in-place respawn to ${args.url} did not recover it (Chrome may be too memory-pressured). Close and reopen: pup_close_window {sessionId:"${resolvedId}"} then pup_open_window {sessionId:"${resolvedId}", url:"${args.url}"}. Closing other heavy pup windows first frees memory.`, errorCode: 'tab_renderer_crashed_unrecovered', sessionId: resolvedId, tabId }); return; } tab._crashed = false; delete tab._crashedAt; delete tab._crashReason;@@ -13341,7 +13341,7 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, url: args.url, sessionId: resolvedId, tabId, ...(authSteerHint(args.url) ? { _hint: authSteerHint(args.url).trim() } : {}) }) }); return; }- case 'browser_dashboard': {+ case 'pup_dashboard': { // v1.9.231 (John): the dashboard ALWAYS gets its OWN pup window, never a tab in someone // else's, so it carries its own taskbar icon (its served icon.svg) and can be found and // closed independently. Fixed sessionId + its own durable profile, so repeat calls REUSE@@ -13353,7 +13353,7 @@ const server = http.createServer(async (req, res) => { if (existing) { try { await existing.page.goto(dashUrl, { waitUntil: 'domcontentloaded', timeout: 15000 }); } catch (e) {} sendJSON(res, { success: true, output: JSON.stringify({ ok: true, sessionId: 'pup-dashboard', url: dashUrl, reused: true,- _hint: `The pup dashboard is already open in its own window (session "pup-dashboard") and was refreshed. It is BACKGROUNDED like any pup window; raise it with browser_raise_os_window + a foregroundReason if the user asked to see it.` }) });+ _hint: `The pup dashboard is already open in its own window (session "pup-dashboard") and was refreshed. It is BACKGROUNDED like any pup window; raise it with pup_raise_os_window + a foregroundReason if the user asked to see it.` }) }); return; } await launchSession('pup-dashboard', dashUrl, { profile: 'pup-dashboard', owner: 'pup-dashboard' });@@ -13364,7 +13364,7 @@ const server = http.createServer(async (req, res) => { } return; }- case 'browser_deps': {+ case 'pup_deps': { // v1.9.218 (issue #15): report dependency health so "pup is broken" can be answered without // reading the spawn log. selfInstalled:true means the bridge repaired itself this boot. let resolved = false; try { require.resolve('puppeteer'); resolved = true; } catch (e) {}@@ -13381,7 +13381,7 @@ const server = http.createServer(async (req, res) => { }) }); return; }- case 'browser_readiness': {+ case 'pup_readiness': { // READ-ONLY readiness probe. NEVER triggers a download. `ready` reflects both // server-up (this verb answered) AND Chrome-for-Testing cached. For the general // "what do I have / is it ready" question prefer AD's `bridge_readiness` (it also@@ -13391,29 +13391,29 @@ const server = http.createServer(async (req, res) => { const defTxt = rd.defaultBrowser ? ` Default: ${rd.defaultBrowser.kind}${rd.defaultBrowser.forced ? ' (pinned)' : ' (auto)'}${rd.defaultBrowser.pendingInstall ? ', downloading' : ''}.` : ' Default: auto (first browser that launches).';- const switchTxt = ' Switch/install with browser_use {browser:"chrome"|"edge"|"cft"|"auto"}.';+ const switchTxt = ' Switch/install with pup_use {browser:"chrome"|"edge"|"cft"|"auto"}.'; sendJSON(res, { success: true, output: JSON.stringify(rd), ...rd, _hint: rd.ready- ? `Ready — pup will drive ${rd.browserKind} (${rd.browserSource}) at ${rd.browserExecutablePath}. Available: ${availTxt}.${defTxt} No download needed. Proceed with browser_open_window.${switchTxt} RECOVERY NOTE: if you were polling this because pup's verbs were TIMING OUT (dim LED in AD / browser.bridgeRunning:false), that means the bridge PROCESS was down — a status probe like this does NOT respawn it; only a window/tab verb (browser_open_window) auto-spawns the bridge. And never SPAM browser_open_window while it says "bridge_starting" — concurrent opens on one profile used to collide (fixed v1.9.4, but still: fire ONE open, then poll browser_list_windows with backoff).`+ ? `Ready — pup will drive ${rd.browserKind} (${rd.browserSource}) at ${rd.browserExecutablePath}. Available: ${availTxt}.${defTxt} No download needed. Proceed with pup_open_window.${switchTxt} RECOVERY NOTE: if you were polling this because pup's verbs were TIMING OUT (dim LED in AD / browser.bridgeRunning:false), that means the bridge PROCESS was down — a status probe like this does NOT respawn it; only a window/tab verb (pup_open_window) auto-spawns the bridge. And never SPAM pup_open_window while it says "bridge_starting" — concurrent opens on one profile used to collide (fixed v1.9.4, but still: fire ONE open, then poll pup_list_windows with backoff).` : rd.installing- ? `Fetching Chrome for Testing (${rd.installProgressPct}%) — this box had no Chrome/Edge. Poll this verb (or bridge_readiness) until ready:true, then browser_open_window. Faster: have the user install Chrome or Edge (Edge ships with Windows) and pup uses it instantly.`+ ? `Fetching Chrome for Testing (${rd.installProgressPct}%) — this box had no Chrome/Edge. Poll this verb (or bridge_readiness) until ready:true, then pup_open_window. Faster: have the user install Chrome or Edge (Edge ships with Windows) and pup uses it instantly.` : rd.installPhase === 'failed'- ? `No Chrome/Edge on this box and the Chrome-for-Testing fallback FAILED: ${rd.lastError}. Have the user install Chrome or Edge (fastest, no download), or retry browser_prewarm (usually network/proxy blocking the download, or low disk).`- : `No Chrome/Edge detected and no cached Chrome for Testing. Available: ${availTxt}. Have the user install Chrome or Edge (Edge ships with Windows — instant, no download), or call browser_use {browser:"cft"} / browser_prewarm to fetch Chrome for Testing.`,+ ? `No Chrome/Edge on this box and the Chrome-for-Testing fallback FAILED: ${rd.lastError}. Have the user install Chrome or Edge (fastest, no download), or retry pup_prewarm (usually network/proxy blocking the download, or low disk).`+ : `No Chrome/Edge detected and no cached Chrome for Testing. Available: ${availTxt}. Have the user install Chrome or Edge (Edge ships with Windows — instant, no download), or call pup_use {browser:"cft"} / pup_prewarm to fetch Chrome for Testing.`, }); return; }- case 'browser_import_scan': {+ case 'pup_import_scan': { // READ-ONLY discovery pass for the comprehensive credential import (adom-you seed). // Enumerates EVERY profile of EVERY Chromium browser (Chrome/Edge/Brave) on this box // with: source account email(s) (the identity tag — Preferences.account_info[].email), // saved-password count (parsed from the Login Data SQLite via the desktop's python, // NEVER a byte-scan — see dev-skills/pup-browser-import), Login Data size, and ABE // status. It DECRYPTS NOTHING — decryption needs the AD-core elevated SYSTEM helper- // (adom/adom-desktop #51, desktop_decrypt_browser_credentials). This verb is the+ // (adom/adom-desktop #51, desktop_decrypt_pup_credentials). This verb is the // foundation the first-run prompt + Settings "Import logins" picker read. if (process.platform !== 'win32') { sendJSON(res, { success: true, output: JSON.stringify({ profiles: [] }), profiles: [], _hint: 'Credential import discovery is Windows-only (Chromium ABE / DPAPI). No profiles enumerated on this platform.' });@@ -13490,12 +13490,12 @@ const server = http.createServer(async (req, res) => { accounts: accts, pythonAvailable: !!scan.pythonAvailable, _hint: profiles.length- ? `Found ${profiles.length} browser profile(s) across ${new Set(profiles.map(p=>p.browser)).size} browser(s), ~${totalPw} saved password(s), identities: ${accts.join(', ') || 'none tagged'}.${anyAbe ? ' These are App-Bound-Encrypted (v20) — a USER process (this bridge) canNOT decrypt them.' : ''} This verb is DISCOVERY ONLY. The actual decrypt is AD's desktop_decrypt_browser_credentials (AD >= 1.9.220): the dashboard "Import logins" button calls it LOCALLY (bridge → AD direct API), the user approves one consent dialog + one UAC prompt, and pup merges the identity-tagged rows into the adom-you vault (credential_vault.js) and drops the plaintext. ${scan.pythonAvailable ? '' : 'NOTE: python not found on PATH, so passwordCount is null — only file presence/size known. '}Next: open the Settings "Import logins from Chrome/Edge" picker, which reads this list.`+ ? `Found ${profiles.length} browser profile(s) across ${new Set(profiles.map(p=>p.browser)).size} browser(s), ~${totalPw} saved password(s), identities: ${accts.join(', ') || 'none tagged'}.${anyAbe ? ' These are App-Bound-Encrypted (v20) — a USER process (this bridge) canNOT decrypt them.' : ''} This verb is DISCOVERY ONLY. The actual decrypt is AD's desktop_decrypt_pup_credentials (AD >= 1.9.220): the dashboard "Import logins" button calls it LOCALLY (bridge → AD direct API), the user approves one consent dialog + one UAC prompt, and pup merges the identity-tagged rows into the adom-you vault (credential_vault.js) and drops the plaintext. ${scan.pythonAvailable ? '' : 'NOTE: python not found on PATH, so passwordCount is null — only file presence/size known. '}Next: open the Settings "Import logins from Chrome/Edge" picker, which reads this list.` : `No Chromium browser profiles with a Preferences file found under %LOCALAPPDATA% (Chrome/Edge/Brave). Nothing to import.`, }); return; }- case 'browser_prewarm': {+ case 'pup_prewarm': { // Install Chrome for Testing WITHOUT opening a window — the warm-before- // first-use path. Default non-blocking so AD/HD can fire-and-forget on // embedded first-run; pass {wait:true} to block until installed.@@ -13514,18 +13514,18 @@ const server = http.createServer(async (req, res) => { success: rd.installPhase !== 'failed', output: JSON.stringify(rd), ...rd,- statusVerb: 'browser_readiness',+ statusVerb: 'pup_readiness', _hint: rd.ready- ? `Chrome for Testing is ready${pinCft ? ' and pinned as the default' : ''} — pup will drive it on the next browser_open_window. (browser_use {browser:"auto"} reverts to installed Chrome/Edge.)`+ ? `Chrome for Testing is ready${pinCft ? ' and pinned as the default' : ''} — pup will drive it on the next pup_open_window. (pup_use {browser:"auto"} reverts to installed Chrome/Edge.)` : rd.installing- ? `Downloading Chrome for Testing (${rd.installProgressPct}%)${pinCft ? '; it is pinned as the default so every open uses it once ready' : ''}. Poll browser_readiness until ready:true. No window is opened by prewarm.`+ ? `Downloading Chrome for Testing (${rd.installProgressPct}%)${pinCft ? '; it is pinned as the default so every open uses it once ready' : ''}. Poll pup_readiness until ready:true. No window is opened by prewarm.` : rd.installPhase === 'failed'- ? `Prewarm FAILED: ${rd.lastError}. Usually network/proxy blocking storage.googleapis.com or low disk. pup still works with installed Chrome/Edge — browser_use {browser:"auto"}.`- : 'Prewarm started; poll browser_readiness for completion.',+ ? `Prewarm FAILED: ${rd.lastError}. Usually network/proxy blocking storage.googleapis.com or low disk. pup still works with installed Chrome/Edge — pup_use {browser:"auto"}.`+ : 'Prewarm started; poll pup_readiness for completion.', }); return; }- case 'browser_use': {+ case 'pup_use': { // Pin (or clear) the browser pup drives by default. browser: 'chrome' | 'edge' // | 'cft' | 'auto' (or pass an explicit executablePath). 'cft' fetches Chrome // for Testing in the background if not cached, then pins it. Persists across@@ -13536,9 +13536,9 @@ const server = http.createServer(async (req, res) => { const availTxt = (result.candidates || []).map(c => `${c.kind}(${c.source})`).join(', ') || 'none'; let hint; if (!result.ok) {- hint = `${result.error} Available on this machine: ${availTxt}. Use browser_use {browser:"chrome"|"edge"|"cft"|"auto"}.`;+ hint = `${result.error} Available on this machine: ${availTxt}. Use pup_use {browser:"chrome"|"edge"|"cft"|"auto"}.`; } else if (result.cleared) {- hint = `Default cleared → AUTO. pup will drive the first browser that launches (${availTxt}) and cache it. Next browser_open_window applies it.`;+ hint = `Default cleared → AUTO. pup will drive the first browser that launches (${availTxt}) and cache it. Next pup_open_window applies it.`; } else if (result.installingChrome) { // v1.9.86 (Kyle): this hint was entirely Windows-shaped (UAC, awaiting_uac, "click YES"). On // macOS the install is a dmg -> ditto -> /Applications (falling back to ~/Applications)@@ -13546,26 +13546,26 @@ const server = http.createServer(async (req, res) => { // would poll for a phase that never arrives and tell the user to approve a prompt that // never appears. Per the SDK's first principle the AI reads THIS, not the skill file. hint = process.platform === 'darwin'- ? `Downloading & installing REAL Google Chrome (~90 MB) from Google's .dmg and pinning it as the default. On macOS there is NO elevation step and NO approval prompt: pup mounts the dmg with hdiutil and copies Google Chrome.app to /Applications, falling back to ~/Applications if that isn't writable. Poll browser_readiness: chromeStablePhase installing → ready (there is no awaiting_uac phase on macOS — do not wait for one, and do not tell the user to approve anything). If it fails at both destinations the install genuinely failed; read the error. Until then pup drives ${availTxt}, so nothing is blocked.`- : `Downloading & installing REAL Google Chrome (~90 MB, user-scoped) and pinning it as the default. On a normal PC this is SILENT (no prompt). On a locked-down box (VM / managed device) it needs a Windows approval — the bridge pops a native "click YES on the UAC" notification on the user's desktop, and if it expires it re-notifies with a one-tap "Approve now" (no AI turn needed). Poll browser_readiness: chromeStablePhase installing → awaiting_uac (waiting on the user's click; chromeStableAwaitingApproval:true) → ready. Until then pup drives ${availTxt}, so nothing is blocked. Pass {browser:"chrome", install:true, elevate:false} to skip the prompt path, or elevate:true to go straight to it.`;+ ? `Downloading & installing REAL Google Chrome (~90 MB) from Google's .dmg and pinning it as the default. On macOS there is NO elevation step and NO approval prompt: pup mounts the dmg with hdiutil and copies Google Chrome.app to /Applications, falling back to ~/Applications if that isn't writable. Poll pup_readiness: chromeStablePhase installing → ready (there is no awaiting_uac phase on macOS — do not wait for one, and do not tell the user to approve anything). If it fails at both destinations the install genuinely failed; read the error. Until then pup drives ${availTxt}, so nothing is blocked.`+ : `Downloading & installing REAL Google Chrome (~90 MB, user-scoped) and pinning it as the default. On a normal PC this is SILENT (no prompt). On a locked-down box (VM / managed device) it needs a Windows approval — the bridge pops a native "click YES on the UAC" notification on the user's desktop, and if it expires it re-notifies with a one-tap "Approve now" (no AI turn needed). Poll pup_readiness: chromeStablePhase installing → awaiting_uac (waiting on the user's click; chromeStableAwaitingApproval:true) → ready. Until then pup drives ${availTxt}, so nothing is blocked. Pass {browser:"chrome", install:true, elevate:false} to skip the prompt path, or elevate:true to go straight to it.`; } else if (result.installing) {- hint = `Pinned Chrome for Testing as the default and started the ~150 MB download in the background. Poll browser_readiness until ready:true — until then pup falls back to installed Chrome/Edge (${availTxt}). Once cached, every open uses CfT.`;+ hint = `Pinned Chrome for Testing as the default and started the ~150 MB download in the background. Poll pup_readiness until ready:true — until then pup falls back to installed Chrome/Edge (${availTxt}). Once cached, every open uses CfT.`; } else {- hint = `Default pinned → ${result.defaultBrowser ? result.defaultBrowser.kind : args.browser}. The next browser_open_window drives it. Available: ${availTxt}. Revert anytime with browser_use {browser:"auto"}.`;+ hint = `Default pinned → ${result.defaultBrowser ? result.defaultBrowser.kind : args.browser}. The next pup_open_window drives it. Available: ${availTxt}. Revert anytime with pup_use {browser:"auto"}.`; } sendJSON(res, { success: !!result.ok, output: JSON.stringify({ ...result, readiness: rd }), ...result,- errorCode: result.ok ? undefined : (result.errorCode || 'browser_use_failed'),- statusVerb: 'browser_readiness',+ errorCode: result.ok ? undefined : (result.errorCode || 'pup_use_failed'),+ statusVerb: 'pup_readiness', _hint: hint, }); return; }- case 'browser_status': {- // v1.9.172 (jam fix): same resilience as browser_list_windows — parallel + per-session- // timeout + in-memory fallback, so a busy window can never hang browser_status into null.+ case 'pup_status': {+ // v1.9.172 (jam fix): same resilience as pup_list_windows — parallel + per-session+ // timeout + in-memory fallback, so a busy window can never hang pup_status into null. const _entries = [...sessions]; const list = await Promise.all(_entries.map(([id, s]) => withTimeout(getSessionInfo(id, s), 2500, 'getSessionInfo').catch(() => getSessionInfoFast(id, s))@@ -13581,7 +13581,7 @@ const server = http.createServer(async (req, res) => { if (_degraded) statusResult._degraded = `${_degraded} of ${list.length} window(s) were busy (Chrome under load) and show last-known state — pup is NOT frozen; retry shortly for live values.`; // Add guidance when no sessions exist so the AI knows what to do next if (sessions.size === 0) {- statusResult.hint = 'No browser sessions are open. Use browser_open_window with sessionId, profile, and url to launch a Chrome window on the desktop before using screenshot/eval/navigate.';+ statusResult.hint = 'No browser sessions are open. Use pup_open_window with sessionId, profile, and url to launch a Chrome window on the desktop before using screenshot/eval/navigate.'; } else { // v1.8.37: nudge to close stale/piled-up windows (each row already has ageMinutes+owner). const cln = staleWindowsHint();@@ -13590,9 +13590,9 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify(statusResult) }); return; }- case 'browser_close': {+ case 'pup_close': { // With sessionId: close just that session. Without: close everything.- // Per-session close mirrors browser_close_window's logic — kills the+ // Per-session close mirrors pup_close_window's logic — kills the // Chrome process for that profile if no other sessions share it. if (args.sessionId) { const target = sessions.get(args.sessionId);@@ -13601,12 +13601,12 @@ const server = http.createServer(async (req, res) => { return; } // v1.8.16: refuse a declared cross-owner close.- { const refusal = ownershipRefusal(target, args.sessionId, args, 'browser_close'); if (refusal) { sendJSON(res, refusal); return; } }+ { const refusal = ownershipRefusal(target, args.sessionId, args, 'pup_close'); if (refusal) { sendJSON(res, refusal); return; } } const profileName = target.profileName; // Are there other sessions on this profile? If yes, only close // this session's tab (Chrome stays alive for the others). If no, // kill the Chrome process so the profile is fully released and- // the next browser_open_window starts from a clean slate.+ // the next pup_open_window starts from a clean slate. // (Session map key IS the sessionId; iterate entries to compare.) const otherSessionsOnProfile = [...sessions.entries()].filter( ([id, s]) => id !== args.sessionId && s.profileName === profileName@@ -13627,7 +13627,7 @@ const server = http.createServer(async (req, res) => { } } } catch (e) {- console.log(`browser_close (per-session) error: ${e.message}`);+ console.log(`pup_close (per-session) error: ${e.message}`); } sessions.delete(args.sessionId); deleteSessionFile(args.sessionId);@@ -13674,11 +13674,11 @@ const server = http.createServer(async (req, res) => { remainingSessions: 0, chromeProcsKilled: _sweep.killed, chromeProcsRemaining: _sweep.remaining,- ...(_sweep.remaining ? { _hint: `WARNING: ${_sweep.remaining} pup chrome process(es) survived the close-all sweep (should be 0) and may peg CPU — the box may be heavily loaded; retry browser_close.` } : {}),+ ...(_sweep.remaining ? { _hint: `WARNING: ${_sweep.remaining} pup chrome process(es) survived the close-all sweep (should be 0) and may peg CPU — the box may be heavily loaded; retry pup_close.` } : {}), })}); return; }- case 'browser_wait': {+ case 'pup_wait': { const ms = args.ms || 3000; await new Promise(r => setTimeout(r, ms)); sendJSON(res, { success: true, output: JSON.stringify({ ok: true, waited: ms }) });@@ -13724,7 +13724,7 @@ const server = http.createServer(async (req, res) => { // vault or explicit creds, with NO Chrome autofill dropdown and NO foregrounding. This is // the answer to Q4: pup owns the creds, so it fills the fields over CDP directly instead of // waiting on Chrome's native floaty menu (which pup can't click without stealing focus).- case 'browser_login': {+ case 'pup_login': { let session, resolvedId; try { ({ session, resolvedId } = await recoverOrRelaunchSession(args)); } catch (e) { sendSessionResolveError(res, e); return; }@@ -13747,7 +13747,7 @@ const server = http.createServer(async (req, res) => { } if (!password) { sendJSON(res, { success: false, errorCode: 'no_credential', output: JSON.stringify({ ok: false, url: curUrl }),- _hint: `No credential to fill. Either pass {username, password} (add {save:true} to remember it for this host), or store one first with credential_set {host:"${(() => { try { return new URL(curUrl).hostname; } catch (e) { return '<host>'; } })()}", username, password} and call browser_login again. pup then fills the form in the BACKGROUND — no Chrome dropdown, no foregrounding.` });+ _hint: `No credential to fill. Either pass {username, password} (add {save:true} to remember it for this host), or store one first with credential_set {host:"${(() => { try { return new URL(curUrl).hostname; } catch (e) { return '<host>'; } })()}", username, password} and call pup_login again. pup then fills the form in the BACKGROUND — no Chrome dropdown, no foregrounding.` }); return; } const r = await credentialVault.fillLoginForm(tab.page, { username, password, submit: !!args.submit });@@ -13764,7 +13764,7 @@ const server = http.createServer(async (req, res) => { filledUsername: r.filledUsername, filledPassword: r.filledPassword, submitted: r.submitted, usernameFilled: r.usernameValue, // username only; password is NEVER returned }),- _hint: `Login form filled in the BACKGROUND from ${source} creds${r.submitted ? ' and SUBMITTED' : ' (not submitted — pass {submit:true} to submit, or drive the submit button yourself)'}. No Chrome autofill dropdown was involved and the window was never foregrounded. Verify with browser_screenshot before assuming you are signed in.` });+ _hint: `Login form filled in the BACKGROUND from ${source} creds${r.submitted ? ' and SUBMITTED' : ' (not submitted — pass {submit:true} to submit, or drive the submit button yourself)'}. No Chrome autofill dropdown was involved and the window was never foregrounded. Verify with pup_screenshot before assuming you are signed in.` }); } catch (e) { sendJSON(res, { success: false, error: e.message }); }@@ -13853,13 +13853,13 @@ const server = http.createServer(async (req, res) => { // v1.9.76 — renew a high-fps lease. The watchdog WARNS before it downgrades, and a // running recording holds the lease open on its own, so an AI only needs this when it // knows up front it wants longer than the default without recording the whole time.- case 'browser_highfps_extend': {+ case 'pup_highfps_extend': { const sid = args.sessionId || activeSessionId; if (!sid || !sessions.has(sid)) { sendJSON(res, { success: false, error: `Session "${sid}" not found` }); return; } const info = highFpsInfo(sid); if (!info) { sendJSON(res, { success: false, error: `Session "${sid}" is not a high-fps window — nothing to extend.`,- _hint: 'This window runs on Chrome\'s normal power behaviour (the default, and the right choice for almost everything). Only a window opened with browser_open_window {highFps:true} holds a lease. If you need continuous full-rate frames while the window is occluded (background video recording), reopen it with highFps:true.' });+ _hint: 'This window runs on Chrome\'s normal power behaviour (the default, and the right choice for almost everything). Only a window opened with pup_open_window {highFps:true} holds a lease. If you need continuous full-rate frames while the window is occluded (background video recording), reopen it with highFps:true.' }); return; } const rec = highFpsExtend(sid, args.minutes);@@ -13867,7 +13867,7 @@ const server = http.createServer(async (req, res) => { _hint: `High-fps lease extended to ${rec.minutes} min. Reminder: while a recording is actually running the lease CANNOT lapse, so you do not need to keep extending during a take.` }) }); return; }- case 'browser_record_start': {+ case 'pup_record_start': { try { const sessionId = args.sessionId || activeSessionId; if (!sessionId) {@@ -13878,7 +13878,7 @@ const server = http.createServer(async (req, res) => { // window is occluded, because Chrome throttles background paints. That is now the // DEFAULT power behaviour (issue #14), so warn loudly instead of shipping a bad take. const _hf = highFpsInfo(sessionId);- if (!_hf) console.log(`[highfps] "${sessionId}" recording started on a NORMAL-power window — frames will drop to ~1-2 fps while it is occluded. Reopen with browser_open_window {highFps:true} for a full-rate background take.`);+ if (!_hf) console.log(`[highfps] "${sessionId}" recording started on a NORMAL-power window — frames will drop to ~1-2 fps while it is occluded. Reopen with pup_open_window {highFps:true} for a full-rate background take.`); const r = await tabRecordStartImpl({ ...args, sessionId }); sendJSON(res, { success: true, output: JSON.stringify({ ok: true, ...r })}); } catch (e) {@@ -13886,7 +13886,7 @@ const server = http.createServer(async (req, res) => { } return; }- case 'browser_record_stop': {+ case 'pup_record_stop': { try { if (!args.recordingId) { sendJSON(res, { success: false, error: 'recordingId is required' });@@ -13899,12 +13899,12 @@ const server = http.createServer(async (req, res) => { } return; }- case 'browser_record_status': {+ case 'pup_record_status': { const active = tabRecordStatusImpl({ sessionId: args.sessionId }); sendJSON(res, { success: true, output: JSON.stringify({ ok: true, active })}); return; }- case 'browser_record_list': {+ case 'pup_record_list': { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, recordings: tabRecordListImpl(), })});@@ -13912,7 +13912,7 @@ const server = http.createServer(async (req, res) => { } default:- sendJSON(res, { success: false, error: `Unknown command: ${command}. Valid commands: browser_open_window, browser_switch_window, browser_list_windows, browser_close_window, browser_alert_window, browser_focus_window, browser_open_tab, browser_switch_tab, browser_close_tab, browser_list_tabs, browser_screenshot, browser_screenshot_full_res, browser_eval, browser_errors, browser_reload, browser_navigate, browser_status, browser_configure, browser_close, browser_wait, desktop_recorder_open, desktop_record_start, desktop_record_stop, desktop_record_status, desktop_record_list, desktop_recorder_close, desktop_list_monitors, browser_record_start, browser_record_stop, browser_record_status, browser_record_list. Tab-aware commands (screenshot, eval, errors, reload, navigate) accept an optional "tabId" to target a specific tab.` }, 404);+ sendJSON(res, { success: false, error: `Unknown command: ${command}. Valid commands: pup_open_window, pup_switch_window, pup_list_windows, pup_close_window, pup_alert_window, pup_focus_window, pup_open_tab, pup_switch_tab, pup_close_tab, pup_list_tabs, pup_screenshot, pup_screenshot_full_res, pup_eval, pup_errors, pup_reload, pup_navigate, pup_status, pup_configure, pup_close, pup_wait, desktop_recorder_open, desktop_record_start, desktop_record_stop, desktop_record_status, desktop_record_list, desktop_recorder_close, desktop_list_monitors, pup_record_start, pup_record_stop, pup_record_status, pup_record_list. Tab-aware commands (screenshot, eval, errors, reload, navigate) accept an optional "tabId" to target a specific tab.` }, 404); return; } }@@ -14054,14 +14054,14 @@ const server = http.createServer(async (req, res) => { let led = 'green', summary, tooltip; if (rd.installPhase === 'failed') { led = 'red'; summary = 'Chrome install failed';- tooltip = `Puppeteer bridge v${BRIDGE_VERSION}\nChrome for Testing install FAILED: ${rd.lastError}\nRetry browser_prewarm (usually a network/proxy or disk issue).`;+ tooltip = `Puppeteer bridge v${BRIDGE_VERSION}\nChrome for Testing install FAILED: ${rd.lastError}\nRetry pup_prewarm (usually a network/proxy or disk issue).`; } else if (rd.installing) { led = 'yellow'; summary = `Installing Chrome ${rd.installProgressPct}%`;- tooltip = `Puppeteer bridge v${BRIDGE_VERSION}\nDownloading Chrome for Testing (${rd.installProgressPct}%) — pup not ready yet.\nPoll browser_readiness until ready.`;+ tooltip = `Puppeteer bridge v${BRIDGE_VERSION}\nDownloading Chrome for Testing (${rd.installProgressPct}%) — pup not ready yet.\nPoll pup_readiness until ready.`; } else if (!rd.ready) { // No drivable browser AT ALL (no Chrome, no Edge, no cached CfT) — the rare case. led = 'yellow'; summary = 'No browser yet';- tooltip = `Puppeteer bridge v${BRIDGE_VERSION}\nNo Chrome/Edge detected and Chrome for Testing not cached — pup will fetch CfT on first use (or call browser_prewarm). Installing Chrome or Edge is instant, no download.`;+ tooltip = `Puppeteer bridge v${BRIDGE_VERSION}\nNo Chrome/Edge detected and Chrome for Testing not cached — pup will fetch CfT on first use (or call pup_prewarm). Installing Chrome or Edge is instant, no download.`; } else { // READY: a drivable browser exists (installed Chrome/Edge, or cached CfT). // Installed-browser-first model — CfT being absent is NOT a warning state.@@ -14899,7 +14899,7 @@ async function desktopRecordStartImpl(opts) { // desktop-recording request. Desktop recording captures the WHOLE screen // (including unrelated apps in the foreground); if the pup window is // occluded the resulting clip won't show what the user actually wanted.- // For 95% of "record a pup" asks the right verb is browser_record_start+ // For 95% of "record a pup" asks the right verb is pup_record_start // (tab-scoped via CDP, captures regardless of foreground state, ~50fps). // Adom-desktop's full-desktop recording is mostly redundant with what // Hydrogen ships natively, and only really useful when Hydrogen is@@ -14910,7 +14910,7 @@ async function desktopRecordStartImpl(opts) { const err = new Error( 'desktop_record_start records the WHOLE DESKTOP, not a pup tab. ' + 'If the user said "record a pup" / "record this Chrome window" / "record the page", ' +- 'they almost certainly want browser_record_start instead — that is tab-scoped via ' ++ 'they almost certainly want pup_record_start instead — that is tab-scoped via ' + 'CDP Page.startScreencast, captures the pup tab regardless of foreground state, ' + 'returns a single .webm at ~50fps, and does not need a HUD. ' + 'Hydrogen also has built-in desktop recording (recording start --share screen) which ' +@@ -14922,7 +14922,7 @@ async function desktopRecordStartImpl(opts) { 'pass confirmDesktopNotTabRecording: true and resend the call.' ); err.code = 'desktop_record_needs_confirmation';- err.hint = 'For pup tab recording: adom-desktop --ai-thread "<your thread>" browser_record_start \'{"sessionId":"<id>","fps":30}\'. ' ++ err.hint = 'For pup tab recording: adom-desktop --ai-thread "<your thread>" pup_record_start \'{"sessionId":"<id>","fps":30}\'. ' + 'For Hydrogen-driven desktop recording: hydrogen recording start --share screen. ' + 'For a parallel desktop clip alongside Hydrogen tab capture: re-call with confirmDesktopNotTabRecording: true.'; throw err;@@ -15016,7 +15016,7 @@ async function desktopRecordStopImpl({ recordingId } = {}) { // recorder's duration-remux step). // // Caller note: Chrome throttles paint on occluded windows, so call-// browser_raise_os_window first if you want frames at the requested fps.+// pup_raise_os_window first if you want frames at the requested fps. // Brief pause to let any in-flight Page.captureScreenshot settle before // detaching the CDP session. Twice the frame interval is a safe upper bound@@ -15227,7 +15227,7 @@ async function tabRecordStopImpl({ sessionId, recordingId }) { console.log(`[${state.recordingId}] zero frames captured — no webm produced`); } - // Sidecar metadata (used by browser_record_list)+ // Sidecar metadata (used by pup_record_list) try { const sidecar = { recordingId, sessionId: state.sessionId, tabId: state.tabId,
src/welcome.html+10−10@@ -370,7 +370,7 @@ <div class="desc-card"> <p> <strong>Pup</strong> is a Node.js bridge on port <code>8851</code> that wraps a persistent- <strong>Chrome for Testing</strong> browser. Your Adom web app sends <code>browser_*</code>+ <strong>Chrome for Testing</strong> browser. Your Adom web app sends <code>pup_*</code> commands through the cloud relay to the Desktop App, which forwards them to Pup. Chrome launches visibly on your screen — not headless — so you can watch the AI browse, screenshot, and debug your projects in real time.@@ -381,15 +381,15 @@ <div class="features"> <div class="features-title">Capabilities</div> <div class="chip-grid">- <div class="chip"><span class="dot"></span>browser_launch</div>- <div class="chip"><span class="dot"></span>browser_screenshot</div>- <div class="chip"><span class="dot"></span>browser_navigate</div>- <div class="chip"><span class="dot"></span>browser_eval</div>- <div class="chip"><span class="dot"></span>browser_errors</div>- <div class="chip"><span class="dot"></span>browser_reload</div>- <div class="chip"><span class="dot"></span>browser_wait</div>- <div class="chip"><span class="dot"></span>browser_status</div>- <div class="chip"><span class="dot"></span>browser_close</div>+ <div class="chip"><span class="dot"></span>pup_launch</div>+ <div class="chip"><span class="dot"></span>pup_screenshot</div>+ <div class="chip"><span class="dot"></span>pup_navigate</div>+ <div class="chip"><span class="dot"></span>pup_eval</div>+ <div class="chip"><span class="dot"></span>pup_errors</div>+ <div class="chip"><span class="dot"></span>pup_reload</div>+ <div class="chip"><span class="dot"></span>pup_wait</div>+ <div class="chip"><span class="dot"></span>pup_status</div>+ <div class="chip"><span class="dot"></span>pup_close</div> </div> </div>