@leadbay/mcp 0.17.0 → 0.17.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.
@@ -0,0 +1,1470 @@
1
+ #!/usr/bin/env node
2
+
3
+ // installer/installer-gui.ts
4
+ import { createServer as createServer2 } from "http";
5
+ import { randomUUID } from "crypto";
6
+ import { realpathSync } from "fs";
7
+ import { resolve as resolvePath } from "path";
8
+ import { fileURLToPath } from "url";
9
+
10
+ // installer/install-claude-code.ts
11
+ import { spawn } from "child_process";
12
+ function buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled, localBinPath) {
13
+ const args = [
14
+ "mcp",
15
+ "add",
16
+ "leadbay",
17
+ "--scope",
18
+ "user",
19
+ "--env",
20
+ `LEADBAY_TOKEN=${token}`,
21
+ "--env",
22
+ `LEADBAY_REGION=${region}`,
23
+ "--env",
24
+ `LEADBAY_TELEMETRY_ENABLED=${telemetryEnabled ? "true" : "false"}`
25
+ ];
26
+ if (!includeWrite) args.push("--env", `LEADBAY_MCP_WRITE=0`);
27
+ if (localBinPath) {
28
+ args.push("--", "node", localBinPath);
29
+ } else {
30
+ args.push("--", "npx", "-y", "-p", "@leadbay/mcp@latest", "leadbay-mcp");
31
+ }
32
+ return args;
33
+ }
34
+ function buildClaudeCodeRemoveArgs() {
35
+ return ["mcp", "remove", "leadbay", "--scope", "user"];
36
+ }
37
+ async function runClaudeMcp(args) {
38
+ return await new Promise((resolve) => {
39
+ const child = spawn("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
40
+ let stdout = "";
41
+ let stderr = "";
42
+ child.stdout.on("data", (chunk) => stdout += chunk.toString());
43
+ child.stderr.on("data", (chunk) => stderr += chunk.toString());
44
+ child.on("close", (code) => resolve({ code, stdout, stderr }));
45
+ child.on("error", (err) => resolve({ code: null, stdout, stderr, spawnError: err.message }));
46
+ });
47
+ }
48
+ async function isLeadbayConfiguredInClaudeCode() {
49
+ const result = await runClaudeMcp(["mcp", "list"]);
50
+ if (result.spawnError || result.code !== 0) return false;
51
+ return /^leadbay:/m.test(result.stdout);
52
+ }
53
+ async function installInClaudeCode(token, region, includeWrite, telemetryEnabled, localBinPath) {
54
+ const args = buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled, localBinPath);
55
+ const first = await runClaudeMcp(args);
56
+ if (first.spawnError) return { ok: false, message: `failed to spawn claude: ${first.spawnError}` };
57
+ if (first.code === 0) return { ok: true, message: "registered" };
58
+ const stderr = first.stderr.trim();
59
+ if (!/already exists/i.test(stderr)) {
60
+ return { ok: false, message: `claude mcp add exited ${first.code}: ${stderr.slice(0, 200)}` };
61
+ }
62
+ const removed = await runClaudeMcp(buildClaudeCodeRemoveArgs());
63
+ if (removed.spawnError) return { ok: false, message: `failed to spawn claude: ${removed.spawnError}` };
64
+ if (removed.code !== 0) {
65
+ return { ok: false, message: `claude mcp remove exited ${removed.code}: ${removed.stderr.trim().slice(0, 200)}` };
66
+ }
67
+ const second = await runClaudeMcp(args);
68
+ if (second.spawnError) return { ok: false, message: `failed to spawn claude: ${second.spawnError}` };
69
+ return {
70
+ ok: second.code === 0,
71
+ message: second.code === 0 ? "updated" : `claude mcp add exited ${second.code}: ${second.stderr.trim().slice(0, 200)}`
72
+ };
73
+ }
74
+ async function uninstallFromClaudeCode() {
75
+ const result = await runClaudeMcp(buildClaudeCodeRemoveArgs());
76
+ if (result.spawnError) return { ok: false, message: `failed to spawn claude: ${result.spawnError}` };
77
+ if (result.code === 0) return { ok: true, message: "removed" };
78
+ const stderr = result.stderr.trim();
79
+ if (/not found|does not exist|no server/i.test(stderr)) return { ok: true, message: "already absent" };
80
+ return { ok: false, message: `claude mcp remove exited ${result.code}: ${stderr.slice(0, 200)}` };
81
+ }
82
+
83
+ // installer/install-json-config.ts
84
+ async function installInJsonConfig(configPath, token, region, includeWrite, telemetryEnabled, localBinPath) {
85
+ try {
86
+ const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync3, mkdirSync, statSync } = await import("fs");
87
+ const { dirname } = await import("path");
88
+ let parsed = {};
89
+ let preserved = {};
90
+ const existed = existsSync3(configPath);
91
+ if (existed) {
92
+ const raw = readFileSync3(configPath, "utf8");
93
+ try {
94
+ preserved = JSON.parse(raw);
95
+ parsed = preserved;
96
+ } catch {
97
+ return { ok: false, message: `existing ${configPath} is not valid JSON; refusing to overwrite` };
98
+ }
99
+ } else {
100
+ mkdirSync(dirname(configPath), { recursive: true });
101
+ }
102
+ parsed.mcpServers = parsed.mcpServers ?? {};
103
+ const env = {
104
+ LEADBAY_TOKEN: token,
105
+ LEADBAY_REGION: region,
106
+ // Always written so MCP-client config UIs can render it as a toggle.
107
+ LEADBAY_TELEMETRY_ENABLED: telemetryEnabled ? "true" : "false"
108
+ };
109
+ if (!includeWrite) env.LEADBAY_MCP_WRITE = "0";
110
+ parsed.mcpServers.leadbay = localBinPath ? { command: "node", args: [localBinPath], env } : { command: "npx", args: ["-y", "-p", "@leadbay/mcp@latest", "leadbay-mcp"], env };
111
+ const tmp = configPath + ".tmp";
112
+ writeFileSync(tmp, JSON.stringify(parsed, null, 2) + "\n", "utf8");
113
+ const { renameSync, chmodSync } = await import("fs");
114
+ renameSync(tmp, configPath);
115
+ try {
116
+ const st = statSync(configPath);
117
+ if ((st.mode & 511) > 384 && Object.keys(preserved).length === 0) {
118
+ chmodSync(configPath, 384);
119
+ }
120
+ } catch {
121
+ }
122
+ return { ok: true, message: existed ? "updated" : "registered" };
123
+ } catch (err) {
124
+ return { ok: false, message: err?.message ?? String(err) };
125
+ }
126
+ }
127
+ function stripJsonMcpEntry(existing) {
128
+ let parsed;
129
+ try {
130
+ parsed = JSON.parse(existing);
131
+ } catch {
132
+ return { content: existing, changed: false };
133
+ }
134
+ if (!parsed?.mcpServers?.leadbay) return { content: existing, changed: false };
135
+ delete parsed.mcpServers.leadbay;
136
+ return { content: JSON.stringify(parsed, null, 2) + "\n", changed: true };
137
+ }
138
+ async function uninstallFromJsonConfig(configPath) {
139
+ try {
140
+ const { existsSync: existsSync3, readFileSync: readFileSync3, writeFileSync } = await import("fs");
141
+ if (!existsSync3(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
142
+ const existing = readFileSync3(configPath, "utf8");
143
+ const { content, changed } = stripJsonMcpEntry(existing);
144
+ if (!changed) return { ok: true, message: "leadbay entry not present" };
145
+ writeFileSync(configPath, content, "utf8");
146
+ return { ok: true, message: "removed" };
147
+ } catch (err) {
148
+ return { ok: false, message: err?.message ?? String(err) };
149
+ }
150
+ }
151
+
152
+ // installer/install-codex.ts
153
+ function shellQuote(value) {
154
+ return '"' + value.replace(/([\"\\$`])/g, "\\$1") + '"';
155
+ }
156
+ function buildCodexConfigBlock(includeWrite, telemetryEnabled, version = "latest", localBinPath) {
157
+ const envVars = ["LEADBAY_TOKEN", "LEADBAY_REGION", "LEADBAY_TELEMETRY_ENABLED"];
158
+ if (!includeWrite) envVars.push("LEADBAY_MCP_WRITE");
159
+ const envVarsToml = envVars.map((v) => `"${v}"`).join(", ");
160
+ const commandLine = localBinPath ? `command = "node"
161
+ args = [${JSON.stringify(localBinPath)}]
162
+ ` : `command = "npx"
163
+ args = ["-y", "@leadbay/mcp@${version}"]
164
+ `;
165
+ return `[mcp_servers.leadbay]
166
+ ` + commandLine + `env_vars = [${envVarsToml}]
167
+ `;
168
+ }
169
+ function buildShellExportBlock(token, region, includeWrite, telemetryEnabled) {
170
+ const lines = [
171
+ "",
172
+ "# Added by leadbay-mcp install",
173
+ `export LEADBAY_TOKEN=${shellQuote(token)}`,
174
+ `export LEADBAY_REGION=${shellQuote(region)}`,
175
+ `export LEADBAY_TELEMETRY_ENABLED=${shellQuote(telemetryEnabled ? "true" : "false")}`
176
+ ];
177
+ if (!includeWrite) {
178
+ lines.push(`export LEADBAY_MCP_WRITE=${shellQuote("0")}`);
179
+ }
180
+ lines.push("");
181
+ return lines.join("\n");
182
+ }
183
+ function mergeCodexConfig(existing, block) {
184
+ const withoutLeadbay = existing.replace(
185
+ /(^|\r?\n)\[mcp_servers\.leadbay\]\r?\n[\s\S]*?(?=\r?\n\[|$)/g,
186
+ (match, prefix) => prefix && match.startsWith(prefix) ? prefix : ""
187
+ );
188
+ const trimmed = withoutLeadbay.trimEnd();
189
+ return `${trimmed ? `${trimmed}
190
+
191
+ ` : ""}${block.trimEnd()}
192
+ `;
193
+ }
194
+ function mergeShellExportBlock(existing, block) {
195
+ const managedBlock = /(^|\r?\n)# Added by leadbay-mcp install\r?\nexport LEADBAY_TOKEN=.*\r?\nexport LEADBAY_REGION=.*\r?\nexport LEADBAY_TELEMETRY_ENABLED=.*\r?\n(?:export LEADBAY_MCP_WRITE=.*\r?\n)?/g;
196
+ const stripped = existing.replace(managedBlock, (match, prefix) => prefix || "");
197
+ if (stripped === existing && existing.includes("LEADBAY_TOKEN=")) {
198
+ return { content: existing, changed: false };
199
+ }
200
+ const trimmed = stripped.trimEnd();
201
+ return {
202
+ content: `${trimmed ? `${trimmed}
203
+ ` : ""}${block}`,
204
+ changed: true
205
+ };
206
+ }
207
+ function stripCodexBlock(existing) {
208
+ const stripped = existing.replace(
209
+ /(^|\r?\n)\[mcp_servers\.leadbay\]\r?\n[\s\S]*?(?=\r?\n\[|$)/g,
210
+ (match, prefix) => prefix && match.startsWith(prefix) ? prefix : ""
211
+ );
212
+ if (stripped === existing) return { content: existing, changed: false };
213
+ const trimmed = stripped.trimEnd();
214
+ return { content: trimmed ? trimmed + "\n" : "", changed: true };
215
+ }
216
+ function stripShellExportBlock(existing) {
217
+ const managedBlock = /(^|\r?\n)# Added by leadbay-mcp install\r?\nexport LEADBAY_TOKEN=.*\r?\nexport LEADBAY_REGION=.*\r?\nexport LEADBAY_TELEMETRY_ENABLED=.*\r?\n(?:export LEADBAY_MCP_WRITE=.*\r?\n)?/g;
218
+ const stripped = existing.replace(managedBlock, (match, prefix) => prefix || "");
219
+ if (stripped === existing) return { content: existing, changed: false };
220
+ return { content: stripped.trimEnd() + (stripped.trimEnd() ? "\n" : ""), changed: true };
221
+ }
222
+ async function installInCodexConfig(configPath, includeWrite, telemetryEnabled, localBinPath) {
223
+ try {
224
+ const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync3, mkdirSync, statSync, renameSync, chmodSync } = await import("fs");
225
+ const { dirname } = await import("path");
226
+ let existing = "";
227
+ const existed = existsSync3(configPath);
228
+ if (existed) {
229
+ existing = readFileSync3(configPath, "utf8");
230
+ } else {
231
+ mkdirSync(dirname(configPath), { recursive: true });
232
+ }
233
+ const hadLeadbayConfig = /(^|\r?\n)\[mcp_servers\.leadbay\]\r?\n/.test(existing);
234
+ const next = mergeCodexConfig(
235
+ existing,
236
+ buildCodexConfigBlock(includeWrite, telemetryEnabled, "latest", localBinPath)
237
+ );
238
+ const tmp = `${configPath}.tmp`;
239
+ writeFileSync(tmp, next, "utf8");
240
+ renameSync(tmp, configPath);
241
+ try {
242
+ const st = statSync(configPath);
243
+ if (!existed || (st.mode & 511) > 384) {
244
+ chmodSync(configPath, 384);
245
+ }
246
+ } catch {
247
+ }
248
+ return { ok: true, message: hadLeadbayConfig ? "updated" : "registered" };
249
+ } catch (err) {
250
+ return { ok: false, message: err?.message ?? String(err) };
251
+ }
252
+ }
253
+ async function appendShellExports(token, region, includeWrite, telemetryEnabled) {
254
+ try {
255
+ const cp = await import("child_process");
256
+ if (process.platform === "win32") {
257
+ const values = {
258
+ LEADBAY_TOKEN: token,
259
+ LEADBAY_REGION: region,
260
+ LEADBAY_TELEMETRY_ENABLED: telemetryEnabled ? "true" : "false"
261
+ };
262
+ if (!includeWrite) values.LEADBAY_MCP_WRITE = "0";
263
+ for (const [key, value] of Object.entries(values)) {
264
+ const ok = await new Promise((resolve) => {
265
+ const child = cp.spawn("setx", [key, value], { stdio: "ignore" });
266
+ child.on("close", (code) => resolve(code === 0));
267
+ child.on("error", () => resolve(false));
268
+ });
269
+ if (!ok) return { ok: false, message: `failed to set ${key} with setx` };
270
+ }
271
+ return { ok: true, message: "env exported with setx; restart Codex/terminal" };
272
+ }
273
+ const { existsSync: existsSync3, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
274
+ const os = await import("os");
275
+ const home = os.homedir();
276
+ const preferred = [`${home}/.zshrc`, `${home}/.bashrc`].filter((path) => existsSync3(path));
277
+ const paths = preferred.length ? preferred : [`${home}/.profile`];
278
+ const block = buildShellExportBlock(token, region, includeWrite, telemetryEnabled);
279
+ const updated = [];
280
+ for (const path of paths) {
281
+ const existing = existsSync3(path) ? readFileSync3(path, "utf8") : "";
282
+ const merged = mergeShellExportBlock(existing, block);
283
+ if (!merged.changed) continue;
284
+ const tmp = `${path}.leadbay.tmp`;
285
+ writeFileSync(tmp, merged.content, "utf8");
286
+ renameSync(tmp, path);
287
+ updated.push(path);
288
+ }
289
+ return {
290
+ ok: true,
291
+ message: updated.length ? `env exported to ${updated.join(", ")}; restart Codex/terminal or source the file` : "env exports already present"
292
+ };
293
+ } catch (err) {
294
+ return { ok: false, message: err?.message ?? String(err) };
295
+ }
296
+ }
297
+ async function uninstallFromCodexConfig(configPath) {
298
+ try {
299
+ const { existsSync: existsSync3, readFileSync: readFileSync3, writeFileSync } = await import("fs");
300
+ if (!existsSync3(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
301
+ const existing = readFileSync3(configPath, "utf8");
302
+ const { content, changed } = stripCodexBlock(existing);
303
+ if (!changed) return { ok: true, message: "leadbay block not present" };
304
+ writeFileSync(configPath, content, "utf8");
305
+ return { ok: true, message: "removed from TOML" };
306
+ } catch (err) {
307
+ return { ok: false, message: err?.message ?? String(err) };
308
+ }
309
+ }
310
+ async function uninstallShellExports() {
311
+ try {
312
+ const { existsSync: existsSync3, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
313
+ const os = await import("os");
314
+ const home = os.homedir();
315
+ const candidates = [`${home}/.zshrc`, `${home}/.bashrc`, `${home}/.profile`].filter(
316
+ (p) => existsSync3(p)
317
+ );
318
+ const updated = [];
319
+ for (const p of candidates) {
320
+ const existing = readFileSync3(p, "utf8");
321
+ const { content, changed } = stripShellExportBlock(existing);
322
+ if (!changed) continue;
323
+ const tmp = `${p}.leadbay.tmp`;
324
+ writeFileSync(tmp, content, "utf8");
325
+ renameSync(tmp, p);
326
+ updated.push(p);
327
+ }
328
+ return {
329
+ ok: true,
330
+ message: updated.length ? `removed from ${updated.join(", ")}; restart terminal or source the file` : "managed export block not found in shell files"
331
+ };
332
+ } catch (err) {
333
+ return { ok: false, message: err?.message ?? String(err) };
334
+ }
335
+ }
336
+
337
+ // installer/install-dxt.ts
338
+ var DXT_EXTENSION_ID = "local.dxt.leadbay.leadbay";
339
+ async function removeDxtExtension(claudeSupportDir) {
340
+ try {
341
+ const { existsSync: existsSync3, readFileSync: readFileSync3, writeFileSync, rmSync } = await import("fs");
342
+ const { join: join2 } = await import("path");
343
+ const extensionDir = join2(claudeSupportDir, "Claude Extensions", DXT_EXTENSION_ID);
344
+ const registryPath = join2(claudeSupportDir, "extensions-installations.json");
345
+ let removedDir = false;
346
+ let removedEntry = false;
347
+ if (existsSync3(extensionDir)) {
348
+ rmSync(extensionDir, { recursive: true, force: true });
349
+ removedDir = true;
350
+ }
351
+ if (existsSync3(registryPath)) {
352
+ try {
353
+ const raw = readFileSync3(registryPath, "utf8");
354
+ const parsed = JSON.parse(raw);
355
+ if (parsed?.extensions?.[DXT_EXTENSION_ID]) {
356
+ delete parsed.extensions[DXT_EXTENSION_ID];
357
+ const tmp = registryPath + ".tmp";
358
+ writeFileSync(tmp, JSON.stringify(parsed, null, 2) + "\n", "utf8");
359
+ const { renameSync } = await import("fs");
360
+ renameSync(tmp, registryPath);
361
+ removedEntry = true;
362
+ }
363
+ } catch {
364
+ }
365
+ }
366
+ const removed = removedDir || removedEntry;
367
+ return {
368
+ ok: true,
369
+ removed,
370
+ message: removed ? "DXT extension removed" : "DXT extension not installed"
371
+ };
372
+ } catch (err) {
373
+ return { ok: false, removed: false, message: err?.message ?? String(err) };
374
+ }
375
+ }
376
+
377
+ // installer/installer-gui.ts
378
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
379
+
380
+ // installer/install-shared.ts
381
+ import { spawn as spawn2 } from "child_process";
382
+ import { existsSync, readFileSync } from "fs";
383
+ import { join } from "path";
384
+ import { homedir } from "os";
385
+ var HOSTED_MCP_URL = "https://leadbay-mcp-prod.fly.dev/mcp";
386
+ function formatInstallOsLabel(platform = process.platform, arch = process.arch) {
387
+ const name = platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform === "linux" ? "Linux" : platform;
388
+ return `${name} (${arch})`;
389
+ }
390
+ function detectClaudeDesktopMode(claudeSupportDir) {
391
+ const markers = [];
392
+ const legacy = existsSync(join(claudeSupportDir, "claude_desktop_config.json"));
393
+ if (existsSync(join(claudeSupportDir, "Claude Extensions"))) {
394
+ markers.push("Claude Extensions/");
395
+ }
396
+ if (existsSync(join(claudeSupportDir, "extensions-installations.json"))) {
397
+ markers.push("extensions-installations.json");
398
+ }
399
+ const cfgPath = join(claudeSupportDir, "config.json");
400
+ if (existsSync(cfgPath)) {
401
+ try {
402
+ const raw = readFileSync(cfgPath, "utf8");
403
+ const parsed = JSON.parse(raw);
404
+ if (parsed && typeof parsed === "object") {
405
+ const hasDxtKey = Object.keys(parsed).some((key) => key.startsWith("dxt:"));
406
+ if (hasDxtKey) markers.push("config.json (dxt:* keys)");
407
+ }
408
+ } catch {
409
+ }
410
+ }
411
+ return { legacy, dxt: markers.length > 0, markers };
412
+ }
413
+ async function findOnPath(bin) {
414
+ return await new Promise((resolve) => {
415
+ const cmd = process.platform === "win32" ? "where" : "which";
416
+ const child = spawn2(cmd, [bin], { stdio: ["ignore", "pipe", "ignore"] });
417
+ let buf = "";
418
+ child.stdout.on("data", (chunk) => buf += chunk.toString());
419
+ child.on("close", (code) => resolve(code === 0 ? buf.split(/\r?\n/)[0] : null));
420
+ child.on("error", () => resolve(null));
421
+ });
422
+ }
423
+ async function windowsStoreAppInstalled(packageName, appName) {
424
+ if (process.platform !== "win32") return false;
425
+ return await new Promise((resolve) => {
426
+ const script = [
427
+ `$pkg = Get-AppxPackage -Name '${packageName}' -ErrorAction SilentlyContinue`,
428
+ `$app = Get-StartApps | Where-Object { $_.AppID -like '${packageName}_*!${appName}' } | Select-Object -First 1`,
429
+ "if ($pkg -or $app) { exit 0 } else { exit 1 }"
430
+ ].join("; ");
431
+ const child = spawn2("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
432
+ stdio: "ignore",
433
+ windowsHide: true
434
+ });
435
+ child.on("close", (code) => resolve(code === 0));
436
+ child.on("error", () => resolve(false));
437
+ });
438
+ }
439
+ async function isClaudeDesktopInstalled(home) {
440
+ if (process.platform === "darwin") {
441
+ return existsSync("/Applications/Claude.app") || existsSync(home + "/Applications/Claude.app");
442
+ }
443
+ if (process.platform === "win32") {
444
+ const local = process.env.LOCALAPPDATA ?? home + "/AppData/Local";
445
+ const programFiles = process.env.ProgramFiles;
446
+ const programFilesX86 = process.env["ProgramFiles(x86)"];
447
+ return [
448
+ local + "/Programs/Claude/Claude.exe",
449
+ local + "/Claude/Claude.exe",
450
+ programFiles ? programFiles + "/Claude/Claude.exe" : null,
451
+ programFilesX86 ? programFilesX86 + "/Claude/Claude.exe" : null
452
+ ].some((candidate) => candidate !== null && existsSync(candidate));
453
+ }
454
+ const desktopBin = await findOnPath("claude-desktop");
455
+ if (desktopBin) return true;
456
+ return existsSync(home + "/.local/share/applications/claude-desktop.desktop") || existsSync("/usr/share/applications/claude-desktop.desktop") || existsSync("/opt/Claude/Claude") || existsSync("/opt/Claude/claude") || existsSync("/opt/claude/claude");
457
+ }
458
+ async function isChatGptDesktopInstalled(home) {
459
+ if (process.platform === "darwin") {
460
+ return existsSync("/Applications/ChatGPT.app") || existsSync(home + "/Applications/ChatGPT.app");
461
+ }
462
+ if (process.platform === "win32") {
463
+ const local = process.env.LOCALAPPDATA ?? home + "/AppData/Local";
464
+ const programFiles = process.env.ProgramFiles;
465
+ const programFilesX86 = process.env["ProgramFiles(x86)"];
466
+ const exeInstalled = [
467
+ local + "/Programs/ChatGPT/ChatGPT.exe",
468
+ local + "/ChatGPT/ChatGPT.exe",
469
+ programFiles ? programFiles + "/OpenAI/ChatGPT/ChatGPT.exe" : null,
470
+ programFiles ? programFiles + "/ChatGPT/ChatGPT.exe" : null,
471
+ programFilesX86 ? programFilesX86 + "/OpenAI/ChatGPT/ChatGPT.exe" : null,
472
+ programFilesX86 ? programFilesX86 + "/ChatGPT/ChatGPT.exe" : null
473
+ ].some((candidate) => candidate !== null && existsSync(candidate));
474
+ return exeInstalled || await windowsStoreAppInstalled("OpenAI.ChatGPT-Desktop", "ChatGPT");
475
+ }
476
+ return false;
477
+ }
478
+ async function isCursorInstalled(home) {
479
+ const cursorBin = await findOnPath("cursor");
480
+ if (cursorBin) return true;
481
+ if (process.platform === "darwin") return existsSync("/Applications/Cursor.app");
482
+ if (process.platform === "win32") {
483
+ const local = process.env.LOCALAPPDATA ?? `${home}\\AppData\\Local`;
484
+ return existsSync(`${local}\\Programs\\Cursor\\Cursor.exe`);
485
+ }
486
+ return existsSync("/usr/share/applications/cursor.desktop") || existsSync("/opt/Cursor/cursor");
487
+ }
488
+ async function detectClients() {
489
+ const out = [];
490
+ const home = homedir();
491
+ const claudeBin = await findOnPath("claude");
492
+ if (claudeBin) {
493
+ out.push({ id: "claude-code", label: "Claude Code", detail: `${claudeBin} mcp add ...` });
494
+ }
495
+ const claudeSupportDir = process.platform === "win32" ? `${process.env.APPDATA ?? `${home}\\AppData\\Roaming`}\\Claude` : process.platform === "darwin" ? `${home}/Library/Application Support/Claude` : `${home}/.config/Claude`;
496
+ const claudeDesktopPath = process.platform === "win32" ? `${claudeSupportDir}\\claude_desktop_config.json` : `${claudeSupportDir}/claude_desktop_config.json`;
497
+ const mode = detectClaudeDesktopMode(claudeSupportDir);
498
+ if (await isClaudeDesktopInstalled(home)) {
499
+ out.push({ id: "claude-desktop", label: "Claude Desktop", detail: claudeDesktopPath, configPath: claudeDesktopPath, mode, supportDir: claudeSupportDir });
500
+ }
501
+ if (await isChatGptDesktopInstalled(home)) {
502
+ out.push({ id: "chatgpt-desktop", label: "ChatGPT Desktop", detail: HOSTED_MCP_URL });
503
+ }
504
+ const cursorPath = process.platform === "win32" ? `${home}\\.cursor\\mcp.json` : `${home}/.cursor/mcp.json`;
505
+ if (await isCursorInstalled(home)) {
506
+ out.push({
507
+ id: "cursor",
508
+ label: "Cursor",
509
+ detail: existsSync(cursorPath) ? cursorPath : `${cursorPath} (will be created)`,
510
+ configPath: cursorPath
511
+ });
512
+ }
513
+ const codexBin = await findOnPath("codex");
514
+ const codexDir = process.platform === "win32" ? `${process.env.USERPROFILE ?? home}\\.codex` : `${home}/.codex`;
515
+ if (codexBin) {
516
+ const codexConfigPath = process.platform === "win32" ? `${codexDir}\\config.toml` : `${codexDir}/config.toml`;
517
+ out.push({ id: "codex", label: "Codex", detail: codexConfigPath, configPath: codexConfigPath });
518
+ }
519
+ return out;
520
+ }
521
+
522
+ // src/oauth.ts
523
+ import { createHash, randomBytes } from "crypto";
524
+ import { createServer } from "http";
525
+ import { request as httpsRequestRaw } from "https";
526
+ import { spawn as spawn3 } from "child_process";
527
+ var STARGATE_URLS = {
528
+ prod: "https://stargate.leadbay.app/1.0/user_info",
529
+ staging: "https://staging.stargate.leadbay.app/1.0/user_info"
530
+ };
531
+ var FR_COUNTRY_CODES = /* @__PURE__ */ new Set([
532
+ "FR",
533
+ // France
534
+ // French overseas territories — same regional partition as France in the
535
+ // backend's stargate /login route (see backend/specs/stargate/1.0).
536
+ "GP",
537
+ "MQ",
538
+ "GF",
539
+ "RE",
540
+ "YT",
541
+ "MF",
542
+ "BL",
543
+ "PM",
544
+ "WF",
545
+ "PF",
546
+ "NC",
547
+ "TF"
548
+ ]);
549
+ async function inferRegionViaStargate(opts) {
550
+ const url = STARGATE_URLS[opts.staging ? "staging" : "prod"];
551
+ const res = await httpsCall("GET", url, { Accept: "application/json" });
552
+ if (res.status !== 200) {
553
+ throw new Error(
554
+ `Stargate region probe failed: GET ${url} returned ${res.status}. Pass --region us|fr to skip auto-detection.`
555
+ );
556
+ }
557
+ let parsed;
558
+ try {
559
+ parsed = JSON.parse(res.body);
560
+ } catch {
561
+ throw new Error(`Stargate region probe returned non-JSON body`);
562
+ }
563
+ const country = parsed.userCountry;
564
+ if (!country || typeof country !== "string") {
565
+ throw new Error(`Stargate response missing userCountry: ${res.body.slice(0, 200)}`);
566
+ }
567
+ if (country === "US") return "us";
568
+ if (FR_COUNTRY_CODES.has(country)) return "fr";
569
+ throw new Error(
570
+ `Stargate detected your country as ${country}, which isn't mapped to a Leadbay region. Pass --region us|fr explicitly.`
571
+ );
572
+ }
573
+ function generatePkce() {
574
+ const verifier = base64UrlEncode(randomBytes(32));
575
+ const challenge = base64UrlEncode(
576
+ createHash("sha256").update(verifier, "ascii").digest()
577
+ );
578
+ return { verifier, challenge, method: "S256" };
579
+ }
580
+ function base64UrlEncode(buf) {
581
+ return buf.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
582
+ }
583
+ function httpsCall(method, url, headers, body) {
584
+ return new Promise((resolve, reject) => {
585
+ const u = new URL(url);
586
+ const reqHeaders = { ...headers };
587
+ if (body !== void 0) reqHeaders["Content-Length"] = Buffer.byteLength(body);
588
+ const req = httpsRequestRaw(
589
+ {
590
+ hostname: u.hostname,
591
+ port: u.port ? Number(u.port) : 443,
592
+ path: u.pathname + u.search,
593
+ method,
594
+ headers: reqHeaders
595
+ },
596
+ (res) => {
597
+ const chunks = [];
598
+ res.on("data", (c) => chunks.push(c));
599
+ res.on(
600
+ "end",
601
+ () => resolve({ status: res.statusCode ?? 0, body: Buffer.concat(chunks).toString("utf8") })
602
+ );
603
+ }
604
+ );
605
+ req.on("error", reject);
606
+ if (body !== void 0) req.write(body);
607
+ req.end();
608
+ });
609
+ }
610
+ async function fetchDiscoveryDoc(authServerBaseUrl) {
611
+ const url = trimSlash(authServerBaseUrl) + "/.well-known/oauth-authorization-server";
612
+ const res = await httpsCall("GET", url, { Accept: "application/json" });
613
+ if (res.status !== 200) {
614
+ throw new Error(
615
+ `OAuth discovery failed: GET ${url} returned ${res.status}. Either OAuth isn't deployed to this backend yet, or the URL is wrong.`
616
+ );
617
+ }
618
+ let doc;
619
+ try {
620
+ doc = JSON.parse(res.body);
621
+ } catch {
622
+ throw new Error(`OAuth discovery returned non-JSON body from ${url}`);
623
+ }
624
+ for (const field of ["authorization_endpoint", "token_endpoint", "registration_endpoint"]) {
625
+ if (typeof doc[field] !== "string" || !doc[field]) {
626
+ throw new Error(`OAuth discovery doc missing required field: ${field}`);
627
+ }
628
+ }
629
+ if (doc.code_challenge_methods_supported && !doc.code_challenge_methods_supported.includes("S256")) {
630
+ throw new Error(
631
+ `OAuth server doesn't support S256 PKCE (only ${doc.code_challenge_methods_supported.join(", ")}). Aborting \u2014 plain PKCE is too weak for a public client.`
632
+ );
633
+ }
634
+ return doc;
635
+ }
636
+ function trimSlash(s) {
637
+ return s.endsWith("/") ? s.slice(0, -1) : s;
638
+ }
639
+ async function registerClient(registrationEndpoint, params) {
640
+ const body = JSON.stringify({
641
+ client_name: params.clientName,
642
+ redirect_uris: [params.redirectUri],
643
+ logo_uri: params.logoUri,
644
+ token_endpoint_auth_method: "none"
645
+ // public client
646
+ });
647
+ const res = await httpsCall(
648
+ "POST",
649
+ registrationEndpoint,
650
+ { "Content-Type": "application/json", Accept: "application/json" },
651
+ body
652
+ );
653
+ if (res.status === 429) {
654
+ throw new Error(
655
+ `OAuth client registration rate-limited (429). The backend allows ~10 registrations per IP per hour. Wait and retry, or use the password flow (drop the --oauth flag).`
656
+ );
657
+ }
658
+ if (res.status !== 201 && res.status !== 200) {
659
+ throw new Error(
660
+ `OAuth client registration failed: POST ${registrationEndpoint} \u2192 ${res.status} ${res.body.slice(0, 300)}`
661
+ );
662
+ }
663
+ let parsed;
664
+ try {
665
+ parsed = JSON.parse(res.body);
666
+ } catch {
667
+ throw new Error(`OAuth client registration returned non-JSON body`);
668
+ }
669
+ if (!parsed.client_id) {
670
+ throw new Error(`OAuth client registration response missing client_id`);
671
+ }
672
+ return parsed;
673
+ }
674
+ async function startLoopbackListener(opts) {
675
+ let resolveCallback;
676
+ let rejectCallback;
677
+ const callbackPromise = new Promise((res, rej) => {
678
+ resolveCallback = res;
679
+ rejectCallback = rej;
680
+ });
681
+ const server = createServer((req, res) => {
682
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
683
+ if (req.method !== "GET" || url.pathname !== "/callback") {
684
+ res.statusCode = 404;
685
+ res.end("Not Found");
686
+ return;
687
+ }
688
+ const params = url.searchParams;
689
+ const errParam = params.get("error");
690
+ if (errParam) {
691
+ const desc = params.get("error_description") ?? "";
692
+ res.statusCode = 400;
693
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
694
+ res.end(renderHtml("Authorization failed", `${errParam}${desc ? `: ${desc}` : ""}`));
695
+ rejectCallback(new Error(`OAuth authorization denied: ${errParam}${desc ? ` (${desc})` : ""}`));
696
+ return;
697
+ }
698
+ const code = params.get("code");
699
+ const state = params.get("state");
700
+ if (!code || !state) {
701
+ res.statusCode = 400;
702
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
703
+ res.end(renderHtml("Authorization failed", "Missing code or state parameter."));
704
+ rejectCallback(new Error("OAuth callback missing code or state"));
705
+ return;
706
+ }
707
+ if (state !== opts.expectedState) {
708
+ res.statusCode = 400;
709
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
710
+ res.end(renderHtml("Authorization failed", "Invalid state parameter (possible CSRF)."));
711
+ rejectCallback(new Error("OAuth callback state mismatch (possible CSRF)"));
712
+ return;
713
+ }
714
+ res.statusCode = 200;
715
+ res.setHeader("Content-Type", "text/html; charset=utf-8");
716
+ res.end(renderHtml("You're signed in", "You can close this tab and return to the terminal."));
717
+ resolveCallback({ code, state });
718
+ });
719
+ await new Promise((resolve, reject) => {
720
+ server.once("error", reject);
721
+ server.listen(0, "127.0.0.1", () => {
722
+ server.off("error", reject);
723
+ resolve();
724
+ });
725
+ });
726
+ const addr = server.address();
727
+ const redirectUri = `http://127.0.0.1:${addr.port}/callback`;
728
+ const timer = setTimeout(() => {
729
+ rejectCallback(new Error(`OAuth login timed out after ${Math.round(opts.timeoutMs / 1e3)}s`));
730
+ }, opts.timeoutMs);
731
+ return {
732
+ redirectUri,
733
+ waitForCallback: () => callbackPromise.finally(() => {
734
+ clearTimeout(timer);
735
+ }),
736
+ close: () => {
737
+ clearTimeout(timer);
738
+ server.close();
739
+ }
740
+ };
741
+ }
742
+ function renderHtml(title, message) {
743
+ const safeTitle = escapeHtml(title);
744
+ const safeMsg = escapeHtml(message);
745
+ return `<!doctype html>
746
+ <html lang="en"><head>
747
+ <meta charset="utf-8"><title>${safeTitle} \u2014 Leadbay MCP</title>
748
+ <style>
749
+ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;
750
+ display:flex;align-items:center;justify-content:center;height:100vh;
751
+ margin:0;background:#fafafa;color:#111}
752
+ .card{padding:32px 40px;border:1px solid #eee;border-radius:12px;
753
+ background:#fff;max-width:420px;text-align:center}
754
+ h1{font-size:18px;margin:0 0 12px;font-weight:600}
755
+ p{margin:0;color:#555;font-size:14px;line-height:1.5}
756
+ </style></head>
757
+ <body><div class="card"><h1>${safeTitle}</h1><p>${safeMsg}</p></div></body></html>`;
758
+ }
759
+ function escapeHtml(s) {
760
+ return s.replace(
761
+ /[&<>"']/g,
762
+ (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]
763
+ );
764
+ }
765
+ async function exchangeCodeForToken(opts) {
766
+ const form = new URLSearchParams({
767
+ grant_type: "authorization_code",
768
+ code: opts.code,
769
+ redirect_uri: opts.redirectUri,
770
+ client_id: opts.clientId,
771
+ code_verifier: opts.codeVerifier
772
+ }).toString();
773
+ const res = await httpsCall(
774
+ "POST",
775
+ opts.tokenEndpoint,
776
+ {
777
+ "Content-Type": "application/x-www-form-urlencoded",
778
+ Accept: "application/json"
779
+ },
780
+ form
781
+ );
782
+ if (res.status !== 200) {
783
+ throw new Error(
784
+ `OAuth token exchange failed: POST ${opts.tokenEndpoint} \u2192 ${res.status} ${res.body.slice(0, 300)}`
785
+ );
786
+ }
787
+ let parsed;
788
+ try {
789
+ parsed = JSON.parse(res.body);
790
+ } catch {
791
+ throw new Error("OAuth token endpoint returned non-JSON body");
792
+ }
793
+ if (!parsed.access_token) {
794
+ throw new Error(`OAuth token response missing access_token: ${res.body.slice(0, 200)}`);
795
+ }
796
+ return { accessToken: parsed.access_token };
797
+ }
798
+ async function openInBrowser(url) {
799
+ const platform = process.platform;
800
+ let cmd;
801
+ let args;
802
+ if (platform === "darwin") {
803
+ cmd = "open";
804
+ args = [url];
805
+ } else if (platform === "win32") {
806
+ cmd = "cmd";
807
+ args = ["/c", "start", '""', url];
808
+ } else {
809
+ cmd = "xdg-open";
810
+ args = [url];
811
+ }
812
+ await new Promise((resolve, reject) => {
813
+ const child = spawn3(cmd, args, { stdio: "ignore", detached: true });
814
+ child.on("error", reject);
815
+ child.on("spawn", () => {
816
+ child.unref();
817
+ resolve();
818
+ });
819
+ });
820
+ }
821
+ async function oauthLogin(opts) {
822
+ const log = opts.log ?? (() => {
823
+ });
824
+ const open = opts.openBrowser ?? openInBrowser;
825
+ const timeoutMs = opts.timeoutMs ?? 5 * 60 * 1e3;
826
+ log(`Discovering OAuth endpoints at ${opts.authServerBaseUrl}\u2026
827
+ `);
828
+ const doc = await fetchDiscoveryDoc(opts.authServerBaseUrl);
829
+ const state = base64UrlEncode(randomBytes(16));
830
+ const pkce = generatePkce();
831
+ log("Starting loopback listener on 127.0.0.1\u2026\n");
832
+ const listener = await startLoopbackListener({ expectedState: state, timeoutMs });
833
+ try {
834
+ log(`Registering client at ${doc.registration_endpoint}\u2026
835
+ `);
836
+ const client = await registerClient(doc.registration_endpoint, {
837
+ clientName: opts.clientName,
838
+ redirectUri: listener.redirectUri,
839
+ logoUri: opts.logoUri
840
+ });
841
+ const authorizeUrl = new URL(doc.authorization_endpoint);
842
+ authorizeUrl.searchParams.set("response_type", "code");
843
+ authorizeUrl.searchParams.set("client_id", client.client_id);
844
+ authorizeUrl.searchParams.set("redirect_uri", listener.redirectUri);
845
+ authorizeUrl.searchParams.set("state", state);
846
+ authorizeUrl.searchParams.set("code_challenge", pkce.challenge);
847
+ authorizeUrl.searchParams.set("code_challenge_method", pkce.method);
848
+ log(`Opening browser to authorize\u2026
849
+ ${authorizeUrl.toString()}
850
+ `);
851
+ try {
852
+ await open(authorizeUrl.toString());
853
+ } catch (err) {
854
+ log(
855
+ `Could not open browser automatically (${err?.message ?? err}). Open this URL manually:
856
+ ${authorizeUrl.toString()}
857
+ `
858
+ );
859
+ }
860
+ log("Waiting for authorization (5 min timeout)\u2026\n");
861
+ const { code } = await listener.waitForCallback();
862
+ log("Exchanging authorization code for access token\u2026\n");
863
+ const { accessToken } = await exchangeCodeForToken({
864
+ tokenEndpoint: doc.token_endpoint,
865
+ code,
866
+ codeVerifier: pkce.verifier,
867
+ clientId: client.client_id,
868
+ redirectUri: listener.redirectUri
869
+ });
870
+ return { accessToken };
871
+ } finally {
872
+ listener.close();
873
+ }
874
+ }
875
+
876
+ // installer/installer-gui.ts
877
+ var VERSION = "0.17.2";
878
+ var PORT = Number(process.env.LEADBAY_INSTALLER_PORT ?? 0);
879
+ var sessions = /* @__PURE__ */ new Map();
880
+ var OAUTH_BASE_URLS = {
881
+ prod: {
882
+ us: "https://api-us.leadbay.app",
883
+ fr: "https://api-fr.leadbay.app"
884
+ }
885
+ };
886
+ async function isLeadbayConfigured(client) {
887
+ if (client.id === "claude-code") {
888
+ return await isLeadbayConfiguredInClaudeCode();
889
+ }
890
+ if (client.id === "codex") {
891
+ const configPath2 = client.detail;
892
+ if (!existsSync2(configPath2)) return false;
893
+ try {
894
+ return readFileSync2(configPath2, "utf8").includes("[mcp_servers.leadbay]");
895
+ } catch {
896
+ return false;
897
+ }
898
+ }
899
+ if (client.id === "chatgpt-desktop") return false;
900
+ const configPath = client.configPath;
901
+ if (!configPath || !existsSync2(configPath)) return false;
902
+ try {
903
+ const parsed = JSON.parse(readFileSync2(configPath, "utf8"));
904
+ return Boolean(parsed?.mcpServers?.leadbay);
905
+ } catch {
906
+ return false;
907
+ }
908
+ }
909
+ async function clientsWithConfiguredStatus() {
910
+ const clients = await detectClients();
911
+ return await Promise.all(
912
+ clients.map(async (client) => ({
913
+ ...client,
914
+ configured: await isLeadbayConfigured(client)
915
+ }))
916
+ );
917
+ }
918
+ function sendJson(res, status, body) {
919
+ const raw = JSON.stringify(body);
920
+ res.writeHead(status, {
921
+ "content-type": "application/json; charset=utf-8",
922
+ "content-length": Buffer.byteLength(raw)
923
+ });
924
+ res.end(raw);
925
+ }
926
+ function sendSse(res, event) {
927
+ res.write(`data: ${JSON.stringify(event)}
928
+
929
+ `);
930
+ }
931
+ function readJson(req) {
932
+ return new Promise((resolve, reject) => {
933
+ const chunks = [];
934
+ req.on("data", (chunk) => chunks.push(Buffer.from(chunk)));
935
+ req.on("end", () => {
936
+ try {
937
+ resolve(JSON.parse(Buffer.concat(chunks).toString("utf8") || "{}"));
938
+ } catch (err) {
939
+ reject(err);
940
+ }
941
+ });
942
+ req.on("error", reject);
943
+ });
944
+ }
945
+ function cleanupSessions() {
946
+ const cutoff = Date.now() - 30 * 60 * 1e3;
947
+ for (const [id, session] of sessions) {
948
+ if (session.createdAt < cutoff) sessions.delete(id);
949
+ }
950
+ }
951
+ async function loginWithOAuth() {
952
+ cleanupSessions();
953
+ try {
954
+ const region = await inferRegionViaStargate({ staging: false });
955
+ const { hostname } = await import("os");
956
+ const { accessToken } = await oauthLogin({
957
+ authServerBaseUrl: OAUTH_BASE_URLS.prod[region],
958
+ clientName: `Leadbay MCP installer @ ${hostname()}`,
959
+ log: () => void 0
960
+ });
961
+ const sessionId = randomUUID();
962
+ const accountLabel = `Leadbay OAuth (${region.toUpperCase()})`;
963
+ sessions.set(sessionId, { token: accessToken, region, accountLabel, createdAt: Date.now() });
964
+ return { ok: true, sessionId, region, accountLabel };
965
+ } catch (err) {
966
+ return { ok: false, error: err?.message ?? String(err) };
967
+ }
968
+ }
969
+ function sanitizeOutput(raw) {
970
+ return raw.replace(/LEADBAY_TOKEN=("[^"]+"|'[^']+'|[^\s]+)/g, "LEADBAY_TOKEN=<redacted>");
971
+ }
972
+ function isManualSetupClient(client) {
973
+ return client.id === "chatgpt-desktop";
974
+ }
975
+ function isAllowedOrigin(req, expectedHost) {
976
+ const host = req.headers["host"] ?? "";
977
+ if (host !== expectedHost) return false;
978
+ const origin = req.headers["origin"];
979
+ if (!origin) return true;
980
+ return origin === `http://${expectedHost}` || origin === `http://127.0.0.1`;
981
+ }
982
+ var LOCAL_BIN_PATH = (() => {
983
+ const flag = process.argv.find((a) => a === "--local" || a.startsWith("--local="));
984
+ if (!flag) return void 0;
985
+ const explicit = flag.startsWith("--local=") ? flag.slice("--local=".length) : "";
986
+ if (explicit) {
987
+ return resolvePath(process.cwd(), explicit);
988
+ }
989
+ const here = typeof __dirname !== "undefined" ? __dirname : resolvePath(fileURLToPath(import.meta.url), "..");
990
+ return resolvePath(here, "..", "dist", "bin.js");
991
+ })();
992
+ async function installInto(client, session, includeWrite, telemetryEnabled) {
993
+ let res;
994
+ if (client.id === "claude-code") {
995
+ res = await installInClaudeCode(session.token, session.region, includeWrite, telemetryEnabled, LOCAL_BIN_PATH);
996
+ } else if (client.id === "codex") {
997
+ const configRes = await installInCodexConfig(client.configPath ?? client.detail, includeWrite, telemetryEnabled, LOCAL_BIN_PATH);
998
+ if (!configRes.ok) {
999
+ res = configRes;
1000
+ } else {
1001
+ const exportRes = await appendShellExports(session.token, session.region, includeWrite, telemetryEnabled);
1002
+ res = exportRes.ok ? { ok: true, message: `${configRes.message}; ${exportRes.message}` } : { ok: false, message: `config ${configRes.message}; ${exportRes.message}` };
1003
+ }
1004
+ } else if (client.id === "chatgpt-desktop") {
1005
+ res = { ok: true, message: "manual setup required; add this MCP URL in ChatGPT Settings > Apps: " + HOSTED_MCP_URL };
1006
+ } else if (client.id === "claude-desktop" && client.mode?.dxt && client.supportDir) {
1007
+ const dxtResult = await removeDxtExtension(client.supportDir);
1008
+ const jsonResult = await installInJsonConfig(client.configPath, session.token, session.region, includeWrite, telemetryEnabled, LOCAL_BIN_PATH);
1009
+ if (!jsonResult.ok) {
1010
+ res = jsonResult;
1011
+ } else {
1012
+ res = {
1013
+ ok: true,
1014
+ message: dxtResult.removed ? `DXT extension removed; ${jsonResult.message}` : jsonResult.message
1015
+ };
1016
+ }
1017
+ } else {
1018
+ res = await installInJsonConfig(client.configPath, session.token, session.region, includeWrite, telemetryEnabled, LOCAL_BIN_PATH);
1019
+ }
1020
+ return { id: client.id, label: client.label, ...res };
1021
+ }
1022
+ async function install(body) {
1023
+ cleanupSessions();
1024
+ const session = body.sessionId ? sessions.get(body.sessionId) : void 0;
1025
+ const clientIds = body.clientIds ?? [];
1026
+ if (!session) return { ok: false, output: "Login expired. Go back and sign in again." };
1027
+ if (!clientIds.length) return { ok: false, output: "Select at least one agent." };
1028
+ const detected = await detectClients();
1029
+ const selected = detected.filter((client) => clientIds.includes(client.id));
1030
+ if (!selected.length) return { ok: false, output: "No selected agents were detected on this machine." };
1031
+ const includeWrite = body.includeWrite !== false;
1032
+ const telemetryEnabled = body.telemetryEnabled !== false;
1033
+ const results = [];
1034
+ for (const client of selected) results.push(await installInto(client, session, includeWrite, telemetryEnabled));
1035
+ const output = [
1036
+ `Logged in to ${session.region.toUpperCase()} backend.`,
1037
+ `Settings: write tools ${includeWrite ? "on" : "off"}, telemetry ${telemetryEnabled ? "on" : "off"}.`,
1038
+ "",
1039
+ "Install summary:",
1040
+ ...results.map((result) => `${result.ok ? "OK" : "ERROR"} ${result.label}: ${result.message}`),
1041
+ "",
1042
+ "Restart your MCP client(s) to pick up the new server."
1043
+ ].join("\n");
1044
+ return { ok: results.some((result) => result.ok), output: sanitizeOutput(output), results };
1045
+ }
1046
+ async function streamInstall(url, res) {
1047
+ cleanupSessions();
1048
+ res.writeHead(200, {
1049
+ "content-type": "text/event-stream; charset=utf-8",
1050
+ "cache-control": "no-cache, no-transform",
1051
+ connection: "keep-alive"
1052
+ });
1053
+ const session = sessions.get(url.searchParams.get("sessionId") ?? "");
1054
+ const clientIds = (url.searchParams.get("clients") ?? "").split(",").filter(Boolean);
1055
+ const includeWrite = url.searchParams.get("write") !== "0";
1056
+ const telemetryEnabled = url.searchParams.get("telemetry") !== "0";
1057
+ const emit = (level, message) => sendSse(res, { level, message: sanitizeOutput(message) });
1058
+ if (!session) {
1059
+ emit("error", "Login expired. Go back and sign in again.");
1060
+ emit("done", "Install stopped.");
1061
+ res.end();
1062
+ return;
1063
+ }
1064
+ if (!clientIds.length) {
1065
+ emit("error", "Select at least one agent.");
1066
+ emit("done", "Install stopped.");
1067
+ res.end();
1068
+ return;
1069
+ }
1070
+ emit("info", `Connected to ${session.accountLabel}.`);
1071
+ emit("info", `Write tools ${includeWrite ? "enabled" : "disabled"}; telemetry ${telemetryEnabled ? "enabled" : "disabled"}.`);
1072
+ emit("info", "Refreshing installed-agent detection...");
1073
+ const detected = await detectClients();
1074
+ const selected = detected.filter((client) => clientIds.includes(client.id));
1075
+ const selectedHasOnlyManualSetup = selected.length > 0 && selected.every(isManualSetupClient);
1076
+ if (!selected.length) {
1077
+ emit("error", "No selected agents were detected on this machine.");
1078
+ emit("done", "Install stopped.");
1079
+ res.end();
1080
+ return;
1081
+ }
1082
+ let okCount = 0;
1083
+ for (const client of selected) {
1084
+ emit("active", isManualSetupClient(client) ? `Preparing ${client.label} manual setup...` : `Installing ${client.label}...`);
1085
+ const result = await installInto(client, session, includeWrite, telemetryEnabled);
1086
+ if (result.ok) {
1087
+ okCount += 1;
1088
+ emit("success", `${result.label}: ${result.message}`);
1089
+ } else {
1090
+ emit("error", `${result.label}: ${result.message}`);
1091
+ }
1092
+ }
1093
+ emit(okCount > 0 ? "success" : "error", selectedHasOnlyManualSetup ? "Manual ChatGPT setup instructions ready." : `${okCount}/${selected.length} agent(s) installed, updated, or prepared.`);
1094
+ emit("done", selectedHasOnlyManualSetup ? "Follow the manual setup instructions shown above." : "Restart your MCP client(s) to pick up the new server.");
1095
+ res.end();
1096
+ }
1097
+ async function streamUninstall(url, res) {
1098
+ res.writeHead(200, {
1099
+ "content-type": "text/event-stream; charset=utf-8",
1100
+ "cache-control": "no-cache, no-transform",
1101
+ connection: "keep-alive"
1102
+ });
1103
+ const clientIds = (url.searchParams.get("clients") ?? "").split(",").filter(Boolean);
1104
+ const emit = (level, message) => sendSse(res, { level, message });
1105
+ if (!clientIds.length) {
1106
+ emit("error", "Select at least one agent.");
1107
+ emit("done", "Uninstall stopped.");
1108
+ res.end();
1109
+ return;
1110
+ }
1111
+ const detected = await detectClients();
1112
+ const selected = detected.filter((c) => clientIds.includes(c.id));
1113
+ if (!selected.length) {
1114
+ emit("error", "No selected agents were detected on this machine.");
1115
+ emit("done", "Uninstall stopped.");
1116
+ res.end();
1117
+ return;
1118
+ }
1119
+ let okCount = 0;
1120
+ for (const client of selected) {
1121
+ emit("active", `Removing from ${client.label}...`);
1122
+ let res2;
1123
+ if (client.id === "claude-code") {
1124
+ res2 = await uninstallFromClaudeCode();
1125
+ } else if (client.id === "codex") {
1126
+ const tomlRes = await uninstallFromCodexConfig(client.configPath ?? client.detail);
1127
+ const shellRes = await uninstallShellExports();
1128
+ res2 = tomlRes.ok && shellRes.ok ? { ok: true, message: `${tomlRes.message}; ${shellRes.message}` } : { ok: false, message: `toml: ${tomlRes.message}; shell: ${shellRes.message}` };
1129
+ } else {
1130
+ res2 = await uninstallFromJsonConfig(client.configPath);
1131
+ }
1132
+ if (res2.ok) {
1133
+ okCount += 1;
1134
+ emit("success", `${client.label}: ${res2.message}`);
1135
+ } else {
1136
+ emit("error", `${client.label}: ${res2.message}`);
1137
+ }
1138
+ }
1139
+ emit(okCount > 0 ? "success" : "error", `${okCount}/${selected.length} agent(s) removed.`);
1140
+ emit("done", "Restart your MCP client(s) to complete the removal.");
1141
+ res.end();
1142
+ }
1143
+ function pageUninstallHtml() {
1144
+ return `<!doctype html>
1145
+ <html lang="en">
1146
+ <head>
1147
+ <meta charset="utf-8" />
1148
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1149
+ <title>Leadbay MCP uninstaller</title>
1150
+ <style>
1151
+ :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); }
1152
+ @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); } }
1153
+ * { box-sizing:border-box; }
1154
+ 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; }
1155
+ main { width:min(880px,100%); background:var(--panel); border:1px solid var(--line); border-radius:8px; box-shadow:var(--shadow); overflow:hidden; }
1156
+ header { padding:22px 24px 16px; border-bottom:1px solid var(--line); display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }
1157
+ h1 { font-size:22px; line-height:1.15; margin:0 0 6px; letter-spacing:0; }
1158
+ .meta { color:var(--muted); }
1159
+ .badge { border:1px solid var(--line); border-radius:999px; padding:5px 10px; color:var(--muted); white-space:nowrap; }
1160
+ .steps { display:grid; grid-template-columns:repeat(2,1fr); border-bottom:1px solid var(--line); }
1161
+ .step-pill { padding:12px 24px; border-right:1px solid var(--line); color:var(--muted); font-weight:700; }
1162
+ .step-pill:last-child { border-right:0; }
1163
+ .step-pill.active { color:var(--text); background:color-mix(in srgb,var(--danger),transparent 88%); }
1164
+ section { padding:22px 24px; }
1165
+ .hidden { display:none; }
1166
+ .hint,.detail { color:var(--muted); }
1167
+ .agents { display:grid; gap:8px; margin-top:12px; }
1168
+ .agent { display:grid; grid-template-columns:auto 1fr; gap:12px; align-items:center; padding:12px; border:1px solid var(--line); border-radius:6px; }
1169
+ .agent strong { display:block; }
1170
+ .agent input { width:18px; min-height:18px; }
1171
+ .actions { display:flex; justify-content:space-between; gap:10px; border-top:1px solid var(--line); padding:16px 24px 20px; }
1172
+ .right-actions { display:flex; gap:10px; }
1173
+ 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; }
1174
+ button.danger { background:var(--danger); border-color:var(--danger); color:#fff; }
1175
+ button:disabled { opacity:.6; cursor:wait; }
1176
+ .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; }
1177
+ .log-row { display:flex; gap:10px; align-items:flex-start; padding:3px 0; white-space:pre-wrap; word-break:break-word; }
1178
+ .log-row::before { width:56px; flex:0 0 56px; font-weight:800; text-transform:uppercase; font-size:11px; letter-spacing:.02em; }
1179
+ .log-info { color:var(--muted); } .log-info::before { content:"info"; }
1180
+ .log-active { color:#c99700; } .log-active::before { content:"run"; }
1181
+ .log-success { color:#19a974; } .log-success::before { content:"ok"; }
1182
+ .log-error { color:var(--danger); } .log-error::before { content:"error"; }
1183
+ .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); }
1184
+ .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); }
1185
+ @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;} }
1186
+ </style>
1187
+ </head>
1188
+ <body>
1189
+ <main>
1190
+ <header><div><h1>Leadbay MCP uninstaller</h1><div class="meta" id="meta">${formatInstallOsLabel()}</div></div><div class="badge">v${VERSION}</div></header>
1191
+ <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>
1192
+
1193
+ <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>
1194
+ <section id="step-2" class="hidden"><strong>Removing</strong><div class="hint">Keep this window open until the final message appears.</div></section>
1195
+
1196
+ <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>
1197
+ <div id="log" class="log-panel"><div class="log-row log-info">Ready.</div></div>
1198
+ </main>
1199
+ <script>
1200
+ const $ = (id) => document.getElementById(id);
1201
+ let step = 1;
1202
+ let clients = [];
1203
+ function clearLog() { $("log").innerHTML = ""; }
1204
+ 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; }
1205
+ function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c])); }
1206
+ 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); }
1207
+ 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(""); }
1208
+ 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."); }
1209
+ 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(); }; }
1210
+ $("back").addEventListener("click", () => setStep(1));
1211
+ $("refresh").addEventListener("click", refresh);
1212
+ $("next").addEventListener("click", doUninstall);
1213
+ refresh();
1214
+ </script>
1215
+ </body>
1216
+ </html>`;
1217
+ }
1218
+ function pageHtml() {
1219
+ return `<!doctype html>
1220
+ <html lang="en">
1221
+ <head>
1222
+ <meta charset="utf-8" />
1223
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
1224
+ <title>Leadbay MCP installer</title>
1225
+ <style>
1226
+ :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); }
1227
+ @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); } }
1228
+ * { box-sizing:border-box; }
1229
+ 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; }
1230
+ main { width:min(880px,100%); background:var(--panel); border:1px solid var(--line); border-radius:8px; box-shadow:var(--shadow); overflow:hidden; }
1231
+ header { padding:22px 24px 16px; border-bottom:1px solid var(--line); display:flex; align-items:flex-start; justify-content:space-between; gap:16px; }
1232
+ h1 { font-size:22px; line-height:1.15; margin:0 0 6px; letter-spacing:0; }
1233
+ .meta,.hint,.detail,label span { color:var(--muted); }
1234
+ .badge { border:1px solid var(--line); border-radius:999px; padding:5px 10px; color:var(--muted); white-space:nowrap; }
1235
+ .steps { display:grid; grid-template-columns:repeat(4,1fr); border-bottom:1px solid var(--line); }
1236
+ .step-pill { padding:12px 24px; border-right:1px solid var(--line); color:var(--muted); font-weight:700; }
1237
+ .step-pill:last-child { border-right:0; }
1238
+ .step-pill.active { color:var(--text); background:color-mix(in srgb,var(--accent),transparent 88%); }
1239
+ section { padding:22px 24px; }
1240
+ .hidden { display:none; }
1241
+ .grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:14px; }
1242
+ label { display:grid; gap:6px; font-weight:650; }
1243
+ 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; }
1244
+ .options { display:flex; gap:14px; flex-wrap:wrap; margin-top:14px; }
1245
+ .toggle { display:inline-flex; align-items:center; gap:8px; font-weight:600; }
1246
+ .toggle input { width:16px; min-height:16px; }
1247
+ .setting-card { display:grid; gap:4px; max-width:360px; }
1248
+ .setting-card .hint { padding-left:24px; }
1249
+ .agents { display:grid; gap:8px; margin-top:12px; }
1250
+ .agent { display:grid; grid-template-columns:auto 1fr; gap:12px; align-items:center; padding:12px; border:1px solid var(--line); border-radius:6px; }
1251
+ .agent strong { display:block; }
1252
+ .agent input { width:18px; min-height:18px; }
1253
+ .actions { display:flex; justify-content:space-between; gap:10px; border-top:1px solid var(--line); padding:16px 24px 20px; }
1254
+ .right-actions { display:flex; gap:10px; }
1255
+ 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; }
1256
+ button.primary { background:var(--accent); border-color:var(--accent); color:#fff; }
1257
+ button.primary:hover { background:var(--accent2); }
1258
+ button:disabled { opacity:.6; cursor:wait; }
1259
+ .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; }
1260
+ .log-row { display:flex; gap:10px; align-items:flex-start; padding:3px 0; white-space:pre-wrap; word-break:break-word; }
1261
+ .log-row::before { width:56px; flex:0 0 56px; font-weight:800; text-transform:uppercase; font-size:11px; letter-spacing:.02em; }
1262
+ .log-info { color:var(--muted); }
1263
+ .log-info::before { content:"info"; }
1264
+ .log-active { color:#c99700; }
1265
+ .log-active::before { content:"run"; }
1266
+ .log-success { color:#19a974; }
1267
+ .log-success::before { content:"ok"; }
1268
+ .log-error { color:var(--danger); }
1269
+ .log-error::before { content:"error"; }
1270
+ .error { color:var(--danger); }
1271
+ .badge-install,.badge-update { font-size:10px; font-weight:800; padding:2px 6px; border-radius:999px; vertical-align:middle; }
1272
+ .badge-install { background:color-mix(in srgb,var(--accent),transparent 80%); color:var(--accent2); }
1273
+ .badge-update { background:color-mix(in srgb,#c99700,transparent 80%); color:#a07800; }
1274
+ @media (prefers-color-scheme: dark) { .badge-update { color:#f0c040; } .badge-install { color:#00c9a0; } }
1275
+ @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;} }
1276
+ </style>
1277
+ </head>
1278
+ <body>
1279
+ <main>
1280
+ <header><div><h1>Leadbay MCP installer</h1><div class="meta" id="meta">${formatInstallOsLabel()}</div></div><div class="badge">v${VERSION}</div></header>
1281
+ <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>
1282
+
1283
+ <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>
1284
+ <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>
1285
+ <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>
1286
+
1287
+ <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>
1288
+ <div id="log" class="log-panel"><div class="log-row log-info">Ready.</div></div>
1289
+ </main>
1290
+ <script>
1291
+ const $ = (id) => document.getElementById(id);
1292
+ let step = 1;
1293
+ let sessionId = null;
1294
+ let clients = [];
1295
+ function clearLog() { $("log").innerHTML = ""; }
1296
+ 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; }
1297
+ function line(text, error = false) { clearLog(); appendLog(error ? "error" : "info", text); }
1298
+ function esc(s) { return String(s).replace(/[&<>"']/g, (c) => ({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c])); }
1299
+ 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"; }
1300
+ 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(""); }
1301
+ 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."); }
1302
+ 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; } }
1303
+ 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(); }; }
1304
+ $("back").addEventListener("click", () => setStep(Math.max(1, step - 1)));
1305
+ $("refresh").addEventListener("click", refresh);
1306
+ $("next").addEventListener("click", async () => { if (step === 1) await doLogin(); else await install(); });
1307
+ </script>
1308
+ </body>
1309
+ </html>`;
1310
+ }
1311
+ async function openBrowser(url) {
1312
+ const { spawn: spawn4 } = await import("child_process");
1313
+ const trySpawn = (command, args) => new Promise((resolve) => {
1314
+ try {
1315
+ const child = spawn4(command, args, { stdio: "ignore", detached: true });
1316
+ child.unref();
1317
+ child.on("error", () => resolve(false));
1318
+ child.on("close", (code) => resolve(code === 0));
1319
+ } catch {
1320
+ resolve(false);
1321
+ }
1322
+ });
1323
+ if (process.platform === "darwin") {
1324
+ await trySpawn("open", [url]);
1325
+ return;
1326
+ }
1327
+ if (process.platform === "win32") {
1328
+ await trySpawn("cmd", ["/c", "start", "", url]);
1329
+ return;
1330
+ }
1331
+ const candidates = ["xdg-open", "sensible-browser", "google-chrome", "chromium-browser", "firefox"];
1332
+ for (const cmd of candidates) {
1333
+ if (await trySpawn(cmd, [url])) return;
1334
+ }
1335
+ process.stderr.write(`
1336
+ Open this URL in your browser to continue:
1337
+ ${url}
1338
+
1339
+ `);
1340
+ }
1341
+ async function startInstallerGui(options = {}) {
1342
+ let expectedHost = `127.0.0.1:${(options.port ?? PORT) || 0}`;
1343
+ const server = createServer2(async (req, res) => {
1344
+ if (!isAllowedOrigin(req, expectedHost)) {
1345
+ sendJson(res, 403, { ok: false, error: "forbidden" });
1346
+ return;
1347
+ }
1348
+ try {
1349
+ if (req.method === "GET" && req.url === "/") {
1350
+ const raw = pageHtml();
1351
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "content-length": Buffer.byteLength(raw) });
1352
+ res.end(raw);
1353
+ return;
1354
+ }
1355
+ if (req.method === "GET" && req.url === "/api/status") {
1356
+ sendJson(res, 200, {
1357
+ os: formatInstallOsLabel(),
1358
+ hostedMcpUrl: HOSTED_MCP_URL,
1359
+ clients: await clientsWithConfiguredStatus()
1360
+ });
1361
+ return;
1362
+ }
1363
+ if (req.method === "POST" && req.url === "/api/oauth-login") {
1364
+ sendJson(res, 200, await loginWithOAuth());
1365
+ return;
1366
+ }
1367
+ if (req.method === "POST" && req.url === "/api/install") {
1368
+ sendJson(res, 200, await install(await readJson(req)));
1369
+ return;
1370
+ }
1371
+ if (req.method === "GET" && req.url?.startsWith("/api/install-stream")) {
1372
+ await streamInstall(new URL(req.url, "http://127.0.0.1"), res);
1373
+ return;
1374
+ }
1375
+ sendJson(res, 404, { ok: false, error: "not found" });
1376
+ } catch (err) {
1377
+ sendJson(res, 500, { ok: false, error: err?.message ?? String(err) });
1378
+ }
1379
+ });
1380
+ return await new Promise((resolve, reject) => {
1381
+ server.once("error", reject);
1382
+ server.listen(options.port ?? PORT, "127.0.0.1", async () => {
1383
+ server.off("error", reject);
1384
+ const address = server.address();
1385
+ const port = typeof address === "object" && address ? address.port : options.port ?? PORT;
1386
+ expectedHost = `127.0.0.1:${port}`;
1387
+ const url = `http://127.0.0.1:${port}/`;
1388
+ process.stderr.write(`Leadbay MCP installer GUI: ${url}
1389
+ `);
1390
+ if (options.openBrowser !== false) await openBrowser(url).catch(() => void 0);
1391
+ resolve({ url, close: () => new Promise((closeResolve, closeReject) => server.close((err) => err ? closeReject(err) : closeResolve())) });
1392
+ });
1393
+ });
1394
+ }
1395
+ async function startUninstallerGui(options = {}) {
1396
+ let expectedHost = `127.0.0.1:${(options.port ?? PORT) || 0}`;
1397
+ const server = createServer2(async (req, res) => {
1398
+ if (!isAllowedOrigin(req, expectedHost)) {
1399
+ sendJson(res, 403, { ok: false, error: "forbidden" });
1400
+ return;
1401
+ }
1402
+ try {
1403
+ if (req.method === "GET" && req.url === "/") {
1404
+ const raw = pageUninstallHtml();
1405
+ res.writeHead(200, { "content-type": "text/html; charset=utf-8", "content-length": Buffer.byteLength(raw) });
1406
+ res.end(raw);
1407
+ return;
1408
+ }
1409
+ if (req.method === "GET" && req.url === "/api/status") {
1410
+ sendJson(res, 200, {
1411
+ os: formatInstallOsLabel(),
1412
+ clients: await clientsWithConfiguredStatus()
1413
+ });
1414
+ return;
1415
+ }
1416
+ if (req.method === "GET" && req.url?.startsWith("/api/uninstall-stream")) {
1417
+ await streamUninstall(new URL(req.url, "http://127.0.0.1"), res);
1418
+ return;
1419
+ }
1420
+ sendJson(res, 404, { ok: false, error: "not found" });
1421
+ } catch (err) {
1422
+ sendJson(res, 500, { ok: false, error: err?.message ?? String(err) });
1423
+ }
1424
+ });
1425
+ return await new Promise((resolve, reject) => {
1426
+ server.once("error", reject);
1427
+ server.listen(options.port ?? PORT, "127.0.0.1", async () => {
1428
+ server.off("error", reject);
1429
+ const address = server.address();
1430
+ const port = typeof address === "object" && address ? address.port : options.port ?? PORT;
1431
+ expectedHost = `127.0.0.1:${port}`;
1432
+ const url = `http://127.0.0.1:${port}/`;
1433
+ process.stderr.write(`Leadbay MCP uninstaller GUI: ${url}
1434
+ `);
1435
+ if (options.openBrowser !== false) await openBrowser(url).catch(() => void 0);
1436
+ resolve({ url, close: () => new Promise((closeResolve, closeReject) => server.close((err) => err ? closeReject(err) : closeResolve())) });
1437
+ });
1438
+ });
1439
+ }
1440
+ async function main() {
1441
+ const uninstall = process.argv.includes("--uninstall");
1442
+ const handle = uninstall ? await startUninstallerGui({ openBrowser: !process.argv.includes("--no-open") }) : await startInstallerGui({ openBrowser: !process.argv.includes("--no-open") });
1443
+ await new Promise((resolve) => {
1444
+ process.once("SIGINT", () => resolve());
1445
+ process.once("SIGTERM", () => resolve());
1446
+ });
1447
+ await handle.close().catch(() => void 0);
1448
+ }
1449
+ var isEntrypoint = (() => {
1450
+ try {
1451
+ const entry = process.argv[1];
1452
+ if (!entry) return false;
1453
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(entry);
1454
+ } catch {
1455
+ return false;
1456
+ }
1457
+ })();
1458
+ if (isEntrypoint) {
1459
+ main().catch((err) => {
1460
+ process.stderr.write(`leadbay-mcp-installer: ${err?.message ?? err}
1461
+ `);
1462
+ process.exit(1);
1463
+ });
1464
+ }
1465
+ export {
1466
+ install,
1467
+ sanitizeOutput,
1468
+ startInstallerGui,
1469
+ startUninstallerGui
1470
+ };