@leadbay/mcp 0.10.1 → 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/CHANGELOG.md +9 -0
- package/README.md +11 -11
- package/dist/bin.js +750 -56
- package/dist/{chunk-F3EWCHME.js → chunk-MZZMIZXA.js} +58 -54
- package/dist/{dist-BHLIJAIH.js → dist-JZ2FLLN6.js} +1 -1
- package/package.json +1 -1
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-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
140
|
+
URL priority (first applicable wins):
|
|
140
141
|
|
|
141
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
592
|
+
URL priority (first applicable wins):
|
|
597
593
|
|
|
598
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
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.
|
|
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.
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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-
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
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.
|
|
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(
|
|
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.
|
|
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-
|
|
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.
|
|
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
|