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
v2.0.370: Annotate: two explicit capture modes and one-click colors. Auto (default) still captures the instant the mouse lifts - the one-red-circle case. New Multi mode accumulates shapes (a red circle here, a blue circle there) and captures ONCE on Done. The single cycling color dot is replaced by a one-click trio (red, blue, yellow) with green/purple/white in a flyout. pup_annotate gains mode:'auto'|'multi'.
5 files changed
+65−24
adom-bridge-puppeteer-manifest.json+5−5@@ -1,10 +1,10 @@ { "manifest_version": 1, "name": "puppeteer",- "version": "2.0.369",- "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.369/adom-bridge-puppeteer-v2.0.369.zip",- "sha256": "7f981cfe80a136336e41346f6225830e8fcdfdf3d400574729383999b81a0815",- "size": 1538075,+ "version": "2.0.370",+ "url": "https://wiki.adom.inc/download/adom/pup-bridge/2.0.370/adom-bridge-puppeteer-v2.0.370.zip",+ "sha256": "aadb666048dd3b6942a41c3575f7a8cbdacda4c7fa690c78276b228bbef470b9",+ "size": 1538925, "verbPrefixes": [ "browser_", "desktop_recorder_",@@ -12,5 +12,5 @@ ], "healthEndpoint": "/status", "statusVerb": "browser_readiness",- "released_at": "2026-09-02T11:36:11.826883Z"+ "released_at": "2026-09-02T11:54:14.944409Z" }
package.json+1−1@@ -1,6 +1,6 @@ { "slug": "pup-bridge",- "version": "2.0.369",+ "version": "2.0.370", "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": [
src/BRIDGE_VERSION+1−1@@ -1 +1 @@-2.0.369+2.0.370
src/bridge.json+1−1@@ -2,7 +2,7 @@ "manifest_version": 1, "name": "puppeteer", "displayName": "Puppeteer (Chrome for Testing + Native)",- "version": "2.0.369",+ "version": "2.0.370", "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+57−16@@ -7314,11 +7314,13 @@ function appIconReprimand(url, hadFavicon) { // chat carries both. A preview toast shows the exact crop so the user can verify the grab. // Injected via CDP (CSP-immune), zero external resources; no backticks/interpolation inside the // overlay string (it lives in a template literal).-const ANNOTATE_OVERLAY_JS = `window.__pupStartAnnotate = function (initTool) {+const ANNOTATE_OVERLAY_JS = `window.__pupStartAnnotate = function (initTool, initMode) { if (window.__pupAnnotate) { if (!window.__pupAnnotateHidden) window.__pupAnnotate.show(); return 'shown'; } // v2.0.81: a re-arm after emit-hide stays INVISIBLE (the post-draw blip John saw) initTool = initTool || 'pen';- var S = { tool: 'pen', color: '#f85149', strokes: [], cur: null, quick: true, seq: 0 };- var COLORS = ['#f85149', '#d29922', '#3fb950', '#64abff', '#8c6bf7', '#e6edf3'];+ // v2.0.370 (John): two explicit capture modes. quick=true (Auto, the default) captures the+ // moment the mouse lifts — the one-red-circle case. quick=false (Multi) accumulates shapes —+ // a red circle here, a blue circle there — and captures ONCE on Done.+ var S = { tool: 'pen', color: '#f85149', strokes: [], cur: null, quick: initMode !== 'multi', seq: 0 }; var wrap = document.createElement('div'); wrap.id = '__pup_annotate'; wrap.style.cssText = 'position:fixed;inset:0;z-index:2147483646;pointer-events:none;';@@ -7494,6 +7496,7 @@ const ANNOTATE_OVERLAY_JS = `window.__pupStartAnnotate = function (initTool) { text: [['M6 6h12', 'M12 6v12']], snip: [['M4 8V5a1 1 0 0 1 1-1h3M16 4h3a1 1 0 0 1 1 1v3M20 16v3a1 1 0 0 1-1 1h-3M8 20H5a1 1 0 0 1-1-1v-3', 'M8.5 8.5h7v7h-7z'], 2], auto: [['M13 3L5 13h5l-1 8 8-10h-5l1-8']],+ multi: [['M4 4.5h10v10H4z', 'M10 9.5h10v10H10z']], undo: [['M8 5L4 9l4 4', 'M4 9h9a6 6 0 0 1 0 12h-2']], clear: [['M5 7h14', 'M9 7V5a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2', 'M7 7l1 13h8l1-13']], done: [['M4.5 12.5l5 5 10-11']],@@ -7516,15 +7519,52 @@ const ANNOTATE_OVERLAY_JS = `window.__pupStartAnnotate = function (initTool) { btn('Box', 'Frame a region with a rectangle. Drag corner to corner.', function () { selTool('box'); }, 'box', 'box'); btn('Text', 'Drop a short text note onto the page. Click where the note should start.', function () { selTool('text'); }, 'text', 'text'); btn('Snip', 'Capture exactly a region, like Win+Shift+S: drag a marquee and it is copied to your clipboard the moment you release.', function () { selTool('snip'); }, 'snip', 'snip');- var dot = document.createElement('button');- dot.removeAttribute && dot.removeAttribute('title'); dot.style.cssText = 'width:20px;height:20px;border-radius:50%;border:2px solid #8b949e;cursor:pointer;background:' + S.color + ';';- dot.onclick = function () { var i = (COLORS.indexOf(S.color) + 1) % COLORS.length; S.color = COLORS[i]; dot.style.background = S.color; setCursor(); };- bar.appendChild(dot); tipFor(dot, 'Drawing color. Click to cycle: red, yellow, green, blue, purple, white. The pen cursor shows the current color.');- var qk = document.createElement('button');- function qkPaint() { qk.style.cssText = 'background:' + (S.quick ? 'rgba(0,184,177,.13)' : 'none') + ';border:1px solid ' + (S.quick ? 'rgba(0,184,177,.55)' : 'transparent') + ';border-radius:8px;color:' + (S.quick ? '#00d4cb' : '#c9d1d9') + ';padding:3px 8px;cursor:pointer;font:inherit;display:flex;align-items:center;gap:5px;'; }- qk.appendChild(icon(ICONS.auto[0])); qk.appendChild(document.createTextNode('Auto')); - qk.onclick = function () { S.quick = !S.quick; qkPaint(); };- qkPaint(); bar.appendChild(qk); tipFor(qk, 'Auto capture (on by default): circle it, lift the mouse, and it is captured, copied to your clipboard, and the toolbar closes. Turn off to compose multi-step drawings and capture once with Done. Esc exits any time.');+ // v2.0.370 (John): the color control is now a one-click TRIO — red (default), blue, yellow —+ // with the rarer colors one click deeper in a flyout. The old single cycling dot made+ // red→blue three clicks; picking between about 3 colors must be instant.+ var SW_MAIN = ['#f85149', '#64abff', '#d29922'];+ var SW_MORE = ['#3fb950', '#8c6bf7', '#e6edf3'];+ var flyout = document.createElement('div');+ flyout.style.cssText = 'position:absolute;top:calc(100% + 8px);left:50%;transform:translateX(-50%);display:none;gap:7px;background:#161b22f2;border:1px solid #30363d;border-radius:999px;padding:7px 10px;box-shadow:0 6px 24px #000a;pointer-events:auto;';+ var more = document.createElement('button');+ function setColor(c) {+ S.color = c; setCursor(); flyout.style.display = 'none';+ [].forEach.call(wrap.querySelectorAll('button[data-sw]'), function (x) { x.style.borderColor = x.dataset.sw === c ? '#fff' : 'rgba(255,255,255,.25)'; });+ more.style.outline = SW_MORE.indexOf(c) >= 0 ? '2px solid #fff' : 'none';+ }+ function swBtn(c, parent) {+ var s = document.createElement('button');+ s.dataset.sw = c;+ s.style.cssText = 'width:18px;height:18px;border-radius:50%;cursor:pointer;background:' + c + ';border:2px solid ' + (S.color === c ? '#fff' : 'rgba(255,255,255,.25)') + ';padding:0;flex:none;';+ s.onclick = function () { setColor(c); };+ parent.appendChild(s);+ return s;+ }+ SW_MAIN.forEach(function (c) { swBtn(c, bar); });+ more.style.cssText = 'width:18px;height:18px;border-radius:50%;cursor:pointer;border:2px solid rgba(255,255,255,.25);padding:0;flex:none;background:conic-gradient(#f85149,#d29922,#3fb950,#64abff,#8c6bf7,#f85149);';+ more.onclick = function () { flyout.style.display = flyout.style.display === 'none' ? 'flex' : 'none'; };+ bar.appendChild(more); tipFor(more, 'More colors: green, purple, white.');+ SW_MORE.forEach(function (c) { swBtn(c, flyout); });+ bar.appendChild(flyout);+ // v2.0.370 (John): Auto vs Multi as an explicit two-state control, not a lone toggle.+ var modeWrap = document.createElement('div');+ modeWrap.style.cssText = 'display:flex;border:1px solid #30363d;border-radius:8px;overflow:hidden;margin-left:2px;';+ function modeBtn(label, iconKey, val, tip) {+ var b = document.createElement('button');+ if (ICONS[iconKey]) b.appendChild(icon(ICONS[iconKey][0], ICONS[iconKey][1]));+ b.appendChild(document.createTextNode(label));+ b.style.cssText = 'background:none;border:none;color:#c9d1d9;padding:3px 8px;cursor:pointer;font:inherit;display:flex;align-items:center;gap:5px;';+ b.onclick = function () { S.quick = val; paintMode(); };+ modeWrap.appendChild(b); tipFor(b, tip);+ return b;+ }+ var mAuto = modeBtn('Auto', 'auto', true, 'One-shot (the default): draw one thing, lift the mouse, and it is captured and copied to your clipboard instantly.');+ var mMulti = modeBtn('Multi', 'multi', false, 'Compose: draw several shapes — switch colors between them — and NOTHING is captured until you click Done. One capture with everything in it.');+ function paintMode() {+ mAuto.style.background = S.quick ? 'rgba(0,184,177,.13)' : 'none'; mAuto.style.color = S.quick ? '#00d4cb' : '#c9d1d9';+ mMulti.style.background = !S.quick ? 'rgba(0,184,177,.13)' : 'none'; mMulti.style.color = !S.quick ? '#00d4cb' : '#c9d1d9';+ }+ paintMode(); bar.appendChild(modeWrap); btn('Undo', 'Remove the last thing you drew. Click again to keep unwinding.', function () { S.strokes.pop(); redraw(); }, null, 'undo'); btn('Clear', 'Erase every drawing on this page. The page itself is untouched.', function () { S.strokes = []; redraw(); }, null, 'clear'); var done = btn('Done', 'Capture the annotated area (tight-cropped around your drawing), copy it to your clipboard, and leave the drawing visible so the AI thread that owns this window sees it too.', function () { emit(null, true); }, null, 'done');@@ -7877,9 +7917,10 @@ async function startAnnotation(session, sessionId, callerThread, opts) { if (!pg) { const t0 = (session.activeTabId && (session.tabs || []).find(t => t.tabId === session.activeTabId)) || (session.tabs || [])[0]; pg = t0 && t0.page; } if (!pg) throw new Error('no page to annotate'); const tool = (opts && opts.tool) || 'pen';+ const mode = ((opts && opts.mode) === 'multi') ? 'multi' : 'auto'; // v2.0.370: multi = compose shapes, capture once on Done await pg.evaluate((code) => { if (!window.__pupStartAnnotate) eval(code); }, ANNOTATE_OVERLAY_JS);- await pg.evaluate((t2) => { window.__pupStartAnnotate(t2); }, tool); // v2.0.77: NEVER clear __pupAnnotateResult here — arming must not destroy a capture emitted moments earlier- console.log(`[annotate] "${sessionId}" overlay started (by ${callerThread || 'unknown'}, tool=${tool})`);+ await pg.evaluate((t2, m2) => { window.__pupStartAnnotate(t2, m2); }, tool, mode); // v2.0.77: NEVER clear __pupAnnotateResult here — arming must not destroy a capture emitted moments earlier+ console.log(`[annotate] "${sessionId}" overlay started (by ${callerThread || 'unknown'}, tool=${tool}, mode=${mode})`); // v2.0.76: the PROVEN watcher (2.0.49 pattern) — the fast-loop pickup never dispatched in // production and quick capture broke; this waits on the page itself and feeds the new fast // pipeline (flash + instant toast + async slow legs). Quick mode re-arms for the next stroke.@@ -14745,7 +14786,7 @@ const server = http.createServer(async (req, res) => { 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_install_toolbar', 'Status/trigger for the toolbar extension, which AUTO-INSTALLS invisibly at every profile launch (transient pipe-launch + CDP Extensions.loadUnpacked; persists in the profile). Running profiles pick it up on their next launch; cold profiles install immediately.', { profile: 'optional, default adom-you' }, 30, {}), V('pup_floaty_click', 'TEST SEAM: simulate a user click on the floaty pencil (fires the same host click handler a mouse does; the sweep then starts annotate within ~4s). Built for the recurring floaty ralph — assert the annotate overlay appears after calling this.', { sessionId: 'required window', tabId: 'optional tab' }, 10, {}),- V('pup_annotate', 'Start the HUMAN annotation overlay on a window\'s active tab (pen/arrow/box/text toolbar injected in-page; also on every window\'s taskbar jump list as "Annotate this page"). Done captures a PNG AND leaves the drawing in the DOM, so the owning thread\'s next pup_screenshot shows exactly what the user marked — check session.lastAnnotation via pup_list_windows. action:"cancel" removes the overlay.', { sessionId: 'required window', action: 'optional "cancel"' }, 15, {}),+ V('pup_annotate', 'Start the HUMAN annotation overlay on a window\'s active tab (pen/arrow/box/text toolbar injected in-page; also on every window\'s taskbar jump list as "Annotate this page"). Two capture modes the user can also flip in the toolbar: "auto" (default) captures the instant the mouse lifts — the one-red-circle case; "multi" accumulates shapes (color swatches: red/blue/yellow one click, more in a flyout) and captures ONCE on Done. Done captures a PNG AND leaves the drawing in the DOM, so the owning thread\'s next pup_screenshot shows exactly what the user marked — check session.lastAnnotation via pup_list_windows. action:"cancel" removes the overlay.', { sessionId: 'required window', mode: 'optional "auto"|"multi"', action: 'optional "cancel"' }, 15, {}), 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' }),@@ -18838,7 +18879,7 @@ const server = http.createServer(async (req, res) => { sendJSON(res, { success: true, output: JSON.stringify({ ok: true, cancelled: true }) }); return; }- await startAnnotation(session, sessionId, _callerThread, { tool: args.tool === 'snip' ? 'snip' : 'pen', tabId: args._resolvedTabId || null });+ await startAnnotation(session, sessionId, _callerThread, { tool: args.tool === 'snip' ? 'snip' : 'pen', mode: args.mode, tabId: args._resolvedTabId || null }); // v2.0.293 (John: "you just foregrounded a pup window and never showed an ab caption"): // the old unconditional raise claimed a jump-list click is user-sanctioned by // construction — TRUE only for USER callers (jump list / toolbar). An AI THREAD calling