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.319",+ "version": "2.0.320", "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.319",+ "version": "2.0.320", "tags": [ "skills", "pup",
tools/pup-dashboard+46−24@@ -371,7 +371,7 @@ async function runDemo() { execFile('adom-tts', ['say', text, '--out', path.join(os.tmpdir(), 'pup-demo-line.mp3'), '--thread', 'pup demo', '--desc', 'pup demo narration: ' + text.slice(0, 60), '--play'], { env: ENV, timeout: 45000 }, (err) => resolve(!err)); });- if (r) { await sleep(Math.max(1500, text.split(/\s+/).length * 330)); return; } // let the clip play out+ if (r) { await sleep(Math.min(6000, Math.max(1000, text.split(/\s+/).length * 240))); return; } // let the clip play out, briskly voice = 'sapi'; lines.push(' (adom-tts unavailable; falling back to the system voice)'); } const safe = text.replace(/[`$"\\]/g, ' ').replace(/'/g, "''");@@ -384,47 +384,51 @@ async function runDemo() { return ab('desktop_caption', { text, duration: ms, id: 'pup-demo', position: 'bottom', size: 'medium' }, 8000, tgt.name).then(() => ms); }; const clearCap = () => ab('desktop_caption', { text: ' ', duration: 300, id: 'pup-demo', position: 'bottom', size: 'medium' }, 8000, tgt.name).catch(() => {});- const say = async (text, extra) => { await Promise.all([cap(text), speak(text)]); await sleep(extra || 400); };+ const say = async (text, extra) => { await Promise.all([cap(text), speak(text)]); await sleep(extra || 150); }; - await say('Hi, this is the pup demo. I am an A I browser that lives on your desktop. Watch, I will narrate everything I do.');- await say('First: I am opening a browser window in the background. Nothing pops over your work, ever. Look at your taskbar: a new teal button just appeared wearing my thread name, pup dock demo, with the page icon badged on its corner.', 200);+ await say('Hi, this is the pup demo: an A I browser living on your desktop. I will narrate everything I do.');+ await say('Opening a window in the BACKGROUND: nothing pops over your work. Look at your taskbar: a new teal button wearing my thread name, with the page icon badged on it.', 100); const open = await ab('pup_open_window', { sessionId: sid, url: 'https://wiki.adom.inc/adom/pup-bridge', owner: 'pup dock demo' }, 120000, tgt.name); if (open._err) { await say('I could not open a window: ' + open._err); return lines; } lines.push(' open: ' + (open.sessionId || sid)); // THE PROGRESS BAR, driven visibly (John: "show how you drive the progress bar... explain you do // that for the length of any verb or long running process").- const wi = await ab('pup_window_info', { sessionId: sid }, 20000, tgt.name);- const hwnd = wi && wi.hwnd;+ let hwnd = null;+ for (let i = 0; i < 4 && !hwnd; i++) { const wi = await ab('pup_window_info', { sessionId: sid }, 20000, tgt.name); hwnd = wi && wi.hwnd; if (!hwnd) await sleep(1500); } if (hwnd) {- await say('Now watch that button closely. I am driving its PROGRESS BAR by hand so you can see it: pup paints one of these under the button for the length of ANY operation, a screenshot, an icon apply, a page load, any long-running verb, sized to how long that operation usually takes.', 0);- for (const v of [12, 30, 48, 66, 82, 95, 100]) { await ab('desktop_taskbar', { hwnd, progress: { state: 'normal', value: v } }, 8000, tgt.name); await sleep(700); }+ await say('Watch that button: I am driving its PROGRESS BAR by hand. pup runs one of these for the length of ANY verb or long operation, sized to how long it usually takes.', 0);+ for (const v of [15, 40, 65, 85, 100]) { await ab('desktop_taskbar', { hwnd, progress: { state: 'normal', value: v } }, 8000, tgt.name); await sleep(550); } await sleep(400); await ab('desktop_taskbar', { hwnd, progress: { state: 'none' } }, 8000, tgt.name);- await say('And done: the bar fills, completes, and clears. That is how you always know when I am working on a window and roughly how far along I am. No orange flashing at you.');+ await say('Fills, completes, clears: that is how you always know when I am working on a window. No orange flashing.'); } else lines.push(' (no hwnd resolved yet; skipped the manual progress sweep)'); - // The raise: ONE caption only (the foreground caption IS the narration; no demo caption under it).+ // The raise: ONE caption on screen at a time, ever. My caption channel goes quiet, the voice+ // narrates, the raise paints ITS caption, and I wait out that caption's word-scaled duration+ // (plus margin) before my channel speaks with a caption again. (v2.0.320: John saw my next+ // caption land on top of the still-visible foreground caption.) await clearCap(); const raiseWhy = 'the pup demo you started: bringing the window up so you can watch me drive it';- await Promise.all([- speak('You started this demo, so let me bring the window up where you can see it. pup always shows a caption first, saying who is foregrounding a window and why. Here it comes.'),- (async () => { lines.push(' [caption via raise] ' + raiseWhy); })(),- ]);+ await speak('Bringing the window up so you can watch. pup always captions a foreground first: who, and why. Here it comes.');+ lines.push(' [caption via raise] ' + raiseWhy);+ const fgCapMs = Math.min(12000, Math.max(2500, 900 + ('AI thread pup dock demo brought ' + sid + ' to the front. Reason: ' + raiseWhy).split(/\s+/).length * 350)); await ab('pup_raise_os_window', { sessionId: sid, foregroundReason: raiseWhy }, 30000, tgt.name);- await sleep(900);+ const fgShownAt = Date.now(); await ab('pup_hover', { sessionId: sid, x: 700, y: 300, intent: 'this is my cursor: it glides and narrates my intent', steps: 16 }, 20000, tgt.name);- await say('That teal cursor is me. It glides to wherever I am about to act and says why, so you can always tell what your A I is doing.');+ await speak('That teal cursor is me: it glides to where I am about to act and says why.');+ const fgLeft = fgCapMs + 700 - (Date.now() - fgShownAt);+ if (fgLeft > 0) await sleep(fgLeft); // never two captions: wait out the foreground caption // The three toolbar tiles (John: "you should have shown the 3 browser extensions"). await ab('pup_hover', { sessionId: sid, x: 1180, y: 8, intent: 'look UP at the toolbar, right of the address bar', steps: 14 }, 20000, tgt.name).catch(() => {});- await say('Now look at the TOP RIGHT of this window, next to the address bar: three teal tiles. The pup dots are my menu: annotate, snip, switch wiki views, open my dashboard. The pencil starts the annotation pen with one click. And the third tile is your adom you identity: your avatar means this window is signed in as you; windows that are NOT you wear an anonymous mark instead.', 300);+ await say('Top right, next to the address bar: three teal tiles. The pup dots are my menu. The pencil starts annotation. The avatar is your adom you identity: this window is signed in as YOU.', 150); - await say('Taking a screenshot now. Watch the taskbar button: the same progress bar runs on its own while I capture. I can do this even when the window is buried in the background.');+ await say('Screenshot now: the same progress bar runs on its own while I capture, even with the window buried in the background.'); const shot = await ab('pup_screenshot', { sessionId: sid }, 60000, tgt.name); lines.push(shot._err ? ' screenshot failed: ' + shot._err : ' screenshot: ' + shot.width + 'x' + shot.height); - await say('Now the annotation pen. I will draw on this live page, slowly, in red. Watch the circle.');+ await say('Now why annotation exists: FEEDBACK to your A I. Circle the thing you mean, and the capture goes to the A I thread that owns this window, and onto your clipboard for any A I chat. Watch.'); const arm = await ab('pup_annotate', { sessionId: sid, tool: 'pen' }, 30000, tgt.name); if (!arm._err) { const draw = "(async()=>{const r=document.getElementById('__pup_annotate');if(!r)return 'no overlay';const cv=r.querySelector('canvas');if(!cv)return 'no canvas';const mk=(t,x,y)=>new PointerEvent(t,{bubbles:true,cancelable:true,clientX:x,clientY:y,pointerId:1,isPrimary:true,button:0,buttons:t==='pointerup'?0:1});const cx=Math.round(innerWidth*0.35),cy=Math.round(innerHeight*0.4),rad=Math.min(150,Math.round(innerHeight*0.2));cv.dispatchEvent(mk('pointerdown',cx+rad,cy));for(let a=0.12;a<=6.55;a+=0.12){cv.dispatchEvent(mk('pointermove',cx+rad*Math.cos(a),cy+rad*Math.sin(a)));await new Promise(q=>setTimeout(q,55));}cv.dispatchEvent(mk('pointerup',cx+rad,cy));return 'drew';})()";@@ -432,16 +436,34 @@ async function runDemo() { const drewOk = !drew._err && String(drew.result || '') === 'drew'; if (drewOk) { lines.push(' drew the red circle live');- await say('Done. The moment the pen lifted, I captured the circled region and put it on your clipboard, with a caption strip naming this thread and page. Paste it anywhere. The ink fades; the capture is yours.');+ await say('Pen up: captured. It is on your clipboard with a strip naming this thread and page, and the owning A I thread sees it. Your A I now knows exactly what you meant.'); } else { lines.push(' draw did not run: ' + (drew._err || drew.result || 'unknown')); await say('The drawing step did not run on this page, so I will move on.'); } } else { lines.push(' annotate did not arm: ' + arm._err); await say('The annotation overlay did not arm on this page, so I will move on.'); } - await say('One more thing: right-click my taskbar button any time for the menu: annotate this page, switch wiki views, open my dashboard, or close every pup window at once.');+ // Auto-open the right-click menu for real (John: "why are you not auto-opening those menus"):+ // find the button's rectangle via UIAutomation, right-click it twice (the shell's known first-+ // click blip), let the menu sit on screen, then dismiss it.+ await say('Every pup button has a right-click menu. Opening it for you now, watch the bottom of your screen.', 0);+ const uia = "Add-Type -AssemblyName UIAutomationClient; $tb=[System.Windows.Automation.AutomationElement]::RootElement.FindFirst('Children',(New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ClassNameProperty,'Shell_TrayWnd'))); $btn=$tb.FindAll('Descendants',(New-Object System.Windows.Automation.PropertyCondition([System.Windows.Automation.AutomationElement]::ControlTypeProperty,[System.Windows.Automation.ControlType]::Button))) | Where-Object { $_.Current.Name -like '*pup dock demo*' } | Select-Object -First 1; if ($btn) { $r=$btn.Current.BoundingRectangle; [int]($r.X+$r.Width/2); [int]($r.Y+$r.Height/2) }";+ const rc = await ab('desktop_run_script', { language: 'powershell', body: uia, timeoutSeconds: 30 }, 35000, tgt.name);+ const nums = String(rc.stdout || '').trim().split(/\r?\n/).map(Number).filter(Number.isFinite);+ if (nums.length >= 2) {+ await ab('desktop_right_click', { x: nums[0], y: nums[1] }, 15000, tgt.name); await sleep(1200);+ await ab('desktop_right_click', { x: nums[0], y: nums[1] }, 15000, tgt.name); await sleep(600);+ await say('There: annotate, switch wiki views, my dashboard, move to workspace, close all. One right-click away.', 400);+ await ab('desktop_press_key', { key: 'Escape' }, 10000, tgt.name).catch(() => {});+ lines.push(' opened the jump list at ' + nums[0] + ',' + nums[1]);+ } else {+ lines.push(' (jump-list button rect not found; narrated instead)');+ await say('It holds: annotate this page, switch wiki views, open my dashboard, and close every pup window at once.');+ } // cursor OFF before leaving (John: "you never turned the cursor off") await ab('pup_evaluate', { sessionId: sid, expression: "['__adom_cursor__','__pupCursor','__adom_cursor_ring__','__adom_cursor_label__'].forEach(id=>{const e=document.getElementById(id);if(e)e.remove();});'cursor off'" }, 15000, tgt.name).catch(() => {});- await say('And now I get out of your way: cursor off, back to the background. That is pup. Say, show me in pup, to any Adom A I.');- await ab('pup_lower_os_window', { sessionId: sid }, 20000, tgt.name);- await cap('pup demo done. The window stays open in the background; close it from its right-click menu, or say: close the pup demo window.');+ await say('And when I am done, I clean up after myself: closing the window now. Watch the button leave your taskbar.');+ await ab('pup_close_window', { sessionId: sid }, 30000, tgt.name);+ lines.push(' closed the demo window');+ await cap('pup demo done: window closed, taskbar clean. Say, show me in pup, to any Adom AI.');+ await speak('That is pup. Say, show me in pup, to any Adom A I.'); return lines; }