public/index.html
Rama main · réplica de lectura del repositorio original.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Ghost Browser</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap">
<link rel="stylesheet" href="css/console.css">
<link rel="stylesheet" href="css/marketplace.css">
<link rel="stylesheet" href="css/automation.css">
</head>
<body>
<!-- ═══ GATE — sign-in screen ═══════════════════════════════════════════════════════════════════ -->
<div id="gate">
<div class="gate-split">
<!-- Left: brand story -->
<div class="gate-brand">
<div class="gate-brand-inner">
<div class="gate-logo">
<span class="gate-logo-mark">G</span>
<span class="gate-logo-name">Ghost<b>Browser</b></span>
</div>
<p class="gate-tagline">A logged-in browser behind a clean HTTP API. Drop in your AI, your model, your keys.</p>
<ul class="gate-features">
<li>Isolated profiles per platform — cookies kept between runs</li>
<li>Set-of-Mark numbered boxes the agent clicks by index</li>
<li>Dispatch jobs, poll results, approve what it drafts</li>
<li>Automation flows that wire roles and profiles together</li>
</ul>
</div>
</div>
<!-- Right: sign-in form -->
<div class="gate-form-wrap">
<div class="gate-form-inner">
<div class="gate-form-header">
<h1>Sign in</h1>
<p id="gateSub">Secure access to your browsing agent</p>
</div>
<form id="gateForm">
<div class="field">
<label for="u">Username or Email</label>
<input id="u" type="text" autocomplete="username" required placeholder="you@example.com">
</div>
<div class="field">
<label for="p">Password</label>
<input id="p" type="password" autocomplete="current-password" required placeholder="••••••••">
</div>
<button id="gateGo" type="submit" class="btn primary full">Continue →</button>
</form>
<div class="msg" id="gateMsg"></div>
<div class="gate-recovery">
<a href="#" id="forgot" class="gate-recovery-link">Forgotten the password?</a>
<div id="resetBox" class="hide">
<p class="gate-recovery-info">
Paste the API key (in the app environment as <code>API_KEYS</code>) to clear the account and start again. Browser profiles are untouched.
</p>
<div class="gate-recovery-row">
<input id="resetKey" placeholder="API key" class="gate-recovery-input">
<button id="resetGo" class="btn ghost sm">Clear account</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- ═══ DASHBOARD — accounts landing screen ══════════════════════════════════════════════════════
Shown after login. The sidebar nav switches between views; all views live inside #dashBody.
Tokens and rules are scoped under #dash so they cannot reach #gate or #app. -->
<div id="dash" class="hide">
<div class="dash-layout">
<!-- Off-canvas wrapper (gets .draw on mobile) + scrim -->
<div class="dwrap">
<div id="dashScrim"></div>
<aside class="dash-sidebar">
<!-- Brand -->
<div class="dash-brand">
<span class="dash-brand-mark">G</span>
<div class="dash-brand-text">
<span class="dash-brand-name">GhostBrowser</span>
<span class="dash-brand-sub">control plane</span>
</div>
</div>
<!-- Navigation -->
<nav class="dash-nav" role="navigation" aria-label="Main navigation">
<button class="dash-nav-item on" data-view="accounts">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="1" y="1" width="6" height="6" rx=".75"/><rect x="9" y="1" width="6" height="6" rx=".75"/><rect x="1" y="9" width="6" height="6" rx=".75"/><rect x="9" y="9" width="6" height="6" rx=".75"/></svg>
<span>Accounts</span>
</button>
<button class="dash-nav-item" data-view="activity">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><polyline points="1,8 4,8 5.5,3 7.5,13 9,8 11,8 12,5.5 13,8 15,8"/></svg>
<span>Activity</span>
<span class="dash-badge hide" id="dashLiveBadge">0</span>
</button>
<button class="dash-nav-item" data-view="roles">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><circle cx="6" cy="5" r="3"/><path d="M1 15c0-2.8 2.2-5 5-5"/><circle cx="12" cy="7" r="2.5"/><path d="M10 15c0-2.2 1-3.8 2-4.5"/></svg>
<span>Roles</span>
</button>
<button class="dash-nav-item" data-view="automation">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="5" y="5" width="6" height="6" rx=".5"/><line x1="8" y1="1" x2="8" y2="5"/><line x1="8" y1="11" x2="8" y2="15"/><line x1="1" y1="8" x2="5" y2="8"/><line x1="11" y1="8" x2="15" y2="8"/><line x1="5" y1="3.5" x2="5" y2="5"/><line x1="11" y1="3.5" x2="11" y2="5"/><line x1="5" y1="11" x2="5" y2="12.5"/><line x1="11" y1="11" x2="11" y2="12.5"/></svg>
<span>Automation</span>
</button>
<a class="dash-nav-item" href="/hub" target="_blank" rel="noopener" title="Device Hub — connected phone/laptop nodes">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><rect x="1" y="3" width="10" height="7" rx="1"/><line x1="4" y1="13" x2="8" y2="13"/><rect x="11.5" y="6" width="3.5" height="8" rx=".75"/></svg>
<span>Device Hub</span>
</a>
<div class="dash-nav-sep"></div>
<button class="dash-nav-item" data-view="platforms">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><circle cx="8" cy="8" r="7"/><path d="M1 8h14"/><path d="M8 1c-1.8 2.2-2.8 4.4-2.8 7S6.2 12.8 8 15"/><path d="M8 1c1.8 2.2 2.8 4.4 2.8 7S9.8 12.8 8 15"/></svg>
<span>Platforms</span>
</button>
<button class="dash-nav-item" data-view="files">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M1 4.5A1 1 0 0 1 2 3.5h4l2 2h6a1 1 0 0 1 1 1v7a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V4.5z"/></svg>
<span>Files</span>
</button>
<button class="dash-nav-item" data-view="history">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><circle cx="8" cy="8" r="7"/><polyline points="8,4 8,8 11,11"/></svg>
<span>History</span>
</button>
<button class="dash-nav-item" data-view="api">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><polyline points="5,4 1,8 5,12"/><polyline points="11,4 15,8 11,12"/><line x1="10" y1="2" x2="6" y2="14"/></svg>
<span>API</span>
</button>
<button class="dash-nav-item" data-view="settings">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><line x1="2.5" y1="5" x2="13.5" y2="5"/><line x1="2.5" y1="8" x2="13.5" y2="8"/><line x1="2.5" y1="11" x2="13.5" y2="11"/><circle cx="5.5" cy="5" r="1.5" fill="currentColor" stroke="none"/><circle cx="11" cy="8" r="1.5" fill="currentColor" stroke="none"/><circle cx="5.5" cy="11" r="1.5" fill="currentColor" stroke="none"/></svg>
<span>Settings</span>
</button>
<!-- Superadmin — shown only when /api/auth/state confirms superadmin -->
<button class="dash-nav-item dash-nav-item--super hide" data-view="admin">
<svg class="nav-icon" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><path d="M8 2L14 5v5c0 3.3-2.3 5.8-6 7-3.7-1.2-6-3.7-6-7V5z"/></svg>
<span>Superadmin</span>
</button>
</nav>
<!-- User footer -->
<div class="dash-sidebar-foot">
<div class="dash-user">
<div class="dash-user-dot" aria-hidden="true"></div>
<div class="dash-user-label">
<span class="dash-user-status">Signed in</span>
<span class="dash-user-device">this browser</span>
</div>
</div>
<button class="dash-signout" id="dashLogout">Sign out</button>
</div>
</aside>
</div><!-- /.dwrap -->
<!-- Main content area -->
<main class="dash-main">
<!-- Top bar -->
<header class="dash-topbar">
<button class="dash-ham" id="dashHam" aria-label="Open navigation" title="Navigation">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><line x1="2" y1="4" x2="14" y2="4"/><line x1="2" y1="8" x2="14" y2="8"/><line x1="2" y1="12" x2="14" y2="12"/></svg>
</button>
<div class="dash-topbar-title">
<h1 class="dash-header-title">Accounts<small>The logins the agent works through — one per platform</small></h1>
</div>
<div class="dash-topbar-right">
<div class="dash-kpis dash-header-stats" id="dashHeaderStats">
<div class="dash-kpi dash-stat dash-stat--connected">
<b id="dashKpiOk">0</b>
<span>Connected</span>
</div>
<div class="dash-kpi dash-stat dash-stat--signin">
<b id="dashKpiOff">0</b>
<span>Sign in</span>
</div>
<div class="dash-kpi dash-stat dash-stat--live">
<b id="dashKpiLive">0</b>
<span>Live</span>
</div>
</div>
<button class="dash-theme" id="dashTheme" title="Toggle theme" aria-label="Toggle light/dark theme">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="5"/><path d="M8 3V1M8 15v-2M3 8H1M15 8h-2M4.9 4.9l-1.4-1.4M12.5 12.5l-1.4-1.4M4.9 11.1l-1.4 1.4M12.5 3.5l-1.4 1.4"/></svg>
</button>
</div>
</header>
<!-- View container -->
<div class="dash-body" id="dashBody">
<!-- ── Accounts view (default) ──────────────────────────────────────────────── -->
<div class="dash-view dash-view--accounts" id="dashAccountsView">
<!-- Status banner (shown when accounts need sign-in) -->
<div class="dash-banner hide" id="dashBanner">
<div class="dash-banner-icon">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><circle cx="8" cy="8" r="7"/><line x1="8" y1="5" x2="8" y2="8"/><circle cx="8" cy="11" r=".5" fill="currentColor" stroke="none"/></svg>
</div>
<div class="dash-banner-body">
<div class="dash-banner-title" id="dashBannerT"></div>
<div class="dash-banner-sub" id="dashBannerS"></div>
</div>
<button class="btn sm" id="dashBannerBtn">Sign in now</button>
</div>
<!-- Platforms grid -->
<section class="dash-section">
<div class="dash-section-hd">
<h2>Your platforms</h2>
<p>Click a platform to open its profile and sign in</p>
</div>
<div class="dash-platforms-grid" id="dashPlatformsGrid"></div>
</section>
<!-- Reply desk: notifications -> drafts -> approval -->
<section class="dash-section">
<div id="replyDesk"></div>
</section>
<!-- Live activity rail -->
<section class="dash-section">
<div class="dash-section-hd">
<h2>Live now</h2>
<p>What the agent is doing right now — open it to watch or step in</p>
</div>
<div class="dash-live-rail" id="dashRail"></div>
</section>
<p class="dash-note">Each account is its own isolated profile — its own saved cookies, its own pinned exit route.</p>
</div>
<!-- ── Settings view ────────────────────────────────────────────────────────── -->
<div class="dash-view dash-view--settings hide" id="dashSettingsView">
<!-- Agent & AI -->
<div class="scard" id="ep-agent-settings">
<h3><span class="si" aria-hidden="true">🧠</span>Agent & AI</h3>
<p class="desc">The model the agent thinks with, and whether it may act on its own. Keys are stored on this browser's own volume and never sent back out — only the last characters are shown.</p>
<div class="srow">
<div class="sgrow">
<label class="slabel">Host</label>
<input class="sin" id="setHost" placeholder="https://ollama.com">
</div>
</div>
<div class="srow">
<div class="sgrow">
<label class="slabel">Model <span class="sstate" id="setModelState"></span></label>
<div style="display:flex;gap:7px">
<select class="sin" id="setModelSel" style="flex:1"></select>
<button class="sbtn" id="setModelList" title="Ask the host what it has">List</button>
</div>
<input class="sin hide" id="setModel" placeholder="model name" style="margin-top:7px">
</div>
</div>
<div class="srow">
<div class="sgrow">
<label class="slabel">API key <span class="sstate" id="setKeyState"></span></label>
<input class="sin" id="setKey" type="password" placeholder="paste a key to set or replace it">
</div>
</div>
<div class="srow">
<div class="sgrow">
<label class="slabel">Backup API key <span class="sstate" id="setKey2State"></span></label>
<input class="sin" id="setKey2" type="password" placeholder="a second account, used when the first runs out">
</div>
</div>
<label class="stog" style="margin-top:4px">
<input type="checkbox" id="setAuto">
<span>
<span class="tt">Let it act without asking me</span>
<span class="ts">Off, it drafts every comment, join and follow and waits for you. On, it sends them itself — under your name, with no undo. Leave it off until you trust what it writes.</span>
</span>
</label>
<div class="srow" style="margin-top:14px">
<div style="flex:0 1 170px">
<label class="slabel">Stop after this many steps</label>
<input class="sin" id="setSteps" type="number" min="5" max="300">
</div>
</div>
<div class="srow" style="margin-top:2px">
<button class="sbtn" id="setTest">Test</button>
<button class="sbtn primary" id="setSave">Save</button>
</div>
<div class="smsg" id="setAgentMsg"></div>
</div>
<!-- External MCP clients -->
<div class="scard" id="mcpSetup">
<h3><span class="si" aria-hidden="true">🔌</span>MCP for Codex & Claude Code</h3>
<p class="desc">Connect an external coding agent to this browser's internet tools. It uses this browser's API key, which grants full browser access. Only add it to a client you trust.</p>
<label class="slabel" for="mcpEndpoint">Server URL</label>
<div class="srow">
<input class="sin mcp-url" id="mcpEndpoint" readonly>
<button class="sbtn" id="mcpCopyUrl" type="button">Copy URL</button>
</div>
<p class="mcp-hint">Your key is under Account → Reveal API key below. Keep it out of project files and commits.</p>
<div class="mcp-client">
<h4>Codex</h4>
<p>Set <code>GHOST_BROWSER_KEY</code> in the environment that starts Codex. Add this to <code>~/.codex/config.toml</code>, then start a new session.</p>
<pre id="mcpCodexConfig" class="mcp-code"></pre>
<button class="sbtn" id="mcpCopyCodex" type="button">Copy Codex config</button>
</div>
<div class="mcp-client">
<h4>Claude Code</h4>
<p>Replace <code>YOUR_GHOST_BROWSER_KEY</code> with your key and run this command in a private terminal.</p>
<pre id="mcpClaudeCommand" class="mcp-code"></pre>
<button class="sbtn" id="mcpCopyClaude" type="button">Copy Claude command</button>
</div>
<div class="smsg" id="mcpSetupMsg" role="status" aria-live="polite"></div>
</div>
<!-- Account -->
<div class="scard">
<h3><span class="si" aria-hidden="true">🔑</span>Account</h3>
<p class="desc">This browser's API key grants everything the console can do — it is also the way back in if the password is lost. Keep it somewhere safe.</p>
<div class="srow">
<button class="sbtn" id="setKeyReveal">Reveal API key</button>
<button class="sbtn hide" id="setKeyCopy">Copy</button>
</div>
<div class="skey hide" id="setKeyBox"></div>
<hr class="shr">
<p class="desc">A password for this Ghost Browser, so you can sign in with your email even when the platform hand-off fails — on any device, in the app or here.</p>
<div class="srow">
<input id="setPw" type="password" autocomplete="new-password" placeholder="New password (10+ characters)" class="gate-recovery-input">
<button class="sbtn" id="setPwGo">Set password</button>
</div>
<div class="smsg" id="setPwMsg"></div>
<hr class="shr">
<div class="srow"><button class="sbtn bad" id="setLogout">Sign out</button></div>
</div>
<!-- Per-login exit route -->
<div class="scard">
<h3><span class="si" aria-hidden="true">🗂️</span>Per-login exit route</h3>
<p class="desc">Override the default for one login. "Always my tailnet" refuses to open while the tailnet is down rather than quietly leaving from this server. Applies on the login's next session.</p>
<div id="setProfiles"><div class="sempty">Loading logins…</div></div>
<div class="smsg" id="setProfMsg"></div>
</div>
</div><!-- /#dashSettingsView -->
<!-- ── API reference view ───────────────────────────────────────────────────── -->
<div class="dash-view dash-view--api apiv hide" id="dashApiView">
<!-- Getting started -->
<div class="scard" id="ep-start">
<h3><span class="si">🚀</span>Getting started</h3>
<p class="apilead">The Ghost Browser is a real logged-in browser behind an HTTP API. Everything the console does — open a session, drive a page, dispatch an autonomous agent job — is a call you can make yourself. This is the same interface the platform's master agent uses. All requests and responses are <b>JSON</b>.</p>
<div class="apiquick">
<a class="apichip" href="#ep-modes">Two ways to use it</a>
<a class="apichip" href="#ep-model">Your own model</a>
<a class="apichip" href="#ep-sessions">Sessions</a>
<a class="apichip" href="#ep-jobs">Agent jobs</a>
<a class="apichip" href="#ep-roles">Roles</a>
<a class="apichip" href="#ep-profiles">Profiles</a>
<a class="apichip" href="#ep-capacity">Capacity</a>
<a class="apichip" href="#ep-master">How the master uses it</a>
</div>
<hr class="shr">
<p class="apilead"><b>Base URL.</b> Every path is relative to the origin serving this console — for you right now that is <code id="apiBaseUrl">this origin</code>.</p>
<p class="apilead"><b>Authentication.</b> Programs authenticate with a bearer key. Signed in here, you can read the key straight from the API — then send it on every call as an <code>Authorization: Bearer <key></code> header.</p>
<div class="endpoint">
<div class="ep-head"><span class="method get">GET</span><code class="epath">/api/auth/key</code><span class="ep-auth">cookie</span></div>
<p class="ep-desc">Return this console's API key(s). Requires the signed-in cookie — a key cannot fetch itself. Every returned key grants everything the console can do.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Request · curl</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">curl -b cookies.txt https://your-ghost-host/api/auth/key</pre>
</div>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Response 200</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{
"keys": [
{ "key": "gb_live_9f3c2a71b0e4…", "plan": "solo", "maxConcurrent": 1 }
]
}</pre>
</div>
</div>
<p class="apilead" style="margin-top:14px">Set these once in your shell and every example below just works:</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Setup</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">BASE="https://your-ghost-host" # the origin serving this console
KEY="gb_live_9f3c2a71b0e4…" # from GET /api/auth/key</pre>
</div>
<p class="apilead">A refusal comes back as an error object with the HTTP status — for example <code>{ "error": "not your session" }</code> with <code>403</code>.</p>
</div>
<!-- Two ways to use it -->
<div class="scard" id="ep-modes">
<h3 class="apihd"><span class="si">🧠</span>Two ways to use it</h3>
<p class="apilead">There are two ways to run the Ghost Browser. Pick either — or start with the built-in agent and move to your own later. Both run on <b>your</b> model and your costs.</p>
<p class="apilead"><b>1 · Your own LLM drives it (this reference).</b> Your agent runs on your side and is the <b>brain</b>. For every step it calls the API below — open a session, read the page as numbered boxes, click / type / act. The Ghost Browser is purely the <b>hands</b>. Start at <a class="epath" href="#ep-sessions">Sessions</a>, then loop: <code>analyze</code> → decide in your LLM → <code>click</code> / <code>type</code>.</p>
<p class="apilead"><b>2 · The built-in agent, on your model.</b> Put your own LLM key in <b>Settings</b> and the agent that ships with the Ghost Browser decides <i>and</i> acts by itself. You write no code — you configure <a class="epath" href="#ep-roles">roles</a> and <a class="epath" href="#ep-jobs">agent jobs</a> / workflows as data.</p>
<hr class="shr">
<h4 class="apihd" id="ep-model"><span class="si">🔌</span>Bring your own model</h4>
<p class="apilead">Add your own key so nothing runs on someone else's account. Supported: <b>OpenRouter</b> and your own <b>Ollama</b> endpoint. Set it in Settings → Agent & AI.</p>
<div class="endpoint">
<div class="ep-head"><span class="method get">GET</span><code class="epath">/v1/agent/settings</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">The current agent configuration (provider + model), secrets redacted.</p>
</div>
<hr class="shr">
<h4 class="apihd"><span class="si">🔑</span>Signing in</h4>
<p class="apilead">Two ways in. <b>SSO from the platform</b> — sign in on the platform and open the Ghost Browser. And a <b>bearer key</b> for scripts — read it with <a class="epath" href="#ep-start">GET /api/auth/key</a> and send it as <code>Authorization: Bearer <key></code> on every call.</p>
</div>
<!-- Sessions -->
<div class="scard" id="ep-sessions">
<h3 class="apihd"><span class="si">🪟</span>Sessions</h3>
<p class="apilead">A session is one browser tab you hold. Open one, point it at a URL, read the page as numbered boxes (Set-of-Mark), and act on a box by its number.</p>
<div class="endpoint">
<div class="ep-head"><span class="method post">POST</span><code class="epath">/v1/sessions</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">Open a session. With <code>reuse</code> you get the one you already hold instead of an error; with <code>profile</code> it opens a named, cookie-keeping login.</p>
<table class="ptable"><thead><tr><th>Field</th><th>Type</th><th>Req</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="nm">profile</td><td class="ty">string</td><td class="opt">no</td><td>Named profile to open — its own saved cookies and pinned exit route.</td></tr>
<tr><td class="nm">preset</td><td class="ty">string</td><td class="opt">no</td><td>A known site preset; configures the profile and lands on its login page.</td></tr>
<tr><td class="nm">reuse</td><td class="ty">boolean</td><td class="opt">no</td><td>Return an existing session rather than failing at the limit.</td></tr>
<tr><td class="nm">takeover</td><td class="ty">boolean</td><td class="opt">no</td><td>Close a conflicting session of yours and open here instead.</td></tr>
</tbody>
</table>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Request · curl</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">curl -X POST "$BASE/v1/sessions" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "profile": "facebook", "reuse": true }'</pre>
</div>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Response 201</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{
"sessionId": "a1b2c3d4",
"profile": "facebook",
"expiresAt": 1756497200000,
"plan": "solo",
"reused": false
}</pre>
</div>
</div>
<div class="endpoint">
<div class="ep-head"><span class="method get">GET</span><code class="epath">/v1/sessions</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">List the sessions you hold, with your concurrency limit and plan.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Request · curl</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">curl "$BASE/v1/sessions" -H "Authorization: Bearer $KEY"</pre>
</div>
</div>
<div class="endpoint">
<div class="ep-head"><span class="method delete">DELETE</span><code class="epath">/v1/sessions</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">Close every session you hold. Close a single one with <code>DELETE /v1/sessions/:id</code>.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Response 200</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{ "closed": 1 }</pre>
</div>
</div>
<div class="endpoint">
<div class="ep-head"><span class="method post">POST</span><code class="epath">/v1/sessions/:id/navigate</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">Go to a URL. Only public addresses are allowed.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Request · curl</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">curl -X POST "$BASE/v1/sessions/a1b2c3d4/navigate" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "url": "https://www.facebook.com" }'</pre>
</div>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Response 200</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{ "url": "https://www.facebook.com/", "status": 200, "title": "Facebook" }</pre>
</div>
</div>
<div class="endpoint">
<div class="ep-head"><span class="method get">GET</span><code class="epath">/v1/sessions/:id/analyze</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">The page as numbered, clickable boxes — Set-of-Mark. Each element has an <code>index</code> you then click or type into.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Request · curl</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">curl "$BASE/v1/sessions/a1b2c3d4/analyze?screenshot=false" \
-H "Authorization: Bearer $KEY"</pre>
</div>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Response 200</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{
"url": "https://www.facebook.com/",
"title": "Facebook",
"elementCount": 42,
"elements": [
{ "index": 1, "tag": "input", "type": "text", "editable": true,
"placeholder": "Email or phone number", "text": "", "x": 512, "y": 288 },
{ "index": 2, "tag": "button", "role": "button", "text": "Log in", "x": 512, "y": 360 }
],
"summary": "[1] ✎ FIELD[text] — \"Email or phone number\" …\n[2] button — \"Log in\" @ (512,360)",
"screenshot": null
}</pre>
</div>
</div>
<div class="endpoint">
<div class="ep-head"><span class="method post">POST</span><code class="epath">/v1/sessions/:id/click</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">Click a box by its <code>index</code> from the last analyze, or by visible <code>text</code>.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Request · curl</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">curl -X POST "$BASE/v1/sessions/a1b2c3d4/click" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "index": 2 }'</pre>
</div>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Response 200</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{ "clicked": { "index": 2, "text": "Log in" }, "url": "https://www.facebook.com/" }</pre>
</div>
</div>
<div class="endpoint">
<div class="ep-head"><span class="method post">POST</span><code class="epath">/v1/sessions/:id/type</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">Type into a field box, human-paced. It re-reads the page if it moved (so the number still names the right field), scrolls the field on-screen first, and clears it before typing. For a <b>rich / markdown / React editor</b>, pass <code>paste: true</code>: it inserts the whole block so the editor actually registers it — plain typing into such a box lands in a hidden backing field and silently does nothing (this is what made a Bugcrowd biography or a GitHub comment look filled but submit empty). <code>submit: true</code> presses Enter after.</p>
<table class="ptable"><thead><tr><th>Field</th><th>Type</th><th>Req</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="nm">index</td><td class="ty">number</td><td class="req">yes</td><td>The field’s number from the last analyze.</td></tr>
<tr><td class="nm">text</td><td class="ty">string</td><td class="req">yes</td><td>What to type.</td></tr>
<tr><td class="nm">paste</td><td class="ty">boolean</td><td class="req">no</td><td>Insert the whole block at once, firing the input events a rich / markdown / contenteditable editor needs. Use for any editor that ignores plain typing.</td></tr>
<tr><td class="nm">clear</td><td class="ty">boolean</td><td class="req">no</td><td>Select and replace the field’s existing content first. Default true.</td></tr>
<tr><td class="nm">submit</td><td class="ty">boolean</td><td class="req">no</td><td>Press Enter after typing.</td></tr>
</tbody>
</table>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Request body — rich editor</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{ "index": 14, "text": "Security researcher focused on web and API security...", "paste": true }</pre>
</div>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Response 200</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{ "typed": 371, "into": "biography", "mode": "paste", "url": "https://bugcrowd.com/h/onboarding" }</pre>
</div>
</div>
</div>
<!-- Agent jobs -->
<div class="scard" id="ep-jobs">
<h3 class="apihd"><span class="si">🤖</span>Agent jobs</h3>
<p class="apilead">The important one. A job is "go and do this in the browser I am already logged into". You dispatch it with a <b>goal</b> and a <b>role</b> against an open session, then poll it.</p>
<div class="endpoint">
<div class="ep-head"><span class="method post">POST</span><code class="epath">/v1/agent/jobs</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">Dispatch a job. It runs in the session you name (open one first). The loop outlives the request — this returns immediately with the job to poll.</p>
<table class="ptable"><thead><tr><th>Field</th><th>Type</th><th>Req</th><th>Description</th></tr></thead>
<tbody>
<tr><td class="nm">goal</td><td class="ty">string</td><td class="req">yes</td><td>Plain-language description of what to do.</td></tr>
<tr><td class="nm">sessionId</td><td class="ty">string</td><td class="req">yes</td><td>The open session to work in.</td></tr>
<tr><td class="nm">role</td><td class="ty">string</td><td class="opt">no</td><td>The specialist to run as. Defaults to <code>general</code>.</td></tr>
<tr><td class="nm">autoApprove</td><td class="ty">boolean</td><td class="opt">no</td><td>Run this job with acting-without-asking on.</td></tr>
<tr><td class="nm">unattended</td><td class="ty">boolean</td><td class="opt">no</td><td>Nobody is watching: conclude and hand the session back.</td></tr>
</tbody>
</table>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Request · curl</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">curl -X POST "$BASE/v1/agent/jobs" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{
"sessionId": "a1b2c3d4",
"role": "reddit.demand",
"goal": "Find people in r/smallbusiness asking how to invoice EU clients"
}'</pre>
</div>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Response 200</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{
"jobId": "k7f2a9",
"status": "running",
"createdAt": 1756490500000,
"steps": [],
"leads": [],
"proposals": []
}</pre>
</div>
</div>
<div class="endpoint">
<div class="ep-head"><span class="method get">GET</span><code class="epath">/v1/agent/jobs/:id</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">Poll one job — <code>status</code>, <code>steps</code>, <code>leads</code>, and any <code>proposals</code> waiting on you.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Request · curl</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">curl "$BASE/v1/agent/jobs/k7f2a9" -H "Authorization: Bearer $KEY"</pre>
</div>
</div>
<div class="endpoint">
<div class="ep-head"><span class="method post">POST</span><code class="epath">/v1/agent/jobs/:id/proposals/:pid</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">Approve or reject one proposal. <code>edit</code> rewrites the text before it is sent.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Request · curl</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">curl -X POST "$BASE/v1/agent/jobs/k7f2a9/proposals/p-1-9f3c" \
-H "Authorization: Bearer $KEY" \
-H "Content-Type: application/json" \
-d '{ "approve": true, "edit": "We built exactly this — happy to show you." }'</pre>
</div>
</div>
<div class="endpoint">
<div class="ep-head"><span class="method post">POST</span><code class="epath">/v1/agent/jobs/:id/say</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">Say something to a running job — steer it, or nudge an idle one back to work.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Request body</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{ "text": "Only save people who mention VAT specifically." }</pre>
</div>
</div>
<div class="endpoint">
<div class="ep-head"><span class="method post">POST</span><code class="epath">/v1/agent/jobs/:id/stop</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">Stop a job. Any pending proposals are skipped and the session is handed back.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Request · curl</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">curl -X POST "$BASE/v1/agent/jobs/k7f2a9/stop" -H "Authorization: Bearer $KEY"</pre>
</div>
</div>
</div>
<!-- Roles -->
<div class="scard" id="ep-roles">
<h3 class="apihd"><span class="si">🎭</span>Roles</h3>
<p class="apilead">The specialists a job can run as. A role fixes the tools and the prompt server-side — a scout cannot post, a poster cannot invent leads.</p>
<div class="endpoint">
<div class="ep-head"><span class="method get">GET</span><code class="epath">/v1/agent/roles</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">Every role, grouped for display. Use <code>name</code> as the <code>role</code> when dispatching a job.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Response 200</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{
"roles": [
{ "name": "general", "label": "General", "description": "Everything in reach, no site assumed.", "site": null, "group": "Anything" },
{ "name": "facebook.scout", "label": "Facebook · Lead scout", "description": "Reads Facebook groups for people who need what you sell. Cannot post.", "site": "facebook", "group": "Facebook" }
]
}</pre>
</div>
</div>
</div>
<!-- Profiles -->
<div class="scard" id="ep-profiles">
<h3 class="apihd"><span class="si">🗂️</span>Profiles</h3>
<p class="apilead">The saved logins on disk — one isolated profile per platform. Open one by passing its name to <code>POST /v1/sessions</code>.</p>
<div class="endpoint">
<div class="ep-head"><span class="method get">GET</span><code class="epath">/v1/profiles</code><span class="ep-auth">bearer</span></div>
<p class="ep-desc">The profile names that exist right now.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Response 200</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{ "profiles": ["facebook", "linkedin", "reddit"] }</pre>
</div>
</div>
</div>
<!-- Capacity -->
<div class="scard" id="ep-capacity">
<h3 class="apihd"><span class="si">📊</span>Capacity & health</h3>
<p class="apilead">Two open endpoints — no key needed.</p>
<div class="endpoint">
<div class="ep-head"><span class="method get">GET</span><code class="epath">/v1/capacity</code><span class="ep-auth open">open</span></div>
<p class="ep-desc">How full the worker is. <code>accepting: false</code> means open a session later.</p>
<div class="codewrap">
<div class="codebar"><span class="codelabel">Response 200</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">{
"sessions": 2, "maxSessions": 8, "memoryPct": 41,
"draining": false, "accepting": true
}</pre>
</div>
</div>
<div class="endpoint">
<div class="ep-head"><span class="method get">GET</span><code class="epath">/healthz</code><span class="ep-auth open">open</span></div>
<p class="ep-desc">Liveness. Always <code>{ "ok": true }</code> while the process is up.</p>
</div>
</div>
<!-- How the master uses it -->
<div class="scard" id="ep-master">
<h3 class="apihd"><span class="si">🧭</span>How the master uses it</h3>
<p class="apilead">The whole loop the platform's master agent runs, end to end. Five calls: get a session, dispatch a job, poll it, approve what it drafts, read the structured result.</p>
<div class="apistep"><div class="num">1</div><div class="tx"><b>Open or reuse a session.</b> The master wants <em>a</em> session, not specifically a new one, so it passes <code>reuse</code>.</div></div>
<div class="codewrap">
<div class="codebar"><span class="codelabel">1 · session</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">SID=$(curl -s -X POST "$BASE/v1/sessions" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "profile": "linkedin", "reuse": true }' | jq -r .sessionId)</pre>
</div>
<div class="apistep"><div class="num">2</div><div class="tx"><b>Dispatch the job</b> with a role and a goal. Returns a <code>jobId</code> at once.</div></div>
<div class="codewrap">
<div class="codebar"><span class="codelabel">2 · dispatch</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">JOB=$(curl -s -X POST "$BASE/v1/agent/jobs" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d "{ \"sessionId\": \"$SID\", \"role\": \"linkedin.buyers\",
\"goal\": \"Find heads of finance at 20-100 person agencies\" }" | jq -r .jobId)</pre>
</div>
<div class="apistep"><div class="num">3</div><div class="tx"><b>Poll until it settles.</b> Keep going while <code>status</code> is <code>running</code>.</div></div>
<div class="codewrap">
<div class="codebar"><span class="codelabel">3 · poll</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">while : ; do
J=$(curl -s "$BASE/v1/agent/jobs/$JOB" -H "Authorization: Bearer $KEY")
echo "$J" | jq -r .status
[ "$(echo "$J" | jq -r .status)" = "running" ] || break
sleep 3
done</pre>
</div>
<div class="apistep"><div class="num">4</div><div class="tx"><b>Approve what it drafts.</b> Pending proposals arrive with <code>state: "pending"</code>.</div></div>
<div class="codewrap">
<div class="codebar"><span class="codelabel">4 · approve</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">curl -s -X POST "$BASE/v1/agent/jobs/$JOB/proposals/$PID" \
-H "Authorization: Bearer $KEY" -H "Content-Type: application/json" \
-d '{ "approve": true }'</pre>
</div>
<div class="apistep"><div class="num">5</div><div class="tx"><b>Read the structured result.</b> When <code>status</code> is <code>done</code>, the deliverable is in <code>leads</code>.</div></div>
<div class="codewrap">
<div class="codebar"><span class="codelabel">5 · results</span><button class="copybtn" type="button">Copy</button></div>
<pre class="code">curl -s "$BASE/v1/agent/jobs/$JOB" -H "Authorization: Bearer $KEY" \
| jq '.leads[] | { name, why, quote, url }'</pre>
</div>
</div>
</div><!-- /#dashApiView -->
<!-- ── Roles view — filled by marketplace.js ──────────────────────────────── -->
<div class="dash-view dash-view--roles rolesv hide" id="dashRolesView"></div>
<!-- ── Automation view — filled by automation.js ──────────────────────────── -->
<div class="dash-view dash-view--automation autov hide" id="dashAutoView"></div>
<!-- ── Files view — filled by files.js ──────────────────────────────────── -->
<div class="dash-view dash-view--files filesv hide" id="dashFilesView"></div>
<!-- ── Platforms view — filled by platforms.js ──────────────────────────── -->
<div class="dash-view dash-view--platforms platformsv hide" id="dashPlatformsView"></div>
<!-- ── Superadmin view ────────────────────────────────────────────────────── -->
<div class="dash-view dash-view--admin adminv hide" id="dashAdminView">
<div class="admin-tabs">
<button class="admin-tab on" data-atab="users" type="button">Users</button>
<button class="admin-tab" data-atab="flows" type="button">Flows</button>
<button class="admin-tab" data-atab="roles" type="button">Roles</button>
<button class="admin-refresh" id="adminRefresh" type="button">↻ Refresh</button>
</div>
<div class="admin-body" id="adminBody"><p class="admin-empty">Loading…</p></div>
</div>
</div><!-- /#dashBody -->
</main>
</div><!-- /.dash-layout -->
</div><!-- /#dash -->
<!-- Click-a-card detail / sign-in modal -->
<div class="dmodal" id="dashModal">
<div class="mcard">
<div class="mhd">
<div class="logo" id="dmLogo"></div>
<div><div class="t" id="dmTitle"></div><div class="s" id="dmSub"></div></div>
<button class="x" id="dmClose">✕</button>
</div>
<div class="mbody" id="dmBody"></div>
<div class="mfoot" id="dmFoot">
<button class="btn" id="addCreate">Create</button>
</div>
</div>
</div>
<!-- ═══ APP — browser console, shown after opening a session ═════════════════════════════════════
Three zones: header toolbar, URL bar, and main (canvas + side panel). The side panel is a
bottom sheet on narrow screens, dragged by the grip strip. -->
<div id="app" class="hide">
<header>
<button id="dashBack" title="Back to Accounts">
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true"><polyline points="10,3 5,8 10,13"/></svg>
<span>Accounts</span>
</button>
<div class="brand">Ghost<span>Browser</span></div>
<select id="profile" title="A named profile keeps its cookies between runs"></select>
<span class="browserlabel" title="One browser, signed into every site">
<span class="dotb"></span>Browser
</span>
<button id="open" class="go">Open</button>
<button id="close" disabled>Close</button>
<div class="overflow">
<button id="moreBtn" class="iconbtn" aria-haspopup="true" aria-label="More actions" title="More actions">⋯</button>
<div id="hdrMenu">
<button id="closeAll">Close all mine</button>
<button id="showKey">API key</button>
<button id="exitBtn" title="Where this login's traffic leaves from">Exit route…</button>
<button id="logout">Sign out</button>
</div>
</div>
<div class="cap" id="exitState" style="margin-left:auto;cursor:pointer" title="Where the login actually exits — click to change it">exit: …</div>
<div class="cap" id="cap" style="margin-left:12px">…</div>
</header>
<div class="bar">
<input class="url" id="url" placeholder="https://…" value="https://www.facebook.com">
<button id="go" disabled>Go</button>
<span title="Live stream"><span class="dot" id="wsdot"></span></span>
<button id="esc" disabled title="Send Escape — closes a dialog or dropdown. It cannot cancel a passkey prompt; use "no passkey" for that.">Esc</button>
<label style="font-size:12px;color:var(--muted);display:flex;align-items:center;gap:5px"
title="Refuse passkey prompts so a site offers a code instead">
<input type="checkbox" id="noPasskey" style="width:auto"> no passkey
</label>
<button id="kbdBtn" class="kbdbtn" title="Show or hide the keyboard">⌨</button>
<button id="marks" disabled title="Show the numbered boxes the agent works from">Agent view</button>
</div>
<!-- Exit / Tailscale panel -->
<div id="exitPanel" class="hide" style="padding:12px 14px;border-bottom:1px solid var(--line);background:var(--panel)">
<div style="display:flex;align-items:center;gap:9px;flex-wrap:wrap">
<b style="font-size:12.5px">Exit through your own device</b>
<span id="tsState" style="font:11px ui-monospace,monospace;color:var(--faint)">checking…</span>
<span style="margin-left:auto"></span>
<input id="tsKey" placeholder="Tailscale auth key (optional)" style="width:230px;font-size:12px">
<button id="tsUp" class="go">Connect</button>
<button id="tsDown">Disconnect</button>
</div>
<label style="display:flex;align-items:center;gap:8px;margin-top:10px;font-size:12.5px;cursor:pointer">
<input type="checkbox" id="tsAll" style="width:auto;margin:0">
<span>Send <b>every</b> login through the exit node</span>
</label>
<p style="font-size:11px;color:var(--faint);margin:5px 0 0;max-width:80ch">
On, this is what every login does unless it says otherwise in its own Setup. A login that
<i>insists</i> on the tailnet refuses to open while the tailnet is down, rather than quietly
leaving from this server — which is the failure nobody notices until an account is locked.
</p>
<div id="tsLogin" class="hide" style="margin-top:9px;font-size:12px;color:var(--muted)"></div>
<div id="tsNodes" style="margin-top:9px"></div>
<p style="font-size:11px;color:var(--faint);margin:9px 0 0;max-width:80ch">
Your phone must have <b>"Use as exit node"</b> switched on in its own Tailscale settings before
it appears here. Android supports it; iOS does not offer it at all.
</p>
</div>
<main>
<div class="stage">
<div class="empty" id="empty">
Open a session and go somewhere. This is a <b>live browser</b> — click, type, scroll and use
the page exactly as you would in a normal tab. Pick a named profile first and whatever you
log into stays logged in for the agent.
</div>
<canvas id="view" tabindex="0" width="1280" height="800" hidden></canvas>
<!-- Keyboard catcher: a real input so phones raise their keyboard -->
<input id="kbd" value="​" autocapitalize="off" autocorrect="off"
autocomplete="off" spellcheck="false" aria-hidden="true">
<div class="hintbar" id="hint" hidden>tap the page, then type</div>
</div>
<aside id="panel">
<!-- Grip — drag handle on narrow screens -->
<div class="grip" id="grip">
<span class="who" id="gripWho">Agent</span>
<span class="st" id="gripState"></span>
</div>
<div class="tabs">
<button class="tab on" data-tab="agent">Agent<span id="askBadge" class="badge hide">0</span></button>
<button class="tab" data-tab="els">Elements</button>
<button class="tab" data-tab="log">Log</button>
</div>
<section id="tab-agent">
<div class="chathead">
<select id="roleSel" title="What kind of work this conversation is"></select>
<div class="chathead-actions">
<button id="newChat" class="chatact" title="Start a fresh conversation">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
New
</button>
<button id="history" class="chatact" title="Earlier conversations">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3v5h5"/><path d="M3.05 13A9 9 0 1 0 6 5.3L3 8"/><path d="M12 7v5l3 2"/></svg>
History
</button>
<button id="stopJob" class="chatact danger" disabled title="Stop what it is doing">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="6" y="6" width="12" height="12" rx="2"/></svg>
Stop
</button>
</div>
<button id="agentCfg" class="hide" tabindex="-1" aria-hidden="true" title="Setup"></button>
<span class="cap hide" id="modelState">checking…</span>
</div>
<div class="roleline" id="roleLine"></div>
<div id="lfBanner" class="ev lead hidden hide"></div>
<div class="feed" id="feed"></div>
<div class="leadbar" id="leadRow" hidden>
<span class="cap" id="leadCount"></span>
<a id="csv" download>Download leads.csv</a>
</div>
<div id="composer">
<div class="composer-box">
<textarea id="sayText" rows="1" placeholder="Ask it to do something. e.g. Search Facebook groups for homeowners asking about a leaking roof, read the posts and save the good ones."></textarea>
<button id="sayBtn" class="go" title="Send">Send</button>
</div>
<span class="cap" id="convState"></span>
</div>
</section>
<section id="tab-els" hidden>
<h2>Elements the agent sees</h2>
<ol id="els"></ol>
</section>
<section id="tab-log" hidden>
<h2>Log</h2>
<div class="log" id="log"></div>
<div id="keyBox" class="kv hide" style="margin-top:10px"></div>
</section>
</aside>
</main>
<!-- Earlier conversations modal -->
<div id="histModal" class="modal hide">
<div>
<h3>Earlier conversations</h3>
<p class="cap" style="margin:0 0 12px">Pick one to reopen it in the chat.</p>
<div id="histList" class="hide"></div>
<div class="foot">
<button id="histClose" class="go">Close</button>
</div>
</div>
</div>
</div>
<!-- ═══ CONFIG MODAL — model and profile settings ═══════════════════════════════════════════════ -->
<div id="cfgModal" class="modal hide">
<div>
<h3>The model the agent thinks with</h3>
<p class="cap">Ollama Cloud, or your own Ollama. The key is stored on this browser's own volume and never leaves it.</p>
<label>Host</label>
<input id="cfgHost" placeholder="https://ollama.com">
<label>Model <span id="cfgModels" class="cap"></span></label>
<div style="display:flex;gap:6px">
<select id="cfgModelSel" style="flex:1"></select>
<button id="cfgRefresh" style="flex:0 0 auto" title="Ask the host what it has">List</button>
</div>
<input id="cfgModel" class="hide" placeholder="model name" style="margin-top:6px">
<label>API key <span id="cfgKeyState" class="cap"></span></label>
<input id="cfgKey" type="password" placeholder="paste a key to set or replace it">
<label>Backup API key <span id="cfgKey2State" class="cap"></span></label>
<input id="cfgKey2" type="password" placeholder="a second account, used when the first runs out">
<div class="cap" style="margin:-2px 0 8px">Only used when the first key reports its allowance is spent.</div>
<label style="display:flex;align-items:center;gap:7px;margin-top:12px">
<input type="checkbox" id="cfgAuto" style="width:auto">
<span>Let it act without asking me</span>
</label>
<p class="cap" style="margin:4px 0 0">Off, it drafts every comment and waits for you. On, it sends them itself — under your name, with no undo.</p>
<label>Stop after this many steps</label>
<input id="cfgSteps" type="number" min="5" max="300">
<div class="foot">
<button id="cfgTest">Test</button>
<button id="cfgSave" class="go">Save</button>
<button id="cfgClose">Close</button>
</div>
<div class="msg" id="cfgMsg"></div>
<hr style="border:0;border-top:1px solid var(--line);margin:18px 0 4px">
<label>How you write</label>
<p class="cap" style="margin:0 0 8px">Reading your own posts is what makes what it writes sound like you.</p>
<div class="row">
<button id="studyMe">Study me</button>
<button id="showMe">What it knows</button>
</div>
<label>This login <span id="siteWho2" class="cap"></span></label>
<p class="cap" style="margin:0 0 8px">Which site it is signed in to, so the agent picks the right account.</p>
<div class="row" id="siteRow2">
<input id="siteVal" placeholder="facebook.com" style="max-width:150px">
<input id="siteNote" placeholder="personal / business">
<select id="siteAs" style="max-width:120px">
<option value="">real (Linux)</option>
<option value="windows">say Windows</option>
<option value="mac">say macOS</option>
</select>
<select id="siteExit" style="max-width:170px" title="Where this login's traffic leaves from">
<option value="">exit: follow the default</option>
<option value="tailscale">always my tailnet</option>
<option value="direct">always this server</option>
</select>
<button id="siteSave">Save</button>
</div>
<p class="cap" style="margin:6px 0 0">
<a href="#" id="siteForget" style="color:var(--bad)">Delete this login</a> — its cookies and everything signed into it. Close the session first.
</p>
</div>
</div>
<!-- ═══ ME MODAL — what the agent knows about you ══════════════════════════════════════════════ -->
<div id="meModal" class="modal hide">
<div>
<h3>What the agent knows</h3>
<p class="cap">Everything it has learned, on this browser's own volume, sent only to the model you configured.</p>
<label>Where leads have come from <span id="pbCount" class="cap"></span></label>
<div id="pbList" class="pb"></div>
<p class="cap" style="margin:6px 0 0">Counted from real runs: how many posts each place returned and how many became leads.</p>
<div class="row" style="margin-top:8px"><button id="pbForget">Forget where leads are</button></div>
<hr style="border:0;border-top:1px solid var(--line);margin:16px 0 4px">
<label style="font-size:13px;color:var(--text);font-weight:600">How you write</label>
<p class="cap">Read from your own posts and replies. This is what makes a reply sound like you.</p>
<label>Name it writes as</label><input id="meName">
<label>How you write <span class="cap">- its own summary, edit freely</span></label>
<textarea id="meStyle" rows="4"></textarea>
<label>Facts</label><div id="meFacts" class="kv"></div>
<label>Things you actually wrote <span class="cap" id="meCount"></span></label>
<div id="meSamples" style="display:flex;flex-direction:column;gap:5px"></div>
<div class="foot">
<button id="meSave" class="go">Save</button>
<button id="meForget">Forget everything</button>
<button id="meClose">Close</button>
</div>
<div class="msg" id="meMsg"></div>
</div>
</div>
<script src="js/console.js"></script>
<script src="js/dashboard.js"></script>
<script src="js/mcp-setup.js"></script>
<script src="js/marketplace.js"></script>
<script src="js/automation.js"></script>
<script src="js/files.js"></script>
<script src="js/platforms.js"></script>
</body>
</html>