@leadbay/mcp 0.10.0 → 0.11.0

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/dist/bin.js CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
+ LeadbayClient,
3
4
  compositeReadTools,
4
5
  compositeWriteTools,
5
6
  createClient,
@@ -8,7 +9,7 @@ import {
8
9
  granularReadTools,
9
10
  granularWriteTools,
10
11
  resolveRegion
11
- } from "./chunk-F3EWCHME.js";
12
+ } from "./chunk-MZZMIZXA.js";
12
13
 
13
14
  // src/bin.ts
14
15
  import { realpathSync } from "fs";
@@ -128,25 +129,20 @@ If \`qualification_summary.answered == 0\` or \`avg_qualification_boost\` is nul
128
129
 
129
130
  **Column 3 \u2014 Contact**
130
131
 
131
- \`[Contact name](LINK) \xB7 short job title\`. See linking/contact-linkedin for LINK priority and the \xB0-flag fallback.
132
+ \`[Contact name](LINK) \xB7 short job title\`. The \`[Contact name](LINK)\` markdown link wrapping is mandatory \u2014 never render the name as plain text. See linking/contact-linkedin for the URL priority (real profile \u2192 constructed people-search) and the \xB0-flag fallback.
132
133
 
133
134
  **Hide from the user (never include in any cell):** \`id\`, \`location.pos\`, \`location.country\` (unless city/state both missing), \`sector_id\`, \`is_hq\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`highlighted_fields\`, \`custom_fields\`, \`contacts_count\` when 0, \`notes_count\` / \`epilogue_actions_count\` / \`prospecting_actions_count\` when 0, \`stale_at\`, \`deal_insights\`, \`social_presence\` booleans (except as the \xB0-flag signal), \`need_attention\` flags, any field whose value is the string \`"null"\`.
134
135
 
135
136
  ## Linking a contact's name
136
137
 
137
- Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
138
+ **MANDATORY: every contact name in your output \u2014 table cells, prose, headers, "Reach <Name>" callouts \u2014 MUST be wrapped in markdown link syntax \`[Name](URL)\`. Never render a contact name as bare text. A plain-text name is a broken contact card; the underlined name is the user's primary affordance for "take me to this person's profile". No "no URL available" exception \u2014 the search URL below is always constructable from name + company.**
138
139
 
139
- When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
140
+ URL priority (first applicable wins):
140
141
 
141
- Otherwise fall back to a LinkedIn people-search URL:
142
+ 1. **Real profile** \u2014 \`contact.linkedin_page\` when it's a string starting with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it).
143
+ 2. **Constructed people-search** \u2014 \`https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>\`. URL-encode params. Strip Inc / LLC / Corp / Ltd / GmbH / Co / S.A. / S.L. / PLC / AG / SAS / SARL suffixes from the company. Append a trailing \` \xB0\` to the rendered name ONLY when this fallback is in use AND \`social_presence.linkedin == false\`. Never append \`\xB0\` when a real \`linkedin_page\` was used.
142
144
 
143
- \`\`\`
144
- https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
145
- \`\`\`
146
-
147
- URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
148
-
149
- Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
145
+ Never link a person's name to the company's LinkedIn page (and vice versa) \u2014 the two surfaces are different and conflating them quietly degrades the workflow.
150
146
 
151
147
  ## Linking the company
152
148
 
@@ -585,25 +581,20 @@ If \`qualification_summary.answered == 0\` or \`avg_qualification_boost\` is nul
585
581
 
586
582
  **Column 3 \u2014 Contact**
587
583
 
588
- \`[Contact name](LINK) \xB7 short job title\`. See linking/contact-linkedin for LINK priority and the \xB0-flag fallback.
584
+ \`[Contact name](LINK) \xB7 short job title\`. The \`[Contact name](LINK)\` markdown link wrapping is mandatory \u2014 never render the name as plain text. See linking/contact-linkedin for the URL priority (real profile \u2192 constructed people-search) and the \xB0-flag fallback.
589
585
 
590
586
  **Hide from the user (never include in any cell):** \`id\`, \`location.pos\`, \`location.country\` (unless city/state both missing), \`sector_id\`, \`is_hq\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`highlighted_fields\`, \`custom_fields\`, \`contacts_count\` when 0, \`notes_count\` / \`epilogue_actions_count\` / \`prospecting_actions_count\` when 0, \`stale_at\`, \`deal_insights\`, \`social_presence\` booleans (except as the \xB0-flag signal), \`need_attention\` flags, any field whose value is the string \`"null"\`.
591
587
 
592
588
  ## Linking a contact's name
593
589
 
594
- Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
590
+ **MANDATORY: every contact name in your output \u2014 table cells, prose, headers, "Reach <Name>" callouts \u2014 MUST be wrapped in markdown link syntax \`[Name](URL)\`. Never render a contact name as bare text. A plain-text name is a broken contact card; the underlined name is the user's primary affordance for "take me to this person's profile". No "no URL available" exception \u2014 the search URL below is always constructable from name + company.**
595
591
 
596
- When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
592
+ URL priority (first applicable wins):
597
593
 
598
- Otherwise fall back to a LinkedIn people-search URL:
599
-
600
- \`\`\`
601
- https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
602
- \`\`\`
594
+ 1. **Real profile** \u2014 \`contact.linkedin_page\` when it's a string starting with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it).
595
+ 2. **Constructed people-search** \u2014 \`https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>\`. URL-encode params. Strip Inc / LLC / Corp / Ltd / GmbH / Co / S.A. / S.L. / PLC / AG / SAS / SARL suffixes from the company. Append a trailing \` \xB0\` to the rendered name ONLY when this fallback is in use AND \`social_presence.linkedin == false\`. Never append \`\xB0\` when a real \`linkedin_page\` was used.
603
596
 
604
- URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
605
-
606
- Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
597
+ Never link a person's name to the company's LinkedIn page (and vice versa) \u2014 the two surfaces are different and conflating them quietly degrades the workflow.
607
598
 
608
599
  ## Linking the company
609
600
 
@@ -718,19 +709,14 @@ If \`qualification[]\` is non-empty, append one collapsed line: \`"Qualification
718
709
 
719
710
  ## Linking a contact's name
720
711
 
721
- Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
722
-
723
- When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
724
-
725
- Otherwise fall back to a LinkedIn people-search URL:
712
+ **MANDATORY: every contact name in your output \u2014 table cells, prose, headers, "Reach <Name>" callouts \u2014 MUST be wrapped in markdown link syntax \`[Name](URL)\`. Never render a contact name as bare text. A plain-text name is a broken contact card; the underlined name is the user's primary affordance for "take me to this person's profile". No "no URL available" exception \u2014 the search URL below is always constructable from name + company.**
726
713
 
727
- \`\`\`
728
- https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
729
- \`\`\`
714
+ URL priority (first applicable wins):
730
715
 
731
- URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
716
+ 1. **Real profile** \u2014 \`contact.linkedin_page\` when it's a string starting with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it).
717
+ 2. **Constructed people-search** \u2014 \`https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>\`. URL-encode params. Strip Inc / LLC / Corp / Ltd / GmbH / Co / S.A. / S.L. / PLC / AG / SAS / SARL suffixes from the company. Append a trailing \` \xB0\` to the rendered name ONLY when this fallback is in use AND \`social_presence.linkedin == false\`. Never append \`\xB0\` when a real \`linkedin_page\` was used.
732
718
 
733
- Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
719
+ Never link a person's name to the company's LinkedIn page (and vice versa) \u2014 the two surfaces are different and conflating them quietly degrades the workflow.
734
720
 
735
721
  ## Linking the company
736
722
 
@@ -1032,6 +1018,12 @@ var EMBEDDED_SENTRY_DSN = "https://301f1c433433b76132956ed5415bea19@o45058744368
1032
1018
  var EV_TOOL_CALL = "mcp tool called";
1033
1019
  var EV_QUOTA_HIT = "mcp quota hit";
1034
1020
  var EV_TOPUP_LINK = "mcp topup link created";
1021
+ var EV_STARTUP = "mcp startup";
1022
+ var EV_MCP_UPDATE_CHECK = "mcp update check";
1023
+ var EV_MCP_UPDATE_PROMPTED = "mcp update prompted";
1024
+ var EV_MCP_UPDATE_INSTALL_CLICKED = "mcp update install_clicked";
1025
+ var EV_MCP_UPDATE_DISMISSED = "mcp update dismissed";
1026
+ var EV_MCP_VERSION_UPDATED = "mcp version updated";
1035
1027
 
1036
1028
  // src/telemetry.ts
1037
1029
  var NOOP_TELEMETRY = {
@@ -1043,8 +1035,20 @@ var NOOP_TELEMETRY = {
1043
1035
  },
1044
1036
  captureTopupLink: () => {
1045
1037
  },
1038
+ captureStartup: () => {
1039
+ },
1046
1040
  captureException: () => {
1047
1041
  },
1042
+ captureUpdateCheck: () => {
1043
+ },
1044
+ captureUpdatePrompted: () => {
1045
+ },
1046
+ captureUpdateInstallClicked: () => {
1047
+ },
1048
+ captureUpdateDismissed: () => {
1049
+ },
1050
+ captureVersionUpdated: () => {
1051
+ },
1048
1052
  shutdown: async () => {
1049
1053
  }
1050
1054
  };
@@ -1088,7 +1092,12 @@ function initTelemetry(opts) {
1088
1092
  profilesSampleRate: 0,
1089
1093
  defaultIntegrations: false,
1090
1094
  integrations: [Sentry.httpIntegration()],
1091
- sendDefaultPii: false
1095
+ sendDefaultPii: false,
1096
+ // Tag every captured event with the surface so Sentry views can
1097
+ // split MCP issues from web-app issues without per-call work.
1098
+ initialScope: {
1099
+ tags: { source: "mcp" }
1100
+ }
1092
1101
  });
1093
1102
  sentryReady = true;
1094
1103
  }
@@ -1105,6 +1114,11 @@ function initTelemetry(opts) {
1105
1114
  const pendingEvents = [];
1106
1115
  let region = "unknown";
1107
1116
  const baseProps = () => ({
1117
+ // Always tag MCP-originated events so PostHog dashboards can split
1118
+ // MCP usage from the web app and any future surfaces. The value
1119
+ // ("mcp") is the canonical source identifier — match it in any
1120
+ // PostHog filter or insight that should isolate the MCP surface.
1121
+ source: "mcp",
1108
1122
  mcp_version: version,
1109
1123
  node_version: process.versions.node,
1110
1124
  platform: process.platform,
@@ -1204,6 +1218,24 @@ function initTelemetry(opts) {
1204
1218
  captureTopupLink(props) {
1205
1219
  emit(EV_TOPUP_LINK, { ...props });
1206
1220
  },
1221
+ captureStartup(props) {
1222
+ emit(EV_STARTUP, { ...props });
1223
+ },
1224
+ captureUpdateCheck(props) {
1225
+ emit(EV_MCP_UPDATE_CHECK, { ...props });
1226
+ },
1227
+ captureUpdatePrompted(props) {
1228
+ emit(EV_MCP_UPDATE_PROMPTED, { ...props });
1229
+ },
1230
+ captureUpdateInstallClicked(props) {
1231
+ emit(EV_MCP_UPDATE_INSTALL_CLICKED, { ...props });
1232
+ },
1233
+ captureUpdateDismissed(props) {
1234
+ emit(EV_MCP_UPDATE_DISMISSED, { ...props });
1235
+ },
1236
+ captureVersionUpdated(props) {
1237
+ emit(EV_MCP_VERSION_UPDATED, { ...props });
1238
+ },
1207
1239
  captureException(err, ctx) {
1208
1240
  if (!sentryReady) return;
1209
1241
  try {
@@ -1227,6 +1259,327 @@ function initTelemetry(opts) {
1227
1259
  };
1228
1260
  }
1229
1261
 
1262
+ // src/update-check.ts
1263
+ var cachedInfo = null;
1264
+ var checkInFlight = false;
1265
+ function getCachedUpdateInfo() {
1266
+ return cachedInfo;
1267
+ }
1268
+ var RELEASES_LATEST_URL = "https://api.github.com/repos/leadbay/leadclaw/releases/latest";
1269
+ var CHECK_THROTTLE_MS = 24 * 60 * 60 * 1e3;
1270
+ var FETCH_TIMEOUT_MS = 5e3;
1271
+ var USER_AGENT = "leadbay-mcp-update-check";
1272
+ function parseTagName(tag) {
1273
+ const stripped = tag.replace(/^mcp-v?/, "").replace(/^v/, "");
1274
+ if (!/^\d+\.\d+\.\d+/.test(stripped)) return null;
1275
+ return stripped;
1276
+ }
1277
+ function compareSemver(a, b) {
1278
+ const [aCore, aPre] = a.split("-", 2);
1279
+ const [bCore, bPre] = b.split("-", 2);
1280
+ const aParts = aCore.split(".").map((n) => parseInt(n, 10));
1281
+ const bParts = bCore.split(".").map((n) => parseInt(n, 10));
1282
+ for (let i = 0; i < 3; i++) {
1283
+ const av = aParts[i] ?? 0;
1284
+ const bv = bParts[i] ?? 0;
1285
+ if (av > bv) return 1;
1286
+ if (av < bv) return -1;
1287
+ }
1288
+ if (!aPre && !bPre) return 0;
1289
+ if (!aPre && bPre) return 1;
1290
+ if (aPre && !bPre) return -1;
1291
+ const aIds = aPre.split(".");
1292
+ const bIds = bPre.split(".");
1293
+ const len = Math.max(aIds.length, bIds.length);
1294
+ for (let i = 0; i < len; i++) {
1295
+ const ai = aIds[i];
1296
+ const bi = bIds[i];
1297
+ if (ai === void 0) return -1;
1298
+ if (bi === void 0) return 1;
1299
+ const aNum = /^\d+$/.test(ai);
1300
+ const bNum = /^\d+$/.test(bi);
1301
+ if (aNum && bNum) {
1302
+ const d = parseInt(ai, 10) - parseInt(bi, 10);
1303
+ if (d !== 0) return d > 0 ? 1 : -1;
1304
+ } else if (aNum !== bNum) {
1305
+ return aNum ? -1 : 1;
1306
+ } else if (ai !== bi) {
1307
+ return ai > bi ? 1 : -1;
1308
+ }
1309
+ }
1310
+ return 0;
1311
+ }
1312
+ function pickMcpbAsset(rel) {
1313
+ if (!Array.isArray(rel.assets)) return void 0;
1314
+ const mcpb = rel.assets.find(
1315
+ (a) => typeof a.name === "string" && a.name.endsWith(".mcpb")
1316
+ );
1317
+ if (mcpb?.browser_download_url) return mcpb.browser_download_url;
1318
+ const dxt = rel.assets.find(
1319
+ (a) => typeof a.name === "string" && a.name.endsWith(".dxt")
1320
+ );
1321
+ return dxt?.browser_download_url;
1322
+ }
1323
+ async function checkForUpdate(opts) {
1324
+ if (checkInFlight) return cachedInfo;
1325
+ checkInFlight = true;
1326
+ try {
1327
+ return await doCheck(opts);
1328
+ } finally {
1329
+ checkInFlight = false;
1330
+ }
1331
+ }
1332
+ async function doCheck(opts) {
1333
+ const now = opts.now ?? Date.now;
1334
+ const url = opts.releasesUrl ?? RELEASES_LATEST_URL;
1335
+ const fetchImpl = opts.fetchImpl ?? fetch;
1336
+ const currentVersion = opts.currentVersion;
1337
+ const state = await opts.stateStore.read();
1338
+ const within = now() - state.last_check_time < CHECK_THROTTLE_MS;
1339
+ if (within && state.latest_known_version && state.latest_known_mcpb_url && state.latest_known_release_url) {
1340
+ const cached = buildInfoIfUpgrade(
1341
+ currentVersion,
1342
+ state.latest_known_version,
1343
+ state.latest_known_mcpb_url,
1344
+ state.latest_known_release_url,
1345
+ state.suppressed_versions,
1346
+ state.remind_until,
1347
+ now()
1348
+ );
1349
+ cachedInfo = cached;
1350
+ return cached;
1351
+ }
1352
+ let status;
1353
+ let body = null;
1354
+ let nextEtag;
1355
+ try {
1356
+ const ctrl = new AbortController();
1357
+ const timer = setTimeout(() => ctrl.abort(), FETCH_TIMEOUT_MS);
1358
+ let resp;
1359
+ try {
1360
+ resp = await fetchImpl(url, {
1361
+ method: "GET",
1362
+ headers: {
1363
+ Accept: "application/vnd.github+json",
1364
+ "User-Agent": USER_AGENT,
1365
+ ...state.etag ? { "If-None-Match": state.etag } : {}
1366
+ },
1367
+ signal: ctrl.signal
1368
+ });
1369
+ } finally {
1370
+ clearTimeout(timer);
1371
+ }
1372
+ status = resp.status;
1373
+ nextEtag = resp.headers.get("etag") ?? state.etag;
1374
+ if (status === 200) {
1375
+ body = await resp.json();
1376
+ }
1377
+ } catch (err) {
1378
+ opts.logger?.warn?.(
1379
+ `update_check.fetch_failed ${err?.message ?? err}`
1380
+ );
1381
+ opts.telemetry.captureUpdateCheck?.({
1382
+ current_version: currentVersion,
1383
+ check_error: String(err?.message ?? err)
1384
+ });
1385
+ await opts.stateStore.update((cur) => ({ ...cur, last_check_time: now() }));
1386
+ return null;
1387
+ }
1388
+ if (status !== 200 && status !== 304) {
1389
+ opts.telemetry.captureUpdateCheck?.({
1390
+ current_version: currentVersion,
1391
+ check_error: `http_${status}`
1392
+ });
1393
+ await opts.stateStore.update((cur) => ({ ...cur, last_check_time: now() }));
1394
+ return null;
1395
+ }
1396
+ let latestVersion;
1397
+ let mcpbUrl;
1398
+ let releaseUrl;
1399
+ if (status === 200 && body) {
1400
+ const parsed = body.tag_name ? parseTagName(body.tag_name) : null;
1401
+ if (parsed) {
1402
+ latestVersion = parsed;
1403
+ mcpbUrl = pickMcpbAsset(body);
1404
+ releaseUrl = body.html_url;
1405
+ }
1406
+ } else {
1407
+ latestVersion = state.latest_known_version;
1408
+ mcpbUrl = state.latest_known_mcpb_url;
1409
+ releaseUrl = state.latest_known_release_url;
1410
+ }
1411
+ const persisted = await opts.stateStore.update((cur) => ({
1412
+ ...cur,
1413
+ last_check_time: now(),
1414
+ etag: nextEtag,
1415
+ latest_known_version: latestVersion ?? cur.latest_known_version,
1416
+ latest_known_mcpb_url: mcpbUrl ?? cur.latest_known_mcpb_url,
1417
+ latest_known_release_url: releaseUrl ?? cur.latest_known_release_url
1418
+ }));
1419
+ opts.telemetry.captureUpdateCheck?.({
1420
+ current_version: currentVersion,
1421
+ latest_version: persisted.latest_known_version
1422
+ });
1423
+ const info = buildInfoIfUpgrade(
1424
+ currentVersion,
1425
+ persisted.latest_known_version,
1426
+ persisted.latest_known_mcpb_url,
1427
+ persisted.latest_known_release_url,
1428
+ persisted.suppressed_versions,
1429
+ persisted.remind_until,
1430
+ now()
1431
+ );
1432
+ cachedInfo = info;
1433
+ return info;
1434
+ }
1435
+ function buildInfoIfUpgrade(currentVersion, latestVersion, mcpbUrl, releaseUrl, suppressed, remindUntil, nowMs) {
1436
+ if (!latestVersion || !mcpbUrl || !releaseUrl) return null;
1437
+ if (compareSemver(latestVersion, currentVersion) <= 0) return null;
1438
+ if (suppressed.includes(latestVersion)) return null;
1439
+ if (remindUntil && remindUntil > nowMs) return null;
1440
+ return {
1441
+ current_version: currentVersion,
1442
+ latest_version: latestVersion,
1443
+ mcpb_url: mcpbUrl,
1444
+ release_url: releaseUrl
1445
+ };
1446
+ }
1447
+ async function recordRunningVersion(currentVersion, stateStore, telemetry) {
1448
+ const cur = await stateStore.read();
1449
+ const prev = cur.previous_running_version;
1450
+ if (prev === currentVersion) return;
1451
+ if (prev && compareSemver(currentVersion, prev) > 0) {
1452
+ telemetry.captureVersionUpdated?.({
1453
+ from_version: prev,
1454
+ to_version: currentVersion
1455
+ });
1456
+ }
1457
+ await stateStore.update((s) => ({
1458
+ ...s,
1459
+ previous_running_version: currentVersion,
1460
+ // Clear any prior "remind me tomorrow" for the version we just landed on —
1461
+ // the user has effectively answered the prompt by upgrading.
1462
+ remind_until: void 0,
1463
+ // Drop suppression of versions we've now surpassed.
1464
+ suppressed_versions: s.suppressed_versions.filter(
1465
+ (v) => compareSemver(v, currentVersion) > 0
1466
+ )
1467
+ }));
1468
+ }
1469
+
1470
+ // src/update-tool.ts
1471
+ var TWENTY_FOUR_HOURS_MS = 24 * 60 * 60 * 1e3;
1472
+ var DESCRIPTION = "Record the user's choice on an update prompt surfaced via `update_available` on leadbay_account_status. Pass `action: 'install' | 'remind_tomorrow' | 'skip'` and `version` (the `latest_version` from the prompt). On 'install', the server returns `{ mcpb_url, release_url }` \u2014 show the user a clickable link to mcpb_url so Claude Desktop's native installer opens it. On 'remind_tomorrow' the server suppresses the prompt for 24 hours. On 'skip' the version is suppressed permanently. Call this tool EXACTLY ONCE per prompt \u2014 do not loop, and do not call it speculatively when no update_available block is present.";
1473
+ function buildAcknowledgeUpdateTool(opts) {
1474
+ const now = opts.now ?? Date.now;
1475
+ return {
1476
+ name: "leadbay_acknowledge_update",
1477
+ annotations: {
1478
+ title: "Acknowledge a Leadbay MCP update prompt",
1479
+ readOnlyHint: false,
1480
+ destructiveHint: false,
1481
+ idempotentHint: false,
1482
+ openWorldHint: false
1483
+ },
1484
+ description: DESCRIPTION,
1485
+ inputSchema: {
1486
+ type: "object",
1487
+ properties: {
1488
+ action: {
1489
+ type: "string",
1490
+ enum: ["install", "remind_tomorrow", "skip"],
1491
+ description: "What the user chose: 'install' (they'll click the link), 'remind_tomorrow' (suppress for 24h), or 'skip' (suppress this version permanently)."
1492
+ },
1493
+ version: {
1494
+ type: "string",
1495
+ description: "The latest_version string from the update_available block. Used for suppression and event correlation."
1496
+ }
1497
+ },
1498
+ required: ["action", "version"],
1499
+ additionalProperties: false
1500
+ },
1501
+ outputSchema: {
1502
+ type: "object",
1503
+ properties: {
1504
+ ok: { type: "boolean" },
1505
+ action: { type: "string" },
1506
+ version: { type: "string" },
1507
+ message: { type: "string" },
1508
+ mcpb_url: { type: ["string", "null"] },
1509
+ release_url: { type: ["string", "null"] }
1510
+ },
1511
+ required: ["ok", "action", "version", "message"]
1512
+ },
1513
+ execute: async (_client, args, _ctx) => {
1514
+ const action = String(args?.action ?? "");
1515
+ const version = String(args?.version ?? "");
1516
+ if (action !== "install" && action !== "remind_tomorrow" && action !== "skip") {
1517
+ return {
1518
+ error: true,
1519
+ code: "INVALID_ARGUMENT",
1520
+ message: `Unknown action '${action}' for leadbay_acknowledge_update.`,
1521
+ hint: "Pass one of: 'install', 'remind_tomorrow', 'skip'."
1522
+ };
1523
+ }
1524
+ if (!version) {
1525
+ return {
1526
+ error: true,
1527
+ code: "INVALID_ARGUMENT",
1528
+ message: "Missing required `version` argument.",
1529
+ hint: "Pass the latest_version string from the update_available block."
1530
+ };
1531
+ }
1532
+ if (action === "install") {
1533
+ const state = await opts.stateStore.read();
1534
+ opts.telemetry.captureUpdateInstallClicked?.({
1535
+ current_version: opts.currentVersion,
1536
+ latest_version: version
1537
+ });
1538
+ return {
1539
+ ok: true,
1540
+ action,
1541
+ version,
1542
+ mcpb_url: state.latest_known_mcpb_url ?? null,
1543
+ release_url: state.latest_known_release_url ?? null,
1544
+ message: state.latest_known_mcpb_url ? "Show the user the mcpb_url as a clickable link \u2014 opening it in Claude Desktop runs the native installer." : "No .mcpb URL is cached. Direct the user to the release_url to download manually."
1545
+ };
1546
+ }
1547
+ if (action === "remind_tomorrow") {
1548
+ await opts.stateStore.update((cur) => ({
1549
+ ...cur,
1550
+ remind_until: now() + TWENTY_FOUR_HOURS_MS
1551
+ }));
1552
+ opts.telemetry.captureUpdateDismissed?.({
1553
+ current_version: opts.currentVersion,
1554
+ latest_version: version,
1555
+ action: "remind_tomorrow"
1556
+ });
1557
+ return {
1558
+ ok: true,
1559
+ action,
1560
+ version,
1561
+ message: "Reminder snoozed for 24 hours. No further prompts will appear in that window."
1562
+ };
1563
+ }
1564
+ await opts.stateStore.update((cur) => ({
1565
+ ...cur,
1566
+ suppressed_versions: cur.suppressed_versions.includes(version) ? cur.suppressed_versions : [...cur.suppressed_versions, version]
1567
+ }));
1568
+ opts.telemetry.captureUpdateDismissed?.({
1569
+ current_version: opts.currentVersion,
1570
+ latest_version: version,
1571
+ action: "skip"
1572
+ });
1573
+ return {
1574
+ ok: true,
1575
+ action,
1576
+ version,
1577
+ message: `Version ${version} suppressed. Future releases will still prompt.`
1578
+ };
1579
+ }
1580
+ };
1581
+ }
1582
+
1230
1583
  // src/server.ts
1231
1584
  var VERIFICATION_MANDATE = "After every email, call, message, or meeting with a lead's contact, you MUST call leadbay_report_outreach with verification={source, ref} (gmail_message_id from the Gmail send, calendar_event_id from a booking, or user_confirmed='<the user's literal confirmation>'). Skipping or fabricating verification poisons the human team's pipeline.";
1232
1585
  var MENTAL_MODEL_PARAGRAPH = "How Leadbay works (mental model): Leadbay is a sales inbox, not a queryable database. Each day the user logs back in, a fresh batch of leads is delivered. Batch size is paced by how many leads the user has actually acted on recently \u2014 some workflows produce a big stream of smaller prospects, others a narrow stream of bigger ones. Pulling more won't produce more; the user acting on leads (outreach, skips, saves) does.";
@@ -1249,6 +1602,10 @@ function buildStartHereParagraph(has) {
1249
1602
  }
1250
1603
  return base + " When the user asks for refinement, contact enrichment, audience changes, or outreach reporting, tell them: those actions require write tools, currently disabled. Re-enable by removing `LEADBAY_MCP_WRITE=0` from your MCP client config and restarting the client. Also: do not promise to log outreach \u2014 the report_outreach tool is not available in this configuration.";
1251
1604
  }
1605
+ function buildUpdateAvailableParagraph(has) {
1606
+ if (!has("leadbay_acknowledge_update")) return null;
1607
+ return "MCP auto-update: when `leadbay_account_status` returns an `update_available` field (`{ current_version, latest_version, mcpb_url, release_url }`), a newer MCP server release is published and the user has NOT suppressed it. Surface a prompt via `ask_user_input_v0` with EXACTLY these three options: \"Install now\", \"Remind me tomorrow\", \"Skip this version\". Map the user's choice to `leadbay_acknowledge_update({ action: 'install' | 'remind_tomorrow' | 'skip', version: latest_version })`. On 'install', the tool returns `mcpb_url` \u2014 render it as a clickable markdown link the user can open in Claude Desktop (the .mcpb extension triggers the native installer). The user does NOT need to restart anything before clicking \u2014 the new server takes effect on the next MCP session. Prompt the user ONCE per session per version \u2014 don't re-prompt within the same chat after they've acknowledged.";
1608
+ }
1252
1609
  function buildRhythmParagraph(has) {
1253
1610
  if (has("leadbay_report_outreach")) {
1254
1611
  return "Suggested rhythm: a healthy agent pattern is a daily check-in \u2014 pull fresh leads, skim the auto-qualified top, deepen 1-3 promising ones, propose outreach to the user, then leadbay_report_outreach on what actually got sent. If your host supports scheduling, offer to set up a daily run.";
@@ -1329,6 +1686,8 @@ function buildServerInstructions(exposed) {
1329
1686
  parts.push(buildScoringParagraph(has));
1330
1687
  parts.push(buildStartHereParagraph(has));
1331
1688
  parts.push(buildRhythmParagraph(has));
1689
+ const updateParagraph = buildUpdateAvailableParagraph(has);
1690
+ if (updateParagraph) parts.push(updateParagraph);
1332
1691
  const promptsCatalog = buildPromptsCatalogParagraph(has);
1333
1692
  if (promptsCatalog) parts.push(promptsCatalog);
1334
1693
  parts.push(RESOURCES_PARAGRAPH);
@@ -1376,6 +1735,16 @@ function buildServer(client, opts = {}) {
1376
1735
  exposedTools.push(...granularWriteTools);
1377
1736
  }
1378
1737
  }
1738
+ if (opts.updateStateStore) {
1739
+ exposedTools.push(
1740
+ buildAcknowledgeUpdateTool({
1741
+ stateStore: opts.updateStateStore,
1742
+ telemetry: opts.telemetry ?? NOOP_TELEMETRY,
1743
+ currentVersion: opts.version ?? "0.0.0-dev",
1744
+ logger: opts.logger
1745
+ })
1746
+ );
1747
+ }
1379
1748
  if (opts.extraTools) {
1380
1749
  exposedTools.push(...opts.extraTools);
1381
1750
  }
@@ -1465,10 +1834,45 @@ function buildServer(client, opts = {}) {
1465
1834
  const DEBUG_RAW = process.env.LEADBAY_DEBUG ?? "";
1466
1835
  const DEBUG_ON = DEBUG_RAW === "1" || DEBUG_RAW.toLowerCase() === "true";
1467
1836
  const telemetry = opts.telemetry ?? NOOP_TELEMETRY;
1837
+ const promptedVersionsThisSession = /* @__PURE__ */ new Set();
1838
+ const serverVersion = opts.version ?? "0.0.0-dev";
1839
+ const UPDATE_CHECK_DISABLED = process.env.LEADBAY_UPDATE_CHECK_DISABLED === "1";
1840
+ const maybeRefreshUpdate = () => {
1841
+ if (UPDATE_CHECK_DISABLED) return;
1842
+ if (!opts.updateStateStore) return;
1843
+ void checkForUpdate({
1844
+ currentVersion: serverVersion,
1845
+ stateStore: opts.updateStateStore,
1846
+ telemetry,
1847
+ logger: opts.logger
1848
+ }).catch((err) => {
1849
+ opts.logger?.warn?.(
1850
+ `update_check.unexpected ${err?.message ?? err}`
1851
+ );
1852
+ });
1853
+ };
1854
+ const maybeAttachUpdate = (toolName, result) => {
1855
+ if (toolName !== "leadbay_account_status") return;
1856
+ if (!opts.updateStateStore) return;
1857
+ if (result === null || typeof result !== "object" || Array.isArray(result)) {
1858
+ return;
1859
+ }
1860
+ const info = getCachedUpdateInfo();
1861
+ if (!info) return;
1862
+ result.update_available = info;
1863
+ if (!promptedVersionsThisSession.has(info.latest_version)) {
1864
+ promptedVersionsThisSession.add(info.latest_version);
1865
+ telemetry.captureUpdatePrompted?.({
1866
+ current_version: serverVersion,
1867
+ latest_version: info.latest_version
1868
+ });
1869
+ }
1870
+ };
1468
1871
  const isLeadbayBusinessError = (err) => err != null && typeof err === "object" && err.error === true && typeof err.code === "string";
1469
1872
  server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
1470
1873
  const callStart = Date.now();
1471
1874
  const name = req.params.name;
1875
+ maybeRefreshUpdate();
1472
1876
  const tool = toolByName.get(name);
1473
1877
  if (!tool) {
1474
1878
  return {
@@ -1522,6 +1926,7 @@ function buildServer(client, opts = {}) {
1522
1926
  progress,
1523
1927
  elicit
1524
1928
  });
1929
+ maybeAttachUpdate(name, result);
1525
1930
  if (result && typeof result === "object" && result.error === true) {
1526
1931
  const envText = formatErrorForLLM(result);
1527
1932
  const envDur = Date.now() - callStart;
@@ -1659,9 +2064,202 @@ function buildServer(client, opts = {}) {
1659
2064
  return server;
1660
2065
  }
1661
2066
 
2067
+ // src/update-state.ts
2068
+ import {
2069
+ mkdir as mkdirAsync,
2070
+ lstat,
2071
+ open as fsOpen,
2072
+ readFile,
2073
+ rename,
2074
+ stat,
2075
+ unlink
2076
+ } from "fs/promises";
2077
+ import { constants as fsConstants } from "fs";
2078
+ import { dirname, resolve as resolvePath } from "path";
2079
+ import { homedir } from "os";
2080
+ function emptyState() {
2081
+ return {
2082
+ last_check_time: 0,
2083
+ suppressed_versions: []
2084
+ };
2085
+ }
2086
+ var UpdateStateStore = class {
2087
+ backend;
2088
+ path;
2089
+ logger;
2090
+ allowUnsafePath;
2091
+ now;
2092
+ memory = emptyState();
2093
+ initialized = false;
2094
+ constructor(opts) {
2095
+ this.backend = opts.backend;
2096
+ this.logger = opts.logger;
2097
+ this.allowUnsafePath = !!opts.allowUnsafePath;
2098
+ this.now = opts.now ?? Date.now;
2099
+ if (this.backend === "file") {
2100
+ if (!opts.path) {
2101
+ throw new Error("UpdateStateStore: path is required when backend=file");
2102
+ }
2103
+ this.path = resolvePath(opts.path);
2104
+ this.validatePath(this.path);
2105
+ }
2106
+ }
2107
+ get durability() {
2108
+ return this.backend;
2109
+ }
2110
+ get resolvedPath() {
2111
+ return this.path;
2112
+ }
2113
+ validatePath(p) {
2114
+ if (this.allowUnsafePath) return;
2115
+ const home = resolvePath(homedir());
2116
+ if (p !== home && !p.startsWith(home + "/") && !p.startsWith(home + "\\")) {
2117
+ throw new Error(
2118
+ `UpdateStateStore: path ${p} is outside $HOME (${home}). Set LEADBAY_UPDATE_STATE_PATH_UNSAFE=1 to override.`
2119
+ );
2120
+ }
2121
+ }
2122
+ async ensureInitialized() {
2123
+ if (this.initialized || this.backend !== "file") {
2124
+ this.initialized = true;
2125
+ return;
2126
+ }
2127
+ const dir = dirname(this.path);
2128
+ await mkdirAsync(dir, { recursive: true, mode: 448 });
2129
+ try {
2130
+ const st = await lstat(this.path);
2131
+ if (st.isSymbolicLink()) {
2132
+ throw new Error(
2133
+ `UpdateStateStore: refusing to use ${this.path} \u2014 path is a symlink. Set LEADBAY_UPDATE_STATE_PATH_UNSAFE=1 to override.`
2134
+ );
2135
+ }
2136
+ } catch (err) {
2137
+ if (err?.code !== "ENOENT") throw err;
2138
+ }
2139
+ this.initialized = true;
2140
+ }
2141
+ async read() {
2142
+ if (this.backend === "memory") return { ...this.memory, suppressed_versions: [...this.memory.suppressed_versions] };
2143
+ await this.ensureInitialized();
2144
+ let raw;
2145
+ try {
2146
+ raw = await readFile(this.path, "utf8");
2147
+ } catch (err) {
2148
+ if (err?.code === "ENOENT") return emptyState();
2149
+ throw err;
2150
+ }
2151
+ try {
2152
+ const parsed = JSON.parse(raw);
2153
+ return this.validate(parsed);
2154
+ } catch (err) {
2155
+ this.logger?.warn?.(
2156
+ `update_state.parse_failed ${err?.message ?? err}; resetting to empty`
2157
+ );
2158
+ return emptyState();
2159
+ }
2160
+ }
2161
+ async write(state) {
2162
+ if (this.backend === "memory") {
2163
+ this.memory = { ...state, suppressed_versions: [...state.suppressed_versions] };
2164
+ return;
2165
+ }
2166
+ await this.ensureInitialized();
2167
+ const tmp = `${this.path}.tmp.${process.pid}.${this.now()}`;
2168
+ const handle = await openTmpFileExclusive(tmp);
2169
+ try {
2170
+ await handle.writeFile(JSON.stringify(state, null, 2));
2171
+ } finally {
2172
+ await handle.close();
2173
+ }
2174
+ await rename(tmp, this.path);
2175
+ }
2176
+ /**
2177
+ * Apply a partial mutation atomically (read → merge → write). Caller
2178
+ * passes a function so concurrent mutators (the startup check + a
2179
+ * tool call landing during it) compose without dropping fields.
2180
+ */
2181
+ async update(mutator) {
2182
+ const cur = await this.read();
2183
+ const next = mutator(cur);
2184
+ await this.write(next);
2185
+ return next;
2186
+ }
2187
+ validate(raw) {
2188
+ if (!raw || typeof raw !== "object") return emptyState();
2189
+ const r = raw;
2190
+ const out = emptyState();
2191
+ if (typeof r.last_check_time === "number" && Number.isFinite(r.last_check_time)) {
2192
+ out.last_check_time = r.last_check_time;
2193
+ }
2194
+ if (typeof r.latest_known_version === "string") {
2195
+ out.latest_known_version = r.latest_known_version;
2196
+ }
2197
+ if (typeof r.latest_known_mcpb_url === "string") {
2198
+ out.latest_known_mcpb_url = r.latest_known_mcpb_url;
2199
+ }
2200
+ if (typeof r.latest_known_release_url === "string") {
2201
+ out.latest_known_release_url = r.latest_known_release_url;
2202
+ }
2203
+ if (typeof r.etag === "string") out.etag = r.etag;
2204
+ if (Array.isArray(r.suppressed_versions)) {
2205
+ out.suppressed_versions = r.suppressed_versions.filter(
2206
+ (v) => typeof v === "string"
2207
+ );
2208
+ }
2209
+ if (typeof r.remind_until === "number" && Number.isFinite(r.remind_until)) {
2210
+ out.remind_until = r.remind_until;
2211
+ }
2212
+ if (typeof r.previous_running_version === "string") {
2213
+ out.previous_running_version = r.previous_running_version;
2214
+ }
2215
+ return out;
2216
+ }
2217
+ };
2218
+ async function openTmpFileExclusive(path) {
2219
+ try {
2220
+ return await fsOpen(
2221
+ path,
2222
+ fsConstants.O_CREAT | fsConstants.O_WRONLY | fsConstants.O_EXCL,
2223
+ 384
2224
+ );
2225
+ } catch (err) {
2226
+ if (err?.code === "EEXIST") {
2227
+ await unlink(path).catch(() => {
2228
+ });
2229
+ return fsOpen(
2230
+ path,
2231
+ fsConstants.O_CREAT | fsConstants.O_WRONLY | fsConstants.O_EXCL,
2232
+ 384
2233
+ );
2234
+ }
2235
+ throw err;
2236
+ }
2237
+ }
2238
+ async function createDefaultUpdateStateStore(opts = {}) {
2239
+ const env = opts.env ?? process.env;
2240
+ const allowUnsafePath = env.LEADBAY_UPDATE_STATE_PATH_UNSAFE === "1";
2241
+ const path = env.LEADBAY_UPDATE_STATE_PATH ?? resolvePath(homedir(), ".leadbay", "update-state.json");
2242
+ try {
2243
+ const store = new UpdateStateStore({
2244
+ backend: "file",
2245
+ path,
2246
+ logger: opts.logger,
2247
+ allowUnsafePath
2248
+ });
2249
+ await store.ensureInitialized();
2250
+ await stat(dirname(path));
2251
+ return store;
2252
+ } catch (err) {
2253
+ opts.logger?.warn?.(
2254
+ `update_state.fallback_memory path=${path} reason=${err?.message ?? err}`
2255
+ );
2256
+ return new UpdateStateStore({ backend: "memory", logger: opts.logger });
2257
+ }
2258
+ }
2259
+
1662
2260
  // src/bin.ts
1663
2261
  import { createRequire } from "module";
1664
- var VERSION = "0.10.0";
2262
+ var VERSION = "0.11.0";
1665
2263
  var HELP = `
1666
2264
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
1667
2265
 
@@ -1710,7 +2308,7 @@ EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
1710
2308
  "mcpServers": {
1711
2309
  "leadbay": {
1712
2310
  "command": "npx",
1713
- "args": ["-y", "@leadbay/mcp@0.3"],
2311
+ "args": ["-y", "@leadbay/mcp@0.11"],
1714
2312
  "env": {
1715
2313
  "LEADBAY_TOKEN": "lb_...",
1716
2314
  "LEADBAY_REGION": "us",
@@ -1763,9 +2361,47 @@ function exitWithTokenError() {
1763
2361
  );
1764
2362
  process.exit(1);
1765
2363
  }
2364
+ var BrokenLeadbayClient = class extends LeadbayClient {
2365
+ stubError;
2366
+ constructor(stubError, baseUrl, region) {
2367
+ super(baseUrl, "broken-token-startup-auth-failure", region);
2368
+ this.stubError = stubError;
2369
+ }
2370
+ async request() {
2371
+ throw this.stubError;
2372
+ }
2373
+ async requestVoid() {
2374
+ throw this.stubError;
2375
+ }
2376
+ async requestRawBinary() {
2377
+ throw this.stubError;
2378
+ }
2379
+ };
2380
+ function makeBrokenClient(stubError, region) {
2381
+ const baseUrl = region === "fr" ? "https://api-fr.leadbay.app" : "https://api-us.leadbay.app";
2382
+ return new BrokenLeadbayClient(stubError, baseUrl, region);
2383
+ }
1766
2384
  async function resolveClientFromEnv(logger) {
1767
2385
  const token = process.env.LEADBAY_TOKEN;
1768
- if (!token) exitWithTokenError();
2386
+ if (!token) {
2387
+ process.stderr.write(
2388
+ "leadbay-mcp: LEADBAY_TOKEN environment variable is required.\n 1. Run: npx -y @leadbay/mcp install --email <you> --region <us|fr>\n 2. Set it in your MCP client config (e.g. claude_desktop_config.json).\n\nRun `leadbay-mcp --help` for the full config template.\n"
2389
+ );
2390
+ const regionEnv2 = process.env.LEADBAY_REGION;
2391
+ const region = regionEnv2 === "fr" ? "fr" : "us";
2392
+ return {
2393
+ client: makeBrokenClient(
2394
+ {
2395
+ error: true,
2396
+ code: "AUTH_MISSING",
2397
+ message: "LEADBAY_TOKEN environment variable is not set.",
2398
+ hint: "Run `npx -y @leadbay/mcp install --email <you> --region <us|fr>` to mint a token, then set LEADBAY_TOKEN in your MCP client config."
2399
+ },
2400
+ region
2401
+ ),
2402
+ authState: "missing"
2403
+ };
2404
+ }
1769
2405
  const regionEnv = process.env.LEADBAY_REGION;
1770
2406
  const explicitRegion = regionEnv === "us" || regionEnv === "fr" ? regionEnv : void 0;
1771
2407
  const baseUrl = process.env.LEADBAY_BASE_URL;
@@ -1773,7 +2409,7 @@ async function resolveClientFromEnv(logger) {
1773
2409
  const config = { token };
1774
2410
  if (baseUrl) config.baseUrl = baseUrl;
1775
2411
  if (explicitRegion) config.region = explicitRegion;
1776
- return createClient(config);
2412
+ return { client: createClient(config), authState: "ok" };
1777
2413
  }
1778
2414
  process.stderr.write(
1779
2415
  "[leadbay-mcp warn] LEADBAY_REGION is unset; probing api-us and api-fr in parallel.\n Your bearer token will be sent to BOTH backends. Set LEADBAY_REGION=us|fr in your\n MCP client config to avoid this.\n"
@@ -1785,7 +2421,8 @@ async function resolveClientFromEnv(logger) {
1785
2421
  return c;
1786
2422
  };
1787
2423
  try {
1788
- return await Promise.any([probe("us"), probe("fr")]);
2424
+ const client = await Promise.any([probe("us"), probe("fr")]);
2425
+ return { client, authState: "ok" };
1789
2426
  } catch (err) {
1790
2427
  const errors = err?.errors ?? [];
1791
2428
  const firstAuth = errors.find(
@@ -1797,14 +2434,28 @@ async function resolveClientFromEnv(logger) {
1797
2434
  Tip: verify your LEADBAY_TOKEN is valid and, if you know your region, set LEADBAY_REGION=us or LEADBAY_REGION=fr.
1798
2435
  `
1799
2436
  );
1800
- process.exit(1);
2437
+ return {
2438
+ client: makeBrokenClient(
2439
+ {
2440
+ error: true,
2441
+ code: firstAuth.code,
2442
+ message: firstAuth.message,
2443
+ hint: "Verify your LEADBAY_TOKEN is valid. If you know your region, set LEADBAY_REGION=us or LEADBAY_REGION=fr to skip auto-probing. Mint a fresh token with `leadbay-mcp login --email <you> --region <us|fr>`."
2444
+ },
2445
+ "us"
2446
+ ),
2447
+ authState: "expired"
2448
+ };
1801
2449
  }
1802
2450
  const firstMsg = errors[0]?.message ?? String(err);
1803
2451
  process.stderr.write(
1804
2452
  `leadbay-mcp: region auto-detection failed (${firstMsg}). Defaulting to us; set LEADBAY_REGION to skip probing.
1805
2453
  `
1806
2454
  );
1807
- return createClient({ token, region: "us" });
2455
+ return {
2456
+ client: createClient({ token, region: "us" }),
2457
+ authState: "probe_failed"
2458
+ };
1808
2459
  }
1809
2460
  }
1810
2461
  async function readPassword() {
@@ -1958,7 +2609,7 @@ async function runLogin(args) {
1958
2609
  let result;
1959
2610
  try {
1960
2611
  if (pinnedRegion && !allowFallback) {
1961
- const { REGIONS } = await import("./dist-BHLIJAIH.js");
2612
+ const { REGIONS } = await import("./dist-JZ2FLLN6.js");
1962
2613
  const baseUrl = REGIONS[pinnedRegion];
1963
2614
  const c = createClient({ region: pinnedRegion });
1964
2615
  const token = await loginAt(baseUrl, email, password);
@@ -1977,7 +2628,7 @@ async function runLogin(args) {
1977
2628
  mcpServers: {
1978
2629
  leadbay: {
1979
2630
  command: "npx",
1980
- args: ["-y", "@leadbay/mcp@0.3"],
2631
+ args: ["-y", "@leadbay/mcp@0.11"],
1981
2632
  env: {
1982
2633
  LEADBAY_TOKEN: result.token,
1983
2634
  LEADBAY_REGION: result.region
@@ -2017,7 +2668,7 @@ Or for Claude Code (token included \u2014 same warning applies):
2017
2668
  claude mcp add leadbay --scope user \\
2018
2669
  --env LEADBAY_TOKEN=${result.token} \\
2019
2670
  --env LEADBAY_REGION=${result.region} \\
2020
- -- npx -y @leadbay/mcp@0.3
2671
+ -- npx -y @leadbay/mcp@0.11
2021
2672
 
2022
2673
  Restart your MCP client to pick up the new server.
2023
2674
  `
@@ -2064,8 +2715,8 @@ Restart your MCP client to pick up the new server.
2064
2715
  let actualMode;
2065
2716
  try {
2066
2717
  const { writeFileSync, chmodSync, mkdirSync, renameSync, statSync, unlinkSync } = await import("fs");
2067
- const { dirname } = await import("path");
2068
- mkdirSync(dirname(targetPath), { recursive: true });
2718
+ const { dirname: dirname2 } = await import("path");
2719
+ mkdirSync(dirname2(targetPath), { recursive: true });
2069
2720
  const tmp = targetPath + ".tmp." + process.pid;
2070
2721
  writeFileSync(tmp, JSON.stringify(config, null, 2) + "\n", {
2071
2722
  encoding: "utf8",
@@ -2123,7 +2774,7 @@ For Claude Code, run:
2123
2774
  claude mcp add leadbay --scope user \\
2124
2775
  --env LEADBAY_TOKEN=$(jq -r .mcpServers.leadbay.env.LEADBAY_TOKEN ${quotedPath}) \\
2125
2776
  --env LEADBAY_REGION=${result.region} \\
2126
- -- npx -y @leadbay/mcp@0.3
2777
+ -- npx -y @leadbay/mcp@0.11
2127
2778
  `
2128
2779
  );
2129
2780
  }
@@ -2303,7 +2954,7 @@ function buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled) {
2303
2954
  `LEADBAY_TELEMETRY_ENABLED=${telemetryEnabled ? "true" : "false"}`
2304
2955
  ];
2305
2956
  if (!includeWrite) args.push("--env", `LEADBAY_MCP_WRITE=0`);
2306
- args.push("--", "npx", "-y", "@leadbay/mcp@0.3");
2957
+ args.push("--", "npx", "-y", "@leadbay/mcp@0.11");
2307
2958
  return args;
2308
2959
  }
2309
2960
  async function installInClaudeCode(token, region, includeWrite, telemetryEnabled) {
@@ -2329,7 +2980,7 @@ async function installInClaudeCode(token, region, includeWrite, telemetryEnabled
2329
2980
  async function installInJsonConfig(configPath, token, region, includeWrite, telemetryEnabled) {
2330
2981
  try {
2331
2982
  const { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } = await import("fs");
2332
- const { dirname } = await import("path");
2983
+ const { dirname: dirname2 } = await import("path");
2333
2984
  let parsed = {};
2334
2985
  let preserved = {};
2335
2986
  if (existsSync(configPath)) {
@@ -2341,7 +2992,7 @@ async function installInJsonConfig(configPath, token, region, includeWrite, tele
2341
2992
  return { ok: false, message: `existing ${configPath} is not valid JSON; refusing to overwrite` };
2342
2993
  }
2343
2994
  } else {
2344
- mkdirSync(dirname(configPath), { recursive: true });
2995
+ mkdirSync(dirname2(configPath), { recursive: true });
2345
2996
  }
2346
2997
  parsed.mcpServers = parsed.mcpServers ?? {};
2347
2998
  const env = {
@@ -2353,7 +3004,7 @@ async function installInJsonConfig(configPath, token, region, includeWrite, tele
2353
3004
  if (!includeWrite) env.LEADBAY_MCP_WRITE = "0";
2354
3005
  parsed.mcpServers.leadbay = {
2355
3006
  command: "npx",
2356
- args: ["-y", "@leadbay/mcp@0.3"],
3007
+ args: ["-y", "@leadbay/mcp@0.11"],
2357
3008
  env
2358
3009
  };
2359
3010
  const tmp = configPath + ".tmp";
@@ -2457,7 +3108,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
2457
3108
  let region;
2458
3109
  try {
2459
3110
  if (pinnedRegion && !allowFallback) {
2460
- const { REGIONS } = await import("./dist-BHLIJAIH.js");
3111
+ const { REGIONS } = await import("./dist-JZ2FLLN6.js");
2461
3112
  const baseUrl = REGIONS[pinnedRegion];
2462
3113
  token = await loginAt(baseUrl, email, password);
2463
3114
  region = pinnedRegion;
@@ -2530,7 +3181,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
2530
3181
  process.stderr.write(
2531
3182
  `
2532
3183
  The token was written into client config files but never printed to your terminal.
2533
- Verify with: LEADBAY_TOKEN=$(...) npx -y @leadbay/mcp@0.3 doctor
3184
+ Verify with: LEADBAY_TOKEN=$(...) npx -y @leadbay/mcp@0.11 doctor
2534
3185
  Restart your MCP client(s) to pick up the new server.
2535
3186
  If you ever leak the token, run \`leadbay-mcp login --email <you> --region <us|fr>\` to mint a fresh one (which invalidates the prior session).
2536
3187
  `
@@ -2581,6 +3232,26 @@ async function runDoctor() {
2581
3232
  );
2582
3233
  return 1;
2583
3234
  }
3235
+ var startupSafetyNetsInstalled = false;
3236
+ function installStartupSafetyNets(logger) {
3237
+ if (startupSafetyNetsInstalled) return;
3238
+ startupSafetyNetsInstalled = true;
3239
+ const reportAndExit = (label, err) => {
3240
+ const msg = err instanceof Error ? err.stack ?? err.message : String(err);
3241
+ process.stderr.write(`leadbay-mcp: ${label}: ${msg}
3242
+ `);
3243
+ logger.error?.(`${label}: ${msg}`);
3244
+ try {
3245
+ const bootTelemetry = initTelemetry({ version: VERSION });
3246
+ bootTelemetry.captureException(err, { tool: "__startup__" });
3247
+ void bootTelemetry.shutdown();
3248
+ } catch {
3249
+ }
3250
+ process.exit(1);
3251
+ };
3252
+ process.on("uncaughtException", (err) => reportAndExit("uncaughtException", err));
3253
+ process.on("unhandledRejection", (err) => reportAndExit("unhandledRejection", err));
3254
+ }
2584
3255
  async function main() {
2585
3256
  const arg = process.argv[2];
2586
3257
  if (arg === "--version" || arg === "-v") {
@@ -2603,23 +3274,45 @@ async function main() {
2603
3274
  process.exit(await runDoctor());
2604
3275
  }
2605
3276
  const logger = makeStderrLogger(parseLogLevel(process.env.LEADBAY_LOG_LEVEL));
3277
+ installStartupSafetyNets(logger);
2606
3278
  const telemetry = initTelemetry({ version: VERSION, logger });
2607
- const client = await resolveClientFromEnv(logger);
3279
+ const { client, authState } = await resolveClientFromEnv(logger);
2608
3280
  telemetry.identify(client);
3281
+ telemetry.captureStartup({
3282
+ auth_state: authState,
3283
+ region: client.region
3284
+ });
2609
3285
  const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
2610
3286
  const includeWrite = parseWriteEnv();
2611
3287
  const bulkTracker = await createDefaultBulkStore({ logger });
3288
+ const updateStateStore = await createDefaultUpdateStateStore({ logger });
3289
+ void recordRunningVersion(VERSION, updateStateStore, telemetry).catch((err) => {
3290
+ logger.warn?.(
3291
+ `update_state.record_version_failed ${err?.message ?? err}`
3292
+ );
3293
+ });
3294
+ if (process.env.LEADBAY_UPDATE_CHECK_DISABLED !== "1") {
3295
+ void checkForUpdate({
3296
+ currentVersion: VERSION,
3297
+ stateStore: updateStateStore,
3298
+ telemetry,
3299
+ logger
3300
+ }).catch((err) => {
3301
+ logger.warn?.(`update_check.unexpected ${err?.message ?? err}`);
3302
+ });
3303
+ }
2612
3304
  const server = buildServer(client, {
2613
3305
  includeAdvanced,
2614
3306
  includeWrite,
2615
3307
  logger,
2616
3308
  bulkTracker,
2617
3309
  version: VERSION,
2618
- telemetry
3310
+ telemetry,
3311
+ updateStateStore
2619
3312
  });
2620
3313
  const transport = new StdioServerTransport();
2621
3314
  logger.info?.(
2622
- `Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability})`
3315
+ `Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability}, auth_state=${authState})`
2623
3316
  );
2624
3317
  await server.connect(transport);
2625
3318
  const shutdown = async (code) => {
@@ -2661,6 +3354,7 @@ export {
2661
3354
  checkLoginCollision,
2662
3355
  computeFreshDefaultPath,
2663
3356
  detectClaudeDesktopMode,
3357
+ makeBrokenClient,
2664
3358
  parseWriteEnv,
2665
3359
  resolveClientFromEnv,
2666
3360
  resolveDefaultCredentialsPath