← Commit history

Publish 0.7.0

John Lauer ·fc7000f3e8 ·2mo ago ·parent 2267a74
6 files changed +379−7
bin/nb-dashboardadded+265
@@ -0,0 +1,265 @@+#!/usr/bin/env node+// nb-dashboard — the Dock Bar dashboard for nb (Adom Native Browser). adom/nb-bridge#30.+//+// THE POINT (John, verbatim intent): "indicate cards for layer 1 and layer 2 of nb so as to+// encourage and advertise to the user to install the adom nbe (native browser extension) cuz if+// they do their experience using adom will be greatly improved." The Layer 2 card IS the product+// here; the rest of the dashboard exists to give it a stage.+//+// Dock publisher contract (adom/hydrogen docs/dockbar.md), the three rules that break every+// first attempt, all honored here:+//   1. serve SELF-BACKGROUNDS: prints the URL, forks a detached child, exits 0.+//   2. PATH is the manifest's job (dockbar.json steps set it); this script assumes nothing.+//   3. serve is IDEMPOTENT and VERSION-AWARE: an instance already up with the same version is+//      reused (print URL, exit 0); a version-mismatched instance is told to shut down and replaced.+// Plus the two riding contracts: prints the DIRECT http://127.0.0.1:<port>/ URL (never the proxy+// form, never the name localhost), and takes ownership of $ADOM_DOCK_WEBVIEW once serving.+'use strict';+const http = require('http');+const { execFile, spawn } = require('child_process');++const VERSION = '0.7.0';           // keep in lockstep with package.json (pkg version)+const PORT = 8954;+const URL_SELF = `http://127.0.0.1:${PORT}/`;++// ---------- probes (run in the background; /api/status answers from cache in ms) ----------+const cache = {+  at: 0,+  facets: [],           // [{label, state, detail, _hint}]+  state: 'unknown',+  targets: [],          // [{name, readiness}]+  version: VERSION,+};++function cli(args, timeoutMs = 8000) {+  return new Promise((resolve) => {+    execFile('adom-bridge', args, { timeout: timeoutMs, maxBuffer: 8 * 1024 * 1024 }, (err, stdout) => {+      const out = String(stdout || '');+      try {+        let d = JSON.parse(out);+        if (typeof d.output === 'string' && d.output.trim().startsWith('{')) { try { d = { ...d, ...JSON.parse(d.output) }; } catch {} }+        resolve(d);+      } catch { resolve(err ? { _err: String((err && err.message) || 'cli failed').slice(0, 200) } : { _raw: out.slice(0, 200) }); }+    });+  });+}++async function refresh() {+  const facets = [];+  const targetRows = [];+  // Facet 1: Bridge (ab) — is the relay reachable and is any desktop connected?+  const tg = await cli(['targets']);+  const targets = (tg && tg.targets) || [];+  if (tg && tg._err && !targets.length) {+    facets.push({ label: 'Adom Bridge', state: 'fail', detail: 'adom-bridge CLI failed or is not installed', _hint: 'Install/repair the ab CLI: adom-wiki pkg install adom/adom-bridge, then adom-bridge targets.' });+  } else if (!targets.length) {+    facets.push({ label: 'Adom Bridge', state: 'fail', detail: 'no desktop connected to the relay', _hint: 'Start Adom Bridge on the user\'s desktop (it connects to the relay automatically).' });+  } else {+    facets.push({ label: 'Adom Bridge', state: 'ok', detail: `${targets.length} desktop(s): ${targets.map((t) => t.name).join(', ')}` });+  }+  // Facets 2+3: layer 1 / layer 2, judged across every connected desktop (any-ok wins).+  let l1ok = false; let l2ready = false; let l2installable = false;+  for (const t of targets.slice(0, 4)) {+    const r = await cli(['--target', t.name, '--ai-thread', 'nb-dashboard', 'nbrowser_readiness', '{}']);+    targetRows.push({ name: t.name, state: r.state || null, profiles: r.profiles || [], extensionVersions: r.extensionVersions || {}, chromeInstalled: r.chromeInstalled });+    if (r.state) l1ok = true;                       // the nb bridge answered = layer 1 verbs route+    if (r.ready === true) l2ready = true;+    if (r.state === 'needs-extension' || r.state === 'resolving') l2installable = true;+  }+  facets.push(l1ok+    ? { label: 'Layer 1 - Native Browser', state: 'ok', detail: 'extension-free opens into the user\'s real browser work now', _hint: 'nbrowser_open_os_window / nbrowser_profiles are live.' }+    : { label: 'Layer 1 - Native Browser', state: targets.length ? 'warn' : 'fail', detail: targets.length ? 'nb bridge not answering on the connected desktop(s)' : 'needs a connected desktop', _hint: 'adom-bridge refresh_bridges {"name":"native-browser"} on the desktop, then re-check nbrowser_readiness.' });+  facets.push(l2ready+    ? { label: 'Layer 2 - nbe (extension)', state: 'ok', detail: 'nbe connected - full driving is live', _hint: 'Drive with nbrowser_open_window; background screenshots, trusted clicks, logins all work.' }+    : l2installable+      ? { label: 'Layer 2 - nbe (extension)', state: 'warn', detail: 'nbe is NOT installed/connected - the user is missing the best part', _hint: 'OFFER the install (installing-the-extension skill runs it FOR the user in about a minute). This is the upsell the dashboard exists for.' }+      : { label: 'Layer 2 - nbe (extension)', state: 'fail', detail: 'no desktop to check', _hint: 'Connect Adom Bridge first.' });+  // Rollup is OURS: the dashboard is healthy when layer 1 works; a missing nbe is a warn (that is+  // the upsell state, not an outage).+  cache.facets = facets;+  cache.targets = targetRows;+  cache.state = l1ok ? (l2ready ? 'ok' : 'warn') : (targets.length ? 'warn' : 'fail');+  cache.at = Date.now();+}++// ---------- the page ----------+function esc(s) { return String(s == null ? '' : s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c])); }+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>+<style>+  :root { --bg:#0b1117; --card:#121a23; --edge:#1e2a36; --ink:#e8eef4; --dim:#8aa0b4; --teal:#00cec2; --amber:#f3b23e; --red:#ef5b5b; }+  * { box-sizing:border-box; margin:0; }+  body { background:var(--bg); color:var(--ink); font:15px/1.5 system-ui,-apple-system,'Segoe UI',sans-serif; min-height:100vh; }+  header { display:flex; align-items:center; gap:12px; padding:0 20px; height:44px; border-bottom:1px solid var(--edge); background:#0d141c; }+  .mark { font-weight:800; font-size:19px; letter-spacing:-.5px; background:linear-gradient(100deg,#00e6dc,#39b8ff 60%,#8c6bf7); -webkit-background-clip:text; background-clip:text; color:transparent; }+  header h1 { font-size:15px; font-weight:600; }+  header .ver { color:var(--dim); font-size:12px; margin-left:auto; }+  .led { width:10px; height:10px; border-radius:50%; display:inline-block; flex:none; }+  .led.ok { background:var(--teal); box-shadow:0 0 8px rgba(0,206,194,.7); }+  .led.warn { background:var(--amber); box-shadow:0 0 8px rgba(243,178,62,.6); }+  .led.fail { background:var(--red); box-shadow:0 0 8px rgba(239,91,91,.6); }+  .led.unknown { background:#3a4a5a; }+  main { max-width:980px; margin:0 auto; padding:24px 20px 40px; }+  .cards { display:grid; grid-template-columns:1fr 1fr; gap:16px; }+  @media (max-width:760px) { .cards { grid-template-columns:1fr; } }+  .card { background:var(--card); border:1px solid var(--edge); border-radius:12px; padding:18px 18px 16px; }+  .card h2 { display:flex; align-items:center; gap:10px; font-size:16px; margin-bottom:2px; }+  .layer { color:var(--dim); font-size:11px; text-transform:uppercase; letter-spacing:.12em; margin-bottom:10px; }+  .card p { color:var(--dim); font-size:13.5px; margin:6px 0; }+  .card p strong { color:var(--ink); font-weight:600; }+  .gain { display:flex; gap:8px; align-items:baseline; font-size:13.5px; color:var(--dim); margin:5px 0; }+  .gain::before { content:'+'; color:var(--teal); font-weight:700; }+  .upsell { border-color:rgba(0,206,194,.45); box-shadow:0 0 24px rgba(0,206,194,.06) inset; }+  .prompt { display:flex; gap:8px; margin-top:12px; }+  .prompt code { flex:1; background:#0a0f14; border:1px solid var(--edge); border-radius:8px; padding:9px 12px; font:12.5px/1.4 ui-monospace,Consolas,monospace; color:var(--teal); overflow-x:auto; white-space:nowrap; }+  .prompt button { background:var(--teal); color:#04211f; border:0; border-radius:8px; padding:0 14px; font-weight:700; font-size:13px; cursor:pointer; }+  .prompt button:active { transform:translateY(1px); }+  .hintline { font-size:12px; color:var(--dim); margin-top:6px; }+  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); }+  td { padding:6px 8px; border-bottom:1px solid rgba(30,42,54,.5); }+  .facets { margin-top:20px; display:flex; flex-direction:column; gap:8px; }+  .facet { display:flex; align-items:center; gap:10px; background:var(--card); border:1px solid var(--edge); border-radius:10px; padding:10px 14px; font-size:13.5px; }+  .facet .d { color:var(--dim); margin-left:auto; text-align:right; font-size:12.5px; }+  footer { text-align:center; color:var(--dim); font-size:12px; padding:18px; }+  footer a { color:var(--teal); text-decoration:none; }+  .age { font-size:11px; color:var(--dim); }+</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="cards">+    <div class="card" id="card-l1">+      <div class="layer">Layer 1 &middot; works out of the box</div>+      <h2><span class="led unknown" id="led-l1"></span>Native Browser</h2>+      <p>Your AI opens pages in <strong>your real, signed-in browser</strong> - nothing to install. Real Chrome and Edge windows, your profiles, your logins.</p>+      <div class="gain">open a URL in the right profile (work vs personal)</div>+      <div class="gain">background by default: your focus is never stolen</div>+      <div class="gain">perfect for OAuth consent: you just click Approve</div>+      <p class="hintline" id="l1-detail"></p>+    </div>+    <div class="card upsell" id="card-l2">+      <div class="layer">Layer 2 &middot; the good stuff</div>+      <h2><span class="led unknown" id="led-l2"></span>nbe &mdash; the Native Browser Extension</h2>+      <div id="l2-body"></div>+    </div>+  </div>+  <div class="facets" id="facets"></div>+  <div id="profiles"></div>+</main>+<footer><a href="https://wiki.adom.inc/adom/nb-bridge" target="_blank" rel="noopener">adom/nb-bridge on the wiki</a></footer>+<script>+const UPSELL = '<p><strong>Not installed yet</strong> - and it is the single best upgrade to your Adom experience. With the nbe your AI can:</p>'+  + '<div class="gain">log into sites with your own saved passwords (pup never can)</div>'+  + '<div class="gain">click and type with trusted, human-grade input - on a background window</div>'+  + '<div class="gain">fill forms, download behind logins, and clean up after itself</div>'+  + '<div class="gain">screenshot pages in the background while you keep working</div>'+  + '<div class="gain">show a gliding cursor that narrates what it is doing</div>'+  + '<p>The install takes about a minute and <strong>your AI runs it for you</strong>:</p>'+  + '<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 to your AI. It drives the whole install; you approve the one folder picker.</p>';+function copyPrompt(){ try { navigator.clipboard.writeText(document.getElementById('pr').textContent); } catch(e){} }+function led(el, s){ el.className = 'led ' + (s||'unknown'); }+async function tick(){+  try {+    const r = await fetch('/api/status'); const d = await r.json();+    led(document.getElementById('topled'), d.state);+    document.getElementById('age').textContent = d.cacheAgeMs < 60000 ? 'live' : 'stale';+    const f = {}; for (const x of d.facets) f[x.label] = x;+    const l1 = f['Layer 1 - Native Browser'] || {}; const l2 = f['Layer 2 - nbe (extension)'] || {};+    led(document.getElementById('led-l1'), l1.state); led(document.getElementById('led-l2'), l2.state);+    document.getElementById('l1-detail').textContent = l1.detail || '';+    const body = document.getElementById('l2-body');+    if (l2.state === 'ok') {+      let rows = '';+      for (const t of (d.targets||[])) for (const [p,v] of Object.entries(t.extensionVersions||{}))+        rows += '<tr><td>'+t.name+'</td><td>'+p+'</td><td>v'+v+'</td><td><span class="led ok"></span></td></tr>';+      body.innerHTML = '<p><strong>Connected.</strong> Full driving is live: trusted clicks, logins, background capture, downloads behind auth.</p>'+        + (rows ? '<table><tr><th>Desktop</th><th>Profile</th><th>nbe</th><th></th></tr>'+rows+'</table>' : '');+    } else { body.innerHTML = UPSELL; }+    document.getElementById('facets').innerHTML = d.facets.map(x =>+      '<div class="facet"><span class="led '+(x.state||'unknown')+'"></span>'+x.label+'<span class="d">'+(x.detail||'')+'</span></div>').join('');+  } catch(e) { led(document.getElementById('topled'), 'unknown'); }+}+tick(); setInterval(tick, 5000);+</script>+</body></html>`;+}++// ---------- server ----------+function startServer() {+  const srv = http.createServer((req, res) => {+    const u = String(req.url || '/');+    if (u.startsWith('/api/status')) {+      res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });+      res.end(JSON.stringify({ state: cache.state, version: VERSION, facets: cache.facets, targets: cache.targets, cacheAgeMs: Date.now() - cache.at }));+      return;+    }+    if (u.startsWith('/api/shutdown') && req.method === 'POST') {+      res.writeHead(200, { 'Content-Type': 'application/json' });+      res.end(JSON.stringify({ ok: true, bye: VERSION }));+      setTimeout(() => process.exit(0), 150);+      return;+    }+    res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' });+    res.end(page());+  });+  srv.listen(PORT, '127.0.0.1', () => {+    // WEBVIEW OWNERSHIP: the dock told us which tab it pre-opened; take it over. Skip when unset.+    const tab = process.env.ADOM_DOCK_WEBVIEW;+    if (tab) {+      execFile('adom-cli', ['hydrogen', 'webview', 'open-or-refresh', '--name', tab, '--url', URL_SELF], { timeout: 15000 }, () => {});+    }+    refresh().catch(() => {});+    setInterval(() => refresh().catch(() => {}), 12000);+  });+  srv.on('error', (e) => { process.stderr.write(`nb-dashboard: ${e.message}\n`); process.exit(1); });+}++function probeExisting() {+  return new Promise((resolve) => {+    const req = http.get({ host: '127.0.0.1', port: PORT, path: '/api/status', timeout: 1500 }, (res) => {+      let b = ''; res.on('data', (c) => (b += c)); res.on('end', () => { try { resolve(JSON.parse(b)); } catch { resolve(null); } });+    });+    req.on('error', () => resolve(null));+    req.on('timeout', () => { req.destroy(); resolve(null); });+  });+}++async function serve(printUrl) {+  const existing = await probeExisting();+  if (existing && existing.version === VERSION) {+    if (printUrl) process.stdout.write(URL_SELF + '\n');+    process.exit(0);+  }+  if (existing) {+    // Version-blind reuse once pinned a bridge's users to a stale server - restart instead.+    await new Promise((resolve) => {+      const req = http.request({ host: '127.0.0.1', port: PORT, path: '/api/shutdown', method: 'POST', timeout: 2000 }, () => resolve());+      req.on('error', () => resolve()); req.on('timeout', () => { req.destroy(); resolve(); }); req.end();+    });+    await new Promise((r) => setTimeout(r, 500));+  }+  // Self-background: fork a detached child that runs the real server, print the URL, exit 0.+  const child = spawn(process.execPath, [__filename, '__run'], { detached: true, stdio: 'ignore', env: process.env });+  child.unref();+  // Wait for it to answer so the dock's readiness probe cannot race a slow start.+  for (let i = 0; i < 40; i++) {+    await new Promise((r) => setTimeout(r, 250));+    const up = await probeExisting();+    if (up && up.version === VERSION) { if (printUrl) process.stdout.write(URL_SELF + '\n'); process.exit(0); }+  }+  process.stderr.write('nb-dashboard: server did not come up\n');+  process.exit(1);+}++const cmd = process.argv[2] || 'serve';+if (cmd === '__run') startServer();+else if (cmd === 'serve') serve(process.argv.includes('--print-url'));+else if (cmd === 'status') probeExisting().then((d) => { console.log(JSON.stringify(d || { down: true }, null, 2)); process.exit(d ? 0 : 1); });+else if (cmd === 'stop') {+  const req = http.request({ host: '127.0.0.1', port: PORT, path: '/api/shutdown', method: 'POST' }, () => { console.log('stopped'); process.exit(0); });+  req.on('error', () => { console.log('not running'); process.exit(0); }); req.end();+} else if (cmd === '--version' || cmd === 'version') { console.log(VERSION); process.exit(0); }+else { console.log('usage: nb-dashboard serve [--print-url] | status | stop | version'); process.exit(1); }
dockbar.jsonadded+38
@@ -0,0 +1,38 @@+{+  "dock_manifest": "1.2.0",+  "webview": {+    "title": "NB Dashboard",+    "favicon": "extension/icons/icon128.png"+  },+  "health": {+    "url": "/api/status",+    "poll_seconds": 15,+    "timeout_ms": 2000+  },+  "launch": {+    "mode": "container_command",+    "console": "show",+    "steps": [+      {+        "label": "Installing nb (Native Browser)",+        "run": "sh -c 'PATH=\"$HOME/.local/bin:$PATH\"; adom-wiki pkg install adom/nb-bridge; command -v nb-dashboard >/dev/null 2>&1 || { adom-wiki pkg uninstall adom/nb-bridge >/dev/null 2>&1; adom-wiki pkg install adom/nb-bridge; }'",+        "skip_if": "sh -c 'PATH=\"$HOME/.local/bin:$PATH\"; command -v nb-dashboard'",+        "timeout_seconds": 180+      },+      {+        "label": "Starting the NB Dashboard",+        "run": "sh -c 'PATH=\"$HOME/.local/bin:$PATH\"; exec nb-dashboard serve --print-url'",+        "await": "url",+        "timeout_seconds": 60+      }+    ],+    "on_ready": "navigate_placeholder",+    "ai": {+      "prompt": "The user just opened the NB Dashboard from the dock. nb (Adom Native Browser) drives their REAL signed-in Chrome/Edge in two layers: Layer 1 works out of the box (open URLs in their real profiles, background by default), Layer 2 is the nbe (Native Browser Extension) which unlocks the good stuff - logins with their own saved passwords, trusted clicks and typing on background windows, form filling, downloads behind auth, background screenshots. Check the dashboard's /api/status: if the Layer 2 LED is not green, the single most valuable thing you can do is OFFER to install the nbe FOR them (the installing-the-extension skill runs it end to end in about a minute; they approve one folder picker). Never make them install it themselves, never foreground their windows, and never drive windows they opened.",+      "thread": "existing",+      "thread_fallback": "new",+      "submit": "staged",+      "typed": false+    }+  }+}
install.sh+6
@@ -17,4 +17,10 @@ for HOME_DIR in "${HOME}/.claude/skills" "${HOME}/.codex/skills"; do   done   echo "installed ${SKILLS// /, } -> $HOME_DIR" done+# The Dock Bar dashboard CLI (adom/nb-bridge#30): the dock's container_command steps expect+# `nb-dashboard` on ~/.local/bin.+mkdir -p "${HOME}/.local/bin"+chmod +x "$(pwd)/bin/nb-dashboard"+ln -sfn "$(pwd)/bin/nb-dashboard" "${HOME}/.local/bin/nb-dashboard"+echo "linked nb-dashboard -> ${HOME}/.local/bin/nb-dashboard" echo "Skills installed to Claude + Codex homes. Nothing else lands here: the bridge runtime auto-installs on your desktop via Bridge, and your AI can install the browser extension into Chrome/Edge for you - just ask it."
package.json+3−1
@@ -1,6 +1,6 @@ {   "slug": "nb-bridge",-  "version": "0.6.3",+  "version": "0.7.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.",@@ -19,6 +19,8 @@     "skills/native-browser-safety/SKILL.md",     "skills/reading-google-messages/SKILL.md",     "skills/uninstalling-the-extension/SKILL.md",+    "bin/nb-dashboard",+    "dockbar.json",     "install.sh",     "uninstall.sh",     "package.json"
page.json+64−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,63 @@   },   "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.7.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",+    "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
uninstall.sh+3
@@ -10,3 +10,6 @@ for HOME_DIR in "${HOME}/.claude/skills" "${HOME}/.codex/skills"; do     rm -rf "$HOME_DIR/$name" && echo "removed $name from $HOME_DIR"   done done+# remove the Dock Bar dashboard CLI + stop a running instance+"${HOME}/.local/bin/nb-dashboard" stop >/dev/null 2>&1 || true+rm -f "${HOME}/.local/bin/nb-dashboard" && echo "removed nb-dashboard"