@leadbay/mcp 0.21.1 → 0.21.3

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.
@@ -550,6 +550,7 @@ import { createHash, randomBytes } from "crypto";
550
550
  import { createServer } from "http";
551
551
  import { request as httpsRequestRaw } from "https";
552
552
  import { spawn as spawn3 } from "child_process";
553
+ import { readdirSync } from "fs";
553
554
  async function inferRegionViaStargate(opts) {
554
555
  const url = STARGATE_URLS[opts.staging ? "staging" : "prod"];
555
556
  const res = await httpsCall("GET", url, { Accept: "application/json" });
@@ -720,13 +721,24 @@ async function startLoopbackListener(opts) {
720
721
  res.end(renderHtml("You're signed in", "You can close this tab and return to the terminal."));
721
722
  resolveCallback({ code, state });
722
723
  });
723
- await new Promise((resolve, reject) => {
724
- server.once("error", reject);
725
- server.listen(0, "127.0.0.1", () => {
726
- server.off("error", reject);
724
+ const bindPort = async (port) => new Promise((resolve, reject) => {
725
+ const onErr = (e) => reject(e);
726
+ server.once("error", onErr);
727
+ server.listen(port, "127.0.0.1", () => {
728
+ server.off("error", onErr);
727
729
  resolve();
728
730
  });
729
731
  });
732
+ let bound = false;
733
+ for (const port of opts.preferredPorts ?? []) {
734
+ try {
735
+ await bindPort(port);
736
+ bound = true;
737
+ break;
738
+ } catch {
739
+ }
740
+ }
741
+ if (!bound) await bindPort(0);
730
742
  const addr = server.address();
731
743
  const redirectUri = `http://127.0.0.1:${addr.port}/callback`;
732
744
  const timer = setTimeout(() => {
@@ -734,6 +746,7 @@ async function startLoopbackListener(opts) {
734
746
  }, opts.timeoutMs);
735
747
  return {
736
748
  redirectUri,
749
+ port: addr.port,
737
750
  waitForCallback: () => callbackPromise.finally(() => {
738
751
  clearTimeout(timer);
739
752
  }),
@@ -799,28 +812,79 @@ async function exchangeCodeForToken(opts) {
799
812
  }
800
813
  return { accessToken: parsed.access_token };
801
814
  }
802
- async function openInBrowser(url) {
815
+ function browserOpenCandidates(url) {
803
816
  const platform = process.platform;
804
- let cmd;
805
- let args;
806
817
  if (platform === "darwin") {
807
- cmd = "open";
808
- args = [url];
809
- } else if (platform === "win32") {
810
- cmd = "cmd";
811
- args = ["/c", "start", '""', url];
812
- } else {
813
- cmd = "xdg-open";
814
- args = [url];
818
+ return [
819
+ { cmd: "/usr/bin/open", args: [url] },
820
+ { cmd: "open", args: [url] }
821
+ ];
815
822
  }
816
- await new Promise((resolve, reject) => {
817
- const child = spawn3(cmd, args, { stdio: "ignore", detached: true });
818
- child.on("error", reject);
819
- child.on("spawn", () => {
820
- child.unref();
821
- resolve();
822
- });
823
- });
823
+ if (platform === "win32") {
824
+ const sysRoot = process.env.SystemRoot || process.env.windir || "C:\\Windows";
825
+ const cmdExe = `${sysRoot}\\System32\\cmd.exe`;
826
+ return [
827
+ { cmd: cmdExe, args: ["/c", "start", '""', url] },
828
+ { cmd: "cmd", args: ["/c", "start", '""', url] }
829
+ ];
830
+ }
831
+ return [
832
+ { cmd: "/usr/bin/xdg-open", args: [url] },
833
+ { cmd: "/usr/local/bin/xdg-open", args: [url] },
834
+ { cmd: "xdg-open", args: [url] }
835
+ ];
836
+ }
837
+ function browserLaunchEnv(debug) {
838
+ const env = { ...process.env };
839
+ if (process.platform !== "linux") return env;
840
+ const runtimeDir = env.XDG_RUNTIME_DIR;
841
+ if (!env.WAYLAND_DISPLAY && runtimeDir) {
842
+ try {
843
+ const sock = readdirSync(runtimeDir).find((f) => /^wayland-\d+$/.test(f));
844
+ if (sock) {
845
+ env.WAYLAND_DISPLAY = sock;
846
+ debug?.(`browserLaunchEnv: injected WAYLAND_DISPLAY=${sock}`);
847
+ }
848
+ } catch {
849
+ }
850
+ }
851
+ if (!env.DISPLAY) {
852
+ try {
853
+ const x = readdirSync("/tmp/.X11-unix").map((f) => f.match(/^X(\d+)$/)?.[1]).filter((n) => !!n).sort((a, b) => Number(a) - Number(b))[0];
854
+ env.DISPLAY = x !== void 0 ? `:${x}` : ":0";
855
+ } catch {
856
+ env.DISPLAY = ":0";
857
+ }
858
+ debug?.(`browserLaunchEnv: injected DISPLAY=${env.DISPLAY}`);
859
+ }
860
+ return env;
861
+ }
862
+ async function openInBrowser(url, debug) {
863
+ const candidates = browserOpenCandidates(url);
864
+ const launchEnv = browserLaunchEnv(debug);
865
+ debug?.(
866
+ `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(", ")}]`
867
+ );
868
+ let lastErr;
869
+ for (const { cmd, args } of candidates) {
870
+ try {
871
+ await new Promise((resolve, reject) => {
872
+ const child = spawn3(cmd, args, { stdio: "ignore", detached: true, env: launchEnv });
873
+ child.on("error", reject);
874
+ child.on("spawn", () => {
875
+ debug?.(`spawn OK: ${cmd} (pid=${child.pid})`);
876
+ child.unref();
877
+ resolve();
878
+ });
879
+ });
880
+ return;
881
+ } catch (err) {
882
+ lastErr = err;
883
+ debug?.(`spawn FAILED: ${cmd} \u2192 ${err?.code ?? err?.message ?? err}`);
884
+ }
885
+ }
886
+ debug?.(`openInBrowser: ALL candidates failed (lastErr=${lastErr?.message ?? lastErr})`);
887
+ throw lastErr ?? new Error("no browser launcher available");
824
888
  }
825
889
  async function oauthLogin(opts) {
826
890
  const log = opts.log ?? (() => {
@@ -833,22 +897,45 @@ async function oauthLogin(opts) {
833
897
  const state = base64UrlEncode(randomBytes(16));
834
898
  const pkce = generatePkce();
835
899
  log("Starting loopback listener on 127.0.0.1\u2026\n");
836
- const listener = await startLoopbackListener({ expectedState: state, timeoutMs });
900
+ const listener = await startLoopbackListener({
901
+ expectedState: state,
902
+ timeoutMs,
903
+ preferredPorts: LEADBAY_LOOPBACK_PORTS
904
+ });
837
905
  try {
838
- log(`Registering client at ${doc.registration_endpoint}\u2026
906
+ const boundPort = listener.port;
907
+ let clientId = opts.getCachedClientId?.(boundPort);
908
+ if (clientId) {
909
+ log(`Reusing cached OAuth client_id (${clientId}) for port ${boundPort} \u2014 skipping registration.
839
910
  `);
840
- const client = await registerClient(doc.registration_endpoint, {
841
- clientName: opts.clientName,
842
- redirectUri: listener.redirectUri,
843
- logoUri: opts.logoUri
844
- });
911
+ } else {
912
+ log(`Registering client at ${doc.registration_endpoint} (redirect ${listener.redirectUri})\u2026
913
+ `);
914
+ const registered = await registerClient(doc.registration_endpoint, {
915
+ clientName: opts.clientName,
916
+ redirectUri: listener.redirectUri,
917
+ // exact bound-port redirect
918
+ logoUri: opts.logoUri
919
+ });
920
+ clientId = registered.client_id;
921
+ try {
922
+ opts.onClientRegistered?.(clientId, boundPort);
923
+ } catch {
924
+ }
925
+ }
845
926
  const authorizeUrl = new URL(doc.authorization_endpoint);
846
927
  authorizeUrl.searchParams.set("response_type", "code");
847
- authorizeUrl.searchParams.set("client_id", client.client_id);
928
+ authorizeUrl.searchParams.set("client_id", clientId);
848
929
  authorizeUrl.searchParams.set("redirect_uri", listener.redirectUri);
849
930
  authorizeUrl.searchParams.set("state", state);
850
931
  authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
851
932
  authorizeUrl.searchParams.set("code_challenge_method", pkce.method);
933
+ if (opts.onAuthorizeUrl) {
934
+ try {
935
+ opts.onAuthorizeUrl(authorizeUrl.toString());
936
+ } catch {
937
+ }
938
+ }
852
939
  log(`Opening browser to authorize\u2026
853
940
  ${authorizeUrl.toString()}
854
941
  `);
@@ -860,6 +947,9 @@ async function oauthLogin(opts) {
860
947
  ${authorizeUrl.toString()}
861
948
  `
862
949
  );
950
+ if (opts.failFastOnOpenError) {
951
+ throw new BrowserOpenFailedError(authorizeUrl.toString(), err);
952
+ }
863
953
  }
864
954
  log("Waiting for authorization (5 min timeout)\u2026\n");
865
955
  const { code } = await listener.waitForCallback();
@@ -868,7 +958,7 @@ async function oauthLogin(opts) {
868
958
  tokenEndpoint: doc.token_endpoint,
869
959
  code,
870
960
  codeVerifier: pkce.verifier,
871
- clientId: client.client_id,
961
+ clientId,
872
962
  redirectUri: listener.redirectUri
873
963
  });
874
964
  return { accessToken };
@@ -876,10 +966,21 @@ async function oauthLogin(opts) {
876
966
  listener.close();
877
967
  }
878
968
  }
879
- var STARGATE_URLS, FR_COUNTRY_CODES;
969
+ var LEADBAY_LOOPBACK_PORTS, BrowserOpenFailedError, STARGATE_URLS, FR_COUNTRY_CODES;
880
970
  var init_oauth = __esm({
881
971
  "src/oauth.ts"() {
882
972
  "use strict";
973
+ LEADBAY_LOOPBACK_PORTS = [51789, 51790, 51791, 51792];
974
+ BrowserOpenFailedError = class extends Error {
975
+ authorizeUrl;
976
+ constructor(authorizeUrl, cause) {
977
+ super(
978
+ `Could not open a browser automatically: ${cause?.message ?? cause}`
979
+ );
980
+ this.name = "BrowserOpenFailedError";
981
+ this.authorizeUrl = authorizeUrl;
982
+ }
983
+ };
883
984
  STARGATE_URLS = {
884
985
  prod: "https://stargate.leadbay.app/1.0/user_info",
885
986
  staging: "https://staging.stargate.leadbay.app/1.0/user_info"
@@ -1190,65 +1291,109 @@ function pageUninstallHtml() {
1190
1291
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1191
1292
  <title>Leadbay MCP uninstaller</title>
1192
1293
  <style>
1193
- :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); }
1194
- @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); } }
1294
+ :root { color-scheme: light; --bg:#fff; --card:#fff; --strong:#1d2228; --muted:#9aa0ab; --line:#e7e9ee; --accent:#0d0f0e; --danger:#d14343; --ok:#16a34a; --warn:#b06a00; }
1195
1295
  * { box-sizing:border-box; }
1196
- 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; }
1197
- main { width:min(880px,100%); background:var(--panel); border:1px solid var(--line); border-radius:8px; box-shadow:var(--shadow); overflow:hidden; }
1198
- header { padding:22px 24px 16px; border-bottom:1px solid var(--line); display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }
1199
- h1 { font-size:22px; line-height:1.15; margin:0 0 6px; letter-spacing:0; }
1200
- .meta { color:var(--muted); }
1201
- .badge { border:1px solid var(--line); border-radius:999px; padding:5px 10px; color:var(--muted); white-space:nowrap; }
1202
- .steps { display:grid; grid-template-columns:repeat(2,1fr); border-bottom:1px solid var(--line); }
1203
- .step-pill { padding:12px 24px; border-right:1px solid var(--line); color:var(--muted); font-weight:700; }
1204
- .step-pill:last-child { border-right:0; }
1205
- .step-pill.active { color:var(--text); background:color-mix(in srgb,var(--danger),transparent 88%); }
1206
- section { padding:22px 24px; }
1207
- .hidden { display:none; }
1208
- .hint,.detail { color:var(--muted); }
1209
- .agents { display:grid; gap:8px; margin-top:12px; }
1210
- .agent { display:grid; grid-template-columns:auto 1fr; gap:12px; align-items:center; padding:12px; border:1px solid var(--line); border-radius:6px; }
1211
- .agent strong { display:block; }
1212
- .agent input { width:18px; min-height:18px; }
1213
- .actions { display:flex; justify-content:space-between; gap:10px; border-top:1px solid var(--line); padding:16px 24px 20px; }
1214
- .right-actions { display:flex; gap:10px; }
1215
- 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; }
1296
+ 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; }
1297
+ main { width:min(420px,100%); }
1298
+ .steps { display:flex; gap:6px; justify-content:center; margin-bottom:18px; }
1299
+ .dot { width:24px; height:3px; border-radius:999px; background:var(--line); transition:background .2s; }
1300
+ .dot.active,.dot.done { background:var(--danger); }
1301
+ .card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:30px 26px; }
1302
+ h1 { font-size:18px; line-height:1.3; margin:0 0 6px; font-weight:700; color:var(--strong); text-align:center; }
1303
+ .sub { color:var(--muted); text-align:center; margin:0; min-height:1.55em; }
1304
+ .sub.err { color:var(--danger); }
1305
+ .hidden { display:none !important; }
1306
+ .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; }
1307
+ @keyframes spin { to { transform:rotate(360deg); } }
1308
+ .agents { display:grid; gap:8px; margin-top:18px; }
1309
+ .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; }
1310
+ .agent:hover { border-color:var(--muted); }
1311
+ .agent strong { display:block; font-weight:650; color:var(--strong); }
1312
+ .agent .detail { color:var(--muted); font-size:12px; word-break:break-all; }
1313
+ .agent input { width:16px; height:16px; accent-color:var(--danger); }
1314
+ .actions { display:flex; gap:12px; justify-content:center; margin-top:22px; }
1315
+ 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; }
1316
+ button:active { transform:translateY(1px); }
1216
1317
  button.danger { background:var(--danger); border-color:var(--danger); color:#fff; }
1217
- button:disabled { opacity:.6; cursor:wait; }
1218
- .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; }
1219
- .log-row { display:flex; gap:10px; align-items:flex-start; padding:3px 0; white-space:pre-wrap; word-break:break-word; }
1220
- .log-row::before { width:56px; flex:0 0 56px; font-weight:800; text-transform:uppercase; font-size:11px; letter-spacing:.02em; }
1221
- .log-info { color:var(--muted); } .log-info::before { content:"info"; }
1222
- .log-active { color:#c99700; } .log-active::before { content:"run"; }
1223
- .log-success { color:#19a974; } .log-success::before { content:"ok"; }
1224
- .log-error { color:var(--danger); } .log-error::before { content:"error"; }
1225
- .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); }
1226
- .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); }
1227
- @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;} }
1318
+ button.danger:hover { opacity:.88; }
1319
+ button.ghost { border:0; background:transparent; color:var(--muted); padding:9px 14px; }
1320
+ button:disabled { opacity:.45; cursor:default; }
1321
+ /* result state \u2014 animated check / cross */
1322
+ .result { display:flex; flex-direction:column; align-items:center; gap:14px; padding:8px 0 4px; }
1323
+ .ring { width:64px; height:64px; }
1324
+ .ring circle { fill:none; stroke-width:3; stroke-linecap:round; stroke-dasharray:170; stroke-dashoffset:170; animation:draw .5s ease-out forwards; }
1325
+ .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; }
1326
+ .ring .disc { stroke:none; }
1327
+ .ring.ok circle:not(.disc) { stroke:var(--ok); }
1328
+ .ring.err circle:not(.disc) { stroke:var(--danger); }
1329
+ .ring.ok .disc { fill:var(--ok); animation:pop .4s ease-out; }
1330
+ .ring.err .disc { fill:var(--danger); animation:pop .4s ease-out; }
1331
+ .result-msg { font-size:15px; font-weight:700; color:var(--strong); text-align:center; }
1332
+ .result.err .result-msg { color:var(--danger); }
1333
+ .result-note { font-size:12.5px; color:var(--muted); text-align:center; margin-top:-6px; }
1334
+ @keyframes draw { to { stroke-dashoffset:0; } }
1335
+ @keyframes pop { 0%{transform:scale(.5);opacity:0;} 60%{transform:scale(1.06);} 100%{transform:scale(1);opacity:1;} }
1336
+ @media (max-width:520px) { .actions{flex-direction:column;} button{width:100%;} }
1228
1337
  </style>
1229
1338
  </head>
1230
1339
  <body>
1231
1340
  <main>
1232
- <header><div><h1>Leadbay MCP uninstaller</h1><div class="meta" id="meta">${formatInstallOsLabel()}</div></div><div class="badge">v${VERSION}</div></header>
1233
- <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>
1341
+ <div class="steps"><div class="dot active" id="dot-1"></div><div class="dot" id="dot-2"></div></div>
1342
+ <div class="card">
1343
+ <h1 id="title">Remove Leadbay MCP</h1>
1344
+ <p class="sub" id="sub">Select the agents to remove Leadbay MCP from.</p>
1345
+
1346
+ <section id="step-1">
1347
+ <div class="spinner" id="spinner"></div>
1348
+ <div class="agents" id="agents"></div>
1349
+ </section>
1234
1350
 
1235
- <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>
1236
- <section id="step-2" class="hidden"><strong>Removing</strong><div class="hint">Keep this window open until the final message appears.</div></section>
1351
+ <section id="result" class="result hidden">
1352
+ <svg class="ring" id="ring" viewBox="0 0 64 64" aria-hidden="true">
1353
+ <circle class="disc" cx="32" cy="32" r="28"></circle>
1354
+ <circle cx="32" cy="32" r="28"></circle>
1355
+ <path id="ring-mark" d="M20 33 l8 8 l16 -18"></path>
1356
+ </svg>
1357
+ <div class="result-msg" id="result-msg"></div>
1358
+ <div class="result-note" id="result-note"></div>
1359
+ </section>
1237
1360
 
1238
- <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>
1239
- <div id="log" class="log-panel"><div class="log-row log-info">Ready.</div></div>
1361
+ <div class="actions">
1362
+ <button id="back" class="ghost hidden">Back</button>
1363
+ <button id="refresh">Refresh</button>
1364
+ <button class="danger" id="next">Remove selected</button>
1365
+ </div>
1366
+ </div>
1240
1367
  </main>
1241
1368
  <script>
1242
1369
  const $ = (id) => document.getElementById(id);
1370
+ const STEPS = {
1371
+ 1: { title: "Remove Leadbay MCP", sub: "Select the agents to remove Leadbay MCP from." },
1372
+ 2: { title: "Removing", sub: "Keep this window open until it's done." },
1373
+ };
1374
+ const CHECK = "M20 33 l8 8 l16 -18";
1375
+ const CROSS = "M22 22 l20 20 M42 22 l-20 20";
1243
1376
  let step = 1;
1244
1377
  let clients = [];
1245
- function clearLog() { $("log").innerHTML = ""; }
1246
- 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; }
1378
+ function say(text, error = false) { const s = $("sub"); s.textContent = text; s.classList.toggle("err", !!error); }
1247
1379
  function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c])); }
1248
- 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); }
1249
- 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(""); }
1250
- 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."); }
1251
- 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(); }; }
1380
+ 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); }
1381
+ // Final completion state: animated green check / red cross + message.
1382
+ function showResult(ok, msg) {
1383
+ $("sub").classList.add("hidden");
1384
+ $("result-msg").textContent = msg;
1385
+ $("result-note").textContent = ok ? "You can close this window." : "";
1386
+ $("ring-mark").setAttribute("d", ok ? CHECK : CROSS);
1387
+ const ring = $("ring"); ring.classList.remove("ok", "err"); void ring.getBoundingClientRect();
1388
+ ring.classList.add(ok ? "ok" : "err");
1389
+ $("result").classList.toggle("err", !ok);
1390
+ $("result").classList.remove("hidden");
1391
+ $("title").textContent = ok ? "All set" : "Something went wrong";
1392
+ ["next", "back", "refresh"].forEach((id) => $(id).classList.add("hidden"));
1393
+ }
1394
+ 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(""); }
1395
+ 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."); }
1396
+ 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."); }; }
1252
1397
  $("back").addEventListener("click", () => setStep(1));
1253
1398
  $("refresh").addEventListener("click", refresh);
1254
1399
  $("next").addEventListener("click", doUninstall);
@@ -1265,85 +1410,148 @@ function pageHtml() {
1265
1410
  <meta name="viewport" content="width=device-width, initial-scale=1" />
1266
1411
  <title>Leadbay MCP installer</title>
1267
1412
  <style>
1268
- :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); }
1269
- @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); } }
1413
+ :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; }
1270
1414
  * { box-sizing:border-box; }
1271
- 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; }
1272
- main { width:min(880px,100%); background:var(--panel); border:1px solid var(--line); border-radius:8px; box-shadow:var(--shadow); overflow:hidden; }
1273
- header { padding:22px 24px 16px; border-bottom:1px solid var(--line); display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }
1274
- h1 { font-size:22px; line-height:1.15; margin:0 0 6px; letter-spacing:0; }
1275
- .meta,.hint,.detail,label span { color:var(--muted); }
1276
- .badge { border:1px solid var(--line); border-radius:999px; padding:5px 10px; color:var(--muted); white-space:nowrap; }
1277
- .steps { display:grid; grid-template-columns:repeat(4,1fr); border-bottom:1px solid var(--line); }
1278
- .step-pill { padding:12px 24px; border-right:1px solid var(--line); color:var(--muted); font-weight:700; }
1279
- .step-pill:last-child { border-right:0; }
1280
- .step-pill.active { color:var(--text); background:color-mix(in srgb,var(--accent),transparent 88%); }
1281
- section { padding:22px 24px; }
1282
- .hidden { display:none; }
1283
- .grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px; }
1284
- label { display:grid; gap:6px; font-weight:650; }
1285
- 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; }
1286
- .options { display:flex; gap:14px; flex-wrap:wrap; margin-top:14px; }
1287
- .toggle { display:inline-flex; align-items:center; gap:8px; font-weight:600; }
1288
- .toggle input { width:16px; min-height:16px; }
1289
- .setting-card { display:grid; gap:4px; max-width:360px; }
1290
- .setting-card .hint { padding-left:24px; }
1291
- .agents { display:grid; gap:8px; margin-top:12px; }
1292
- .agent { display:grid; grid-template-columns:auto 1fr; gap:12px; align-items:center; padding:12px; border:1px solid var(--line); border-radius:6px; }
1293
- .agent strong { display:block; }
1294
- .agent input { width:18px; min-height:18px; }
1295
- .actions { display:flex; justify-content:space-between; gap:10px; border-top:1px solid var(--line); padding:16px 24px 20px; }
1296
- .right-actions { display:flex; gap:10px; }
1297
- 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; }
1415
+ 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; }
1416
+ main { width:min(420px,100%); }
1417
+ .steps { display:flex; gap:6px; justify-content:center; margin-bottom:18px; }
1418
+ .dot { width:24px; height:3px; border-radius:999px; background:var(--line); transition:background .2s; }
1419
+ .dot.active,.dot.done { background:var(--accent); }
1420
+ .card { background:var(--card); border:1px solid var(--line); border-radius:14px; padding:30px 26px; }
1421
+ h1 { font-size:18px; line-height:1.3; margin:0 0 6px; font-weight:700; color:var(--strong); text-align:center; }
1422
+ .sub { color:var(--muted); text-align:center; margin:0; min-height:1.55em; }
1423
+ .sub.err { color:var(--danger); }
1424
+ .hidden { display:none !important; }
1425
+ .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; }
1426
+ @keyframes spin { to { transform:rotate(360deg); } }
1427
+ .agents { display:grid; gap:8px; margin-top:18px; }
1428
+ .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; }
1429
+ .agent:hover { border-color:var(--muted); }
1430
+ .agent strong { display:block; font-weight:650; color:var(--strong); }
1431
+ .agent .detail { color:var(--muted); font-size:12px; word-break:break-all; }
1432
+ .agent input { width:16px; height:16px; accent-color:var(--accent); }
1433
+ .badge-pill { font-size:9.5px; font-weight:700; padding:1px 6px; border-radius:999px; vertical-align:middle; text-transform:uppercase; letter-spacing:.03em; }
1434
+ .badge-install { background:color-mix(in srgb,var(--ok),transparent 88%); color:var(--ok); }
1435
+ .badge-update { background:color-mix(in srgb,var(--warn),transparent 86%); color:var(--warn); }
1436
+ .actions { display:flex; gap:12px; justify-content:center; margin-top:22px; }
1437
+ 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; }
1438
+ button:active { transform:translateY(1px); }
1298
1439
  button.primary { background:var(--accent); border-color:var(--accent); color:#fff; }
1299
- button.primary:hover { background:var(--accent2); }
1300
- button:disabled { opacity:.6; cursor:wait; }
1301
- .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; }
1302
- .log-row { display:flex; gap:10px; align-items:flex-start; padding:3px 0; white-space:pre-wrap; word-break:break-word; }
1303
- .log-row::before { width:56px; flex:0 0 56px; font-weight:800; text-transform:uppercase; font-size:11px; letter-spacing:.02em; }
1304
- .log-info { color:var(--muted); }
1305
- .log-info::before { content:"info"; }
1306
- .log-active { color:#c99700; }
1307
- .log-active::before { content:"run"; }
1308
- .log-success { color:#19a974; }
1309
- .log-success::before { content:"ok"; }
1310
- .log-error { color:var(--danger); }
1311
- .log-error::before { content:"error"; }
1312
- .error { color:var(--danger); }
1313
- .badge-install,.badge-update { font-size:10px; font-weight:800; padding:2px 6px; border-radius:999px; vertical-align:middle; }
1314
- .badge-install { background:color-mix(in srgb,var(--accent),transparent 80%); color:var(--accent2); }
1315
- .badge-update { background:color-mix(in srgb,#c99700,transparent 80%); color:#a07800; }
1316
- @media (prefers-color-scheme: dark) { .badge-update { color:#f0c040; } .badge-install { color:#00c9a0; } }
1317
- @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;} }
1440
+ button.primary:hover { opacity:.88; }
1441
+ button.cancel { border-color:var(--cancel-line); }
1442
+ button.ghost { border:0; background:transparent; color:var(--muted); padding:9px 14px; }
1443
+ button:disabled { opacity:.45; cursor:default; }
1444
+ /* result state \u2014 animated check / cross */
1445
+ .result { display:flex; flex-direction:column; align-items:center; gap:14px; padding:8px 0 4px; }
1446
+ .ring { width:64px; height:64px; }
1447
+ .ring circle { fill:none; stroke-width:3; stroke-linecap:round; stroke-dasharray:170; stroke-dashoffset:170; animation:draw .5s ease-out forwards; }
1448
+ .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; }
1449
+ .ring .disc { stroke:none; }
1450
+ .ring.ok circle:not(.disc) { stroke:var(--ok); }
1451
+ .ring.err circle:not(.disc) { stroke:var(--danger); }
1452
+ .ring.ok .disc { fill:var(--ok); animation:pop .4s ease-out; }
1453
+ .ring.err .disc { fill:var(--danger); animation:pop .4s ease-out; }
1454
+ .result-msg { font-size:15px; font-weight:700; color:var(--strong); text-align:center; }
1455
+ .result.err .result-msg { color:var(--danger); }
1456
+ .result-note { font-size:12.5px; color:var(--muted); text-align:center; margin-top:-6px; }
1457
+ @keyframes draw { to { stroke-dashoffset:0; } }
1458
+ @keyframes pop { 0%{transform:scale(.5);opacity:0;} 60%{transform:scale(1.06);} 100%{transform:scale(1);opacity:1;} }
1459
+ @media (max-width:520px) { .actions{flex-direction:column;} button{width:100%;} }
1318
1460
  </style>
1319
1461
  </head>
1320
1462
  <body>
1321
1463
  <main>
1322
- <header><div><h1>Leadbay MCP installer</h1><div class="meta" id="meta">${formatInstallOsLabel()}</div></div><div class="badge">v${VERSION}</div></header>
1323
- <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>
1464
+ <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>
1465
+ <div class="card">
1466
+ <h1 id="title">Connect Leadbay</h1>
1467
+ <p class="sub" id="sub">Sign in to install Leadbay across your AI agents.</p>
1468
+
1469
+ <section id="step-2" class="hidden">
1470
+ <div class="spinner" id="spinner"></div>
1471
+ <div class="agents" id="agents"></div>
1472
+ </section>
1324
1473
 
1325
- <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>
1326
- <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>
1327
- <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>
1474
+ <section id="result" class="result hidden">
1475
+ <svg class="ring" id="ring" viewBox="0 0 64 64" aria-hidden="true">
1476
+ <circle class="disc" cx="32" cy="32" r="28"></circle>
1477
+ <circle cx="32" cy="32" r="28"></circle>
1478
+ <path id="ring-mark" d="M20 33 l8 8 l16 -18"></path>
1479
+ </svg>
1480
+ <div class="result-msg" id="result-msg"></div>
1481
+ <div class="result-note" id="result-note"></div>
1482
+ </section>
1328
1483
 
1329
- <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>
1330
- <div id="log" class="log-panel"><div class="log-row log-info">Ready.</div></div>
1484
+ <div class="actions">
1485
+ <button id="back" class="cancel hidden">Back</button>
1486
+ <button id="refresh" class="ghost hidden">Refresh</button>
1487
+ <button class="primary" id="next">Sign in with Leadbay</button>
1488
+ </div>
1489
+ </div>
1331
1490
  </main>
1332
1491
  <script>
1333
1492
  const $ = (id) => document.getElementById(id);
1493
+ const STEPS = {
1494
+ 1: { title: "Connect Leadbay", sub: "Sign in to install Leadbay across your AI agents." },
1495
+ 2: { title: "Choose your agents", sub: "Pick where to install Leadbay." },
1496
+ 3: { title: "Installing", sub: "Keep this window open until it's done." },
1497
+ };
1498
+ const CHECK = "M20 33 l8 8 l16 -18";
1499
+ const CROSS = "M22 22 l20 20 M42 22 l-20 20";
1334
1500
  let step = 1;
1335
1501
  let sessionId = null;
1336
1502
  let clients = [];
1337
- function clearLog() { $("log").innerHTML = ""; }
1338
- 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; }
1339
- function line(text, error = false) { clearLog(); appendLog(error ? "error" : "info", text); }
1503
+ function say(text, error = false) { const s = $("sub"); s.textContent = text; s.classList.toggle("err", !!error); }
1340
1504
  function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c])); }
1341
- 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"; }
1342
- 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(""); }
1343
- 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."); }
1344
- 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; } }
1345
- 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(); }; }
1346
- $("back").addEventListener("click", () => setStep(Math.max(1, step - 1)));
1505
+ function setStep(next) {
1506
+ step = next;
1507
+ [1,2,3].forEach((n) => { const dot = $("dot-" + n); dot.classList.toggle("active", n === step); dot.classList.toggle("done", n < step); });
1508
+ $("step-2").classList.toggle("hidden", step !== 2);
1509
+ $("result").classList.add("hidden");
1510
+ $("title").textContent = STEPS[step].title;
1511
+ say(STEPS[step].sub);
1512
+ $("back").classList.toggle("hidden", step !== 2);
1513
+ $("refresh").classList.toggle("hidden", step !== 2);
1514
+ $("next").classList.toggle("hidden", step === 3);
1515
+ $("next").textContent = step === 2 ? "Install" : "Sign in with Leadbay";
1516
+ }
1517
+ // Final completion state: animated green check / red cross + message.
1518
+ function showResult(ok, msg) {
1519
+ $("sub").classList.add("hidden");
1520
+ $("result-msg").textContent = msg;
1521
+ $("result-note").textContent = ok ? "You can close this window." : "";
1522
+ $("ring-mark").setAttribute("d", ok ? CHECK : CROSS);
1523
+ const ring = $("ring"); ring.classList.remove("ok", "err"); void ring.getBoundingClientRect();
1524
+ ring.classList.add(ok ? "ok" : "err");
1525
+ $("result").classList.toggle("err", !ok);
1526
+ $("result").classList.remove("hidden");
1527
+ $("title").textContent = ok ? "All set" : "Something went wrong";
1528
+ ["next", "back", "refresh"].forEach((id) => $(id).classList.add("hidden"));
1529
+ }
1530
+ 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(""); }
1531
+ 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."); }
1532
+ 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; } }
1533
+ async function install() {
1534
+ const selected = [...document.querySelectorAll("[data-client]:checked")].map((el) => el.dataset.client);
1535
+ if (!selected.length) return say("Select at least one agent.", true);
1536
+ setStep(3);
1537
+ let okCount = 0, lastError = "";
1538
+ const params = new URLSearchParams({ sessionId, clients: selected.join(","), write: "1", telemetry: "1" });
1539
+ const events = new EventSource("/api/install-stream?" + params.toString());
1540
+ events.onmessage = (event) => {
1541
+ const data = JSON.parse(event.data);
1542
+ if (data.level === "error") lastError = data.message;
1543
+ if (data.level === "success") okCount += 1;
1544
+ if (data.level === "done") {
1545
+ events.close();
1546
+ const ok = okCount > 0 && !lastError;
1547
+ showResult(ok, ok ? "MCP successfully installed" : (lastError || "No agents were installed."));
1548
+ } else {
1549
+ say(data.message, data.level === "error");
1550
+ }
1551
+ };
1552
+ events.onerror = () => { events.close(); showResult(false, "Install stream disconnected."); };
1553
+ }
1554
+ $("back").addEventListener("click", () => setStep(1));
1347
1555
  $("refresh").addEventListener("click", refresh);
1348
1556
  $("next").addEventListener("click", async () => { if (step === 1) await doLogin(); else await install(); });
1349
1557
  </script>
@@ -1456,7 +1664,7 @@ function startUninstallerGui(options = {}) {
1456
1664
  return false;
1457
1665
  }, "uninstaller");
1458
1666
  }
1459
- var VERSION, PORT, sessions, OAUTH_BASE_URLS, LOCAL_BIN_PATH;
1667
+ var PORT, sessions, OAUTH_BASE_URLS, LOCAL_BIN_PATH;
1460
1668
  var init_installer_gui = __esm({
1461
1669
  "installer/installer-gui.ts"() {
1462
1670
  "use strict";
@@ -1466,7 +1674,6 @@ var init_installer_gui = __esm({
1466
1674
  init_install_dxt();
1467
1675
  init_install_shared();
1468
1676
  init_oauth();
1469
- VERSION = "0.21.1";
1470
1677
  PORT = Number(process.env.LEADBAY_INSTALLER_PORT ?? 0);
1471
1678
  sessions = /* @__PURE__ */ new Map();
1472
1679
  OAUTH_BASE_URLS = {