@leadbay/mcp 0.21.1 → 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.
- package/CHANGELOG.md +12 -0
- package/dist/bin.js +412 -58
- package/dist/http-server.js +94 -1
- package/dist/installer-electron.js +351 -144
- package/dist/installer-gui.js +349 -142
- package/package.json +1 -1
|
@@ -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
|
-
|
|
724
|
-
|
|
725
|
-
server.
|
|
726
|
-
|
|
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
|
-
|
|
815
|
+
function browserOpenCandidates(url) {
|
|
803
816
|
const platform = process.platform;
|
|
804
|
-
let cmd;
|
|
805
|
-
let args;
|
|
806
817
|
if (platform === "darwin") {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
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
|
-
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
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({
|
|
900
|
+
const listener = await startLoopbackListener({
|
|
901
|
+
expectedState: state,
|
|
902
|
+
timeoutMs,
|
|
903
|
+
preferredPorts: LEADBAY_LOOPBACK_PORTS
|
|
904
|
+
});
|
|
837
905
|
try {
|
|
838
|
-
|
|
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
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
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",
|
|
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
|
|
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
|
|
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(--
|
|
1197
|
-
main { width:min(
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
.
|
|
1201
|
-
.
|
|
1202
|
-
|
|
1203
|
-
.
|
|
1204
|
-
.
|
|
1205
|
-
.
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
.
|
|
1209
|
-
.
|
|
1210
|
-
.agent {
|
|
1211
|
-
.agent strong { display:block; }
|
|
1212
|
-
.agent
|
|
1213
|
-
.
|
|
1214
|
-
.
|
|
1215
|
-
button { min-height:
|
|
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:
|
|
1218
|
-
.
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
.
|
|
1222
|
-
.
|
|
1223
|
-
.
|
|
1224
|
-
.
|
|
1225
|
-
.
|
|
1226
|
-
.
|
|
1227
|
-
|
|
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
|
-
<
|
|
1233
|
-
<div class="
|
|
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
|
-
|
|
1236
|
-
|
|
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
|
-
|
|
1239
|
-
|
|
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
|
|
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) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); }
|
|
1248
|
-
function setStep(n) { step = n; [1,2].forEach((i) => { $("
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
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
|
|
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(--
|
|
1272
|
-
main { width:min(
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
.
|
|
1276
|
-
.
|
|
1277
|
-
|
|
1278
|
-
.
|
|
1279
|
-
.
|
|
1280
|
-
.
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
.
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
.
|
|
1287
|
-
.
|
|
1288
|
-
.
|
|
1289
|
-
.
|
|
1290
|
-
.
|
|
1291
|
-
.
|
|
1292
|
-
.
|
|
1293
|
-
|
|
1294
|
-
|
|
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 {
|
|
1300
|
-
button
|
|
1301
|
-
.
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
.
|
|
1305
|
-
.
|
|
1306
|
-
.
|
|
1307
|
-
.
|
|
1308
|
-
.
|
|
1309
|
-
.
|
|
1310
|
-
.
|
|
1311
|
-
.
|
|
1312
|
-
.
|
|
1313
|
-
.
|
|
1314
|
-
.
|
|
1315
|
-
.
|
|
1316
|
-
@
|
|
1317
|
-
@
|
|
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
|
-
<
|
|
1323
|
-
<div class="
|
|
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
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
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
|
-
|
|
1330
|
-
|
|
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
|
|
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) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); }
|
|
1341
|
-
function setStep(next) {
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
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
|
|
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 = {
|