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
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "pup-bridge",- "version": "2.0.302",+ "version": "2.0.303", "type": "app", "description": "The pup SKILL PACK \u2014 the AI skills that teach a cloud/container AI how to drive the Adom Desktop Puppeteer (pup) bridge (browser_* verbs): the everyday `pup` skill plus focused sub-skills for windows/sessions/tabs, screenshots+recording, browsers+Chrome install, and driving the Adom wiki. Installs into ~/.claude/skills + ~/.codex/skills. DOCS ONLY \u2014 this is NOT the bridge runtime. The bridge itself (the Node program that drives the browser) runs on the DESKTOP: Adom Desktop bundles it and auto-installs/updates it, and it's published separately on the Releases tab as adom-bridge-puppeteer-v<ver>.zip. The pkg version is kept in lockstep with the Release version so the page's max version row always carries BOTH a pkg tarball (skills) and a runtime zip (bridge) \u2014 a skills-only or runtime-only bump on the top row breaks constraint installs. TL;DR: pkg install = the skills for your AI; the bridge = the Release zip, which Adom Desktop manages for you.", "keywords": [
page.json+1−1@@ -4,7 +4,7 @@ "slug": "pup-bridge", "title": "Pup - Puppeteer Bridge", "brief": "pup is the AI's own browser: a real, full Chrome on the user's desktop that the AI fully controls (a sandbox, not the user's signed-in browser). Rides Bridge; pup_* verbs open windows and tabs, navigate, screenshot, and eval JS.",- "version": "2.0.302",+ "version": "2.0.303", "tags": [ "skills", "pup",
tools/pup-dashboard+40−3@@ -29,6 +29,34 @@ const BIN = path.join(HOME, '.local', 'bin'); // so a dashboard that omits ADOM_AI_THREAD gets an error back and paints a false "not installed". const ENV = { ...process.env, PATH: `${BIN}:${process.env.PATH || ''}`, ADOM_AI_THREAD: process.env.ADOM_AI_THREAD || 'pup dashboard' }; const STATE = path.join(os.tmpdir(), 'pup-dashboard.state.json');+// Dock contract: version-aware reuse + webview takeover. TOOL_VERSION is the skillpack version this+// file shipped in (package.json sits one level up in the installed module), published in the state+// file and in /api/status as dashboardVersion. A running instance whose version differs from ours+// is restarted rather than reused: version-blind reuse pinned fusion's users to a stale server.+let TOOL_VERSION = '0';+try { TOOL_VERSION = String(require(path.join(__dirname, '..', 'package.json')).version || '0'); } catch (e) {}+const DOCK_WEBVIEW = process.env.ADOM_DOCK_WEBVIEW || '';+const DOCK_APP = process.env.ADOM_DOCK_APP || '';+// Once the dashboard is serving, take over the tab the dock pre-opened (named ADOM_DOCK_WEBVIEW):+// navigate it to our direct URL and own it from then on. Skipped when the var is unset (hand-run).+function takeOverDockWebview(url) {+ if (!DOCK_WEBVIEW) return;+ try {+ const { spawn } = require('child_process');+ const c = spawn('adom-cli', ['hydrogen', 'webview', 'open-or-refresh', '--name', DOCK_WEBVIEW, '--url', url],+ { detached: true, stdio: 'ignore', env: ENV });+ c.on('error', () => {});+ c.unref();+ } catch (e) {}+}+function instanceVersion(port) {+ return new Promise((resolve) => {+ const req = http.get({ host: '127.0.0.1', port, path: '/api/status', timeout: 1500 }, (res) => {+ let b = ''; res.on('data', d => { b += d; }); res.on('end', () => { try { resolve(String(JSON.parse(b).dashboardVersion || '')); } catch (e) { resolve(''); } });+ });+ req.on('error', () => resolve('')); req.on('timeout', () => { req.destroy(); resolve(''); });+ });+} // Dock contract (ah build thread, 2026-08-23): print the DIRECT loopback form. The dock's // readiness gate consumes this URL from a local WSL2 workspace with mirrored networking, // where the 'localhost' NAME resolves IPv6-first and costs ~800ms; 127.0.0.1 is instant.@@ -156,6 +184,7 @@ async function buildStatus() { label: 'pup', detail: state === 'ok' ? 'ready for action' : (state === 'fail' ? 'desktop bridge not answering' : 'ready after a one-time Chrome install'), version: status.version || null,+ dashboardVersion: TOOL_VERSION, desktop: tgt.name || null, desktops: tgt.all, leds,@@ -251,7 +280,15 @@ async function serve() { // rule 3: IDEMPOTENT — an instance already up wins; print ITS url and exit 0. try { const st = JSON.parse(fs.readFileSync(STATE, 'utf8'));- if (st.port && await alive(st.port)) { process.stdout.write(PROXY(st.port) + '\n'); process.exit(0); }+ if (st.port && await alive(st.port)) {+ const v = await instanceVersion(st.port);+ if (v === TOOL_VERSION) { takeOverDockWebview(PROXY(st.port)); process.stdout.write(PROXY(st.port) + '\n'); process.exit(0); }+ // a running instance of another version: stop it (by the pid it published) and start ours+ process.stderr.write(`pup-dashboard: running instance is ${v || 'unversioned'}, this is ${TOOL_VERSION}; restarting\n`);+ try { if (st.pid) process.kill(st.pid); } catch (e) {}+ try { fs.unlinkSync(STATE); } catch (e) {}+ await new Promise(r => setTimeout(r, 400));+ } } catch (e) {} // rule 1: SELF-BACKGROUND, done in the only order that is actually correct. The PARENT spawns a@@ -269,7 +306,7 @@ async function serve() { await new Promise(r => setTimeout(r, 250)); try { const st = JSON.parse(fs.readFileSync(STATE, 'utf8'));- if (st.port && await alive(st.port)) { process.stdout.write(PROXY(st.port) + '\n'); process.exit(0); }+ if (st.port && await alive(st.port)) { takeOverDockWebview(PROXY(st.port)); process.stdout.write(PROXY(st.port) + '\n'); process.exit(0); } } catch (e) {} } process.stderr.write('pup-dashboard: the server did not come up in time\n');@@ -303,7 +340,7 @@ async function serve() { }); server.listen(0, '127.0.0.1', () => { const port = server.address().port;- try { fs.writeFileSync(STATE, JSON.stringify({ port, pid: process.pid, at: Date.now() })); } catch (e) {}+ try { fs.writeFileSync(STATE, JSON.stringify({ port, pid: process.pid, at: Date.now(), version: TOOL_VERSION, dockWebview: DOCK_WEBVIEW || null, dockApp: DOCK_APP || null })); } catch (e) {} }); }