← Commit history

Publish 0.9.0

John Lauer ·4692133bfa ·2mo ago ·parent e0a65fc
5 files changed +208−9
bin/nb-dashboard+107−1
@@ -20,7 +20,57 @@ const { execFile, spawn } = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const VERSION = '0.8.2';           // keep in lockstep with package.json (pkg version)+const VERSION = '0.9.0';           // keep in lockstep with package.json (pkg version)+const path = require('path');+// TEACH THE NOUN (John, 2026-08-23): "I say 'open digikey in nb' cuz for that I might know I need nb+// since it has my login. Or 'open mouser in nb chrome personal'..." The phrases users should SAY,+// with the qualifiers they need (browser, profile). Single source is dockbar.json's say[] next to this+// script's package; the built-in list is the fallback when the manifest is not beside us.+const SAY_FALLBACK = [+  'open digikey in nb',+  'open mouser in nb chrome personal',+  'open my wiki page in nb edge',+  'log into autodesk in nb',+  'install the adom native browser extension in my chrome',+];+function sayPhrases() {+  try {+    const m = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'dockbar.json'), 'utf8'));+    if (Array.isArray(m.say) && m.say.length) return m.say.slice(0, 6);+  } catch {}+  return SAY_FALLBACK;+}+// "Try it": stage a phrase into this app's OWN Hydrogen AI thread (tag app:adom/nb-bridge) via the+// control API, unsent (submit:false) so the user stays in control. Control URL comes from the+// discovery file Hydrogen writes each launch; 47084 is only the default.+function controlUrl() {+  try { const u = fs.readFileSync(path.join(require('os').homedir(), '.adom', 'hydrogen-control-url'), 'utf8').trim(); if (u) return u.replace(/\/$/, ''); } catch {}+  return 'http://127.0.0.1:47084';+}+function httpJson(method, url, body) {+  return new Promise((resolve) => {+    try {+      const u = new URL(url);+      const data = body ? JSON.stringify(body) : null;+      const req = http.request({ host: u.hostname, port: u.port, path: u.pathname + u.search, method, timeout: 4000,+        headers: data ? { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(data) } : {} }, (res) => {+        let b = ''; res.on('data', (c) => (b += c)); res.on('end', () => { let j = null; try { j = JSON.parse(b); } catch {} resolve({ status: res.statusCode, json: j, text: b.slice(0, 300) }); });+      });+      req.on('error', (e) => resolve({ status: 0, error: e.message })); req.on('timeout', () => { req.destroy(); resolve({ status: 0, error: 'timeout' }); });+      if (data) req.write(data); req.end();+    } catch (e) { resolve({ status: 0, error: e.message }); }+  });+}+async function tryPhrase(prompt) {+  const base = controlUrl();+  const list = await httpJson('GET', base + '/ai-threads');+  const arr = (list.json && (Array.isArray(list.json) ? list.json : (list.json.threads || list.json.items || list.json.data))) || [];+  const mine = arr.find((t) => JSON.stringify(t.tags || t.tag || '').includes('app:adom/nb-bridge'));+  const sid = mine && (mine.sid || mine.id || mine.session_id);+  if (!sid) return { ok: false, staged: false, reason: list.status ? 'no thread tagged app:adom/nb-bridge yet (open nb from the dock once)' : ('control API unreachable at ' + base), control: base };+  const r = await httpJson('POST', base + '/ai-threads/run-prompt', { sid, prompt, submit: false });+  return { ok: r.status >= 200 && r.status < 300, staged: r.status >= 200 && r.status < 300, sid, status: r.status, detail: r.text || r.error, control: base };+} // BUILD = hash of this very file. Version-aware reuse compared the VERSION string alone and happily // kept serving a stale process whose code had changed under the same version (2026-08-23) - the // dock guide's version-blind trap at one finer grain. Reuse now requires version AND build to match.@@ -113,6 +163,7 @@ function page() {   return `<!doctype html><html lang="en"><head><meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>NB Dashboard</title>+<link rel="icon" href="icon.svg" type="image/svg+xml"> <style>   :root { --bg:#0b1117; --card:#121a23; --edge:#1e2a36; --ink:#e8eef4; --dim:#8aa0b4; --teal:#00cec2; --amber:#f3b23e; --red:#ef5b5b; }   * { box-sizing:border-box; margin:0; }@@ -145,6 +196,16 @@ function page() {   .mini { background:transparent; border:1px solid var(--teal); color:var(--teal); border-radius:6px; padding:2px 10px; font-size:12px; cursor:pointer; }   .mini:hover { background:rgba(0,206,194,.12); }   tr.off td { color:var(--dim); opacity:.75; }+  .say { margin:0 0 18px; border:1px solid var(--edge); border-radius:12px; background:linear-gradient(180deg,#121a23,#0f161e); padding:14px 16px 12px; }+  .say-head { font-size:11px; text-transform:uppercase; letter-spacing:.14em; color:var(--teal); margin-bottom:10px; }+  .say-row { display:flex; flex-wrap:wrap; gap:8px; }+  .chip { display:inline-flex; align-items:center; gap:0; border:1px solid var(--edge); border-radius:999px; overflow:hidden; background:#0a0f14; }+  .chip .ph { padding:7px 12px; font:13px/1.2 ui-monospace,Consolas,monospace; color:var(--ink); cursor:pointer; }+  .chip .ph b { color:var(--teal); font-weight:700; }+  .chip .ph:hover { background:rgba(0,206,194,.08); }+  .chip .try { padding:7px 11px; font-size:12px; font-weight:700; color:#04211f; background:var(--teal); border:0; cursor:pointer; }+  .chip .try:hover { filter:brightness(1.08); }+  .say-note { margin-top:9px; font-size:12px; color:var(--dim); } .say-note em { color:var(--ink); font-style:normal; }   .explain { margin-top:12px; border:1px solid rgba(0,206,194,.35); border-radius:10px; padding:12px 14px; background:rgba(0,206,194,.05); }   table { width:100%; border-collapse:collapse; margin-top:10px; font-size:13px; }   th { text-align:left; color:var(--dim); font-weight:500; font-size:11.5px; text-transform:uppercase; letter-spacing:.08em; padding:4px 8px; border-bottom:1px solid var(--edge); }@@ -158,6 +219,11 @@ function page() { </style></head><body> <header><span class="mark">nb</span><h1>Native Browser</h1><span class="led unknown" id="topled"></span><span class="ver">v${VERSION} ยท <span class="age" id="age"></span></span></header> <main>+  <div class="say" id="say">+    <div class="say-head">Say it to your AI</div>+    <div class="say-row" id="say-row"></div>+    <div class="say-note" id="say-note">nb is the noun. Add the browser and profile when it matters: <em>nb chrome personal</em>, <em>nb edge</em>.</div>+  </div>   <div class="cards">     <div class="card" id="card-l1">       <div class="layer">Layer 1 &middot; works out of the box</div>@@ -189,6 +255,28 @@ const UPSELL = '<p><strong>Not installed yet</strong> - and it is the single bes   + '<div class="prompt"><code id="pr">install the adom native browser extension in my chrome</code><button onclick="copyPrompt()">Copy</button></div>'   + '<p class="hintline">Paste that into any Adom AI thread to start.</p>'; function copyPrompt(){ try { navigator.clipboard.writeText(document.getElementById('pr').textContent); } catch(e){} }+function hiNoun(p){ return p.replace(/\bnb\b( chrome| edge)?( personal| work)?/g, (m)=>'<b>'+m+'</b>').replace('adom native browser extension','<b>adom native browser extension</b>'); }+async function loadSay(){+  try {+    const r = await fetch('api/say'); const d = await r.json();+    document.getElementById('say-row').innerHTML = (d.say||[]).map((p,i)=>+      '<span class="chip"><span class="ph" title="click to copy" onclick="copySay('+i+',this)">'+hiNoun(p)+'</span><button class="try" onclick="trySay('+i+',this)">Try it</button></span>').join('');+    window.__say = d.say || [];+  } catch(e){}+}+function copySay(i, el){ try { navigator.clipboard.writeText(window.__say[i]); const o=el.innerHTML; el.textContent='copied'; setTimeout(()=>{el.innerHTML=o;},900); } catch(e){} }+async function trySay(i, btn){+  const phrase = window.__say[i]; const note = document.getElementById('say-note');+  btn.textContent = '...';+  try {+    const r = await fetch('api/try', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({ phrase }) });+    const d = await r.json();+    if (d.staged) { btn.textContent = 'Staged'; note.innerHTML = 'Staged <em>"'+phrase+'"</em> into your nb AI thread, unsent - review it and press enter.'; }+    else { try { navigator.clipboard.writeText(phrase); } catch(e){} btn.textContent = 'Copied'; note.innerHTML = 'Copied <em>"'+phrase+'"</em>. Could not stage it into a thread ('+(d.reason||('status '+d.status))+'), so paste it to your AI.'; }+  } catch(e) { btn.textContent = 'Try it'; }+  setTimeout(()=>{ btn.textContent='Try it'; }, 2500);+}+loadSay(); function copyPrompt2(btn){ try { navigator.clipboard.writeText(document.getElementById('pr2').textContent); btn.textContent = 'Copied!'; } catch(e){} } function installPrompt(i){   const r = (window.__matrix||[])[i]; if (!r) return;@@ -271,6 +359,24 @@ function startServer() {       res.end(JSON.stringify({ state: cache.state, version: VERSION, build: BUILD, facets: cache.facets, targets: cache.targets, matrix: cache.matrix || [], cacheAgeMs: Date.now() - cache.at }));       return;     }+    if (u.startsWith('/icon.svg')) {+      try { const svg = fs.readFileSync(path.join(__dirname, '..', 'docs', 'img', 'dock-nb.svg')); res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'max-age=3600' }); res.end(svg); }+      catch { res.writeHead(404); res.end(); }+      return;+    }+    if (u.startsWith('/api/say')) {+      res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store', ...CORS });+      res.end(JSON.stringify({ say: sayPhrases() }));+      return;+    }+    if (u.startsWith('/api/try') && req.method === 'POST') {+      let b = ''; req.on('data', (c) => (b += c)); req.on('end', async () => {+        let phrase = ''; try { phrase = String(JSON.parse(b).phrase || ''); } catch {}+        const out = phrase ? await tryPhrase(phrase) : { ok: false, reason: 'phrase required' };+        res.writeHead(200, { 'Content-Type': 'application/json', ...CORS }); res.end(JSON.stringify(out));+      });+      return;+    }     if (u.startsWith('/api/shutdown') && req.method === 'POST') {       res.writeHead(200, { 'Content-Type': 'application/json', ...CORS });       res.end(JSON.stringify({ ok: true, bye: VERSION }));
dockbar.json+8−1
@@ -1,8 +1,15 @@ {   "dock_manifest": "1.2.0",+  "say": [+    "open digikey in nb",+    "open mouser in nb chrome personal",+    "open my wiki page in nb edge",+    "log into autodesk in nb",+    "install the adom native browser extension in my chrome"+  ],   "webview": {     "title": "NB Dashboard",-    "favicon": "extension/icons/icon128.png"+    "favicon": "docs/img/dock-nb.svg"   },   "health": {     "url": "/api/status",
docs/img/dock-nb.svgadded+26
@@ -0,0 +1,26 @@+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="nb - Native Browser">+  <defs>+    <linearGradient id="bg" x1="0" y1="0" x2="1" y2="1">+      <stop offset="0" stop-color="#122a33"/>+      <stop offset="1" stop-color="#0b1117"/>+    </linearGradient>+    <linearGradient id="nb" x1="0" y1="0" x2="1" y2="0">+      <stop offset="0" stop-color="#00e6dc"/>+      <stop offset="0.6" stop-color="#39b8ff"/>+      <stop offset="1" stop-color="#8c6bf7"/>+    </linearGradient>+  </defs>+  <!-- tile -->+  <rect x="2" y="2" width="60" height="60" rx="13" fill="url(#bg)"/>+  <!-- browser window: the motif that says "native browser" at 16px - a frame with a tab strip -->+  <rect x="9" y="13" width="46" height="38" rx="5" fill="none" stroke="#00cec2" stroke-width="2.6"/>+  <path d="M9 23 H55" stroke="#00cec2" stroke-width="2.6"/>+  <rect x="13" y="15.5" width="12" height="5" rx="1.5" fill="#00cec2"/>+  <!-- the nb lettermark, drawn as paths so it renders identically everywhere (no font dependency) -->+  <g fill="url(#nb)" transform="translate(32 38.5) scale(1.12) translate(-32 -37)">+    <!-- n -->+    <path d="M16 45 V29 h4.6 v2.1 c1.2-1.6 3-2.5 5.2-2.5 4.2 0 6.4 2.6 6.4 7 V45 h-4.8 V36.4 c0-2.3-1-3.5-3-3.5 -2.1 0-3.6 1.4-3.6 3.8 V45 Z"/>+    <!-- b -->+    <path d="M35.5 45 V24.5 h4.8 v6.4 c1.1-1.4 2.8-2.3 4.9-2.3 4.3 0 7.3 3.3 7.3 8.3 0 5-3 8.4-7.3 8.4 -2.2 0-3.9-.9-5-2.4 V45 Z M40.2 36.9 c0 2.6 1.4 4.3 3.6 4.3 2.2 0 3.7-1.7 3.7-4.3 0-2.6-1.5-4.3-3.7-4.3 -2.2 0-3.6 1.7-3.6 4.3 Z"/>+  </g>+</svg>
package.json+2−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.8.2",+  "version": "0.9.0",   "type": "app",   "title": "NB - Native Browser Bridge",   "description": "This package installs the SKILLS into your container so your AI knows how to drive your real signed-in Chrome and Edge; Bridge loads the bridge runtime itself from the release zip.",@@ -20,6 +20,7 @@     "skills/reading-google-messages/SKILL.md",     "skills/uninstalling-the-extension/SKILL.md",     "bin/nb-dashboard",+    "docs/img/dock-nb.svg",     "dockbar.json",     "install.sh",     "uninstall.sh",
page.json+65−6
@@ -16,10 +16,10 @@     "chrome",     "edge",     "nbrowser",-    "ai-agent",-    "developer-tools",-    "vibe-coding",-    "native-messaging"+    "pup",+    "native-messaging",+    "credentials",+    "chip-fetcher"   ],   "status": "live",   "discovery_triggers": [@@ -70,5 +70,64 @@   },   "visibility": "public",   "brief": "Your real browser, driven by AI.",-  "discovery_pitch": "nbe (the Adom Native Browser Extension, layer 2 of nb) drives the user's REAL signed-in Chrome/Edge via nbrowser_* verbs: use it for LOGGING IN, filling forms, and downloading behind auth with their OWN saved passwords - pup cannot (it is a fresh Chrome-for-Testing with no logins). Next moves: run 'adom-bridge bridge_list' to check it is installed; if not, OFFER to install it (see the installing-the-extension skill) - never make the user do it. If this container lacks the skills, 'adom-wiki pkg install adom/nb-bridge'. Check readiness with 'adom-bridge nbrowser_readiness' (state:needs-extension means offer the install)."-}+  "discovery_pitch": "nbe (the Adom Native Browser Extension, layer 2 of nb) drives the user's REAL signed-in Chrome/Edge via nbrowser_* verbs: use it for LOGGING IN, filling forms, and downloading behind auth with their OWN saved passwords - pup cannot (it is a fresh Chrome-for-Testing with no logins). Next moves: run 'adom-bridge bridge_list' to check it is installed; if not, OFFER to install it (see the installing-the-extension skill) - never make the user do it. If this container lacks the skills, 'adom-wiki pkg install adom/nb-bridge'. Check readiness with 'adom-bridge nbrowser_readiness' (state:needs-extension means offer the install).",+  "version": "0.9.0",+  "type": "app",+  "description": "This package installs the SKILLS into your container so your AI knows how to drive your real signed-in Chrome and Edge; Bridge loads the bridge runtime itself from the release zip.",+  "install": {+    "binary_name": "nb-bridge"+  },+  "files": [+    "screenshots/hero.png",+    "skills/adom-native-browser/SKILL.md",+    "skills/driving-sites/SKILL.md",+    "skills/driving-the-desktop/SKILL.md",+    "skills/driving-the-extension/SKILL.md",+    "skills/driving-the-extension-demo/SKILL.md",+    "skills/installing-the-extension/SKILL.md",+    "skills/native-browser-recording/SKILL.md",+    "skills/native-browser-safety/SKILL.md",+    "skills/reading-google-messages/SKILL.md",+    "skills/uninstalling-the-extension/SKILL.md",+    "bin/nb-dashboard",+    "docs/img/dock-nb.svg",+    "dockbar.json",+    "install.sh",+    "uninstall.sh",+    "package.json"+  ],+  "scripts": {+    "install": "./install.sh",+    "uninstall": "./uninstall.sh"+  },+  "dependencies": {+    "adom/adom-bridge": "^2.0.3"+  },+  "sample_prompts": [+    {+      "label": "Open in my browser",+      "prompt": "open the wiki in my real chrome work profile"+    },+    {+      "label": "Log in for me",+      "prompt": "log into digikey for me using my saved password"+    },+    {+      "label": "Form behind a login",+      "prompt": "fill out this form on the site I'm already signed into"+    },+    {+      "label": "Download behind auth",+      "prompt": "download that datasheet, it's behind a login"+    },+    {+      "label": "Install the extension",+      "prompt": "install the adom native browser extension in my chrome"+    }+  ],+  "org": "adom",+  "author": {+    "name": "John Lauer",+    "email": "[email protected]"+  }+}\ No newline at end of file