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
v1.4.0: spawn-verified browser fallback (Chrome->Edge->CfT, launch-tested), persisted default, new browser_use verb to pin chrome/edge/cft/auto, verbose browser hints on every open
6 files changed
+327−73
SKILL.md+18−4@@ -32,11 +32,24 @@ When you run `browser_open_window`, one of these happens — handle each: ### "What do I have / is it ready?" -- **`browser_readiness`** (this bridge, **READ-ONLY**) → `{ready, browserKind, browserSource, browserExecutablePath, …}` — tells you which browser pup will drive (`chrome` / `edge` / `chrome-for-testing`) and whether it's ready. Never triggers a download.+- **`browser_readiness`** (this bridge, **READ-ONLY**) → `{ready, browserKind, browserSource, browserExecutablePath, candidates, defaultBrowser, …}` — tells you which browser pup will drive (`chrome` / `edge` / `chrome-for-testing`), the **full candidate list** it will fall through, the current **default**, and whether it's ready. Never triggers a download. - **`adom-desktop bridge_readiness`** (AD-core) is the platform-level "is this bridge ready?" probe — use it for the general readiness question. **`browser_status` is NOT a readiness check** (it only lists open sessions). **Always surface a verb's `_hint` verbatim** when something isn't ready. +### Picking the browser — `browser_use`++pup auto-picks the first browser that actually launches (Chrome → Edge → cached CfT) and **spawn-verifies** it — if a browser is present but won't start (corrupt install), it falls through to the next, so an open never hard-fails on a broken browser. The one that launches is **cached as the default** so it isn't re-probed each time. To override:++```bash+adom-desktop browser_use '{"browser":"chrome"}' # pin installed Chrome+adom-desktop browser_use '{"browser":"edge"}' # pin Edge+adom-desktop browser_use '{"browser":"cft"}' # install Chrome for Testing (~150 MB once) + pin it+adom-desktop browser_use '{"browser":"auto"}' # back to auto-pick+```++Every `browser_open_window` response includes `browser` (what was driven), `availableBrowsers`, `defaultBrowser`, and an `_hint` spelling out how to switch — surface it if the user asks which browser pup is using.+ ## How to Run Commands ```bash@@ -74,9 +87,10 @@ adom-desktop browser_alert_window '{"sessionId":"job"}' # non-intrusive "do | Command | Description | Key args | |---|---|---|-| `browser_readiness` | Cold-start probe: is Chrome for Testing installed/installing? | none |-| `browser_prewarm` | Install Chrome for Testing WITHOUT opening a window | `wait` (optional bool) |-| `browser_open_window` | Spawn a new session/window (CfT, or native via `nativeBrowser`+`nativeProfile`) | `sessionId`, `profile`, `url` |+| `browser_readiness` | Which browser pup will drive + candidate list + default (READ-ONLY) | none |+| `browser_prewarm` | Install Chrome for Testing WITHOUT opening a window, and pin it | `wait` (optional bool) |+| `browser_use` | Pin the browser pup drives: `chrome`/`edge`/`cft`/`auto` | `browser`, `executablePath?` |+| `browser_open_window` | Spawn a new session/window (installed Chrome/Edge or CfT; native via `nativeBrowser`+`nativeProfile`) | `sessionId`, `profile`, `url` | | `browser_open_tab` | Add a tab to an existing session | `sessionId`, `url` | | `browser_navigate` | Drive an existing tab to a URL (reuses — no new window) | `sessionId`, `url`, `tabId?` | | `browser_screenshot` | Capture the page (lossless PNG) | `sessionId`, `fullPage?` |
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "1.3.0",- "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.3.0/adom-bridge-puppeteer-v1.3.0.zip",- "sha256": "8b80afbe95e0af54dfa2db88a12ff3b71f86711dfc3b479226b6db526cdc86f6",- "size": 128877,+ "version": "1.4.0",+ "url": "https://wiki.adom.inc/download/adom/adom-desktop-puppeteer-bridge/1.4.0/adom-bridge-puppeteer-v1.4.0.zip",+ "sha256": "1d4db13428c81438b6116b1ff31b2a5602a62e34dd4d36ee6acce678e551fd48",+ "size": 134284, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-1.3.0+1.4.0
src/bridge.json+3−2@@ -2,8 +2,8 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "1.3.0",- "description": "Drives a browser ALREADY on the machine — installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) — 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. Chrome for Testing is fetched only as a LAST RESORT if the machine has no Chromium browser at all. browser_readiness (READ-ONLY) reports which browser will be used (browserKind/browserSource) + whether it's ready; browser_open never blocks on a download. 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. v1.0.6+ optional native-drive: pass nativeBrowser+nativeProfile (from desktop_list_browsers) to browser_open_window to drive the user's REAL signed-in Chrome/Edge/Brave (their actual profile, org policies, device enrollment) — with the smart relaunch/consent flow (native_relaunch_required / native_cdp_blocked). The generic path (no nativeBrowser) always works.",+ "version": "1.4.0",+ "description": "Drives a browser ALREADY on the machine — installed Chrome, else Microsoft Edge (every Windows PC ships with Edge) — 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. 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 → Edge → CfT) and actually launches it, falling through to the next if one won't start, 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. browser_use pins a specific browser (chrome | edge | cft | auto); browser_readiness (READ-ONLY) reports which browser will be used + the full candidate list + the current default. Every open returns a verbose _hint: what was driven, what else is available, and how to switch or install full CfT. 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. v1.0.6+ optional native-drive: pass nativeBrowser+nativeProfile (from desktop_list_browsers) to browser_open_window to drive the user's REAL signed-in Chrome/Edge/Brave (their actual profile, org policies, device enrollment) — with the smart relaunch/consent flow (native_relaunch_required / native_cdp_blocked). The generic path (no nativeBrowser) always works.", "homepage": "https://wiki.adom.inc/adom/adom-desktop", "author": "Adom Inc.", "license": "MIT",@@ -38,6 +38,7 @@ "browser_input_dispatch", "browser_readiness", "browser_prewarm",+ "browser_use", "browser_record_start", "browser_record_stop", "browser_record_status",
src/chrome.js+139−18@@ -97,8 +97,9 @@ function detectChrome() { // Every Windows PC ships with Edge, and most have Chrome — both are Chromium and // both are drivable by puppeteer. Preferring an ALREADY-INSTALLED browser (launched // with a FRESH isolated profile) means the common case needs NO ~150 MB Chrome-for--// Testing download. detectSystemBrowser probes the standard install locations.-function detectSystemBrowser() {+// Testing download. listSystemBrowsers probes the standard install locations and+// returns EVERY drivable system browser it finds, in preference order (Chrome, then Edge).+function listSystemBrowsers() { const pf = process.env['PROGRAMFILES'] || 'C:\\Program Files'; const pf86 = process.env['PROGRAMFILES(X86)'] || 'C:\\Program Files (x86)'; const lad = process.env['LOCALAPPDATA'] || '';@@ -112,27 +113,139 @@ function detectSystemBrowser() { j(pf86, 'Microsoft', 'Edge', 'Application', 'msedge.exe'), j(pf, 'Microsoft', 'Edge', 'Application', 'msedge.exe'), ].filter(Boolean);- for (const p of chromePaths) if (fileExists(p)) return { found: true, kind: 'chrome', executablePath: p };- for (const p of edgePaths) if (fileExists(p)) return { found: true, kind: 'edge', executablePath: p };- // POSIX fallbacks (pup is Windows-first, but keep dev boxes working):- for (const p of ['/usr/bin/google-chrome', '/usr/bin/chromium', '/usr/bin/chromium-browser',- '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',- '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge']) {- if (fileExists(p)) return { found: true, kind: p.toLowerCase().includes('edge') ? 'edge' : 'chrome', executablePath: p };+ const posix = [+ ['chrome', '/usr/bin/google-chrome'], ['chrome', '/usr/bin/chromium'], ['chrome', '/usr/bin/chromium-browser'],+ ['chrome', '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'],+ ['edge', '/Applications/Microsoft Edge.app/Contents/MacOS/Microsoft Edge'],+ ];+ const out = [];+ const seen = new Set();+ const push = (kind, p) => { if (p && !seen.has(p) && fileExists(p)) { seen.add(p); out.push({ kind, executablePath: p, source: 'system' }); } };+ for (const p of chromePaths) push('chrome', p);+ for (const p of edgePaths) push('edge', p);+ for (const [kind, p] of posix) push(kind, p);+ return out;+}++// Back-compat: the FIRST system browser (or {found:false}).+function detectSystemBrowser() {+ const list = listSystemBrowsers();+ return list.length ? { found: true, kind: list[0].kind, executablePath: list[0].executablePath } : { found: false };+}++// ── Persisted browser default ─────────────────────────────────────────────────+// 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+// 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'); }+function readDefault() {+ try { return JSON.parse(fs.readFileSync(defaultFile(), 'utf8')); } catch { return null; }+}+function writeDefault(obj) {+ try { fs.mkdirSync(cacheDir(), { recursive: true }); fs.writeFileSync(defaultFile(), JSON.stringify(obj, null, 2)); return true; }+ catch (e) { console.error(`[chrome] could not persist browser default: ${e.message}`); return false; }+}+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.+function markVerified(cand) {+ if (!cand || !cand.executablePath) return;+ const d = readDefault();+ if (d && d.forced) return;+ writeDefault({ kind: cand.kind, executablePath: cand.executablePath, source: cand.source || 'system', buildId: cand.buildId || null, forced: false, verifiedAt: Date.now() });+}++// Every drivable browser, in the order pup will TRY them:+// [stored default, if still present] → installed Chrome → installed Edge → cached CfT.+// Each entry is a fully-launchable {kind, executablePath, source}. The launch path+// tries them top-to-bottom and spawn-verifies each, so a broken top pick falls through.+function launchCandidates() {+ const all = [...listSystemBrowsers()];+ const cft = detectChrome();+ if (cft.installed) all.push({ kind: 'chrome-for-testing', executablePath: cft.executablePath, source: 'cache', buildId: cft.buildId });++ const def = readDefault();+ if (def) {+ let idx = -1;+ if (def.executablePath) idx = all.findIndex(c => c.executablePath === def.executablePath);+ if (idx < 0 && def.kind) idx = all.findIndex(c => c.kind === def.kind);+ if (idx > 0) { const [d] = all.splice(idx, 1); all.unshift(d); }+ else if (idx < 0 && def.executablePath && fileExists(def.executablePath)) {+ all.unshift({ kind: def.kind || 'custom', executablePath: def.executablePath, source: def.source || 'default', buildId: def.buildId });+ }+ // A forced-but-not-yet-present default (e.g. 'cft' still downloading) simply+ // doesn't reorder — the system browsers serve as the fallback until it arrives. }- return { found: false };+ return all; } // The browser pup will actually launch, in preference order:-// installed Chrome → installed Edge → cached Chrome-for-Testing → (none → fetch CfT).+// [pinned/verified default] → installed Chrome → installed Edge → cached Chrome-for-Testing. // Launch uses a fresh --user-data-dir, so a system browser is driven CLEAN — it never-// touches the user's real tabs/logins (that's native-mode's job).+// touches the user's real tabs/logins (that's native-mode's job). `candidates` is the+// full ordered fallback list; the launch path spawn-verifies each in turn. function resolveBrowser() {- const sys = detectSystemBrowser();- if (sys.found) return { found: true, kind: sys.kind, executablePath: sys.executablePath, source: 'system' };- const cft = detectChrome();- if (cft.installed) return { found: true, kind: 'chrome-for-testing', executablePath: cft.executablePath, source: 'cache', buildId: cft.buildId };- return { found: false, kind: 'none', executablePath: null };+ const candidates = launchCandidates();+ const def = readDefault();+ const top = candidates[0];+ if (!top) return { found: false, kind: 'none', executablePath: null, source: null, candidates: [], forced: !!(def && def.forced) };+ return {+ found: true, kind: top.kind, executablePath: top.executablePath, source: top.source, buildId: top.buildId || null,+ candidates, forced: !!(def && def.forced), defaultBrowser: def || null,+ };+}++// browser_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 } = {}) {+ 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() };+ writeDefault({ kind: 'custom', executablePath, source: 'explicit', forced: true, verifiedAt: Date.now() });+ return { ok: true, ...pickSummary() };+ }+ if (b === 'auto' || b === 'clear' || b === 'reset' || b === 'default' || b === '') {+ clearDefault();+ return { ok: true, cleared: true, ...pickSummary() };+ }+ if (b === 'chrome' || b === 'edge') {+ const cand = listSystemBrowsers().find(c => c.kind === b);+ if (!cand) {+ const avail = listSystemBrowsers().map(c => c.kind).join(', ') || 'none';+ return { ok: false, errorCode: 'browser_not_found', error: `No installed ${b} found. Installed system browsers: ${avail}. Use browser:'cft' to fetch Chrome for Testing.`, ...pickSummary() };+ }+ writeDefault({ kind: cand.kind, executablePath: cand.executablePath, source: 'system', forced: true, verifiedAt: Date.now() });+ return { ok: true, ...pickSummary() };+ }+ if (b === 'cft' || b === 'chromefortesting') {+ const det = detectChrome();+ if (det.installed) {+ writeDefault({ kind: 'chrome-for-testing', executablePath: det.executablePath, source: 'cache', buildId: det.buildId, forced: true, verifiedAt: Date.now() });+ return { ok: true, ...pickSummary() };+ }+ // Not cached — pin the intent and start the download; once it lands it becomes default.+ writeDefault({ kind: 'chrome-for-testing', executablePath: null, source: 'cache', forced: true, pendingInstall: true, verifiedAt: Date.now() });+ ensureChromeReady({ background: true });+ return { ok: true, installing: true, ...pickSummary() };+ }+ 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.+function pickSummary() {+ const r = resolveBrowser();+ const d = readDefault();+ return {+ picked: r.found ? { kind: r.kind, source: r.source, executablePath: r.executablePath } : null,+ candidates: (r.candidates || []).map(c => ({ kind: c.kind, source: c.source, executablePath: c.executablePath })),+ defaultBrowser: d ? { kind: d.kind, source: d.source || null, forced: !!d.forced, pendingInstall: !!d.pendingInstall } : null,+ }; } // Programmatic install of the pinned CfT build. Resolves to { ok, executablePath, buildId }@@ -258,11 +371,15 @@ function readiness() { const det = detectChrome(); const installing = !!_installInFlight || state.phase === 'installing'; const ready = b.found && !installing; // a drivable browser is available (system OR cached CfT)+ const d = readDefault(); return { ready, browserKind: b.kind, // 'chrome' | 'edge' | 'chrome-for-testing' | 'none' browserSource: b.source || null, // 'system' | 'cache' | null browserExecutablePath: b.executablePath || null,+ // Full ordered fallback list the launcher will spawn-verify, + the persisted default.+ candidates: (b.candidates || []).map(c => ({ kind: c.kind, source: c.source, executablePath: c.executablePath })),+ defaultBrowser: d ? { kind: d.kind, source: d.source || null, forced: !!d.forced, pendingInstall: !!d.pendingInstall } : null, nodeInstalled: true, nodeVersion: process.version, chromeForTestingInstalled: det.installed,@@ -280,4 +397,8 @@ function readiness() { }; } -module.exports = { detectChrome, detectSystemBrowser, resolveBrowser, installChrome, ensureChromeReady, readiness, expectedBuildId, cacheDir };+module.exports = {+ detectChrome, detectSystemBrowser, listSystemBrowsers, launchCandidates, resolveBrowser,+ installChrome, ensureChromeReady, readiness, expectedBuildId, cacheDir,+ setDefaultBrowser, readDefault, clearDefault, markVerified, pickSummary,+};
src/server.js+162−44@@ -1305,6 +1305,35 @@ 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 —+// which can differ from resolveBrowser()'s top pick if the top candidate had to fall+// through. Read right after getOrLaunchBrowser() returns.+let _lastChosenBrowser = null;++// Build the verbose browser report the AI gets on every open: what we drove, what+// else is available, the current default, and how to switch or install full CfT.+function browserPickReport() {+ const sum = chrome.pickSummary();+ const chosen = _lastChosenBrowser || sum.picked;+ 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 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.`+ : `No browser resolved. Available on this machine: ${availTxt}.`;+ return {+ browser: chosen ? { kind: chosen.kind, source: chosen.source, executablePath: chosen.executablePath || null } : null,+ availableBrowsers: sum.candidates,+ defaultBrowser: def,+ _hint: hint,+ };+}+ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPermissions = false, nativeSite = false } = {}) { // Check if browser already running for this profile. // isConnected() can be stale after laptop sleep/wake — the WebSocket hasn't@@ -1332,15 +1361,23 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer // so the common case needs NO ~150 MB Chrome-for-Testing download. Falls back to a // cached CfT. A fresh --user-data-dir (below) keeps it CLEAN — it never touches the // user's real tabs/logins (that's native-mode's job).- const resolvedBrowser = chrome.resolveBrowser();- if (resolvedBrowser.found) {- console.log(`[launch] browser: ${resolvedBrowser.kind} (${resolvedBrowser.source}) — ${resolvedBrowser.executablePath}`);+ //+ // launchCandidates() returns the FULL ordered fallback list ([pinned default] →+ // Chrome → Edge → cached CfT). The launch loop below tries each in turn and+ // SPAWN-VERIFIES it — a broken/partial top pick (e.g. a corrupt Chrome install)+ // falls through to the next real browser instead of hard-failing the open. The+ // browser that actually launches is cached as the default (chrome.markVerified).+ const candidates = chrome.launchCandidates();+ if (candidates.length) {+ console.log(`[launch] ${candidates.length} browser candidate(s): ` + candidates.map(c => `${c.kind}(${c.source})`).join(' → '));+ } else {+ console.log('[launch] no system browser or cached CfT detected — falling back to puppeteer.executablePath()'); } const launchOpts = { headless: false, defaultViewport: null,- ...(resolvedBrowser.executablePath ? { executablePath: resolvedBrowser.executablePath } : {}),+ // executablePath is set PER-CANDIDATE in the spawn-verified launch loop below. args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security', '--start-maximized', '--remote-debugging-port=0',@@ -1487,10 +1524,12 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer }; // ─── Helper: one detached-launch attempt, returns browser or throws ───- const tryDetachedLaunch = async (attemptNum) => {+ const tryDetachedLaunch = async (attemptNum, chromePathArg) => { const { spawn } = require('child_process');- // Launch the resolved browser (installed Chrome/Edge preferred; cached CfT fallback).- const chromePath = resolvedBrowser.executablePath || puppeteer.executablePath();+ // Launch the candidate the loop is currently trying (installed Chrome/Edge+ // preferred; cached CfT fallback). Falls back to puppeteer's own path only if+ // no candidate path was resolved (the no-system-browser edge case).+ const chromePath = chromePathArg || puppeteer.executablePath(); const debugPort = 9200 + Math.floor(Math.random() * 800); const chromeArgs = [ ...launchOpts.args.filter(a => a !== '--remote-debugging-port=0'),@@ -1543,46 +1582,74 @@ async function getOrLaunchBrowser(profileName, { freshProfile = false, strictPer return b; }; - // ─── Launch with up to 3 attempts and orphan cleanup between attempts ───+ // ─── Spawn-verified launch: try each candidate browser in order, up to 3 ───+ // attempts each (with orphan/lock cleanup between attempts). If a browser can't+ // actually spawn (corrupt install, wrong arch, locked), fall THROUGH to the next+ // candidate rather than failing the open. First one that launches wins + is cached. const isWinDetached = process.platform === 'win32' && userDataDir; const MAX_ATTEMPTS = 3; let lastErr = null;-- for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {- try {- if (isWinDetached) {- browser = await tryDetachedLaunch(attempt);- } else {- browser = await puppeteer.launch(launchOpts);+ let chosen = null;+ const triedList = [];+ // No detected candidates (no system browser + no cached CfT) → let puppeteer pick+ // its own executablePath. This is the rare cold-start edge the gate usually catches.+ const launchList = candidates.length ? candidates : [{ kind: 'default', source: 'puppeteer', executablePath: null }];++ for (const cand of launchList) {+ if (cand.executablePath) launchOpts.executablePath = cand.executablePath;+ else delete launchOpts.executablePath;+ lastErr = null;+ for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {+ try {+ if (isWinDetached) {+ browser = await tryDetachedLaunch(attempt, cand.executablePath);+ } else {+ browser = await puppeteer.launch(launchOpts);+ }+ chosen = cand;+ lastErr = null;+ break;+ } catch (err) {+ lastErr = err;+ console.log(`Launch attempt ${attempt}/${MAX_ATTEMPTS} for ${cand.kind}(${cand.source}) failed: ${err.message}`);+ if (attempt >= MAX_ATTEMPTS) break;+ // Recovery before retry: kill any lingering Chrome holding the profile + clear locks+ await killChromeHoldingProfile(); }- lastErr = null;- break;- } catch (err) {- lastErr = err;- console.log(`Launch attempt ${attempt}/${MAX_ATTEMPTS} failed: ${err.message}`);- if (attempt >= MAX_ATTEMPTS) break;- // Recovery before retry: kill any lingering Chrome holding the profile + clear locks- await killChromeHoldingProfile(); }+ if (browser) break;+ triedList.push({ kind: cand.kind, source: cand.source, executablePath: cand.executablePath || null, error: lastErr && lastErr.message });+ if (launchList.length > 1) console.log(`[launch] ${cand.kind}(${cand.source}) exhausted ${MAX_ATTEMPTS} attempts — trying next browser`);+ }++ if (browser && chosen) {+ // Record what actually launched so the open response + hints are accurate, and+ // cache it as the auto-default so we don't re-probe next time.+ browser._pupChosen = { kind: chosen.kind, source: chosen.source, executablePath: chosen.executablePath || null };+ _lastChosenBrowser = { ...browser._pupChosen, fallbackFrom: triedList.map(t => `${t.kind}(${t.source})`) };+ if (chosen.executablePath) chrome.markVerified(chosen);+ if (triedList.length) console.log(`[launch] launched ${chosen.kind}(${chosen.source}) after ${triedList.length} browser(s) failed to spawn`); } if (!browser) {- // Belt-and-suspenders: the open_window cold-start gate normally catches a- // missing Chrome for Testing BEFORE we get here. If we still failed, branch- // on whether CfT is actually present so the error is actionable, not a dev- // string telling the AI to run npx.+ // Every candidate failed to spawn. Branch on whether ANY browser was even+ // detectable so the error is actionable, not a dev string telling the AI to run npx.+ const triedTxt = triedList.length+ ? triedList.map(t => `${t.kind}(${t.source}): ${t.error}`).join(' | ')+ : '(no browsers were detected to try)'; const cdet = chrome.detectChrome();- if (!cdet.installed) {- const e = new Error('Chrome for Testing is not installed — pup cannot launch a browser yet.');- e.errorCode = 'chrome_for_testing_not_installed';- e._hint = 'Chrome for Testing (the browser pup drives) is missing. Call browser_prewarm to install it (~150 MB, one time), poll browser_readiness until ready:true, then retry. You do not need to install anything manually.';+ 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.'; throw e; }- throw new Error(- `Failed to launch Chrome for profile "${profileName}" after ${MAX_ATTEMPTS} attempts. ` +- `Last error: ${lastErr && lastErr.message}. ` +- `Chrome for Testing IS installed (${cdet.executablePath}) — this is more likely low memory, a locked/busy profile, or a crashed Chrome. Retry; if it persists, surface the last error to the user.`+ 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.`;+ throw e; } } @@ -2958,8 +3025,9 @@ const server = http.createServer(async (req, res) => { 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', 'Cold-start readiness: is Chrome for Testing installed / installing? Poll before+after a first open on a fresh PC.', {}, 10, {}),- V('browser_prewarm', 'Install Chrome for Testing WITHOUT opening a window (warm the cold-start before first use).', { wait: 'optional bool — block until installed' }, 240, {}),+ 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. Persists across restarts; launch still spawn-verifies + falls back.', { browser: '"chrome" | "edge" | "cft" | "auto"', executablePath: 'optional — pin an exact browser binary' }, 30, {}), V('browser_describe', 'This list — the bridge\'s verbs + schemas, for AD\'s Verbs tab.', {}, 10, {}), ] }); return;@@ -3085,6 +3153,9 @@ const server = http.createServer(async (req, res) => { // tab open or popup auto-attach in this session. if (session) session._downloadPath = downloadPath; const info = await getSessionInfo(sessionId, session);+ // Verbose browser report: which browser we actually drove, what else is+ // available, the current default, and how to switch / install full CfT.+ const brep = browserPickReport(); sendJSON(res, { success: true, output: JSON.stringify({@@ -3101,6 +3172,11 @@ const server = http.createServer(async (req, res) => { // it to desktop_watch_files without re-deriving). downloadsEnabled: !args.strictPermissions, downloadPath,+ // v1.4.0: which browser pup drove + the full fallback picture.+ browser: brep.browser,+ availableBrowsers: brep.availableBrowsers,+ defaultBrowser: brep.defaultBrowser,+ _hint: brep._hint, ...info, }), });@@ -4093,17 +4169,22 @@ const server = http.createServer(async (req, res) => { // "what do I have / is it ready" question prefer AD's `bridge_readiness` (it also // reports AD-core's Tier-1 prewarm progress); this verb is pup's own detailed view. const rd = chrome.readiness();+ const availTxt = (rd.candidates || []).map(c => `${c.kind}(${c.source})`).join(', ') || 'none';+ 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"}.'; sendJSON(res, { success: true, output: JSON.stringify(rd), ...rd, _hint: rd.ready- ? 'Ready — Chrome for Testing is cached and the server is up; pup can open windows. Proceed with browser_open_window.'+ ? `Ready — pup will drive ${rd.browserKind} (${rd.browserSource}) at ${rd.browserExecutablePath}. Available: ${availTxt}.${defTxt} No download needed. Proceed with browser_open_window.${switchTxt}` : rd.installing- ? `Chrome for Testing is being fetched (${rd.installProgressPct}%). Poll bridge_readiness (or this verb) until ready:true, then browser_open_window.`+ ? `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.` : rd.installPhase === 'failed'- ? `Chrome for Testing fetch FAILED: ${rd.lastError}. Retry browser_prewarm; if it persists, surface this to the user (usually network/proxy blocking the download, or low disk).`- : 'Chrome for Testing is not cached yet. Adom Desktop fetches it in the background (Tier-1 prewarm) — poll AD\'s bridge_readiness; or call browser_prewarm to have the bridge fetch it.',+ ? `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.`, }); return; }@@ -4113,6 +4194,14 @@ const server = http.createServer(async (req, res) => { // embedded first-run; pass {wait:true} to block until installed. const block = !!(args && (args.wait === true || args.block === true)); await chrome.ensureChromeReady({ background: !block });+ // Prewarm = the AI explicitly wants Chrome for Testing → pin it as the default+ // (unless a different browser was already explicitly pinned). Once cached, every+ // open drives CfT. Pass {setDefault:false} to only download without pinning.+ const pinCft = args && args.setDefault === false ? false : true;+ const curDef = chrome.readDefault();+ if (pinCft && !(curDef && curDef.forced && curDef.kind !== 'chrome-for-testing')) {+ chrome.setDefaultBrowser({ browser: 'cft' });+ } const rd = chrome.readiness(); sendJSON(res, { success: rd.installPhase !== 'failed',@@ -4120,15 +4209,44 @@ const server = http.createServer(async (req, res) => { ...rd, statusVerb: 'browser_readiness', _hint: rd.ready- ? 'Chrome for Testing is ready — pup can open windows instantly now.'+ ? `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.)` : rd.installing- ? `Warming up: Chrome for Testing downloading (${rd.installProgressPct}%). 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 browser_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.`+ ? `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.', }); return; }+ case 'browser_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+ // bridge restarts. The launch path still spawn-verifies + falls back, so a+ // pinned browser that later breaks won't hard-fail an open.+ const result = chrome.setDefaultBrowser({ browser: args && args.browser, executablePath: args && args.executablePath });+ const rd = chrome.readiness();+ 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"}.`;+ } 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.`;+ } 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.`;+ } 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"}.`;+ }+ sendJSON(res, {+ success: !!result.ok,+ output: JSON.stringify({ ...result, readiness: rd }),+ ...result,+ errorCode: result.ok ? undefined : (result.errorCode || 'browser_use_failed'),+ statusVerb: 'browser_readiness',+ _hint: hint,+ });+ return;+ } case 'browser_status': { const list = []; for (const [id, s] of sessions) {