@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
package/dist/installer-gui.js
CHANGED
|
@@ -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
|
-
|
|
719
|
-
|
|
720
|
-
server.
|
|
721
|
-
|
|
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
|
-
|
|
821
|
+
function browserOpenCandidates(url) {
|
|
798
822
|
const platform = process.platform;
|
|
799
|
-
let cmd;
|
|
800
|
-
let args;
|
|
801
823
|
if (platform === "darwin") {
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
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
|
-
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
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({
|
|
906
|
+
const listener = await startLoopbackListener({
|
|
907
|
+
expectedState: state,
|
|
908
|
+
timeoutMs,
|
|
909
|
+
preferredPorts: LEADBAY_LOOPBACK_PORTS
|
|
910
|
+
});
|
|
832
911
|
try {
|
|
833
|
-
|
|
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
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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",
|
|
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
|
|
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.1";
|
|
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
|
|
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(--
|
|
1171
|
-
main { width:min(
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
.
|
|
1175
|
-
.
|
|
1176
|
-
|
|
1177
|
-
.
|
|
1178
|
-
.
|
|
1179
|
-
.
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
.
|
|
1183
|
-
.
|
|
1184
|
-
.agent {
|
|
1185
|
-
.agent strong { display:block; }
|
|
1186
|
-
.agent
|
|
1187
|
-
.
|
|
1188
|
-
.
|
|
1189
|
-
button { min-height:
|
|
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:
|
|
1192
|
-
.
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
.
|
|
1196
|
-
.
|
|
1197
|
-
.
|
|
1198
|
-
.
|
|
1199
|
-
.
|
|
1200
|
-
.
|
|
1201
|
-
|
|
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
|
-
<
|
|
1207
|
-
<div class="
|
|
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
|
-
|
|
1210
|
-
|
|
1319
|
+
<section id="step-1">
|
|
1320
|
+
<div class="spinner" id="spinner"></div>
|
|
1321
|
+
<div class="agents" id="agents"></div>
|
|
1322
|
+
</section>
|
|
1211
1323
|
|
|
1212
|
-
|
|
1213
|
-
|
|
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
|
|
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) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); }
|
|
1222
|
-
function setStep(n) { step = n; [1,2].forEach((i) => { $("
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
|
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(--
|
|
1246
|
-
main { width:min(
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
.
|
|
1250
|
-
.
|
|
1251
|
-
|
|
1252
|
-
.
|
|
1253
|
-
.
|
|
1254
|
-
.
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
.
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
.
|
|
1261
|
-
.
|
|
1262
|
-
.
|
|
1263
|
-
.
|
|
1264
|
-
.
|
|
1265
|
-
.
|
|
1266
|
-
.
|
|
1267
|
-
|
|
1268
|
-
|
|
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 {
|
|
1274
|
-
button
|
|
1275
|
-
.
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
.
|
|
1279
|
-
.
|
|
1280
|
-
.
|
|
1281
|
-
.
|
|
1282
|
-
.
|
|
1283
|
-
.
|
|
1284
|
-
.
|
|
1285
|
-
.
|
|
1286
|
-
.
|
|
1287
|
-
.
|
|
1288
|
-
.
|
|
1289
|
-
.
|
|
1290
|
-
@
|
|
1291
|
-
@
|
|
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
|
-
<
|
|
1297
|
-
<div class="
|
|
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
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
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
|
-
|
|
1304
|
-
|
|
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
|
|
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) => ({"&":"&","<":"<",">":">",'"':""","'":"'"}[c])); }
|
|
1315
|
-
function setStep(next) {
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
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>
|