@leadbay/mcp 0.23.7 → 0.23.9

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 CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog — @leadbay/mcp
2
2
 
3
+ ## 0.23.9 — 2026-06-27
4
+
5
+ MCP data plane now targets backend **/1.6** (was /1.5).
6
+
7
+ - **`client`** — every request path is built from a single `API_VERSION = "1.6"` constant (exported as `API_PREFIX`). `LeadbayClient` (request / requestVoid / requestRawBinary + mock-mode paths) and `bin.ts`'s login probe all reference it, so future backend version bumps are a one-line change. Verified live on `api-us` before shipping: `/1.6` mirrors `/1.5` across the full route surface probed (monitor, lenses, auth/login, kpi, geo) — identical status on every call. Tool-description prose and route-doc comments updated to /1.6 to match.
8
+
9
+ ## 0.23.8 — 2026-06-26
10
+
11
+ Installer no longer hangs in Claude Cowork / headless terminals (product#3805).
12
+
13
+ - **`installer`** — the guided installer awaited a detached browser-opener that resolved only on the child's `close` event, which a detached process often never emits; with no overall timeout the process dangled until the host (Claude Cowork) killed it ("running… then timeout", intermittently). It now reuses the hardened `openInBrowser` (resolves on `spawn`, not `close`), no longer blocks GUI startup on the browser open, and bounds the run with a watchdog that exits cleanly with hosted-MCP instructions instead of hanging. The installer always tries to open the browser — it never guesses "headless" and refuses to start.
14
+ - **Docs** — README documents adding Leadbay on **Claude web / Cowork web** as a custom Connector (`Customize → Connectors → Add custom connector`, URL `https://leadbay-mcp-prod.fly.dev/mcp`; OAuth runs in-app), and the `claude mcp add --transport http` equivalent. Stale `@0.16` install snippets corrected to `@latest`.
15
+
3
16
  ## 0.23.7 — 2026-06-25
4
17
 
5
18
  Field-sales tour always renders the map (product#3779).
package/README.md CHANGED
@@ -59,6 +59,28 @@ On Linux, use the terminal-only path instead (no desktop installer window):
59
59
  npx -y @leadbay/mcp@latest install --oauth
60
60
  ```
61
61
 
62
+ ### Claude web / Cowork on the web (no terminal)
63
+
64
+ On the web there's no terminal, so `npx … installer` doesn't apply — add
65
+ Leadbay as a **custom Connector** instead. Claude connects to the hosted MCP
66
+ from Anthropic's cloud and runs the OAuth sign-in in-app (no localhost
67
+ callback, no client id/secret to fill in):
68
+
69
+ 1. **Customize → Connectors → "+" → Add custom connector**
70
+ 2. **Name:** `Leadbay` · **URL:** `https://leadbay-mcp-prod.fly.dev/mcp`
71
+ 3. **Add**, then complete the Leadbay sign-in prompt.
72
+
73
+ The same hosted URL works from a terminal that has the Claude Code CLI:
74
+
75
+ ```bash
76
+ claude mcp add --transport http leadbay https://leadbay-mcp-prod.fly.dev/mcp
77
+ ```
78
+
79
+ In **Claude Cowork with a terminal**, the guided `installer` works as usual —
80
+ it opens your browser for sign-in and configures the detected clients. If the
81
+ browser never opens, the installer no longer hangs: it exits after a short
82
+ watchdog with the Connector / `claude mcp add` instructions above.
83
+
62
84
  From a repo checkout, run the same native installer with:
63
85
 
64
86
  ```bash
@@ -139,7 +161,7 @@ You can verify the skills installed by running `/skill list` after install. To u
139
161
 
140
162
  ### Claude Desktop
141
163
 
142
- The guided installer writes the local `mcpServers.leadbay` entry in `claude_desktop_config.json` with `npx -y @leadbay/mcp@0.16`, like the Linux flow. Restart Claude Desktop after install.
164
+ The guided installer writes the local `mcpServers.leadbay` entry in `claude_desktop_config.json` with `npx -y @leadbay/mcp@latest`, like the Linux flow. Restart Claude Desktop after install.
143
165
 
144
166
  The `.dxt` / `.mcpb` bundle from [Releases](https://github.com/leadbay/leadclaw/releases/latest) remains available as an alternative install path.
145
167
 
@@ -147,14 +169,14 @@ The `.dxt` / `.mcpb` bundle from [Releases](https://github.com/leadbay/leadclaw/
147
169
 
148
170
  If you installed Node from the official [nodejs.org](https://nodejs.org) `.pkg`, `/usr/local/lib/node_modules` is root-owned. Any of these works:
149
171
 
150
- - **Use `npx` (recommended, no global install):** all examples above use `npx -y @leadbay/mcp@0.16 ...` — no global install needed.
172
+ - **Use `npx` (recommended, no global install):** all examples above use `npx -y @leadbay/mcp@latest ...` — no global install needed.
151
173
  - **`sudo npm install -g @leadbay/mcp`** (enter your macOS password).
152
174
  - **Use a Node version manager** — [nvm](https://github.com/nvm-sh/nvm), [volta](https://volta.sh), [fnm](https://github.com/Schniz/fnm). They install Node under your home directory, so `npm install -g` works without sudo.
153
175
 
154
176
  ### If you'd rather authenticate without auto-install
155
177
 
156
178
  ```bash
157
- npx -y @leadbay/mcp@0.16 login --oauth
179
+ npx -y @leadbay/mcp@latest login --oauth
158
180
  ```
159
181
 
160
182
  Default writes a `0600`-mode JSON file at the platform-correct credentials path (`$XDG_CONFIG_HOME/leadbay/credentials.json` on Linux, `~/Library/Application Support/leadbay/credentials.json` on macOS, `%APPDATA%\leadbay\credentials.json` on Windows). Pass `--write-config /some/path.json` to override the path. Pass `--force` to overwrite an existing file from a different account. The legacy email/password path still exists for scripts that cannot use a browser.
@@ -220,7 +242,7 @@ That is convenient for quick tests, but less safe for production because the dep
220
242
  ## 3. Quickstart
221
243
 
222
244
  Prefer the installer in [§1](#1-install). If you need to hand-write config,
223
- first authenticate with `npx -y @leadbay/mcp@0.16 login --oauth`, then copy the
245
+ first authenticate with `npx -y @leadbay/mcp@latest login --oauth`, then copy the
224
246
  `LEADBAY_TOKEN` and `LEADBAY_REGION` values from the credentials file it writes.
225
247
 
226
248
  ### Claude Desktop
@@ -232,7 +254,7 @@ Edit `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) o
232
254
  "mcpServers": {
233
255
  "leadbay": {
234
256
  "command": "npx",
235
- "args": ["-y", "@leadbay/mcp@0.16"],
257
+ "args": ["-y", "@leadbay/mcp@latest"],
236
258
  "env": {
237
259
  "LEADBAY_TOKEN": "<paste-oauth-token>",
238
260
  "LEADBAY_REGION": "us"
@@ -253,7 +275,7 @@ In Cursor settings, add the MCP server:
253
275
  "mcp.servers": {
254
276
  "leadbay": {
255
277
  "command": "npx",
256
- "args": ["-y", "@leadbay/mcp@0.16"],
278
+ "args": ["-y", "@leadbay/mcp@latest"],
257
279
  "env": { "LEADBAY_TOKEN": "<paste-oauth-token>", "LEADBAY_REGION": "us" }
258
280
  }
259
281
  }
@@ -266,7 +288,7 @@ In Cursor settings, add the MCP server:
266
288
  claude mcp add leadbay --scope user \
267
289
  --env LEADBAY_TOKEN=<paste-oauth-token> \
268
290
  --env LEADBAY_REGION=us \
269
- -- npx -y @leadbay/mcp@0.16
291
+ -- npx -y @leadbay/mcp@latest
270
292
  ```
271
293
 
272
294
  > **`--scope user`** registers Leadbay globally for your account (visible from any project). Without it, `claude mcp add` defaults to project-local scope and the server only appears in conversations opened from the directory where you ran the command.
@@ -278,7 +300,7 @@ claude mcp add leadbay --scope user \
278
300
  Before starting Claude, run:
279
301
 
280
302
  ```bash
281
- LEADBAY_TOKEN=<paste-oauth-token> npx -y @leadbay/mcp@0.16 doctor
303
+ LEADBAY_TOKEN=<paste-oauth-token> npx -y @leadbay/mcp@latest doctor
282
304
  ```
283
305
 
284
306
  Expected output:
@@ -524,14 +546,14 @@ The user's literal text replaces `verification.ref` in the outreach record, and
524
546
  | `No enrichment credits remaining` | Out of quota | Contact Leadbay support to extend quota |
525
547
  | Claude Desktop "loading forever" on first use | `npx` cold-start fetching the package | First run takes ~10s. Prefer `npm install -g @leadbay/mcp` for faster startup. |
526
548
  | Claude Desktop doesn't show Leadbay tools | Server crashed at startup | Check `~/Library/Logs/Claude/mcp*.log` (macOS) or `%APPDATA%\Claude\logs\mcp*.log` (Windows). |
527
- | Claude Code can't find Leadbay in a new conversation | MCP server installed at project scope (default before 0.3.0) | Re-run with `--scope user`: `claude mcp remove leadbay && claude mcp add leadbay --scope user --env LEADBAY_TOKEN=… --env LEADBAY_REGION=us -- npx -y @leadbay/mcp@0.16` |
549
+ | Claude Code can't find Leadbay in a new conversation | MCP server installed at project scope (default before 0.3.0) | Re-run with `--scope user`: `claude mcp remove leadbay && claude mcp add leadbay --scope user --env LEADBAY_TOKEN=… --env LEADBAY_REGION=us -- npx -y @leadbay/mcp@latest` |
528
550
  | Agent reports "tool not found" for `refine_prompt` / `adjust_audience` etc. | Pre-0.3.0 install with `LEADBAY_MCP_WRITE` unset (writes were off) | Either re-run `npx @leadbay/mcp install` or remove `LEADBAY_MCP_WRITE=0` from your client config (writes are on by default in 0.3.0+) |
529
551
 
530
552
  ## 7. Upgrade & rotation
531
553
 
532
- **Upgrade**: change the pinned minor in your config, e.g. `"@leadbay/mcp@0.2"` `"@leadbay/mcp@0.16"`, then restart the client. **0.3.0 enables composite write tools by default** — see [MIGRATION.md](./MIGRATION.md). See also the [changelog](https://github.com/leadbay/leadclaw/releases).
554
+ **Upgrade**: the guided installer pins `@leadbay/mcp@latest`, so each MCP session already picks up the newest published version — just restart the client. (If you hand-pinned a specific version in your config, change it to `@latest` or the version you want, then restart.) **0.3.0 enables composite write tools by default** — see [MIGRATION.md](./MIGRATION.md). See also the [changelog](https://github.com/leadbay/leadclaw/releases).
533
555
 
534
- **Rotate local credential**: re-run `npx -y @leadbay/mcp@0.16 install --oauth` (or `login --oauth`) — the new credential replaces the old one in your MCP client config.
556
+ **Rotate local credential**: re-run `npx -y @leadbay/mcp@latest install --oauth` (or `login --oauth`) — the new credential replaces the old one in your MCP client config.
535
557
 
536
558
  ## 8. Advanced
537
559
 
@@ -684,7 +706,7 @@ After your first authenticated call, your PostHog `distinctId` is set to your Le
684
706
  "mcpServers": {
685
707
  "leadbay": {
686
708
  "command": "npx",
687
- "args": ["-y", "@leadbay/mcp@0.16"],
709
+ "args": ["-y", "@leadbay/mcp@latest"],
688
710
  "env": {
689
711
  "LEADBAY_TOKEN": "u.…",
690
712
  "LEADBAY_REGION": "us",
package/dist/bin.js CHANGED
@@ -68,7 +68,7 @@ async function resolveRegion(email, password, startWith = "us") {
68
68
  const baseUrl = REGIONS[region];
69
69
  const body = JSON.stringify({ email, password });
70
70
  try {
71
- const res = await httpsRequest("POST", `${baseUrl}/1.5/auth/login`, { "Content-Type": "application/json" }, body);
71
+ const res = await httpsRequest("POST", `${baseUrl}${API_PREFIX}/auth/login`, { "Content-Type": "application/json" }, body);
72
72
  if (res.status === 200) {
73
73
  const parsed = JSON.parse(res.body);
74
74
  if (parsed?.token) {
@@ -159,7 +159,7 @@ function parseRetryAfter(value) {
159
159
  }
160
160
  return null;
161
161
  }
162
- var LENS_CACHE_TTL_MS, TASTE_CACHE_TTL_MS, ME_CACHE_TTL_MS, MAX_CONCURRENT, REGIONS, _mockFixtures, _mockJournal, LeadbayClient;
162
+ var LENS_CACHE_TTL_MS, TASTE_CACHE_TTL_MS, ME_CACHE_TTL_MS, MAX_CONCURRENT, REGIONS, API_VERSION, API_PREFIX, _mockFixtures, _mockJournal, LeadbayClient;
163
163
  var init_client = __esm({
164
164
  "../core/dist/client.js"() {
165
165
  "use strict";
@@ -171,6 +171,8 @@ var init_client = __esm({
171
171
  us: "https://api-us.leadbay.app",
172
172
  fr: "https://api-fr.leadbay.app"
173
173
  };
174
+ API_VERSION = "1.6";
175
+ API_PREFIX = `/${API_VERSION}`;
174
176
  _mockFixtures = null;
175
177
  _mockJournal = [];
176
178
  LeadbayClient = class {
@@ -313,7 +315,7 @@ var init_client = __esm({
313
315
  const retryOn401 = opts?.retryOn401 !== false;
314
316
  await this.acquireSemaphore();
315
317
  try {
316
- const url = `${this._baseUrl}/1.5${path}`;
318
+ const url = `${this._baseUrl}${API_PREFIX}${path}`;
317
319
  const headers = {
318
320
  Authorization: `Bearer ${this.token}`
319
321
  };
@@ -348,7 +350,7 @@ var init_client = __esm({
348
350
  }
349
351
  await this.acquireSemaphore();
350
352
  try {
351
- const url = `${this._baseUrl}/1.5${path}`;
353
+ const url = `${this._baseUrl}${API_PREFIX}${path}`;
352
354
  const headers = {
353
355
  Authorization: `Bearer ${this.token}`
354
356
  };
@@ -373,7 +375,7 @@ var init_client = __esm({
373
375
  // serialized body (string for text payloads such as CSV; Buffer for binary
374
376
  // uploads). Auth, semaphore, error mapping, _lastMeta, and mock-mode all
375
377
  // mirror request() exactly. Used by leadbay_import_leads to upload CSVs to
376
- // the wizard at POST /1.5/imports.
378
+ // the wizard at POST /1.6/imports.
377
379
  async requestRawBinary(method, path, contentType, body) {
378
380
  if (process.env.LEADBAY_MOCK === "1") {
379
381
  return this.mockRequestBinary(method, path, contentType, body);
@@ -383,7 +385,7 @@ var init_client = __esm({
383
385
  }
384
386
  await this.acquireSemaphore();
385
387
  try {
386
- const url = `${this._baseUrl}/1.5${path}`;
388
+ const url = `${this._baseUrl}${API_PREFIX}${path}`;
387
389
  const headers = {
388
390
  Authorization: `Bearer ${this.token}`,
389
391
  "Content-Type": contentType
@@ -407,7 +409,7 @@ var init_client = __esm({
407
409
  }
408
410
  }
409
411
  mockRequest(method, path, body) {
410
- const fullPath = `/1.5${path}`;
412
+ const fullPath = `${API_PREFIX}${path}`;
411
413
  this._lastMeta = {
412
414
  region: this._region,
413
415
  endpoint: `${method} ${path}`,
@@ -430,7 +432,7 @@ var init_client = __esm({
430
432
  };
431
433
  }
432
434
  mockRequestBinary(method, path, contentType, body) {
433
- const fullPath = `/1.5${path}`;
435
+ const fullPath = `${API_PREFIX}${path}`;
434
436
  this._lastMeta = {
435
437
  region: this._region,
436
438
  endpoint: `${method} ${path}`,
@@ -5695,7 +5697,7 @@ WHEN TO USE: at the start of a session to know what the agent can/can't do, afte
5695
5697
 
5696
5698
  WHEN NOT TO USE: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating. And: a recent quota snapshot showing "exhausted" is NOT a reason to refuse a write call when the user has just topped up \u2014 re-call this tool first, then proceed.
5697
5699
  `;
5698
- leadbay_acknowledge_notification = `Acknowledge a Leadbay notification \u2014 i.e. tell the MCP and the backend "I've seen this and acted on it." Wraps \`POST /1.5/notifications/{id}/seen\` (default) or \`/archive\` (when \`archive:true\`) and drops the entry from the local inbox so subsequent \`_meta.notifications\` payloads stop carrying it.
5700
+ leadbay_acknowledge_notification = `Acknowledge a Leadbay notification \u2014 i.e. tell the MCP and the backend "I've seen this and acted on it." Wraps \`POST /1.6/notifications/{id}/seen\` (default) or \`/archive\` (when \`archive:true\`) and drops the entry from the local inbox so subsequent \`_meta.notifications\` payloads stop carrying it.
5699
5701
 
5700
5702
  **When to call.** After you read an entry from \`_meta.notifications\` or \`account_status.notifications\` and have revised whatever prior output the just-finished background work might have made stale (outreach drafts, lead lists, "available leads" claims, followup plans). Mark-seen tells the human team's pipeline you handled this and prevents the notification from re-surfacing on every subsequent tool response.
5701
5703
 
@@ -6305,7 +6307,7 @@ WHEN NOT TO USE: from agent flow \u2014 leadbay_adjust_audience handles the draf
6305
6307
 
6306
6308
  This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
6307
6309
  `;
6308
- leadbay_create_topup_link = `Generate a one-shot Stripe checkout session for an AI-credits top-up. Wraps \`POST /1.5/stripe/topup_checkout\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL (~1 hour TTL); creating one does NOT charge the user \u2014 payment happens only after the user opens the URL and completes checkout in their browser.
6310
+ leadbay_create_topup_link = `Generate a one-shot Stripe checkout session for an AI-credits top-up. Wraps \`POST /1.6/stripe/topup_checkout\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL (~1 hour TTL); creating one does NOT charge the user \u2014 payment happens only after the user opens the URL and completes checkout in their browser.
6309
6311
 
6310
6312
  **When a quota window is hit, offer top-up as the FIRST option.** A top-up clears the throttle immediately (no need to wait for the daily/weekly/monthly window reset). The flow:
6311
6313
 
@@ -7489,7 +7491,7 @@ follow-ups. On \`ambiguous_sectors\`, the only move is to pick a sector and re-c
7489
7491
  If nothing fits, default to "switch to the new lens and pull leads" \u2014 never
7490
7492
  invent a tool that doesn't exist.
7491
7493
  `;
7492
- leadbay_open_billing_portal = `Generate a one-shot Stripe customer-portal URL. Wraps \`GET /1.5/stripe/portal\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL the user can open to manage their existing Leadbay subscription: change plan tier, swap payment method, view invoices. The agent does NOT make subscription changes itself \u2014 it surfaces the URL and lets the user act.
7494
+ leadbay_open_billing_portal = `Generate a one-shot Stripe customer-portal URL. Wraps \`GET /1.6/stripe/portal\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL the user can open to manage their existing Leadbay subscription: change plan tier, swap payment method, view invoices. The agent does NOT make subscription changes itself \u2014 it surfaces the URL and lets the user act.
7493
7495
 
7494
7496
  Sibling of \`leadbay_create_topup_link\`. Use cases differ:
7495
7497
 
@@ -7767,7 +7769,7 @@ table. Detail + status priority below.
7767
7769
 
7768
7770
  Pull KNOWN leads from the user's Monitor view \u2014 the re-engagement entry point. Use when the user asks "what should I follow up on", "leads I haven't contacted", "leads in [city]", "before my trip", or any phrasing implying pre-existing pipeline context. For NEW leads from Discover, use \`leadbay_pull_leads\`.
7769
7771
 
7770
- Backend: wraps \`GET /1.5/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.5/monitor/filter\`. The Monitor filter is a single \`FilterItem\` per user \u2014 refreshing restores it.
7772
+ Backend: wraps \`GET /1.6/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.6/monitor/filter\`. The Monitor filter is a single \`FilterItem\` per user \u2014 refreshing restores it.
7771
7773
 
7772
7774
  **Filter mechanism \u2014 store-then-apply.** Pass \`set_filter: { criteria: FilterCriterion[] }\` to overwrite the server-stored filter, then the composite re-fetches with \`filtered:true\`. \`FilterCriterion\` is the backend's \`anyOf\` over 10 typed criteria: \`size\`, \`keywords\`, \`sector_ids\`, \`location_ids\`, \`custom_field\`(\`_comparison\`), \`yc\`, \`liked\`, \`last_action\` (MonitorActionType enum), \`last_action_date\` (with \`last_days\`).
7773
7775
 
@@ -22669,6 +22671,8 @@ var init_artifact_kit = __esm({
22669
22671
  // ../core/dist/index.js
22670
22672
  var dist_exports = {};
22671
22673
  __export(dist_exports, {
22674
+ API_PREFIX: () => API_PREFIX,
22675
+ API_VERSION: () => API_VERSION,
22672
22676
  AgentMemoryCaptureInputSchema: () => AgentMemoryCaptureInputSchema,
22673
22677
  AgentMemoryEntrySchema: () => AgentMemoryEntrySchema,
22674
22678
  AgentMemoryInjectionError: () => AgentMemoryInjectionError,
@@ -27717,7 +27721,7 @@ var OAUTH_BASE_URLS = {
27717
27721
  fr: "https://staging.api.leadbay.app"
27718
27722
  }
27719
27723
  };
27720
- var VERSION = "0.23.7";
27724
+ var VERSION = "0.23.9";
27721
27725
  var HELP = `
27722
27726
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
27723
27727
 
@@ -28518,7 +28522,7 @@ async function loginAt(baseUrl, email, password) {
28518
28522
  const https2 = await import("https");
28519
28523
  return await new Promise((resolve, reject) => {
28520
28524
  const body = JSON.stringify({ email, password });
28521
- const u = new URL(baseUrl + "/1.5/auth/login");
28525
+ const u = new URL(baseUrl + `${API_PREFIX}/auth/login`);
28522
28526
  const r = https2.request(
28523
28527
  {
28524
28528
  hostname: u.hostname,
@@ -1592,6 +1592,8 @@ var REGIONS = {
1592
1592
  us: "https://api-us.leadbay.app",
1593
1593
  fr: "https://api-fr.leadbay.app"
1594
1594
  };
1595
+ var API_VERSION = "1.6";
1596
+ var API_PREFIX = `/${API_VERSION}`;
1595
1597
  function httpsRequest(method, url, headers, body) {
1596
1598
  return new Promise((resolve, reject) => {
1597
1599
  const start = Date.now();
@@ -1647,7 +1649,7 @@ async function resolveRegion(email, password, startWith = "us") {
1647
1649
  const baseUrl = REGIONS[region];
1648
1650
  const body = JSON.stringify({ email, password });
1649
1651
  try {
1650
- const res = await httpsRequest("POST", `${baseUrl}/1.5/auth/login`, { "Content-Type": "application/json" }, body);
1652
+ const res = await httpsRequest("POST", `${baseUrl}${API_PREFIX}/auth/login`, { "Content-Type": "application/json" }, body);
1651
1653
  if (res.status === 200) {
1652
1654
  const parsed = JSON.parse(res.body);
1653
1655
  if (parsed?.token) {
@@ -1861,7 +1863,7 @@ var LeadbayClient = class {
1861
1863
  const retryOn401 = opts?.retryOn401 !== false;
1862
1864
  await this.acquireSemaphore();
1863
1865
  try {
1864
- const url = `${this._baseUrl}/1.5${path}`;
1866
+ const url = `${this._baseUrl}${API_PREFIX}${path}`;
1865
1867
  const headers = {
1866
1868
  Authorization: `Bearer ${this.token}`
1867
1869
  };
@@ -1896,7 +1898,7 @@ var LeadbayClient = class {
1896
1898
  }
1897
1899
  await this.acquireSemaphore();
1898
1900
  try {
1899
- const url = `${this._baseUrl}/1.5${path}`;
1901
+ const url = `${this._baseUrl}${API_PREFIX}${path}`;
1900
1902
  const headers = {
1901
1903
  Authorization: `Bearer ${this.token}`
1902
1904
  };
@@ -1921,7 +1923,7 @@ var LeadbayClient = class {
1921
1923
  // serialized body (string for text payloads such as CSV; Buffer for binary
1922
1924
  // uploads). Auth, semaphore, error mapping, _lastMeta, and mock-mode all
1923
1925
  // mirror request() exactly. Used by leadbay_import_leads to upload CSVs to
1924
- // the wizard at POST /1.5/imports.
1926
+ // the wizard at POST /1.6/imports.
1925
1927
  async requestRawBinary(method, path, contentType, body) {
1926
1928
  if (process.env.LEADBAY_MOCK === "1") {
1927
1929
  return this.mockRequestBinary(method, path, contentType, body);
@@ -1931,7 +1933,7 @@ var LeadbayClient = class {
1931
1933
  }
1932
1934
  await this.acquireSemaphore();
1933
1935
  try {
1934
- const url = `${this._baseUrl}/1.5${path}`;
1936
+ const url = `${this._baseUrl}${API_PREFIX}${path}`;
1935
1937
  const headers = {
1936
1938
  Authorization: `Bearer ${this.token}`,
1937
1939
  "Content-Type": contentType
@@ -1955,7 +1957,7 @@ var LeadbayClient = class {
1955
1957
  }
1956
1958
  }
1957
1959
  mockRequest(method, path, body) {
1958
- const fullPath = `/1.5${path}`;
1960
+ const fullPath = `${API_PREFIX}${path}`;
1959
1961
  this._lastMeta = {
1960
1962
  region: this._region,
1961
1963
  endpoint: `${method} ${path}`,
@@ -1978,7 +1980,7 @@ var LeadbayClient = class {
1978
1980
  };
1979
1981
  }
1980
1982
  mockRequestBinary(method, path, contentType, body) {
1981
- const fullPath = `/1.5${path}`;
1983
+ const fullPath = `${API_PREFIX}${path}`;
1982
1984
  this._lastMeta = {
1983
1985
  region: this._region,
1984
1986
  endpoint: `${method} ${path}`,
@@ -6824,7 +6826,7 @@ WHEN TO USE: at the start of a session to know what the agent can/can't do, afte
6824
6826
 
6825
6827
  WHEN NOT TO USE: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating. And: a recent quota snapshot showing "exhausted" is NOT a reason to refuse a write call when the user has just topped up \u2014 re-call this tool first, then proceed.
6826
6828
  `;
6827
- var leadbay_acknowledge_notification = `Acknowledge a Leadbay notification \u2014 i.e. tell the MCP and the backend "I've seen this and acted on it." Wraps \`POST /1.5/notifications/{id}/seen\` (default) or \`/archive\` (when \`archive:true\`) and drops the entry from the local inbox so subsequent \`_meta.notifications\` payloads stop carrying it.
6829
+ var leadbay_acknowledge_notification = `Acknowledge a Leadbay notification \u2014 i.e. tell the MCP and the backend "I've seen this and acted on it." Wraps \`POST /1.6/notifications/{id}/seen\` (default) or \`/archive\` (when \`archive:true\`) and drops the entry from the local inbox so subsequent \`_meta.notifications\` payloads stop carrying it.
6828
6830
 
6829
6831
  **When to call.** After you read an entry from \`_meta.notifications\` or \`account_status.notifications\` and have revised whatever prior output the just-finished background work might have made stale (outreach drafts, lead lists, "available leads" claims, followup plans). Mark-seen tells the human team's pipeline you handled this and prevents the notification from re-surfacing on every subsequent tool response.
6830
6832
 
@@ -7434,7 +7436,7 @@ WHEN NOT TO USE: from agent flow \u2014 leadbay_adjust_audience handles the draf
7434
7436
 
7435
7437
  This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
7436
7438
  `;
7437
- var leadbay_create_topup_link = `Generate a one-shot Stripe checkout session for an AI-credits top-up. Wraps \`POST /1.5/stripe/topup_checkout\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL (~1 hour TTL); creating one does NOT charge the user \u2014 payment happens only after the user opens the URL and completes checkout in their browser.
7439
+ var leadbay_create_topup_link = `Generate a one-shot Stripe checkout session for an AI-credits top-up. Wraps \`POST /1.6/stripe/topup_checkout\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL (~1 hour TTL); creating one does NOT charge the user \u2014 payment happens only after the user opens the URL and completes checkout in their browser.
7438
7440
 
7439
7441
  **When a quota window is hit, offer top-up as the FIRST option.** A top-up clears the throttle immediately (no need to wait for the daily/weekly/monthly window reset). The flow:
7440
7442
 
@@ -8618,7 +8620,7 @@ follow-ups. On \`ambiguous_sectors\`, the only move is to pick a sector and re-c
8618
8620
  If nothing fits, default to "switch to the new lens and pull leads" \u2014 never
8619
8621
  invent a tool that doesn't exist.
8620
8622
  `;
8621
- var leadbay_open_billing_portal = `Generate a one-shot Stripe customer-portal URL. Wraps \`GET /1.5/stripe/portal\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL the user can open to manage their existing Leadbay subscription: change plan tier, swap payment method, view invoices. The agent does NOT make subscription changes itself \u2014 it surfaces the URL and lets the user act.
8623
+ var leadbay_open_billing_portal = `Generate a one-shot Stripe customer-portal URL. Wraps \`GET /1.6/stripe/portal\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL the user can open to manage their existing Leadbay subscription: change plan tier, swap payment method, view invoices. The agent does NOT make subscription changes itself \u2014 it surfaces the URL and lets the user act.
8622
8624
 
8623
8625
  Sibling of \`leadbay_create_topup_link\`. Use cases differ:
8624
8626
 
@@ -8896,7 +8898,7 @@ table. Detail + status priority below.
8896
8898
 
8897
8899
  Pull KNOWN leads from the user's Monitor view \u2014 the re-engagement entry point. Use when the user asks "what should I follow up on", "leads I haven't contacted", "leads in [city]", "before my trip", or any phrasing implying pre-existing pipeline context. For NEW leads from Discover, use \`leadbay_pull_leads\`.
8898
8900
 
8899
- Backend: wraps \`GET /1.5/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.5/monitor/filter\`. The Monitor filter is a single \`FilterItem\` per user \u2014 refreshing restores it.
8901
+ Backend: wraps \`GET /1.6/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.6/monitor/filter\`. The Monitor filter is a single \`FilterItem\` per user \u2014 refreshing restores it.
8900
8902
 
8901
8903
  **Filter mechanism \u2014 store-then-apply.** Pass \`set_filter: { criteria: FilterCriterion[] }\` to overwrite the server-stored filter, then the composite re-fetches with \`filtered:true\`. \`FilterCriterion\` is the backend's \`anyOf\` over 10 typed criteria: \`size\`, \`keywords\`, \`sector_ids\`, \`location_ids\`, \`custom_field\`(\`_comparison\`), \`yc\`, \`liked\`, \`last_action\` (MonitorActionType enum), \`last_action_date\` (with \`last_days\`).
8902
8904
 
@@ -24014,7 +24016,7 @@ function parseWriteEnv(env = process.env) {
24014
24016
  }
24015
24017
 
24016
24018
  // src/http-server.ts
24017
- var VERSION = true ? "0.23.7" : "0.0.0-dev";
24019
+ var VERSION = true ? "0.23.9" : "0.0.0-dev";
24018
24020
  var PORT = Number(process.env.PORT ?? 8080);
24019
24021
  var HOST = process.env.HOST ?? "0.0.0.0";
24020
24022
  var sseSessions = /* @__PURE__ */ new Map();
@@ -9,8 +9,188 @@ var __export = (target, all) => {
9
9
  __defProp(target, name, { get: all[name], enumerable: true });
10
10
  };
11
11
 
12
- // installer/install-claude-code.ts
12
+ // installer/install-shared.ts
13
13
  import { spawn } from "child_process";
14
+ import { existsSync, readFileSync, readdirSync } from "fs";
15
+ import { join } from "path";
16
+ import { homedir } from "os";
17
+ function printHostedMcpHelp(write = (s) => process.stderr.write(s)) {
18
+ write(
19
+ `
20
+ The guided installer couldn't complete (no browser opened in time).
21
+ You can add Leadbay's hosted MCP directly \u2014 Claude signs in in-app,
22
+ no localhost callback needed:
23
+
24
+ \u2022 Claude web / Cowork web:
25
+ Customize \u2192 Connectors \u2192 "+" \u2192 Add custom connector
26
+ Name: Leadbay URL: ${HOSTED_MCP_URL}
27
+
28
+ \u2022 A terminal with the Claude Code CLI:
29
+ claude mcp add --transport http leadbay ${HOSTED_MCP_URL}
30
+
31
+ \u2022 Or re-run the terminal install flow:
32
+ npx -y @leadbay/mcp@latest install --oauth
33
+
34
+ `
35
+ );
36
+ }
37
+ function formatInstallOsLabel(platform = process.platform, arch = process.arch) {
38
+ const name = platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform === "linux" ? "Linux" : platform;
39
+ return `${name} (${arch})`;
40
+ }
41
+ function detectClaudeDesktopMode(claudeSupportDir) {
42
+ const markers = [];
43
+ const legacy = existsSync(join(claudeSupportDir, "claude_desktop_config.json"));
44
+ if (existsSync(join(claudeSupportDir, "Claude Extensions"))) {
45
+ markers.push("Claude Extensions/");
46
+ }
47
+ if (existsSync(join(claudeSupportDir, "extensions-installations.json"))) {
48
+ markers.push("extensions-installations.json");
49
+ }
50
+ const cfgPath = join(claudeSupportDir, "config.json");
51
+ if (existsSync(cfgPath)) {
52
+ try {
53
+ const raw = readFileSync(cfgPath, "utf8");
54
+ const parsed = JSON.parse(raw);
55
+ if (parsed && typeof parsed === "object") {
56
+ const hasDxtKey = Object.keys(parsed).some((key) => key.startsWith("dxt:"));
57
+ if (hasDxtKey) markers.push("config.json (dxt:* keys)");
58
+ }
59
+ } catch {
60
+ }
61
+ }
62
+ return { legacy, dxt: markers.length > 0, markers };
63
+ }
64
+ async function findOnPath(bin) {
65
+ return await new Promise((resolve) => {
66
+ const cmd = process.platform === "win32" ? "where" : "which";
67
+ const child = spawn(cmd, [bin], { stdio: ["ignore", "pipe", "ignore"] });
68
+ let buf = "";
69
+ child.stdout.on("data", (chunk) => buf += chunk.toString());
70
+ child.on("close", (code) => resolve(code === 0 ? buf.split(/\r?\n/)[0] : null));
71
+ child.on("error", () => resolve(null));
72
+ });
73
+ }
74
+ async function windowsStoreAppInstalled(packageName, appName) {
75
+ if (process.platform !== "win32") return false;
76
+ return await new Promise((resolve) => {
77
+ const script = [
78
+ `$pkg = Get-AppxPackage -Name '${packageName}' -ErrorAction SilentlyContinue`,
79
+ `$app = Get-StartApps | Where-Object { $_.AppID -like '${packageName}_*!${appName}' } | Select-Object -First 1`,
80
+ "if ($pkg -or $app) { exit 0 } else { exit 1 }"
81
+ ].join("; ");
82
+ const child = spawn("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
83
+ stdio: "ignore",
84
+ windowsHide: true
85
+ });
86
+ child.on("close", (code) => resolve(code === 0));
87
+ child.on("error", () => resolve(false));
88
+ });
89
+ }
90
+ function isClaudeStorePackagePresent(localAppData) {
91
+ const packagesDir = join(localAppData, "Packages");
92
+ if (!existsSync(packagesDir)) return false;
93
+ try {
94
+ return readdirSync(packagesDir).some((name) => /^Claude_/i.test(name));
95
+ } catch {
96
+ return false;
97
+ }
98
+ }
99
+ async function isClaudeDesktopInstalled(home) {
100
+ if (process.platform === "darwin") {
101
+ return existsSync("/Applications/Claude.app") || existsSync(home + "/Applications/Claude.app");
102
+ }
103
+ if (process.platform === "win32") {
104
+ const local = process.env.LOCALAPPDATA ?? home + "/AppData/Local";
105
+ const programFiles = process.env.ProgramFiles;
106
+ const programFilesX86 = process.env["ProgramFiles(x86)"];
107
+ const exeInstalled = [
108
+ local + "/Programs/Claude/Claude.exe",
109
+ local + "/Claude/Claude.exe",
110
+ programFiles ? programFiles + "/Claude/Claude.exe" : null,
111
+ programFilesX86 ? programFilesX86 + "/Claude/Claude.exe" : null
112
+ ].some((candidate) => candidate !== null && existsSync(candidate));
113
+ if (exeInstalled) return true;
114
+ if (isClaudeStorePackagePresent(local)) return true;
115
+ return await windowsStoreAppInstalled("AnthropicPBC.Claude", "Claude");
116
+ }
117
+ const desktopBin = await findOnPath("claude-desktop");
118
+ if (desktopBin) return true;
119
+ 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");
120
+ }
121
+ async function isChatGptDesktopInstalled(home) {
122
+ if (process.platform === "darwin") {
123
+ return existsSync("/Applications/ChatGPT.app") || existsSync(home + "/Applications/ChatGPT.app");
124
+ }
125
+ if (process.platform === "win32") {
126
+ const local = process.env.LOCALAPPDATA ?? home + "/AppData/Local";
127
+ const programFiles = process.env.ProgramFiles;
128
+ const programFilesX86 = process.env["ProgramFiles(x86)"];
129
+ const exeInstalled = [
130
+ local + "/Programs/ChatGPT/ChatGPT.exe",
131
+ local + "/ChatGPT/ChatGPT.exe",
132
+ programFiles ? programFiles + "/OpenAI/ChatGPT/ChatGPT.exe" : null,
133
+ programFiles ? programFiles + "/ChatGPT/ChatGPT.exe" : null,
134
+ programFilesX86 ? programFilesX86 + "/OpenAI/ChatGPT/ChatGPT.exe" : null,
135
+ programFilesX86 ? programFilesX86 + "/ChatGPT/ChatGPT.exe" : null
136
+ ].some((candidate) => candidate !== null && existsSync(candidate));
137
+ return exeInstalled || await windowsStoreAppInstalled("OpenAI.ChatGPT-Desktop", "ChatGPT");
138
+ }
139
+ return false;
140
+ }
141
+ async function isCursorInstalled(home) {
142
+ const cursorBin = await findOnPath("cursor");
143
+ if (cursorBin) return true;
144
+ if (process.platform === "darwin") return existsSync("/Applications/Cursor.app");
145
+ if (process.platform === "win32") {
146
+ const local = process.env.LOCALAPPDATA ?? `${home}\\AppData\\Local`;
147
+ return existsSync(`${local}\\Programs\\Cursor\\Cursor.exe`);
148
+ }
149
+ return existsSync("/usr/share/applications/cursor.desktop") || existsSync("/opt/Cursor/cursor");
150
+ }
151
+ async function detectClients() {
152
+ const out = [];
153
+ const home = homedir();
154
+ const claudeBin = await findOnPath("claude");
155
+ if (claudeBin) {
156
+ out.push({ id: "claude-code", label: "Claude Code", detail: `${claudeBin} mcp add ...` });
157
+ }
158
+ const claudeSupportDir = process.platform === "win32" ? `${process.env.APPDATA ?? `${home}\\AppData\\Roaming`}\\Claude` : process.platform === "darwin" ? `${home}/Library/Application Support/Claude` : `${home}/.config/Claude`;
159
+ const claudeDesktopPath = process.platform === "win32" ? `${claudeSupportDir}\\claude_desktop_config.json` : `${claudeSupportDir}/claude_desktop_config.json`;
160
+ const mode = detectClaudeDesktopMode(claudeSupportDir);
161
+ if (await isClaudeDesktopInstalled(home)) {
162
+ out.push({ id: "claude-desktop", label: "Claude Desktop", detail: claudeDesktopPath, configPath: claudeDesktopPath, mode, supportDir: claudeSupportDir });
163
+ }
164
+ if (await isChatGptDesktopInstalled(home)) {
165
+ out.push({ id: "chatgpt-desktop", label: "ChatGPT Desktop", detail: HOSTED_MCP_URL });
166
+ }
167
+ const cursorPath = process.platform === "win32" ? `${home}\\.cursor\\mcp.json` : `${home}/.cursor/mcp.json`;
168
+ if (await isCursorInstalled(home)) {
169
+ out.push({
170
+ id: "cursor",
171
+ label: "Cursor",
172
+ detail: existsSync(cursorPath) ? cursorPath : `${cursorPath} (will be created)`,
173
+ configPath: cursorPath
174
+ });
175
+ }
176
+ const codexBin = await findOnPath("codex");
177
+ const codexDir = process.platform === "win32" ? `${process.env.USERPROFILE ?? home}\\.codex` : `${home}/.codex`;
178
+ if (codexBin) {
179
+ const codexConfigPath = process.platform === "win32" ? `${codexDir}\\config.toml` : `${codexDir}/config.toml`;
180
+ out.push({ id: "codex", label: "Codex", detail: codexConfigPath, configPath: codexConfigPath });
181
+ }
182
+ return out;
183
+ }
184
+ var HOSTED_MCP_URL;
185
+ var init_install_shared = __esm({
186
+ "installer/install-shared.ts"() {
187
+ "use strict";
188
+ HOSTED_MCP_URL = "https://leadbay-mcp-prod.fly.dev/mcp";
189
+ }
190
+ });
191
+
192
+ // installer/install-claude-code.ts
193
+ import { spawn as spawn2 } from "child_process";
14
194
  function buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled, localBinPath) {
15
195
  const args = [
16
196
  "mcp",
@@ -38,7 +218,7 @@ function buildClaudeCodeRemoveArgs() {
38
218
  }
39
219
  async function runClaudeMcp(args) {
40
220
  return await new Promise((resolve) => {
41
- const child = spawn("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
221
+ const child = spawn2("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
42
222
  let stdout = "";
43
223
  let stderr = "";
44
224
  child.stdout.on("data", (chunk) => stdout += chunk.toString());
@@ -397,166 +577,6 @@ var init_install_dxt = __esm({
397
577
  }
398
578
  });
399
579
 
400
- // installer/install-shared.ts
401
- import { spawn as spawn2 } from "child_process";
402
- import { existsSync, readFileSync, readdirSync } from "fs";
403
- import { join } from "path";
404
- import { homedir } from "os";
405
- function formatInstallOsLabel(platform = process.platform, arch = process.arch) {
406
- const name = platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform === "linux" ? "Linux" : platform;
407
- return `${name} (${arch})`;
408
- }
409
- function detectClaudeDesktopMode(claudeSupportDir) {
410
- const markers = [];
411
- const legacy = existsSync(join(claudeSupportDir, "claude_desktop_config.json"));
412
- if (existsSync(join(claudeSupportDir, "Claude Extensions"))) {
413
- markers.push("Claude Extensions/");
414
- }
415
- if (existsSync(join(claudeSupportDir, "extensions-installations.json"))) {
416
- markers.push("extensions-installations.json");
417
- }
418
- const cfgPath = join(claudeSupportDir, "config.json");
419
- if (existsSync(cfgPath)) {
420
- try {
421
- const raw = readFileSync(cfgPath, "utf8");
422
- const parsed = JSON.parse(raw);
423
- if (parsed && typeof parsed === "object") {
424
- const hasDxtKey = Object.keys(parsed).some((key) => key.startsWith("dxt:"));
425
- if (hasDxtKey) markers.push("config.json (dxt:* keys)");
426
- }
427
- } catch {
428
- }
429
- }
430
- return { legacy, dxt: markers.length > 0, markers };
431
- }
432
- async function findOnPath(bin) {
433
- return await new Promise((resolve) => {
434
- const cmd = process.platform === "win32" ? "where" : "which";
435
- const child = spawn2(cmd, [bin], { stdio: ["ignore", "pipe", "ignore"] });
436
- let buf = "";
437
- child.stdout.on("data", (chunk) => buf += chunk.toString());
438
- child.on("close", (code) => resolve(code === 0 ? buf.split(/\r?\n/)[0] : null));
439
- child.on("error", () => resolve(null));
440
- });
441
- }
442
- async function windowsStoreAppInstalled(packageName, appName) {
443
- if (process.platform !== "win32") return false;
444
- return await new Promise((resolve) => {
445
- const script = [
446
- `$pkg = Get-AppxPackage -Name '${packageName}' -ErrorAction SilentlyContinue`,
447
- `$app = Get-StartApps | Where-Object { $_.AppID -like '${packageName}_*!${appName}' } | Select-Object -First 1`,
448
- "if ($pkg -or $app) { exit 0 } else { exit 1 }"
449
- ].join("; ");
450
- const child = spawn2("powershell.exe", ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", script], {
451
- stdio: "ignore",
452
- windowsHide: true
453
- });
454
- child.on("close", (code) => resolve(code === 0));
455
- child.on("error", () => resolve(false));
456
- });
457
- }
458
- function isClaudeStorePackagePresent(localAppData) {
459
- const packagesDir = join(localAppData, "Packages");
460
- if (!existsSync(packagesDir)) return false;
461
- try {
462
- return readdirSync(packagesDir).some((name) => /^Claude_/i.test(name));
463
- } catch {
464
- return false;
465
- }
466
- }
467
- async function isClaudeDesktopInstalled(home) {
468
- if (process.platform === "darwin") {
469
- return existsSync("/Applications/Claude.app") || existsSync(home + "/Applications/Claude.app");
470
- }
471
- if (process.platform === "win32") {
472
- const local = process.env.LOCALAPPDATA ?? home + "/AppData/Local";
473
- const programFiles = process.env.ProgramFiles;
474
- const programFilesX86 = process.env["ProgramFiles(x86)"];
475
- const exeInstalled = [
476
- local + "/Programs/Claude/Claude.exe",
477
- local + "/Claude/Claude.exe",
478
- programFiles ? programFiles + "/Claude/Claude.exe" : null,
479
- programFilesX86 ? programFilesX86 + "/Claude/Claude.exe" : null
480
- ].some((candidate) => candidate !== null && existsSync(candidate));
481
- if (exeInstalled) return true;
482
- if (isClaudeStorePackagePresent(local)) return true;
483
- return await windowsStoreAppInstalled("AnthropicPBC.Claude", "Claude");
484
- }
485
- const desktopBin = await findOnPath("claude-desktop");
486
- if (desktopBin) return true;
487
- 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");
488
- }
489
- async function isChatGptDesktopInstalled(home) {
490
- if (process.platform === "darwin") {
491
- return existsSync("/Applications/ChatGPT.app") || existsSync(home + "/Applications/ChatGPT.app");
492
- }
493
- if (process.platform === "win32") {
494
- const local = process.env.LOCALAPPDATA ?? home + "/AppData/Local";
495
- const programFiles = process.env.ProgramFiles;
496
- const programFilesX86 = process.env["ProgramFiles(x86)"];
497
- const exeInstalled = [
498
- local + "/Programs/ChatGPT/ChatGPT.exe",
499
- local + "/ChatGPT/ChatGPT.exe",
500
- programFiles ? programFiles + "/OpenAI/ChatGPT/ChatGPT.exe" : null,
501
- programFiles ? programFiles + "/ChatGPT/ChatGPT.exe" : null,
502
- programFilesX86 ? programFilesX86 + "/OpenAI/ChatGPT/ChatGPT.exe" : null,
503
- programFilesX86 ? programFilesX86 + "/ChatGPT/ChatGPT.exe" : null
504
- ].some((candidate) => candidate !== null && existsSync(candidate));
505
- return exeInstalled || await windowsStoreAppInstalled("OpenAI.ChatGPT-Desktop", "ChatGPT");
506
- }
507
- return false;
508
- }
509
- async function isCursorInstalled(home) {
510
- const cursorBin = await findOnPath("cursor");
511
- if (cursorBin) return true;
512
- if (process.platform === "darwin") return existsSync("/Applications/Cursor.app");
513
- if (process.platform === "win32") {
514
- const local = process.env.LOCALAPPDATA ?? `${home}\\AppData\\Local`;
515
- return existsSync(`${local}\\Programs\\Cursor\\Cursor.exe`);
516
- }
517
- return existsSync("/usr/share/applications/cursor.desktop") || existsSync("/opt/Cursor/cursor");
518
- }
519
- async function detectClients() {
520
- const out = [];
521
- const home = homedir();
522
- const claudeBin = await findOnPath("claude");
523
- if (claudeBin) {
524
- out.push({ id: "claude-code", label: "Claude Code", detail: `${claudeBin} mcp add ...` });
525
- }
526
- const claudeSupportDir = process.platform === "win32" ? `${process.env.APPDATA ?? `${home}\\AppData\\Roaming`}\\Claude` : process.platform === "darwin" ? `${home}/Library/Application Support/Claude` : `${home}/.config/Claude`;
527
- const claudeDesktopPath = process.platform === "win32" ? `${claudeSupportDir}\\claude_desktop_config.json` : `${claudeSupportDir}/claude_desktop_config.json`;
528
- const mode = detectClaudeDesktopMode(claudeSupportDir);
529
- if (await isClaudeDesktopInstalled(home)) {
530
- out.push({ id: "claude-desktop", label: "Claude Desktop", detail: claudeDesktopPath, configPath: claudeDesktopPath, mode, supportDir: claudeSupportDir });
531
- }
532
- if (await isChatGptDesktopInstalled(home)) {
533
- out.push({ id: "chatgpt-desktop", label: "ChatGPT Desktop", detail: HOSTED_MCP_URL });
534
- }
535
- const cursorPath = process.platform === "win32" ? `${home}\\.cursor\\mcp.json` : `${home}/.cursor/mcp.json`;
536
- if (await isCursorInstalled(home)) {
537
- out.push({
538
- id: "cursor",
539
- label: "Cursor",
540
- detail: existsSync(cursorPath) ? cursorPath : `${cursorPath} (will be created)`,
541
- configPath: cursorPath
542
- });
543
- }
544
- const codexBin = await findOnPath("codex");
545
- const codexDir = process.platform === "win32" ? `${process.env.USERPROFILE ?? home}\\.codex` : `${home}/.codex`;
546
- if (codexBin) {
547
- const codexConfigPath = process.platform === "win32" ? `${codexDir}\\config.toml` : `${codexDir}/config.toml`;
548
- out.push({ id: "codex", label: "Codex", detail: codexConfigPath, configPath: codexConfigPath });
549
- }
550
- return out;
551
- }
552
- var HOSTED_MCP_URL;
553
- var init_install_shared = __esm({
554
- "installer/install-shared.ts"() {
555
- "use strict";
556
- HOSTED_MCP_URL = "https://leadbay-mcp-prod.fly.dev/mcp";
557
- }
558
- });
559
-
560
580
  // src/oauth.ts
561
581
  import { createHash, randomBytes } from "crypto";
562
582
  import { createServer } from "http";
@@ -1157,7 +1177,20 @@ async function loginWithOAuth() {
1157
1177
  const { accessToken } = await oauthLogin({
1158
1178
  authServerBaseUrl: OAUTH_BASE_URLS.prod[region],
1159
1179
  clientName: `Leadbay MCP installer @ ${hostname()}`,
1160
- log: () => void 0
1180
+ log: () => void 0,
1181
+ // Wrap the opener so a launch failure surfaces the (still-reachable) URL
1182
+ // on stderr instead of silently dropping it, while the listener stays up.
1183
+ openBrowser: async (u) => {
1184
+ try {
1185
+ await openInBrowser(u);
1186
+ } catch {
1187
+ process.stderr.write(`
1188
+ Open this URL in your browser to sign in:
1189
+ ${u}
1190
+
1191
+ `);
1192
+ }
1193
+ }
1161
1194
  });
1162
1195
  const sessionId = randomUUID();
1163
1196
  const accountLabel = `Leadbay OAuth (${region.toUpperCase()})`;
@@ -1632,36 +1665,6 @@ function pageHtml(locale = "en") {
1632
1665
  </body>
1633
1666
  </html>`;
1634
1667
  }
1635
- async function openBrowser(url) {
1636
- const { spawn: spawn4 } = await import("child_process");
1637
- const trySpawn = (command, args) => new Promise((resolve) => {
1638
- try {
1639
- const child = spawn4(command, args, { stdio: "ignore", detached: true });
1640
- child.unref();
1641
- child.on("error", () => resolve(false));
1642
- child.on("close", (code) => resolve(code === 0));
1643
- } catch {
1644
- resolve(false);
1645
- }
1646
- });
1647
- if (process.platform === "darwin") {
1648
- await trySpawn("open", [url]);
1649
- return;
1650
- }
1651
- if (process.platform === "win32") {
1652
- await trySpawn("cmd", ["/c", "start", "", url]);
1653
- return;
1654
- }
1655
- const candidates = ["xdg-open", "sensible-browser", "google-chrome", "chromium-browser", "firefox"];
1656
- for (const cmd of candidates) {
1657
- if (await trySpawn(cmd, [url])) return;
1658
- }
1659
- process.stderr.write(`
1660
- Open this URL in your browser to continue:
1661
- ${url}
1662
-
1663
- `);
1664
- }
1665
1668
  function makeGuiServer(options, pageContent, extraRoutes, logLabel) {
1666
1669
  let expectedHost = `127.0.0.1:${(options.port ?? PORT) || 0}`;
1667
1670
  let resolveDone;
@@ -1671,11 +1674,16 @@ function makeGuiServer(options, pageContent, extraRoutes, logLabel) {
1671
1674
  const onDone = () => setTimeout(() => {
1672
1675
  resolveDone();
1673
1676
  }, 1500);
1677
+ let resolveActivity;
1678
+ const activity = new Promise((r) => {
1679
+ resolveActivity = r;
1680
+ });
1674
1681
  const server = createServer2(async (req, res) => {
1675
1682
  if (!isAllowedOrigin(req, expectedHost)) {
1676
1683
  sendJson(res, 403, { ok: false, error: "forbidden" });
1677
1684
  return;
1678
1685
  }
1686
+ resolveActivity();
1679
1687
  try {
1680
1688
  if (req.method === "GET" && req.url === "/") {
1681
1689
  const raw = await pageContent();
@@ -1699,8 +1707,16 @@ function makeGuiServer(options, pageContent, extraRoutes, logLabel) {
1699
1707
  const url = `http://127.0.0.1:${port}/`;
1700
1708
  process.stderr.write(`Leadbay MCP ${logLabel} GUI: ${url}
1701
1709
  `);
1702
- if (options.openBrowser !== false) await openBrowser(url).catch(() => void 0);
1703
- resolve({ url, done, close: () => new Promise((res, rej) => server.close((e) => e ? rej(e) : res())) });
1710
+ if (options.openBrowser !== false) {
1711
+ openInBrowser(url).catch(() => {
1712
+ process.stderr.write(`
1713
+ Open this URL in your browser to continue:
1714
+ ${url}
1715
+
1716
+ `);
1717
+ });
1718
+ }
1719
+ resolve({ url, done, activity, close: () => new Promise((res, rej) => server.close((e) => e ? rej(e) : res())) });
1704
1720
  });
1705
1721
  });
1706
1722
  }
@@ -1748,7 +1764,7 @@ var init_installer_gui = __esm({
1748
1764
  init_install_dxt();
1749
1765
  init_install_shared();
1750
1766
  init_oauth();
1751
- VERSION = true ? "0.23.7" : "0.0.0-dev";
1767
+ VERSION = true ? "0.23.9" : "0.0.0-dev";
1752
1768
  MESSAGES = {
1753
1769
  en: {
1754
1770
  installer: {
@@ -1911,25 +1927,55 @@ var init_installer_gui = __esm({
1911
1927
  });
1912
1928
 
1913
1929
  // installer/installer-electron.ts
1930
+ init_install_shared();
1914
1931
  import { realpathSync } from "fs";
1915
1932
  import { fileURLToPath as fileURLToPath2 } from "url";
1933
+ var WATCHDOG_MS = 12e4;
1934
+ function shouldArmWatchdog(args) {
1935
+ return !args.includes("--uninstall") && !args.includes("--no-open");
1936
+ }
1937
+ async function runInstallerLoop(handle, watchdogMs = WATCHDOG_MS) {
1938
+ let timer;
1939
+ try {
1940
+ const racers = [
1941
+ handle.done.then(() => ({ outcome: "completed" })),
1942
+ new Promise((resolve) => {
1943
+ process.once("SIGINT", () => resolve({ outcome: "signal" }));
1944
+ process.once("SIGTERM", () => resolve({ outcome: "signal" }));
1945
+ })
1946
+ ];
1947
+ if (watchdogMs !== null) {
1948
+ racers.push(
1949
+ new Promise((resolve) => {
1950
+ timer = setTimeout(() => resolve({ outcome: "timeout" }), watchdogMs);
1951
+ handle.activity.then(() => {
1952
+ if (timer) clearTimeout(timer);
1953
+ }).catch(() => void 0);
1954
+ })
1955
+ );
1956
+ }
1957
+ return await Promise.race(racers);
1958
+ } finally {
1959
+ if (timer) clearTimeout(timer);
1960
+ }
1961
+ }
1916
1962
  async function main() {
1917
1963
  const args = process.argv.slice(2);
1918
1964
  const { startInstallerGui: startInstallerGui2, startUninstallerGui: startUninstallerGui2 } = await Promise.resolve().then(() => (init_installer_gui(), installer_gui_exports));
1919
1965
  const opts = { openBrowser: !args.includes("--no-open") };
1920
- const handle = args.includes("--uninstall") ? await startUninstallerGui2(opts) : await startInstallerGui2(opts);
1921
- let completed = false;
1922
- await Promise.race([
1923
- handle.done.then(() => {
1924
- completed = true;
1925
- }),
1926
- new Promise((resolve) => {
1927
- process.once("SIGINT", () => resolve());
1928
- process.once("SIGTERM", () => resolve());
1929
- })
1930
- ]);
1966
+ const isUninstall = args.includes("--uninstall");
1967
+ const handle = isUninstall ? await startUninstallerGui2(opts) : await startInstallerGui2(opts);
1968
+ const { outcome } = await runInstallerLoop(handle, shouldArmWatchdog(args) ? WATCHDOG_MS : null);
1931
1969
  await handle.close().catch(() => void 0);
1932
- process.stderr.write(completed ? "\nInstallation complete. Exiting.\n" : "\nExiting.\n");
1970
+ if (outcome === "timeout") {
1971
+ process.stderr.write("\nInstaller timed out waiting for the browser flow.\n");
1972
+ printHostedMcpHelp();
1973
+ process.exit(1);
1974
+ }
1975
+ const verb = isUninstall ? "Uninstall" : "Installation";
1976
+ process.stderr.write(outcome === "completed" ? `
1977
+ ${verb} complete. Exiting.
1978
+ ` : "\nExiting.\n");
1933
1979
  }
1934
1980
  var isEntrypoint = (() => {
1935
1981
  try {
@@ -1947,3 +1993,8 @@ if (isEntrypoint) {
1947
1993
  process.exit(1);
1948
1994
  });
1949
1995
  }
1996
+ export {
1997
+ WATCHDOG_MS,
1998
+ runInstallerLoop,
1999
+ shouldArmWatchdog
2000
+ };
@@ -1027,7 +1027,7 @@ async function oauthLogin(opts) {
1027
1027
  }
1028
1028
 
1029
1029
  // installer/installer-gui.ts
1030
- var VERSION = true ? "0.23.7" : "0.0.0-dev";
1030
+ var VERSION = true ? "0.23.9" : "0.0.0-dev";
1031
1031
  var MESSAGES = {
1032
1032
  en: {
1033
1033
  installer: {
@@ -1259,7 +1259,20 @@ async function loginWithOAuth() {
1259
1259
  const { accessToken } = await oauthLogin({
1260
1260
  authServerBaseUrl: OAUTH_BASE_URLS.prod[region],
1261
1261
  clientName: `Leadbay MCP installer @ ${hostname()}`,
1262
- log: () => void 0
1262
+ log: () => void 0,
1263
+ // Wrap the opener so a launch failure surfaces the (still-reachable) URL
1264
+ // on stderr instead of silently dropping it, while the listener stays up.
1265
+ openBrowser: async (u) => {
1266
+ try {
1267
+ await openInBrowser(u);
1268
+ } catch {
1269
+ process.stderr.write(`
1270
+ Open this URL in your browser to sign in:
1271
+ ${u}
1272
+
1273
+ `);
1274
+ }
1275
+ }
1263
1276
  });
1264
1277
  const sessionId = randomUUID();
1265
1278
  const accountLabel = `Leadbay OAuth (${region.toUpperCase()})`;
@@ -1744,36 +1757,6 @@ function pageHtml(locale = "en") {
1744
1757
  </body>
1745
1758
  </html>`;
1746
1759
  }
1747
- async function openBrowser(url) {
1748
- const { spawn: spawn4 } = await import("child_process");
1749
- const trySpawn = (command, args) => new Promise((resolve) => {
1750
- try {
1751
- const child = spawn4(command, args, { stdio: "ignore", detached: true });
1752
- child.unref();
1753
- child.on("error", () => resolve(false));
1754
- child.on("close", (code) => resolve(code === 0));
1755
- } catch {
1756
- resolve(false);
1757
- }
1758
- });
1759
- if (process.platform === "darwin") {
1760
- await trySpawn("open", [url]);
1761
- return;
1762
- }
1763
- if (process.platform === "win32") {
1764
- await trySpawn("cmd", ["/c", "start", "", url]);
1765
- return;
1766
- }
1767
- const candidates = ["xdg-open", "sensible-browser", "google-chrome", "chromium-browser", "firefox"];
1768
- for (const cmd of candidates) {
1769
- if (await trySpawn(cmd, [url])) return;
1770
- }
1771
- process.stderr.write(`
1772
- Open this URL in your browser to continue:
1773
- ${url}
1774
-
1775
- `);
1776
- }
1777
1760
  function makeGuiServer(options, pageContent, extraRoutes, logLabel) {
1778
1761
  let expectedHost = `127.0.0.1:${(options.port ?? PORT) || 0}`;
1779
1762
  let resolveDone;
@@ -1783,11 +1766,16 @@ function makeGuiServer(options, pageContent, extraRoutes, logLabel) {
1783
1766
  const onDone = () => setTimeout(() => {
1784
1767
  resolveDone();
1785
1768
  }, 1500);
1769
+ let resolveActivity;
1770
+ const activity = new Promise((r) => {
1771
+ resolveActivity = r;
1772
+ });
1786
1773
  const server = createServer2(async (req, res) => {
1787
1774
  if (!isAllowedOrigin(req, expectedHost)) {
1788
1775
  sendJson(res, 403, { ok: false, error: "forbidden" });
1789
1776
  return;
1790
1777
  }
1778
+ resolveActivity();
1791
1779
  try {
1792
1780
  if (req.method === "GET" && req.url === "/") {
1793
1781
  const raw = await pageContent();
@@ -1811,8 +1799,16 @@ function makeGuiServer(options, pageContent, extraRoutes, logLabel) {
1811
1799
  const url = `http://127.0.0.1:${port}/`;
1812
1800
  process.stderr.write(`Leadbay MCP ${logLabel} GUI: ${url}
1813
1801
  `);
1814
- if (options.openBrowser !== false) await openBrowser(url).catch(() => void 0);
1815
- resolve({ url, done, close: () => new Promise((res, rej) => server.close((e) => e ? rej(e) : res())) });
1802
+ if (options.openBrowser !== false) {
1803
+ openInBrowser(url).catch(() => {
1804
+ process.stderr.write(`
1805
+ Open this URL in your browser to continue:
1806
+ ${url}
1807
+
1808
+ `);
1809
+ });
1810
+ }
1811
+ resolve({ url, done, activity, close: () => new Promise((res, rej) => server.close((e) => e ? rej(e) : res())) });
1816
1812
  });
1817
1813
  });
1818
1814
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leadbay/mcp",
3
- "version": "0.23.7",
3
+ "version": "0.23.9",
4
4
  "mcpName": "io.github.leadbay/leadbay-mcp",
5
5
  "description": "Model Context Protocol (MCP) server for Leadbay — AI lead discovery, qualification, and enrichment for Claude Desktop, Cursor, and Claude Code.",
6
6
  "type": "module",