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
Jump-list tasks now call pup directly (John: be way smarter about how to call back into ab for the jump lists; should pup talk to itself when it is its own bridge). Answer: yes for pup verbs. Old tasks set no target so ab supplied the program (its bundled CLI), an exe path pup neither controls nor verifies and exactly what the CLI rename broke. Each task now carries target=powershell.exe plus a pup-owned launcher script that reads pup's own loopback port from a discovery file pup rewrites on every bind, POSTs the verb straight to pup (pup to pup, no ab hop, no CLI, no stale path), and only falls back to the resolved adom-bridge-cli.exe (which wakes pup via ab) if the direct hit fails because pup idled. JSON args ride as base64 so nothing mangles through the shell-link to powershell argv reparse. No ab change needed: the per-task target field already existed
4 files changed
+94−18
adom-bridge-puppeteer-manifest.json+4−4@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.40",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.40/adom-bridge-puppeteer-v2.0.40.zip",- "sha256": "556491fb6ed63030b148c2fb7563c1dae3ea47e2bda09d97a60a0fffc0c6003f",- "size": 643707,+ "version": "2.0.41",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.41/adom-bridge-puppeteer-v2.0.41.zip",+ "sha256": "96ab1da7a5fc1116253024666d0a91f5811ecec8e6cac6ea1bef120cfff3cd30",+ "size": 645619, "verbPrefixes": [ "browser_", "desktop_recorder_",
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.40+2.0.41
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.40",+ "version": "2.0.41", "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-bridge", "author": "Adom Inc.",
src/server.js+88−12@@ -3321,6 +3321,81 @@ function pupRelaunchCommand(sessionId) { // pinned pup launcher, so it needs identity or AD refuses it and the click does nothing. return `"${cli}" pup_focus_window "${payload}"`; // v1.9.121: identity rides in the payload JSON (see pupRelaunchCommand above) }+// ─── v2.0.41: jump-list tasks call pup DIRECTLY (John: "be way smarter about how to call back into+// ab for the jump lists ... should pup be able to talk back to itself when it's its own bridge?").+// Answer: YES, for pup_* verbs. Old design set NO task `target`, so ab supplied the program (its+// bundled CLI) — an exe path pup neither controls nor can verify, and the very thing the rename broke.+// Now every task carries target=powershell.exe + a pup-OWNED launcher script that:+// 1) reads pup's OWN loopback port from a discovery file pup rewrites on every bind, then+// 2) POSTs the verb straight to http://127.0.0.1:<port>/command — pup talking to itself, no ab+// relay hop, no CLI exe, no stale path (the two things that broke it), and+// 3) ONLY if that direct hit fails (pup idled/died — the Chrome windows outlive the node process,+// and just pup→pup can't wake a dead pup) falls back to the resolved adom-bridge-cli.exe, which+// goes through ab and RESPAWNS pup. Best of both: fast+accurate when up, ab-wake when asleep.+// JSON args ride as BASE64 (argv-safe: no spaces/quotes) so nothing mangles through the ShellLink →+// Explorer → powershell argv re-parse that historically ate raw escaped JSON (v1.9.9).+function pupPortFile() {+ const la = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');+ return path.join(la, 'Adom Bridge', 'pup-bridge-port.json');+}+function writePupPortFile() {+ if (process.platform !== 'win32') return;+ try {+ const f = pupPortFile();+ fs.mkdirSync(path.dirname(f), { recursive: true });+ fs.writeFileSync(f, JSON.stringify({ port: PORT, pid: process.pid, version: BRIDGE_VERSION, host: '127.0.0.1' }));+ } catch (e) { console.error('[jumplist] port-discovery write failed:', e.message); }+}+const PUP_JL_LAUNCHER_VERSION = 3; // bump to force-rewrite the .ps1 on upgrade+function pupJumplistLauncherPath() {+ const la = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');+ return path.join(la, 'Adom Bridge', 'pup-jumplist-call.ps1');+}+function ensurePupJumplistLauncher() {+ if (process.platform !== 'win32') return pupJumplistLauncherPath();+ const p = pupJumplistLauncherPath();+ // The script is pup's OWN callback shim. $Verb + $B64 (base64 of the verb's JSON args). It hits pup+ // directly, and only falls back to the CLI (which wakes pup) if the direct POST throws.+ const script = `# Adom Pup jump-list callback (v${PUP_JL_LAUNCHER_VERSION}) — pup talks to itself; CLI wakes it if asleep.+param([Parameter(Mandatory=$true)][string]$Verb, [string]$B64='')+$ErrorActionPreference='Stop'+$json = if ($B64) { [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($B64)) } else { '{}' }+try { $a = $json | ConvertFrom-Json } catch { $a = @{} }+# stamp caller identity into args too (some verbs read args.caller; the header covers the rest)+try { $a | Add-Member -NotePropertyName caller -NotePropertyValue (@{aiThread='user-taskbar-menu'}) -Force } catch {}+$body = (@{ command=$Verb; args=$a } | ConvertTo-Json -Depth 12 -Compress)+$portFile = Join-Path $env:LOCALAPPDATA 'Adom Bridge\\pup-bridge-port.json'+$ok=$false+try {+ $port = (Get-Content $portFile -Raw -ErrorAction Stop | ConvertFrom-Json).port+ Invoke-RestMethod -Uri ("http://127.0.0.1:{0}/command" -f $port) -Method Post -ContentType 'application/json' -Headers @{'x-adom-caller-thread'='user-taskbar-menu';'x-adom-caller-reason'='jump-list click'} -Body $body -TimeoutSec 6 | Out-Null+ $ok=$true+} catch { $ok=$false }+if (-not $ok) {+ # pup is not answering (idled/died). Go through the CLI so ab respawns it.+ $cli = Join-Path $env:LOCALAPPDATA 'Adom Bridge\\adom-bridge-cli.exe'+ if (-not (Test-Path $cli)) { $cli = Join-Path $env:LOCALAPPDATA 'Adom Desktop\\adom-desktop-cli.exe' }+ if (Test-Path $cli) { & $cli $Verb $json | Out-Null }+}+`;+ try {+ let cur = null; try { cur = fs.readFileSync(p, 'utf8'); } catch (e) {}+ if (cur !== script) { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, script); }+ } catch (e) { console.error('[jumplist] launcher write failed:', e.message); }+ return p;+}+// Build a jump-list task that routes through the pup-owned launcher (direct-to-pup, CLI fallback).+function pupTask(verb, obj, meta) {+ const b64 = Buffer.from(JSON.stringify(obj || {}), 'utf8').toString('base64');+ const launcher = pupJumplistLauncherPath();+ return {+ title: meta.title,+ description: meta.description,+ iconPath: meta.iconPath,+ target: 'powershell.exe',+ args: `-NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File "${launcher}" ${verb} ${b64}`,+ };+} function _isUnknownVerb(r, raw) { return /unknown (desktop )?command|unknown verb/i.test(((r && r.error) || '') + ' ' + (raw || '')); } async function ensurePupIdentityRegistered() { if (process.platform !== 'win32' || _identitySupported === false) return false;@@ -3813,17 +3888,16 @@ async function updateWikiJumplist(session, sessionId, opts) { // is args > flag > env, and a `caller` key inside the verb's own JSON is just another arg, so it // 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('pup_wiki_set_view', { sessionId, view: v });+ // v2.0.41: every task now routes through pupTask() → the pup-owned launcher (direct-to-pup POST,+ // CLI fallback). We ensure the launcher .ps1 + the port-discovery file exist before committing.+ try { ensurePupJumplistLauncher(); writePupPortFile(); } catch (e) {} // v1.9.51: a CLOSE-ALL task on every pup window (John: "i have like 8 open right now"). It calls- // 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 = {+ // pup_close with no sessionId, which kills every pup Chrome + clears every session.+ const closeAllTask = pupTask('pup_close', {}, { title: 'Close ALL Adom Pup windows', description: 'Close every Adom Pup browser window at once (all sessions, every tab)', iconPath: _taskIcon,- 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 // point of the toggle is the login state, so lead with that and let the DESCRIPTION carry the@@ -3838,8 +3912,8 @@ async function updateWikiJumplist(session, sessionId, opts) { let tasks = []; // v1.9.275: each entry family has its own pref (dep of the jumpLists master). if (pupSettings().jlViewTasks === 'on' && (view === 'authed' || view === 'public')) {- tasks.push({ title: 'Switch to logged-in view', description: 'Reload this Adom wiki page SIGNED IN — private source, drafts, owner-only cards (already signed in? harmless reload)', iconPath: _taskIcon, args: taskArgs('authed') });- tasks.push({ title: 'Switch to public view', description: 'Reload this Adom wiki page logged OUT — exactly what the public/world sees (already public? harmless reload)', iconPath: _taskIcon, args: taskArgs('public') });+ tasks.push(pupTask('pup_wiki_set_view', { sessionId, view: 'authed' }, { title: 'Switch to logged-in view', description: 'Reload this Adom wiki page SIGNED IN — private source, drafts, owner-only cards (already signed in? harmless reload)', iconPath: _taskIcon }));+ tasks.push(pupTask('pup_wiki_set_view', { sessionId, view: 'public' }, { title: 'Switch to public view', description: 'Reload this Adom wiki page logged OUT — exactly what the public/world sees (already public? harmless reload)', iconPath: _taskIcon })); } if (pupSettings().jlCloseAll === 'on') tasks.push(closeAllTask); // v2.0.21 (John: "stop trying to mangle the titles... instead give a jump list item that explains@@ -3849,12 +3923,11 @@ async function updateWikiJumplist(session, sessionId, opts) { // with everything about this window. This replaces the title-tag approach for good. if (pupSettings().jlOwnerTask !== 'off') { const _ownT = (session && (session.owner || session._ownerThread)) || null;- if (_ownT) tasks.unshift({+ if (_ownT) tasks.unshift(pupTask('pup_window_info', { sessionId }, { title: `Owned by: ${String(_ownT).slice(0, 40)}`, description: `Show everything about this window as a native card above the taskbar — the AI thread that owns it ("${_ownT}"), its container, profile, tabs, age, and the last agent action`, iconPath: _taskIcon,- args: jarg('pup_window_info', { sessionId }),- });+ })); } // The shell ignores a jump list on an unregistered AUMID — ensure it's registered first. // v1.9.314 (John: "i turned on jump lists and none of them work"): this guard REQUIRED the appId to@@ -14100,6 +14173,9 @@ let _pupBoundOnce = false; const _onPupBound = async () => { if (_pupBoundOnce) return; _pupBoundOnce = true; console.log(`Puppeteer Bridge running on http://${BIND_HOST}:${PORT} (bind=${BIND_HOST}; no firewall prompt)`);+ // v2.0.41: publish pup's own port so jump-list tasks can talk straight back to us (pup→pup), and+ // (re)write the callback launcher so the .ps1 tracks any format change on upgrade.+ try { writePupPortFile(); ensurePupJumplistLauncher(); } catch (e) {} console.log('Multi-session support: sessions are tabs sharing Chrome profiles via userDataDir'); console.log('Endpoints: /health, /command, /launch, /navigate, /reload, /screenshot, /screenshot.png, /errors, /eval, /wait, /status, /close'); // v2.0.13 (John, "everyone keeps saying pup is down"): DEFER the heavy native load + warm-up +