← Commit history

Publish 2.0.303

John Lauer ·fed6ad199a ·2mo ago ·parent b39e834
3 files changed +42−5
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) {}   }); }