@leadbay/mcp 0.21.0 → 0.21.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -523,6 +523,18 @@ import { createHash, randomBytes } from "crypto";
523
523
  import { createServer } from "http";
524
524
  import { request as httpsRequestRaw } from "https";
525
525
  import { spawn as spawn3 } from "child_process";
526
+ import { readdirSync } from "fs";
527
+ var LEADBAY_LOOPBACK_PORTS = [51789, 51790, 51791, 51792];
528
+ var BrowserOpenFailedError = class extends Error {
529
+ authorizeUrl;
530
+ constructor(authorizeUrl, cause) {
531
+ super(
532
+ `Could not open a browser automatically: ${cause?.message ?? cause}`
533
+ );
534
+ this.name = "BrowserOpenFailedError";
535
+ this.authorizeUrl = authorizeUrl;
536
+ }
537
+ };
526
538
  var STARGATE_URLS = {
527
539
  prod: "https://stargate.leadbay.app/1.0/user_info",
528
540
  staging: "https://staging.stargate.leadbay.app/1.0/user_info"
@@ -715,13 +727,24 @@ async function startLoopbackListener(opts) {
715
727
  res.end(renderHtml("You're signed in", "You can close this tab and return to the terminal."));
716
728
  resolveCallback({ code, state });
717
729
  });
718
- await new Promise((resolve, reject) => {
719
- server.once("error", reject);
720
- server.listen(0, "127.0.0.1", () => {
721
- server.off("error", reject);
730
+ const bindPort = async (port) => new Promise((resolve, reject) => {
731
+ const onErr = (e) => reject(e);
732
+ server.once("error", onErr);
733
+ server.listen(port, "127.0.0.1", () => {
734
+ server.off("error", onErr);
722
735
  resolve();
723
736
  });
724
737
  });
738
+ let bound = false;
739
+ for (const port of opts.preferredPorts ?? []) {
740
+ try {
741
+ await bindPort(port);
742
+ bound = true;
743
+ break;
744
+ } catch {
745
+ }
746
+ }
747
+ if (!bound) await bindPort(0);
725
748
  const addr = server.address();
726
749
  const redirectUri = `http://127.0.0.1:${addr.port}/callback`;
727
750
  const timer = setTimeout(() => {
@@ -729,6 +752,7 @@ async function startLoopbackListener(opts) {
729
752
  }, opts.timeoutMs);
730
753
  return {
731
754
  redirectUri,
755
+ port: addr.port,
732
756
  waitForCallback: () => callbackPromise.finally(() => {
733
757
  clearTimeout(timer);
734
758
  }),
@@ -794,28 +818,79 @@ async function exchangeCodeForToken(opts) {
794
818
  }
795
819
  return { accessToken: parsed.access_token };
796
820
  }
797
- async function openInBrowser(url) {
821
+ function browserOpenCandidates(url) {
798
822
  const platform = process.platform;
799
- let cmd;
800
- let args;
801
823
  if (platform === "darwin") {
802
- cmd = "open";
803
- args = [url];
804
- } else if (platform === "win32") {
805
- cmd = "cmd";
806
- args = ["/c", "start", '""', url];
807
- } else {
808
- cmd = "xdg-open";
809
- args = [url];
824
+ return [
825
+ { cmd: "/usr/bin/open", args: [url] },
826
+ { cmd: "open", args: [url] }
827
+ ];
810
828
  }
811
- await new Promise((resolve, reject) => {
812
- const child = spawn3(cmd, args, { stdio: "ignore", detached: true });
813
- child.on("error", reject);
814
- child.on("spawn", () => {
815
- child.unref();
816
- resolve();
817
- });
818
- });
829
+ if (platform === "win32") {
830
+ const sysRoot = process.env.SystemRoot || process.env.windir || "C:\\Windows";
831
+ const cmdExe = `${sysRoot}\\System32\\cmd.exe`;
832
+ return [
833
+ { cmd: cmdExe, args: ["/c", "start", '""', url] },
834
+ { cmd: "cmd", args: ["/c", "start", '""', url] }
835
+ ];
836
+ }
837
+ return [
838
+ { cmd: "/usr/bin/xdg-open", args: [url] },
839
+ { cmd: "/usr/local/bin/xdg-open", args: [url] },
840
+ { cmd: "xdg-open", args: [url] }
841
+ ];
842
+ }
843
+ function browserLaunchEnv(debug) {
844
+ const env = { ...process.env };
845
+ if (process.platform !== "linux") return env;
846
+ const runtimeDir = env.XDG_RUNTIME_DIR;
847
+ if (!env.WAYLAND_DISPLAY && runtimeDir) {
848
+ try {
849
+ const sock = readdirSync(runtimeDir).find((f) => /^wayland-\d+$/.test(f));
850
+ if (sock) {
851
+ env.WAYLAND_DISPLAY = sock;
852
+ debug?.(`browserLaunchEnv: injected WAYLAND_DISPLAY=${sock}`);
853
+ }
854
+ } catch {
855
+ }
856
+ }
857
+ if (!env.DISPLAY) {
858
+ try {
859
+ const x = readdirSync("/tmp/.X11-unix").map((f) => f.match(/^X(\d+)$/)?.[1]).filter((n) => !!n).sort((a, b) => Number(a) - Number(b))[0];
860
+ env.DISPLAY = x !== void 0 ? `:${x}` : ":0";
861
+ } catch {
862
+ env.DISPLAY = ":0";
863
+ }
864
+ debug?.(`browserLaunchEnv: injected DISPLAY=${env.DISPLAY}`);
865
+ }
866
+ return env;
867
+ }
868
+ async function openInBrowser(url, debug) {
869
+ const candidates = browserOpenCandidates(url);
870
+ const launchEnv = browserLaunchEnv(debug);
871
+ debug?.(
872
+ `openInBrowser: platform=${process.platform} DISPLAY=${launchEnv.DISPLAY ?? "<unset>"} WAYLAND=${launchEnv.WAYLAND_DISPLAY ?? "<unset>"} DBUS=${launchEnv.DBUS_SESSION_BUS_ADDRESS ? "set" : "<unset>"} candidates=[${candidates.map((c) => c.cmd).join(", ")}]`
873
+ );
874
+ let lastErr;
875
+ for (const { cmd, args } of candidates) {
876
+ try {
877
+ await new Promise((resolve, reject) => {
878
+ const child = spawn3(cmd, args, { stdio: "ignore", detached: true, env: launchEnv });
879
+ child.on("error", reject);
880
+ child.on("spawn", () => {
881
+ debug?.(`spawn OK: ${cmd} (pid=${child.pid})`);
882
+ child.unref();
883
+ resolve();
884
+ });
885
+ });
886
+ return;
887
+ } catch (err) {
888
+ lastErr = err;
889
+ debug?.(`spawn FAILED: ${cmd} \u2192 ${err?.code ?? err?.message ?? err}`);
890
+ }
891
+ }
892
+ debug?.(`openInBrowser: ALL candidates failed (lastErr=${lastErr?.message ?? lastErr})`);
893
+ throw lastErr ?? new Error("no browser launcher available");
819
894
  }
820
895
  async function oauthLogin(opts) {
821
896
  const log = opts.log ?? (() => {
@@ -828,22 +903,45 @@ async function oauthLogin(opts) {
828
903
  const state = base64UrlEncode(randomBytes(16));
829
904
  const pkce = generatePkce();
830
905
  log("Starting loopback listener on 127.0.0.1\u2026\n");
831
- const listener = await startLoopbackListener({ expectedState: state, timeoutMs });
906
+ const listener = await startLoopbackListener({
907
+ expectedState: state,
908
+ timeoutMs,
909
+ preferredPorts: LEADBAY_LOOPBACK_PORTS
910
+ });
832
911
  try {
833
- log(`Registering client at ${doc.registration_endpoint}\u2026
912
+ const boundPort = listener.port;
913
+ let clientId = opts.getCachedClientId?.(boundPort);
914
+ if (clientId) {
915
+ log(`Reusing cached OAuth client_id (${clientId}) for port ${boundPort} \u2014 skipping registration.
834
916
  `);
835
- const client = await registerClient(doc.registration_endpoint, {
836
- clientName: opts.clientName,
837
- redirectUri: listener.redirectUri,
838
- logoUri: opts.logoUri
839
- });
917
+ } else {
918
+ log(`Registering client at ${doc.registration_endpoint} (redirect ${listener.redirectUri})\u2026
919
+ `);
920
+ const registered = await registerClient(doc.registration_endpoint, {
921
+ clientName: opts.clientName,
922
+ redirectUri: listener.redirectUri,
923
+ // exact bound-port redirect
924
+ logoUri: opts.logoUri
925
+ });
926
+ clientId = registered.client_id;
927
+ try {
928
+ opts.onClientRegistered?.(clientId, boundPort);
929
+ } catch {
930
+ }
931
+ }
840
932
  const authorizeUrl = new URL(doc.authorization_endpoint);
841
933
  authorizeUrl.searchParams.set("response_type", "code");
842
- authorizeUrl.searchParams.set("client_id", client.client_id);
934
+ authorizeUrl.searchParams.set("client_id", clientId);
843
935
  authorizeUrl.searchParams.set("redirect_uri", listener.redirectUri);
844
936
  authorizeUrl.searchParams.set("state", state);
845
937
  authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
846
938
  authorizeUrl.searchParams.set("code_challenge_method", pkce.method);
939
+ if (opts.onAuthorizeUrl) {
940
+ try {
941
+ opts.onAuthorizeUrl(authorizeUrl.toString());
942
+ } catch {
943
+ }
944
+ }
847
945
  log(`Opening browser to authorize\u2026
848
946
  ${authorizeUrl.toString()}
849
947
  `);
@@ -855,6 +953,9 @@ async function oauthLogin(opts) {
855
953
  ${authorizeUrl.toString()}
856
954
  `
857
955
  );
956
+ if (opts.failFastOnOpenError) {
957
+ throw new BrowserOpenFailedError(authorizeUrl.toString(), err);
958
+ }
858
959
  }
859
960
  log("Waiting for authorization (5 min timeout)\u2026\n");
860
961
  const { code } = await listener.waitForCallback();
@@ -863,7 +964,7 @@ async function oauthLogin(opts) {
863
964
  tokenEndpoint: doc.token_endpoint,
864
965
  code,
865
966
  codeVerifier: pkce.verifier,
866
- clientId: client.client_id,
967
+ clientId,
867
968
  redirectUri: listener.redirectUri
868
969
  });
869
970
  return { accessToken };
@@ -873,7 +974,6 @@ async function oauthLogin(opts) {
873
974
  }
874
975
 
875
976
  // installer/installer-gui.ts
876
- var VERSION = "0.21.0";
877
977
  var PORT = Number(process.env.LEADBAY_INSTALLER_PORT ?? 0);
878
978
  var sessions = /* @__PURE__ */ new Map();
879
979
  var OAUTH_BASE_URLS = {
@@ -1164,65 +1264,109 @@ function pageUninstallHtml() {
1164
1264
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1165
1265
  <title>Leadbay MCP uninstaller</title>
1166
1266
  <style>
1167
- :root { color-scheme: light dark; --bg:#f6f7f4; --panel:#fff; --text:#1d241f; --muted:#65706a; --line:#dbe2dc; --accent:#008f7a; --accent2:#06705f; --danger:#b42318; --shadow:0 18px 45px rgba(32,45,38,.12); }
1168
- @media (prefers-color-scheme: dark) { :root { --bg:#121612; --panel:#1b211c; --text:#eef4ed; --muted:#a4afa7; --line:#303930; --shadow:0 18px 45px rgba(0,0,0,.28); } }
1267
+ :root { color-scheme: light; --bg:#fff; --card:#fff; --strong:#1d2228; --muted:#9aa0ab; --line:#e7e9ee; --accent:#0d0f0e; --danger:#d14343; --ok:#16a34a; --warn:#b06a00; }
1169
1268
  * { box-sizing:border-box; }
1170
- body { margin:0; min-height:100vh; background:var(--bg); color:var(--text); font:14px/1.45 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; display:grid; place-items:center; padding:28px; }
1171
- main { width:min(880px,100%); background:var(--panel); border:1px solid var(--line); border-radius:8px; box-shadow:var(--shadow); overflow:hidden; }
1172
- header { padding:22px 24px 16px; border-bottom:1px solid var(--line); display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }
1173
- h1 { font-size:22px; line-height:1.15; margin:0 0 6px; letter-spacing:0; }
1174
- .meta { color:var(--muted); }
1175
- .badge { border:1px solid var(--line); border-radius:999px; padding:5px 10px; color:var(--muted); white-space:nowrap; }
1176
- .steps { display:grid; grid-template-columns:repeat(2,1fr); border-bottom:1px solid var(--line); }
1177
- .step-pill { padding:12px 24px; border-right:1px solid var(--line); color:var(--muted); font-weight:700; }
1178
- .step-pill:last-child { border-right:0; }
1179
- .step-pill.active { color:var(--text); background:color-mix(in srgb,var(--danger),transparent 88%); }
1180
- section { padding:22px 24px; }
1181
- .hidden { display:none; }
1182
- .hint,.detail { color:var(--muted); }
1183
- .agents { display:grid; gap:8px; margin-top:12px; }
1184
- .agent { display:grid; grid-template-columns:auto 1fr; gap:12px; align-items:center; padding:12px; border:1px solid var(--line); border-radius:6px; }
1185
- .agent strong { display:block; }
1186
- .agent input { width:18px; min-height:18px; }
1187
- .actions { display:flex; justify-content:space-between; gap:10px; border-top:1px solid var(--line); padding:16px 24px 20px; }
1188
- .right-actions { display:flex; gap:10px; }
1189
- button { min-height:40px; border-radius:6px; border:1px solid var(--line); background:transparent; color:var(--text); padding:8px 14px; font:inherit; font-weight:700; cursor:pointer; }
1269
+ body { margin:0; min-height:100vh; background:var(--bg); color:var(--strong); font:14px/1.55 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,sans-serif; display:flex; align-items:center; justify-content:center; padding:32px 24px; -webkit-font-smoothing:antialiased; }
1270
+ main { width:min(420px,100%); }
1271
+ .steps { display:flex; gap:6px; justify-content:center; margin-bottom:18px; }
1272
+ .dot { width:24px; height:3px; border-radius:999px; background:var(--line); transition:background .2s; }
1273
+ .dot.active,.dot.done { background:var(--danger); }
1274
+ .card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:30px 26px; }
1275
+ h1 { font-size:18px; line-height:1.3; margin:0 0 6px; font-weight:700; color:var(--strong); text-align:center; }
1276
+ .sub { color:var(--muted); text-align:center; margin:0; min-height:1.55em; }
1277
+ .sub.err { color:var(--danger); }
1278
+ .hidden { display:none !important; }
1279
+ .spinner { width:26px; height:26px; margin:18px auto 0; border:3px solid var(--line); border-top-color:var(--danger); border-radius:50%; animation:spin .7s linear infinite; }
1280
+ @keyframes spin { to { transform:rotate(360deg); } }
1281
+ .agents { display:grid; gap:8px; margin-top:18px; }
1282
+ .agent { display:grid; grid-template-columns:auto 1fr; gap:11px; align-items:center; padding:11px 13px; border:1px solid var(--line); border-radius:10px; cursor:pointer; transition:border-color .15s; }
1283
+ .agent:hover { border-color:var(--muted); }
1284
+ .agent strong { display:block; font-weight:650; color:var(--strong); }
1285
+ .agent .detail { color:var(--muted); font-size:12px; word-break:break-all; }
1286
+ .agent input { width:16px; height:16px; accent-color:var(--danger); }
1287
+ .actions { display:flex; gap:12px; justify-content:center; margin-top:22px; }
1288
+ button { min-height:42px; border-radius:9px; border:1px solid var(--line); background:var(--card); color:var(--strong); padding:9px 22px; font:inherit; font-weight:650; cursor:pointer; transition:opacity .15s,transform .05s; }
1289
+ button:active { transform:translateY(1px); }
1190
1290
  button.danger { background:var(--danger); border-color:var(--danger); color:#fff; }
1191
- button:disabled { opacity:.6; cursor:wait; }
1192
- .log-panel { margin:0; background:color-mix(in srgb,var(--panel),#000 7%); border-top:1px solid var(--line); padding:16px 24px; min-height:76px; max-height:280px; overflow:auto; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:13px; }
1193
- .log-row { display:flex; gap:10px; align-items:flex-start; padding:3px 0; white-space:pre-wrap; word-break:break-word; }
1194
- .log-row::before { width:56px; flex:0 0 56px; font-weight:800; text-transform:uppercase; font-size:11px; letter-spacing:.02em; }
1195
- .log-info { color:var(--muted); } .log-info::before { content:"info"; }
1196
- .log-active { color:#c99700; } .log-active::before { content:"run"; }
1197
- .log-success { color:#19a974; } .log-success::before { content:"ok"; }
1198
- .log-error { color:var(--danger); } .log-error::before { content:"error"; }
1199
- .badge-configured { font-size:10px; font-weight:800; padding:2px 6px; border-radius:999px; vertical-align:middle; background:color-mix(in srgb,var(--danger),transparent 80%); color:var(--danger); }
1200
- .badge-absent { font-size:10px; font-weight:800; padding:2px 6px; border-radius:999px; vertical-align:middle; background:color-mix(in srgb,var(--muted),transparent 80%); color:var(--muted); }
1201
- @media (max-width:680px) { body{padding:12px;place-items:start center;} header{display:block;} .badge{display:inline-block;margin-top:12px;} .steps{grid-template-columns:1fr;} .step-pill{border-right:0;border-bottom:1px solid var(--line);} .actions{display:grid;} .right-actions{display:grid;} }
1291
+ button.danger:hover { opacity:.88; }
1292
+ button.ghost { border:0; background:transparent; color:var(--muted); padding:9px 14px; }
1293
+ button:disabled { opacity:.45; cursor:default; }
1294
+ /* result state \u2014 animated check / cross */
1295
+ .result { display:flex; flex-direction:column; align-items:center; gap:14px; padding:8px 0 4px; }
1296
+ .ring { width:64px; height:64px; }
1297
+ .ring circle { fill:none; stroke-width:3; stroke-linecap:round; stroke-dasharray:170; stroke-dashoffset:170; animation:draw .5s ease-out forwards; }
1298
+ .ring path { fill:none; stroke:#fff; stroke-width:3.5; stroke-linecap:round; stroke-linejoin:round; stroke-dasharray:48; stroke-dashoffset:48; animation:draw .35s .45s ease-out forwards; }
1299
+ .ring .disc { stroke:none; }
1300
+ .ring.ok circle:not(.disc) { stroke:var(--ok); }
1301
+ .ring.err circle:not(.disc) { stroke:var(--danger); }
1302
+ .ring.ok .disc { fill:var(--ok); animation:pop .4s ease-out; }
1303
+ .ring.err .disc { fill:var(--danger); animation:pop .4s ease-out; }
1304
+ .result-msg { font-size:15px; font-weight:700; color:var(--strong); text-align:center; }
1305
+ .result.err .result-msg { color:var(--danger); }
1306
+ .result-note { font-size:12.5px; color:var(--muted); text-align:center; margin-top:-6px; }
1307
+ @keyframes draw { to { stroke-dashoffset:0; } }
1308
+ @keyframes pop { 0%{transform:scale(.5);opacity:0;} 60%{transform:scale(1.06);} 100%{transform:scale(1);opacity:1;} }
1309
+ @media (max-width:520px) { .actions{flex-direction:column;} button{width:100%;} }
1202
1310
  </style>
1203
1311
  </head>
1204
1312
  <body>
1205
1313
  <main>
1206
- <header><div><h1>Leadbay MCP uninstaller</h1><div class="meta" id="meta">${formatInstallOsLabel()}</div></div><div class="badge">v${VERSION}</div></header>
1207
- <div class="steps"><div class="step-pill active" id="pill-1">1. Select agents</div><div class="step-pill" id="pill-2">2. Remove</div></div>
1314
+ <div class="steps"><div class="dot active" id="dot-1"></div><div class="dot" id="dot-2"></div></div>
1315
+ <div class="card">
1316
+ <h1 id="title">Remove Leadbay MCP</h1>
1317
+ <p class="sub" id="sub">Select the agents to remove Leadbay MCP from.</p>
1208
1318
 
1209
- <section id="step-1"><strong>Detected agents</strong><div class="hint">Select which agents to remove Leadbay MCP from.</div><div class="agents" id="agents"></div></section>
1210
- <section id="step-2" class="hidden"><strong>Removing</strong><div class="hint">Keep this window open until the final message appears.</div></section>
1319
+ <section id="step-1">
1320
+ <div class="spinner" id="spinner"></div>
1321
+ <div class="agents" id="agents"></div>
1322
+ </section>
1211
1323
 
1212
- <div class="actions"><button id="back" disabled>Back</button><div class="right-actions"><button id="refresh">Refresh</button><button class="danger" id="next">Remove selected</button></div></div>
1213
- <div id="log" class="log-panel"><div class="log-row log-info">Ready.</div></div>
1324
+ <section id="result" class="result hidden">
1325
+ <svg class="ring" id="ring" viewBox="0 0 64 64" aria-hidden="true">
1326
+ <circle class="disc" cx="32" cy="32" r="28"></circle>
1327
+ <circle cx="32" cy="32" r="28"></circle>
1328
+ <path id="ring-mark" d="M20 33 l8 8 l16 -18"></path>
1329
+ </svg>
1330
+ <div class="result-msg" id="result-msg"></div>
1331
+ <div class="result-note" id="result-note"></div>
1332
+ </section>
1333
+
1334
+ <div class="actions">
1335
+ <button id="back" class="ghost hidden">Back</button>
1336
+ <button id="refresh">Refresh</button>
1337
+ <button class="danger" id="next">Remove selected</button>
1338
+ </div>
1339
+ </div>
1214
1340
  </main>
1215
1341
  <script>
1216
1342
  const $ = (id) => document.getElementById(id);
1343
+ const STEPS = {
1344
+ 1: { title: "Remove Leadbay MCP", sub: "Select the agents to remove Leadbay MCP from." },
1345
+ 2: { title: "Removing", sub: "Keep this window open until it's done." },
1346
+ };
1347
+ const CHECK = "M20 33 l8 8 l16 -18";
1348
+ const CROSS = "M22 22 l20 20 M42 22 l-20 20";
1217
1349
  let step = 1;
1218
1350
  let clients = [];
1219
- function clearLog() { $("log").innerHTML = ""; }
1220
- function appendLog(level, text) { const row = document.createElement("div"); row.className = "log-row log-" + level; row.textContent = text; $("log").appendChild(row); $("log").scrollTop = $("log").scrollHeight; }
1351
+ function say(text, error = false) { const s = $("sub"); s.textContent = text; s.classList.toggle("err", !!error); }
1221
1352
  function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c])); }
1222
- function setStep(n) { step = n; [1,2].forEach((i) => { $("step-" + i).classList.toggle("hidden", i !== step); $("pill-" + i).classList.toggle("active", i === step); }); $("back").disabled = step === 1 || step === 2; $("next").classList.toggle("hidden", step === 2); $("refresh").classList.toggle("hidden", step === 2); }
1223
- function renderAgents() { const root = $("agents"); if (!clients.length) { root.innerHTML = '<div class="hint">No Leadbay MCP installation detected on this machine.</div>'; return; } root.innerHTML = clients.map((c) => '<label class="agent"><input type="checkbox" data-client="' + esc(c.id) + '" checked /><span><strong>' + esc(c.label) + '</strong><span class="detail">' + esc(c.detail) + '</span></span></label>').join(""); }
1224
- async function refresh() { clearLog(); appendLog("info", "Detecting agents..."); const res = await fetch("/api/status"); const data = await res.json(); clients = (data.clients || []).filter((c) => c.configured); renderAgents(); appendLog("info", clients.length ? "Agents detected." : "No Leadbay MCP installation detected on this machine."); }
1225
- async function doUninstall() { const selected = [...document.querySelectorAll("[data-client]:checked")].map((el) => el.dataset.client); if (!selected.length) { clearLog(); appendLog("error", "Select at least one agent."); return; } setStep(2); clearLog(); appendLog("info", "Starting removal..."); const params = new URLSearchParams({ clients: selected.join(",") }); const events = new EventSource("/api/uninstall-stream?" + params.toString()); events.onmessage = (event) => { const data = JSON.parse(event.data); appendLog(data.level === "done" ? "success" : data.level, data.message); if (data.level === "done") events.close(); }; events.onerror = () => { appendLog("error", "Uninstall stream disconnected."); events.close(); }; }
1353
+ function setStep(n) { step = n; [1,2].forEach((i) => { const dot = $("dot-" + i); dot.classList.toggle("active", i === step); dot.classList.toggle("done", i < step); }); $("step-1").classList.toggle("hidden", step !== 1); $("result").classList.add("hidden"); $("title").textContent = STEPS[step].title; say(STEPS[step].sub); $("next").classList.toggle("hidden", step === 2); $("refresh").classList.toggle("hidden", step === 2); }
1354
+ // Final completion state: animated green check / red cross + message.
1355
+ function showResult(ok, msg) {
1356
+ $("sub").classList.add("hidden");
1357
+ $("result-msg").textContent = msg;
1358
+ $("result-note").textContent = ok ? "You can close this window." : "";
1359
+ $("ring-mark").setAttribute("d", ok ? CHECK : CROSS);
1360
+ const ring = $("ring"); ring.classList.remove("ok", "err"); void ring.getBoundingClientRect();
1361
+ ring.classList.add(ok ? "ok" : "err");
1362
+ $("result").classList.toggle("err", !ok);
1363
+ $("result").classList.remove("hidden");
1364
+ $("title").textContent = ok ? "All set" : "Something went wrong";
1365
+ ["next", "back", "refresh"].forEach((id) => $(id).classList.add("hidden"));
1366
+ }
1367
+ function renderAgents() { $("spinner").classList.add("hidden"); const root = $("agents"); if (!clients.length) { root.innerHTML = '<div class="sub">No Leadbay MCP installation detected on this machine.</div>'; return; } root.innerHTML = clients.map((c) => '<label class="agent"><input type="checkbox" data-client="' + esc(c.id) + '" checked /><span><strong>' + esc(c.label) + '</strong><span class="detail">' + esc(c.detail) + '</span></span></label>').join(""); }
1368
+ async function refresh() { $("spinner").classList.remove("hidden"); $("agents").innerHTML = ""; const res = await fetch("/api/status"); const data = await res.json(); clients = (data.clients || []).filter((c) => c.configured); renderAgents(); if (!clients.length) say("No Leadbay MCP installation detected on this machine."); }
1369
+ async function doUninstall() { const selected = [...document.querySelectorAll("[data-client]:checked")].map((el) => el.dataset.client); if (!selected.length) return say("Select at least one agent.", true); setStep(2); let okCount = 0, lastError = ""; const params = new URLSearchParams({ clients: selected.join(",") }); const events = new EventSource("/api/uninstall-stream?" + params.toString()); events.onmessage = (event) => { const data = JSON.parse(event.data); if (data.level === "error") lastError = data.message; if (data.level === "success") okCount += 1; if (data.level === "done") { events.close(); const ok = okCount > 0 && !lastError; showResult(ok, ok ? "MCP successfully removed" : (lastError || "No agents were removed.")); } else { say(data.message, data.level === "error"); } }; events.onerror = () => { events.close(); showResult(false, "Uninstall stream disconnected."); }; }
1226
1370
  $("back").addEventListener("click", () => setStep(1));
1227
1371
  $("refresh").addEventListener("click", refresh);
1228
1372
  $("next").addEventListener("click", doUninstall);
@@ -1239,85 +1383,148 @@ function pageHtml() {
1239
1383
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1240
1384
  <title>Leadbay MCP installer</title>
1241
1385
  <style>
1242
- :root { color-scheme: light dark; --bg:#f6f7f4; --panel:#fff; --text:#1d241f; --muted:#65706a; --line:#dbe2dc; --accent:#008f7a; --accent2:#06705f; --danger:#b42318; --shadow:0 18px 45px rgba(32,45,38,.12); }
1243
- @media (prefers-color-scheme: dark) { :root { --bg:#121612; --panel:#1b211c; --text:#eef4ed; --muted:#a4afa7; --line:#303930; --shadow:0 18px 45px rgba(0,0,0,.28); } }
1386
+ :root { color-scheme: light; --bg:#fff; --card:#fff; --strong:#1d2228; --muted:#9aa0ab; --line:#e7e9ee; --accent:#0d0f0e; --cancel-line:#f0c8b8; --danger:#d14343; --ok:#16a34a; --warn:#b06a00; }
1244
1387
  * { box-sizing:border-box; }
1245
- body { margin:0; min-height:100vh; background:var(--bg); color:var(--text); font:14px/1.45 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; display:grid; place-items:center; padding:28px; }
1246
- main { width:min(880px,100%); background:var(--panel); border:1px solid var(--line); border-radius:8px; box-shadow:var(--shadow); overflow:hidden; }
1247
- header { padding:22px 24px 16px; border-bottom:1px solid var(--line); display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }
1248
- h1 { font-size:22px; line-height:1.15; margin:0 0 6px; letter-spacing:0; }
1249
- .meta,.hint,.detail,label span { color:var(--muted); }
1250
- .badge { border:1px solid var(--line); border-radius:999px; padding:5px 10px; color:var(--muted); white-space:nowrap; }
1251
- .steps { display:grid; grid-template-columns:repeat(4,1fr); border-bottom:1px solid var(--line); }
1252
- .step-pill { padding:12px 24px; border-right:1px solid var(--line); color:var(--muted); font-weight:700; }
1253
- .step-pill:last-child { border-right:0; }
1254
- .step-pill.active { color:var(--text); background:color-mix(in srgb,var(--accent),transparent 88%); }
1255
- section { padding:22px 24px; }
1256
- .hidden { display:none; }
1257
- .grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px; }
1258
- label { display:grid; gap:6px; font-weight:650; }
1259
- input,select { width:100%; min-height:40px; border:1px solid var(--line); border-radius:6px; background:transparent; color:var(--text); padding:8px 10px; font:inherit; }
1260
- .options { display:flex; gap:14px; flex-wrap:wrap; margin-top:14px; }
1261
- .toggle { display:inline-flex; align-items:center; gap:8px; font-weight:600; }
1262
- .toggle input { width:16px; min-height:16px; }
1263
- .setting-card { display:grid; gap:4px; max-width:360px; }
1264
- .setting-card .hint { padding-left:24px; }
1265
- .agents { display:grid; gap:8px; margin-top:12px; }
1266
- .agent { display:grid; grid-template-columns:auto 1fr; gap:12px; align-items:center; padding:12px; border:1px solid var(--line); border-radius:6px; }
1267
- .agent strong { display:block; }
1268
- .agent input { width:18px; min-height:18px; }
1269
- .actions { display:flex; justify-content:space-between; gap:10px; border-top:1px solid var(--line); padding:16px 24px 20px; }
1270
- .right-actions { display:flex; gap:10px; }
1271
- button { min-height:40px; border-radius:6px; border:1px solid var(--line); background:transparent; color:var(--text); padding:8px 14px; font:inherit; font-weight:700; cursor:pointer; }
1388
+ body { margin:0; min-height:100vh; background:var(--bg); color:var(--strong); font:14px/1.55 ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Inter,sans-serif; display:flex; align-items:center; justify-content:center; padding:32px 24px; -webkit-font-smoothing:antialiased; }
1389
+ main { width:min(420px,100%); }
1390
+ .steps { display:flex; gap:6px; justify-content:center; margin-bottom:18px; }
1391
+ .dot { width:24px; height:3px; border-radius:999px; background:var(--line); transition:background .2s; }
1392
+ .dot.active,.dot.done { background:var(--accent); }
1393
+ .card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:30px 26px; }
1394
+ h1 { font-size:18px; line-height:1.3; margin:0 0 6px; font-weight:700; color:var(--strong); text-align:center; }
1395
+ .sub { color:var(--muted); text-align:center; margin:0; min-height:1.55em; }
1396
+ .sub.err { color:var(--danger); }
1397
+ .hidden { display:none !important; }
1398
+ .spinner { width:26px; height:26px; margin:18px auto 0; border:3px solid var(--line); border-top-color:var(--accent); border-radius:50%; animation:spin .7s linear infinite; }
1399
+ @keyframes spin { to { transform:rotate(360deg); } }
1400
+ .agents { display:grid; gap:8px; margin-top:18px; }
1401
+ .agent { display:grid; grid-template-columns:auto 1fr; gap:11px; align-items:center; padding:11px 13px; border:1px solid var(--line); border-radius:10px; cursor:pointer; transition:border-color .15s; }
1402
+ .agent:hover { border-color:var(--muted); }
1403
+ .agent strong { display:block; font-weight:650; color:var(--strong); }
1404
+ .agent .detail { color:var(--muted); font-size:12px; word-break:break-all; }
1405
+ .agent input { width:16px; height:16px; accent-color:var(--accent); }
1406
+ .badge-pill { font-size:9.5px; font-weight:700; padding:1px 6px; border-radius:999px; vertical-align:middle; text-transform:uppercase; letter-spacing:.03em; }
1407
+ .badge-install { background:color-mix(in srgb,var(--ok),transparent 88%); color:var(--ok); }
1408
+ .badge-update { background:color-mix(in srgb,var(--warn),transparent 86%); color:var(--warn); }
1409
+ .actions { display:flex; gap:12px; justify-content:center; margin-top:22px; }
1410
+ button { min-height:42px; border-radius:9px; border:1px solid var(--line); background:var(--card); color:var(--strong); padding:9px 26px; font:inherit; font-weight:650; cursor:pointer; transition:opacity .15s,transform .05s; }
1411
+ button:active { transform:translateY(1px); }
1272
1412
  button.primary { background:var(--accent); border-color:var(--accent); color:#fff; }
1273
- button.primary:hover { background:var(--accent2); }
1274
- button:disabled { opacity:.6; cursor:wait; }
1275
- .log-panel { margin:0; background:color-mix(in srgb,var(--panel),#000 7%); border-top:1px solid var(--line); padding:16px 24px; min-height:76px; max-height:280px; overflow:auto; font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace; font-size:13px; }
1276
- .log-row { display:flex; gap:10px; align-items:flex-start; padding:3px 0; white-space:pre-wrap; word-break:break-word; }
1277
- .log-row::before { width:56px; flex:0 0 56px; font-weight:800; text-transform:uppercase; font-size:11px; letter-spacing:.02em; }
1278
- .log-info { color:var(--muted); }
1279
- .log-info::before { content:"info"; }
1280
- .log-active { color:#c99700; }
1281
- .log-active::before { content:"run"; }
1282
- .log-success { color:#19a974; }
1283
- .log-success::before { content:"ok"; }
1284
- .log-error { color:var(--danger); }
1285
- .log-error::before { content:"error"; }
1286
- .error { color:var(--danger); }
1287
- .badge-install,.badge-update { font-size:10px; font-weight:800; padding:2px 6px; border-radius:999px; vertical-align:middle; }
1288
- .badge-install { background:color-mix(in srgb,var(--accent),transparent 80%); color:var(--accent2); }
1289
- .badge-update { background:color-mix(in srgb,#c99700,transparent 80%); color:#a07800; }
1290
- @media (prefers-color-scheme: dark) { .badge-update { color:#f0c040; } .badge-install { color:#00c9a0; } }
1291
- @media (max-width:680px) { body{padding:12px;place-items:start center;} header{display:block;} .badge{display:inline-block;margin-top:12px;} .grid,.steps{grid-template-columns:1fr;} .step-pill{border-right:0;border-bottom:1px solid var(--line);} .actions{display:grid;} .right-actions{display:grid;} }
1413
+ button.primary:hover { opacity:.88; }
1414
+ button.cancel { border-color:var(--cancel-line); }
1415
+ button.ghost { border:0; background:transparent; color:var(--muted); padding:9px 14px; }
1416
+ button:disabled { opacity:.45; cursor:default; }
1417
+ /* result state \u2014 animated check / cross */
1418
+ .result { display:flex; flex-direction:column; align-items:center; gap:14px; padding:8px 0 4px; }
1419
+ .ring { width:64px; height:64px; }
1420
+ .ring circle { fill:none; stroke-width:3; stroke-linecap:round; stroke-dasharray:170; stroke-dashoffset:170; animation:draw .5s ease-out forwards; }
1421
+ .ring path { fill:none; stroke:#fff; stroke-width:3.5; stroke-linecap:round; stroke-linejoin:round; stroke-dasharray:48; stroke-dashoffset:48; animation:draw .35s .45s ease-out forwards; }
1422
+ .ring .disc { stroke:none; }
1423
+ .ring.ok circle:not(.disc) { stroke:var(--ok); }
1424
+ .ring.err circle:not(.disc) { stroke:var(--danger); }
1425
+ .ring.ok .disc { fill:var(--ok); animation:pop .4s ease-out; }
1426
+ .ring.err .disc { fill:var(--danger); animation:pop .4s ease-out; }
1427
+ .result-msg { font-size:15px; font-weight:700; color:var(--strong); text-align:center; }
1428
+ .result.err .result-msg { color:var(--danger); }
1429
+ .result-note { font-size:12.5px; color:var(--muted); text-align:center; margin-top:-6px; }
1430
+ @keyframes draw { to { stroke-dashoffset:0; } }
1431
+ @keyframes pop { 0%{transform:scale(.5);opacity:0;} 60%{transform:scale(1.06);} 100%{transform:scale(1);opacity:1;} }
1432
+ @media (max-width:520px) { .actions{flex-direction:column;} button{width:100%;} }
1292
1433
  </style>
1293
1434
  </head>
1294
1435
  <body>
1295
1436
  <main>
1296
- <header><div><h1>Leadbay MCP installer</h1><div class="meta" id="meta">${formatInstallOsLabel()}</div></div><div class="badge">v${VERSION}</div></header>
1297
- <div class="steps"><div class="step-pill active" id="pill-1">1. Sign in</div><div class="step-pill" id="pill-2">2. Agents</div><div class="step-pill" id="pill-3">3. Install</div></div>
1437
+ <div class="steps"><div class="dot active" id="dot-1"></div><div class="dot" id="dot-2"></div><div class="dot" id="dot-3"></div></div>
1438
+ <div class="card">
1439
+ <h1 id="title">Connect Leadbay</h1>
1440
+ <p class="sub" id="sub">Sign in to install Leadbay across your AI agents.</p>
1441
+
1442
+ <section id="step-2" class="hidden">
1443
+ <div class="spinner" id="spinner"></div>
1444
+ <div class="agents" id="agents"></div>
1445
+ </section>
1298
1446
 
1299
- <section id="step-1"><strong>Connect your Leadbay account</strong><div class="hint">This opens Leadbay in your browser. After approval, come back here to choose where to install the MCP.</div></section>
1300
- <section id="step-2" class="hidden"><strong>Detected agents</strong><div class="hint">Local agents are installed automatically when supported. ChatGPT Desktop requires manual setup with the hosted MCP URL.</div><div class="agents" id="agents"></div><div class="options"><div class="setting-card"><label class="toggle"><input id="write" type="checkbox" checked /> Write tools</label><div class="hint">Allows Leadbay actions that change data or spend credits, like import, enrich, qualify, refine audience, and log outreach.</div></div><div class="setting-card"><label class="toggle"><input id="telemetry" type="checkbox" checked /> Telemetry</label><div class="hint">Sends product usage and crash events so we can debug installs. It does not send tool arguments, lead data, or the token.</div></div></div></section>
1301
- <section id="step-3" class="hidden"><strong>Installing</strong><div class="hint">Keep this window open until the final message appears. ChatGPT Desktop setup is manual in ChatGPT Settings > Apps.</div></section>
1447
+ <section id="result" class="result hidden">
1448
+ <svg class="ring" id="ring" viewBox="0 0 64 64" aria-hidden="true">
1449
+ <circle class="disc" cx="32" cy="32" r="28"></circle>
1450
+ <circle cx="32" cy="32" r="28"></circle>
1451
+ <path id="ring-mark" d="M20 33 l8 8 l16 -18"></path>
1452
+ </svg>
1453
+ <div class="result-msg" id="result-msg"></div>
1454
+ <div class="result-note" id="result-note"></div>
1455
+ </section>
1302
1456
 
1303
- <div class="actions"><button id="back" disabled>Back</button><div class="right-actions"><button id="refresh" class="hidden">Refresh</button><button class="primary" id="next">Sign in with Leadbay</button></div></div>
1304
- <div id="log" class="log-panel"><div class="log-row log-info">Ready.</div></div>
1457
+ <div class="actions">
1458
+ <button id="back" class="cancel hidden">Back</button>
1459
+ <button id="refresh" class="ghost hidden">Refresh</button>
1460
+ <button class="primary" id="next">Sign in with Leadbay</button>
1461
+ </div>
1462
+ </div>
1305
1463
  </main>
1306
1464
  <script>
1307
1465
  const $ = (id) => document.getElementById(id);
1466
+ const STEPS = {
1467
+ 1: { title: "Connect Leadbay", sub: "Sign in to install Leadbay across your AI agents." },
1468
+ 2: { title: "Choose your agents", sub: "Pick where to install Leadbay." },
1469
+ 3: { title: "Installing", sub: "Keep this window open until it's done." },
1470
+ };
1471
+ const CHECK = "M20 33 l8 8 l16 -18";
1472
+ const CROSS = "M22 22 l20 20 M42 22 l-20 20";
1308
1473
  let step = 1;
1309
1474
  let sessionId = null;
1310
1475
  let clients = [];
1311
- function clearLog() { $("log").innerHTML = ""; }
1312
- function appendLog(level, text) { const row = document.createElement("div"); row.className = "log-row log-" + level; row.textContent = text; $("log").appendChild(row); $("log").scrollTop = $("log").scrollHeight; }
1313
- function line(text, error = false) { clearLog(); appendLog(error ? "error" : "info", text); }
1476
+ function say(text, error = false) { const s = $("sub"); s.textContent = text; s.classList.toggle("err", !!error); }
1314
1477
  function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c])); }
1315
- function setStep(next) { step = next; [1,2,3].forEach((n) => { $("step-" + n).classList.toggle("hidden", n !== step); $("pill-" + n).classList.toggle("active", n === step); }); $("back").disabled = step === 1 || step === 3; $("refresh").classList.toggle("hidden", step !== 2); $("next").classList.toggle("hidden", step === 3); $("next").textContent = step === 2 ? "Continue" : "Sign in with Leadbay"; }
1316
- function renderAgents() { const root = $("agents"); if (!clients.length) { root.innerHTML = '<div class="hint">No supported MCP client detected on this machine.</div>'; return; } root.innerHTML = clients.map((client) => { const manual = client.id === "chatgpt-desktop"; const badgeText = manual ? "manual setup" : client.configured ? "update" : "install"; const badgeClass = manual ? "badge-update" : client.configured ? "badge-update" : "badge-install"; return '<label class="agent"><input type="checkbox" data-client="' + esc(client.id) + '" checked /><span><strong>' + esc(client.label) + ' <span class="' + badgeClass + '">' + badgeText + '</span></strong><span class="detail">' + esc(client.detail) + '</span></span></label>'; }).join(""); }
1317
- async function refresh() { line("Detecting agents..."); const res = await fetch("/api/status"); const data = await res.json(); clients = data.clients || []; renderAgents(); line(clients.length ? "Agents detected." : "No supported agents detected."); }
1318
- async function doLogin() { $("next").disabled = true; line("Opening Leadbay sign-in in your browser..."); try { const res = await fetch("/api/oauth-login", { method:"POST" }); const data = await res.json(); if (!data.ok) return line(data.error || "OAuth login failed.", true); sessionId = data.sessionId; line("Signed in. Detecting installed agents..."); setStep(2); await refresh(); } finally { $("next").disabled = false; } }
1319
- async function install() { const selected = [...document.querySelectorAll("[data-client]:checked")].map((el) => el.dataset.client); if (!selected.length) return line("Select at least one agent.", true); setStep(3); clearLog(); appendLog("info", "Starting install..."); const params = new URLSearchParams({ sessionId, clients: selected.join(","), write: $("write").checked ? "1" : "0", telemetry: $("telemetry").checked ? "1" : "0" }); const events = new EventSource("/api/install-stream?" + params.toString()); events.onmessage = (event) => { const data = JSON.parse(event.data); appendLog(data.level === "done" ? "success" : data.level, data.message); if (data.level === "done") events.close(); }; events.onerror = () => { appendLog("error", "Install log stream disconnected."); events.close(); }; }
1320
- $("back").addEventListener("click", () => setStep(Math.max(1, step - 1)));
1478
+ function setStep(next) {
1479
+ step = next;
1480
+ [1,2,3].forEach((n) => { const dot = $("dot-" + n); dot.classList.toggle("active", n === step); dot.classList.toggle("done", n < step); });
1481
+ $("step-2").classList.toggle("hidden", step !== 2);
1482
+ $("result").classList.add("hidden");
1483
+ $("title").textContent = STEPS[step].title;
1484
+ say(STEPS[step].sub);
1485
+ $("back").classList.toggle("hidden", step !== 2);
1486
+ $("refresh").classList.toggle("hidden", step !== 2);
1487
+ $("next").classList.toggle("hidden", step === 3);
1488
+ $("next").textContent = step === 2 ? "Install" : "Sign in with Leadbay";
1489
+ }
1490
+ // Final completion state: animated green check / red cross + message.
1491
+ function showResult(ok, msg) {
1492
+ $("sub").classList.add("hidden");
1493
+ $("result-msg").textContent = msg;
1494
+ $("result-note").textContent = ok ? "You can close this window." : "";
1495
+ $("ring-mark").setAttribute("d", ok ? CHECK : CROSS);
1496
+ const ring = $("ring"); ring.classList.remove("ok", "err"); void ring.getBoundingClientRect();
1497
+ ring.classList.add(ok ? "ok" : "err");
1498
+ $("result").classList.toggle("err", !ok);
1499
+ $("result").classList.remove("hidden");
1500
+ $("title").textContent = ok ? "All set" : "Something went wrong";
1501
+ ["next", "back", "refresh"].forEach((id) => $(id).classList.add("hidden"));
1502
+ }
1503
+ function renderAgents() { $("spinner").classList.add("hidden"); const root = $("agents"); if (!clients.length) { root.innerHTML = '<div class="sub">No supported MCP client detected on this machine.</div>'; return; } root.innerHTML = clients.map((client) => { const manual = client.id === "chatgpt-desktop"; const badgeText = manual ? "manual" : client.configured ? "update" : "install"; const badgeClass = manual ? "badge-update" : client.configured ? "badge-update" : "badge-install"; return '<label class="agent"><input type="checkbox" data-client="' + esc(client.id) + '" checked /><span><strong>' + esc(client.label) + ' <span class="badge-pill ' + badgeClass + '">' + badgeText + '</span></strong><span class="detail">' + esc(client.detail) + '</span></span></label>'; }).join(""); }
1504
+ async function refresh() { $("spinner").classList.remove("hidden"); $("agents").innerHTML = ""; const res = await fetch("/api/status"); const data = await res.json(); clients = data.clients || []; renderAgents(); if (!clients.length) say("No supported agents detected."); }
1505
+ async function doLogin() { $("next").disabled = true; say("Opening Leadbay sign-in in your browser..."); try { const res = await fetch("/api/oauth-login", { method:"POST" }); const data = await res.json(); if (!data.ok) return say(data.error || "OAuth login failed.", true); sessionId = data.sessionId; setStep(2); await refresh(); } finally { $("next").disabled = false; } }
1506
+ async function install() {
1507
+ const selected = [...document.querySelectorAll("[data-client]:checked")].map((el) => el.dataset.client);
1508
+ if (!selected.length) return say("Select at least one agent.", true);
1509
+ setStep(3);
1510
+ let okCount = 0, lastError = "";
1511
+ const params = new URLSearchParams({ sessionId, clients: selected.join(","), write: "1", telemetry: "1" });
1512
+ const events = new EventSource("/api/install-stream?" + params.toString());
1513
+ events.onmessage = (event) => {
1514
+ const data = JSON.parse(event.data);
1515
+ if (data.level === "error") lastError = data.message;
1516
+ if (data.level === "success") okCount += 1;
1517
+ if (data.level === "done") {
1518
+ events.close();
1519
+ const ok = okCount > 0 && !lastError;
1520
+ showResult(ok, ok ? "MCP successfully installed" : (lastError || "No agents were installed."));
1521
+ } else {
1522
+ say(data.message, data.level === "error");
1523
+ }
1524
+ };
1525
+ events.onerror = () => { events.close(); showResult(false, "Install stream disconnected."); };
1526
+ }
1527
+ $("back").addEventListener("click", () => setStep(1));
1321
1528
  $("refresh").addEventListener("click", refresh);
1322
1529
  $("next").addEventListener("click", async () => { if (step === 1) await doLogin(); else await install(); });
1323
1530
  </script>