@leadbay/mcp 0.38.2 → 0.39.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 +20 -0
- package/dist/bin.js +374 -106
- package/dist/http-server.js +278 -37
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -1369,12 +1369,12 @@ var init_ws_client = __esm({
|
|
|
1369
1369
|
this.pingTimer = setInterval(() => this.sendPing(), PING_INTERVAL_MS);
|
|
1370
1370
|
});
|
|
1371
1371
|
ws.addEventListener("message", (ev) => {
|
|
1372
|
-
const
|
|
1373
|
-
if (!
|
|
1372
|
+
const text3 = typeof ev.data === "string" ? ev.data : String(ev.data ?? "");
|
|
1373
|
+
if (!text3)
|
|
1374
1374
|
return;
|
|
1375
1375
|
let msg;
|
|
1376
1376
|
try {
|
|
1377
|
-
msg = JSON.parse(
|
|
1377
|
+
msg = JSON.parse(text3);
|
|
1378
1378
|
} catch {
|
|
1379
1379
|
this.logger?.warn?.("notifications.ws non_json_frame");
|
|
1380
1380
|
return;
|
|
@@ -2843,7 +2843,7 @@ measured:
|
|
|
2843
2843
|
\`employees_max: number\`. FLAT numbers \u2014 nested \`employees: {min, max}\`
|
|
2844
2844
|
exists only in RESULT payloads. \`example_lead.employees\` does not filter.
|
|
2845
2845
|
\`locations\` take city/state/region names ("Dallas, TX", "\xCEle-de-France");
|
|
2846
|
-
a country
|
|
2846
|
+
never a country: this workspace's own is dropped, any other is refused.
|
|
2847
2847
|
7. **Prefer \`example_lead\` over \`query\`.** Query matches topic *vocabulary*:
|
|
2848
2848
|
"gyms that need durable flooring" surfaced flooring VENDORS, 0 delivered.
|
|
2849
2849
|
Use \`query\` only for signal an example can't express.
|
|
@@ -3913,8 +3913,31 @@ server-side: past the 30-min wall clock a job reads \`completed_partial\`
|
|
|
3913
3913
|
delivered leads via leadbay_qualify_leads \`prior_deliveries\`). A 404 means
|
|
3914
3914
|
unknown job or another org's job.
|
|
3915
3915
|
|
|
3916
|
+
\`compact: true\` (pass it whenever \`next_poll\` carries it) returns the rows of
|
|
3917
|
+
a free identity pass, 100 at a time from \`offset\`, instead of leads.
|
|
3918
|
+
|
|
3916
3919
|
---
|
|
3917
3920
|
|
|
3921
|
+
## RENDERING \u2014 identity pass (\`rows[]\`)
|
|
3922
|
+
|
|
3923
|
+
\`rows[]\` replaces \`leads[]\`: one row per company, in the user's order. One
|
|
3924
|
+
table: **Company** (\`input\`, plus \`name\` when it differs) \xB7 **Website** \xB7
|
|
3925
|
+
**LinkedIn**. Skipped rows stay in it: \`not_in_universe\` \u2192 "not in Leadbay",
|
|
3926
|
+
\`low_confidence_identity\` \u2192 "several matches \u2014 add a city or website".
|
|
3927
|
+
|
|
3928
|
+
Then ONE coverage line from \`summary\`: found \`resolved\` of \`rows_total\` \xB7
|
|
3929
|
+
\`with_website\` with a website \xB7 \`with_linkedin\` with a LinkedIn \xB7
|
|
3930
|
+
\`ambiguous\` unclear \xB7 \`not_found\` not found. When most rows lack what the
|
|
3931
|
+
user asked for, that line is the answer: say so, and do not look the
|
|
3932
|
+
missing ones up one by one.
|
|
3933
|
+
|
|
3934
|
+
Done job with \`next_poll.offset\`: say "showing N of \`rows_total\`" and offer
|
|
3935
|
+
the rest via \`leadbay_lead_job_status(job_id, compact: true, offset)\`.
|
|
3936
|
+
|
|
3937
|
+
\`file\` set (local install only): every row is saved there as a CSV. Give the
|
|
3938
|
+
user that path.
|
|
3939
|
+
|
|
3940
|
+
|
|
3918
3941
|
## RENDERING \u2014 delivery table + honest funnel line
|
|
3919
3942
|
|
|
3920
3943
|
Render delivered leads (\`leads[]\`, i.e. items with status \`delivered\` or
|
|
@@ -5157,13 +5180,14 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
|
|
|
5157
5180
|
`;
|
|
5158
5181
|
leadbay_qualify_leads = `## WHEN TO USE
|
|
5159
5182
|
|
|
5160
|
-
Trigger phrases: "qualify these companies", "vet this list", "which of these fit our ICP", "score these websites / accounts", "get me the right contact at these companies", "re-qualify what you delivered last week".
|
|
5183
|
+
Trigger phrases: "for each of these companies, give me the website / LinkedIn", "qualify these companies", "vet this list", "which of these fit our ICP", "score these websites / accounts", "get me the right contact at these companies", "re-qualify what you delivered last week".
|
|
5161
5184
|
|
|
5162
5185
|
Do NOT use for: "find me new leads / companies that <profile>" \u2192 \`leadbay_find_new_leads\`; "qualify the top N of my lens batch" \u2192 \`leadbay_bulk_qualify_leads\`; "import this CSV file" \u2192 \`leadbay_import_leads\`; "tell me about <one company> in depth" \u2192 \`leadbay_research_lead_by_name_fuzzy\`; "add emails to the contacts I selected" \u2192 \`leadbay_enrich_titles\`.
|
|
5163
5186
|
|
|
5164
5187
|
Prefer when: the user points at SPECIFIC companies (ids, websites, names, a pasted list, "what you found yesterday") and wants fit verdicts and/or the right person to talk to.
|
|
5165
5188
|
|
|
5166
5189
|
Examples that SHOULD invoke this tool:
|
|
5190
|
+
- "For each of these 300 brokers, give me the website and LinkedIn, nothing else."
|
|
5167
5191
|
- "Here are 60 restaurant websites from my Austin sweep \u2014 which fit, and who's the owner?"
|
|
5168
5192
|
- "Re-qualify last week's delivery and get phone numbers for the good ones."
|
|
5169
5193
|
- "Vet these 12 accounts from my spreadsheet against our criteria."
|
|
@@ -5175,7 +5199,8 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
|
5175
5199
|
|
|
5176
5200
|
## RENDER (quick)
|
|
5177
5201
|
|
|
5178
|
-
|
|
5202
|
+
Identity pass (\`rows[]\`): one table Company / Website / LinkedIn, then the
|
|
5203
|
+
coverage line. Otherwise a 3-col table for delivered items (fit bar + company / why-fits \u226420 words /
|
|
5179
5204
|
contact + channels) in returned order, then a compact Ref \u2192 Outcome table
|
|
5180
5205
|
for skipped refs (not_in_universe, low_confidence_identity, ... in plain
|
|
5181
5206
|
words), then the honest funnel line. Full algorithm below.
|
|
@@ -5190,6 +5215,14 @@ when asked \u2014 enriched with verified channels. Answers arrive per-item from
|
|
|
5190
5215
|
job; this tool polls up to \`wait_seconds\` (default 45) and hands off to
|
|
5191
5216
|
\`leadbay_lead_job_status\` when the batch needs longer.
|
|
5192
5217
|
|
|
5218
|
+
**A list of companies is ONE call, never a loop** of
|
|
5219
|
+
\`leadbay_research_lead_by_name_fuzzy\`: "for each of these companies give me
|
|
5220
|
+
X", a pasted list, a file. Identity only (in Leadbay or not, website,
|
|
5221
|
+
LinkedIn) \u2192 \`qualify: false\`: free, no \`confirm\`, returns \`rows[]\` and
|
|
5222
|
+
coverage counts. A fit question over the list ("which of these do
|
|
5223
|
+
factoring") keeps \`qualify: true\` and its quote. Over 500: one call per 500,
|
|
5224
|
+
each with its own \`request_id\`.
|
|
5225
|
+
|
|
5193
5226
|
**Refs are flexible; outcomes are per-item.** \`lead_refs\` accepts any mix of
|
|
5194
5227
|
\`lead_id\`, \`website\`, \`name\`(+\`location\`), or a stable \`contact_id\` from a
|
|
5195
5228
|
prior result (enrichment then targets exactly that person, never a re-match).
|
|
@@ -5226,6 +5259,26 @@ Retry-After beyond \u2014 wait, don't hammer), 30-min job wall clock.
|
|
|
5226
5259
|
|
|
5227
5260
|
---
|
|
5228
5261
|
|
|
5262
|
+
## RENDERING \u2014 identity pass (\`rows[]\`)
|
|
5263
|
+
|
|
5264
|
+
\`rows[]\` replaces \`leads[]\`: one row per company, in the user's order. One
|
|
5265
|
+
table: **Company** (\`input\`, plus \`name\` when it differs) \xB7 **Website** \xB7
|
|
5266
|
+
**LinkedIn**. Skipped rows stay in it: \`not_in_universe\` \u2192 "not in Leadbay",
|
|
5267
|
+
\`low_confidence_identity\` \u2192 "several matches \u2014 add a city or website".
|
|
5268
|
+
|
|
5269
|
+
Then ONE coverage line from \`summary\`: found \`resolved\` of \`rows_total\` \xB7
|
|
5270
|
+
\`with_website\` with a website \xB7 \`with_linkedin\` with a LinkedIn \xB7
|
|
5271
|
+
\`ambiguous\` unclear \xB7 \`not_found\` not found. When most rows lack what the
|
|
5272
|
+
user asked for, that line is the answer: say so, and do not look the
|
|
5273
|
+
missing ones up one by one.
|
|
5274
|
+
|
|
5275
|
+
Done job with \`next_poll.offset\`: say "showing N of \`rows_total\`" and offer
|
|
5276
|
+
the rest via \`leadbay_lead_job_status(job_id, compact: true, offset)\`.
|
|
5277
|
+
|
|
5278
|
+
\`file\` set (local install only): every row is saved there as a CSV. Give the
|
|
5279
|
+
user that path.
|
|
5280
|
+
|
|
5281
|
+
|
|
5229
5282
|
## RENDERING \u2014 delivery table + honest funnel line
|
|
5230
5283
|
|
|
5231
5284
|
Render delivered leads (\`leads[]\`, i.e. items with status \`delivered\` or
|
|
@@ -5380,6 +5433,8 @@ Pick the 2-3 options that match what actually happened:
|
|
|
5380
5433
|
| Items skipped \`low_confidence_identity\` | "Pick the right match" (show \`resolution.alternatives\`) | leadbay_qualify_leads with the chosen lead_id |
|
|
5381
5434
|
| Contacts delivered without channels | "Get verified emails/phones for the keepers (uses quota \u2014 say so first)" | leadbay_qualify_leads(lead_refs with contact_id, channels) |
|
|
5382
5435
|
| Disqualified with evidence | "Review why \u2014 adjust qualification questions if the criteria are off" | leadbay_get_qualification_questions |
|
|
5436
|
+
| Identity pass, list over 500 | "Run the next 500" | leadbay_qualify_leads(next 500, qualify: false) |
|
|
5437
|
+
| Identity pass, \`summary.without_website\` > 0. LAST option, never run unasked | "N have no website in Leadbay. Web research can look for one. It uses your plan's quota and may find none." | leadbay_qualify_leads(lead_refs: [{lead_id}] of rows without website, qualify: true) \u2192 quote first, confirm on the user's yes |
|
|
5383
5438
|
`;
|
|
5384
5439
|
leadbay_qualify_status = `Retrieve the current state of a bulk_qualify_leads or import_and_qualify launch. Which ids to pass depends on which tool launched it, because only one of them creates a qualification job on the backend:
|
|
5385
5440
|
|
|
@@ -5858,9 +5913,9 @@ out?"\`
|
|
|
5858
5913
|
|
|
5859
5914
|
Trigger phrases: "look up <Company>", "research <Company>", "what do we know about <Company>".
|
|
5860
5915
|
|
|
5861
|
-
Do NOT use for: "picked row with leadId" \u2192 \`leadbay_research_lead_by_id\`; "draft outreach for <Contact>" \u2192 \`leadbay_prepare_outreach\`.
|
|
5916
|
+
Do NOT use for: "picked row with leadId" \u2192 \`leadbay_research_lead_by_id\`; "draft outreach for <Contact>" \u2192 \`leadbay_prepare_outreach\`; "for each of these companies\u2026" \u2192 \`leadbay_qualify_leads\`.
|
|
5862
5917
|
|
|
5863
|
-
Prefer when:
|
|
5918
|
+
Prefer when: ONE company name or domain in prose, no Leadbay id yet \u2014 always pass \`website\` if a domain was mentioned
|
|
5864
5919
|
|
|
5865
5920
|
Examples that SHOULD invoke this tool:
|
|
5866
5921
|
- "Look up Acme Corp for me."
|
|
@@ -5870,7 +5925,7 @@ Examples that SHOULD invoke this tool:
|
|
|
5870
5925
|
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
5871
5926
|
- "Tell me about that lead I just picked."
|
|
5872
5927
|
- "Draft outreach to Acme's CTO."
|
|
5873
|
-
- "
|
|
5928
|
+
- "Websites and LinkedIn for these 200 companies."
|
|
5874
5929
|
|
|
5875
5930
|
---
|
|
5876
5931
|
|
|
@@ -5884,15 +5939,13 @@ list" into an answer. With only a contact email, pass \`email\`: the company
|
|
|
5884
5939
|
domain is derived from it, consumer mailboxes ignored.
|
|
5885
5940
|
|
|
5886
5941
|
When the registry cannot pick one company it returns \`{resolution:
|
|
5887
|
-
"ambiguous", query, candidates:[
|
|
5888
|
-
|
|
5942
|
+
"ambiguous", query, candidates:[\u2026]}\` instead of a card. Ask which one; never
|
|
5943
|
+
guess from \`score\`.
|
|
5889
5944
|
|
|
5890
5945
|
\`LEAD_NOT_FOUND\` is not a dead end: its hint names the field that would have
|
|
5891
5946
|
found it \u2014 \`website\` or \`registry_number\`, both params. Ask for it and call
|
|
5892
5947
|
again. Do not offer an import before asking.
|
|
5893
5948
|
|
|
5894
|
-
Offer \`_meta.match_candidates\` when present.
|
|
5895
|
-
|
|
5896
5949
|
---
|
|
5897
5950
|
|
|
5898
5951
|
## RENDERING \u2014 single-record research card, mode-adaptive
|
|
@@ -7199,7 +7252,7 @@ measured:
|
|
|
7199
7252
|
\`employees_max: number\`. FLAT numbers \u2014 nested \`employees: {min, max}\`
|
|
7200
7253
|
exists only in RESULT payloads. \`example_lead.employees\` does not filter.
|
|
7201
7254
|
\`locations\` take city/state/region names ("Dallas, TX", "\xCEle-de-France");
|
|
7202
|
-
a country
|
|
7255
|
+
never a country: this workspace's own is dropped, any other is refused.
|
|
7203
7256
|
7. **Prefer \`example_lead\` over \`query\`.** Query matches topic *vocabulary*:
|
|
7204
7257
|
"gyms that need durable flooring" surfaced flooring VENDORS, 0 delivered.
|
|
7205
7258
|
Use \`query\` only for signal an example can't express.
|
|
@@ -14469,12 +14522,12 @@ var init_pull_leads = __esm({
|
|
|
14469
14522
|
});
|
|
14470
14523
|
|
|
14471
14524
|
// ../core/dist/composite/_geo-helpers.js
|
|
14472
|
-
function expandAlias(
|
|
14473
|
-
const key =
|
|
14474
|
-
return CITY_ALIASES[key] ??
|
|
14525
|
+
function expandAlias(text3) {
|
|
14526
|
+
const key = text3.trim().toLowerCase();
|
|
14527
|
+
return CITY_ALIASES[key] ?? text3;
|
|
14475
14528
|
}
|
|
14476
|
-
function scoreMatch(
|
|
14477
|
-
const t =
|
|
14529
|
+
function scoreMatch(text3, match) {
|
|
14530
|
+
const t = text3.trim().toLowerCase();
|
|
14478
14531
|
const n = match.name.trim().toLowerCase();
|
|
14479
14532
|
if (n === t)
|
|
14480
14533
|
return 1;
|
|
@@ -14497,8 +14550,8 @@ async function resolveLocations(client, texts) {
|
|
|
14497
14550
|
const resolved = [...direct];
|
|
14498
14551
|
const ambiguities = [];
|
|
14499
14552
|
for (const originalText of free) {
|
|
14500
|
-
const
|
|
14501
|
-
const path = `/geo/search?q=${encodeURIComponent(
|
|
14553
|
+
const text3 = expandAlias(originalText);
|
|
14554
|
+
const path = `/geo/search?q=${encodeURIComponent(text3)}`;
|
|
14502
14555
|
let response;
|
|
14503
14556
|
try {
|
|
14504
14557
|
response = await client.request("GET", path);
|
|
@@ -14537,7 +14590,7 @@ async function resolveLocations(client, texts) {
|
|
|
14537
14590
|
name: r.name,
|
|
14538
14591
|
country: r.country,
|
|
14539
14592
|
level: r.level,
|
|
14540
|
-
score: scoreMatch(
|
|
14593
|
+
score: scoreMatch(text3, r)
|
|
14541
14594
|
})).sort((a, b) => {
|
|
14542
14595
|
if (b.score !== a.score)
|
|
14543
14596
|
return b.score - a.score;
|
|
@@ -15807,9 +15860,9 @@ ${firm.short_description}`);
|
|
|
15807
15860
|
out.push(`### ${sec.section_emoji ?? ""} ${label}`.trim());
|
|
15808
15861
|
const entries = Array.isArray(sec.entries) ? sec.entries : [];
|
|
15809
15862
|
for (const e of entries.slice(0, 5)) {
|
|
15810
|
-
const
|
|
15863
|
+
const text3 = e.text ?? e.summary ?? JSON.stringify(e).slice(0, 200);
|
|
15811
15864
|
const hot = e.hot === true ? " \u{1F525}" : "";
|
|
15812
|
-
out.push(`- ${
|
|
15865
|
+
out.push(`- ${text3}${hot}`);
|
|
15813
15866
|
}
|
|
15814
15867
|
if (entries.length > 5)
|
|
15815
15868
|
out.push(`- _${entries.length - 5} more \u2026_`);
|
|
@@ -20702,8 +20755,8 @@ function tokens(s) {
|
|
|
20702
20755
|
return [];
|
|
20703
20756
|
return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
20704
20757
|
}
|
|
20705
|
-
function bestMatches(
|
|
20706
|
-
const want = new Set(tokens(
|
|
20758
|
+
function bestMatches(text3, taxonomy) {
|
|
20759
|
+
const want = new Set(tokens(text3));
|
|
20707
20760
|
if (want.size === 0)
|
|
20708
20761
|
return [];
|
|
20709
20762
|
const ranked = taxonomy.map((s) => {
|
|
@@ -20732,12 +20785,12 @@ async function resolveSectors(client, texts, ctx) {
|
|
|
20732
20785
|
}
|
|
20733
20786
|
const resolved = [...direct];
|
|
20734
20787
|
const ambiguities = [];
|
|
20735
|
-
for (const
|
|
20736
|
-
const matches = bestMatches(
|
|
20788
|
+
for (const text3 of free) {
|
|
20789
|
+
const matches = bestMatches(text3, taxonomy);
|
|
20737
20790
|
if (matches.length === 1 || matches.length >= 2 && matches[0].score >= 0.66 && matches[0].score - matches[1].score >= 0.34) {
|
|
20738
20791
|
resolved.push(matches[0].id);
|
|
20739
20792
|
} else {
|
|
20740
|
-
ambiguities.push({ sector_text:
|
|
20793
|
+
ambiguities.push({ sector_text: text3, matches });
|
|
20741
20794
|
}
|
|
20742
20795
|
}
|
|
20743
20796
|
return { resolved, ambiguities };
|
|
@@ -22066,6 +22119,7 @@ var init_find_new_leads = __esm({
|
|
|
22066
22119
|
"../core/dist/composite/find-new-leads.js"() {
|
|
22067
22120
|
"use strict";
|
|
22068
22121
|
init_mcp_job_helpers();
|
|
22122
|
+
init_country_guard();
|
|
22069
22123
|
init_tool_descriptions_generated();
|
|
22070
22124
|
DEFAULT_WAIT_SECONDS = 45;
|
|
22071
22125
|
findNewLeads = {
|
|
@@ -22190,6 +22244,21 @@ var init_find_new_leads = __esm({
|
|
|
22190
22244
|
"channels",
|
|
22191
22245
|
"exclude_lead_ids"
|
|
22192
22246
|
]);
|
|
22247
|
+
const homeCountry = detectCountryLocations(params.filters?.locations, "filters.locations", client.region).filter((hit) => hit.kind === "home_country");
|
|
22248
|
+
let countryNote;
|
|
22249
|
+
if (homeCountry.length > 0) {
|
|
22250
|
+
const dropped = new Set(homeCountry.map((hit) => hit.value));
|
|
22251
|
+
const raw = params.filters.locations;
|
|
22252
|
+
const rest = (Array.isArray(raw) ? raw : [raw]).filter((v) => !dropped.has(v));
|
|
22253
|
+
params = {
|
|
22254
|
+
...params,
|
|
22255
|
+
filters: { ...params.filters, locations: rest.length > 0 ? rest : void 0 }
|
|
22256
|
+
};
|
|
22257
|
+
const removed = [...dropped].map((v) => `"${v}"`).join(", ");
|
|
22258
|
+
countryNote = {
|
|
22259
|
+
note: rest.length > 0 ? `Removed ${removed} from filters.locations: a country is never a location filter. The search covers ${rest.map((v) => `"${v}"`).join(", ")} only.` : `Removed ${removed} from filters.locations: this workspace holds ${homeCountry[0].country} companies only, so the search already covers all of it.`
|
|
22260
|
+
};
|
|
22261
|
+
}
|
|
22193
22262
|
rejectCountryLocations(params.filters?.locations, client.region);
|
|
22194
22263
|
rejectMalformedExclusions(params.exclude_lead_ids);
|
|
22195
22264
|
rejectOversizedExclusions(params.exclude_lead_ids);
|
|
@@ -22267,6 +22336,7 @@ var init_find_new_leads = __esm({
|
|
|
22267
22336
|
return {
|
|
22268
22337
|
dry_run: true,
|
|
22269
22338
|
...forecast,
|
|
22339
|
+
...countryNote,
|
|
22270
22340
|
region: client.region
|
|
22271
22341
|
};
|
|
22272
22342
|
}
|
|
@@ -22287,6 +22357,7 @@ var init_find_new_leads = __esm({
|
|
|
22287
22357
|
estimated_cost: forecast?.estimated_cost ?? null,
|
|
22288
22358
|
items_requested: forecast?.items_requested ?? null,
|
|
22289
22359
|
hint: vetoed ? "confirm:false vetoed the run \u2014 nothing was submitted. Re-call with confirm:true to proceed, or drop qualify/channels for a free search." : "Tell the user what will run and that it uses their plan's quota (no amounts, no money), get an explicit go-ahead, then re-call with confirm:true. For a free search instead: omit qualify and channels.",
|
|
22360
|
+
...countryNote,
|
|
22290
22361
|
region: client.region
|
|
22291
22362
|
};
|
|
22292
22363
|
}
|
|
@@ -22341,6 +22412,7 @@ var init_find_new_leads = __esm({
|
|
|
22341
22412
|
since: snapshot.next_since ?? null,
|
|
22342
22413
|
suggested_wait_seconds: done ? 0 : 60
|
|
22343
22414
|
},
|
|
22415
|
+
...countryNote,
|
|
22344
22416
|
region: client.region
|
|
22345
22417
|
};
|
|
22346
22418
|
}
|
|
@@ -22348,6 +22420,144 @@ var init_find_new_leads = __esm({
|
|
|
22348
22420
|
}
|
|
22349
22421
|
});
|
|
22350
22422
|
|
|
22423
|
+
// ../core/dist/composite/_identity-rows.js
|
|
22424
|
+
function text(v) {
|
|
22425
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
22426
|
+
}
|
|
22427
|
+
function toRow(item) {
|
|
22428
|
+
const asked = item.ref?.requested_as ?? {};
|
|
22429
|
+
const row = {
|
|
22430
|
+
input_indexes: item.ref?.input_indexes ?? null,
|
|
22431
|
+
input: text(asked.name) ?? text(asked.website) ?? text(asked.lead_id) ?? text(asked.contact_id),
|
|
22432
|
+
status: item.status
|
|
22433
|
+
};
|
|
22434
|
+
if (item.status_reason)
|
|
22435
|
+
row.status_reason = item.status_reason;
|
|
22436
|
+
const lead = item.lead;
|
|
22437
|
+
if (!lead)
|
|
22438
|
+
return row;
|
|
22439
|
+
const company = lead.company ?? {};
|
|
22440
|
+
row.lead_id = lead.lead_id ?? item.ref?.lead_id ?? void 0;
|
|
22441
|
+
row.name = company.name;
|
|
22442
|
+
row.website = text(company.website);
|
|
22443
|
+
row.linkedin = text(company.socials?.linkedin);
|
|
22444
|
+
return row;
|
|
22445
|
+
}
|
|
22446
|
+
function identityRows(items) {
|
|
22447
|
+
const first = (i) => {
|
|
22448
|
+
const idx = i.ref?.input_indexes;
|
|
22449
|
+
return Array.isArray(idx) && idx.length > 0 ? Math.min(...idx) : Number.MAX_SAFE_INTEGER;
|
|
22450
|
+
};
|
|
22451
|
+
return [...items].sort((a, b) => first(a) - first(b) || a.seq - b.seq).map(toRow);
|
|
22452
|
+
}
|
|
22453
|
+
function identityCoverage(rows) {
|
|
22454
|
+
let resolved = 0;
|
|
22455
|
+
let ambiguous = 0;
|
|
22456
|
+
let notFound = 0;
|
|
22457
|
+
let otherSkipped = 0;
|
|
22458
|
+
let withWebsite = 0;
|
|
22459
|
+
let withLinkedin = 0;
|
|
22460
|
+
for (const r of rows) {
|
|
22461
|
+
if (r.status !== "skipped") {
|
|
22462
|
+
resolved += 1;
|
|
22463
|
+
if (r.website)
|
|
22464
|
+
withWebsite += 1;
|
|
22465
|
+
if (r.linkedin)
|
|
22466
|
+
withLinkedin += 1;
|
|
22467
|
+
} else if (r.status_reason === "low_confidence_identity")
|
|
22468
|
+
ambiguous += 1;
|
|
22469
|
+
else if (r.status_reason === "not_in_universe")
|
|
22470
|
+
notFound += 1;
|
|
22471
|
+
else
|
|
22472
|
+
otherSkipped += 1;
|
|
22473
|
+
}
|
|
22474
|
+
return {
|
|
22475
|
+
resolved,
|
|
22476
|
+
ambiguous,
|
|
22477
|
+
not_found: notFound,
|
|
22478
|
+
other_skipped: otherSkipped,
|
|
22479
|
+
with_website: withWebsite,
|
|
22480
|
+
with_linkedin: withLinkedin,
|
|
22481
|
+
without_website: resolved - withWebsite
|
|
22482
|
+
};
|
|
22483
|
+
}
|
|
22484
|
+
function identityCsv(rows) {
|
|
22485
|
+
return synthesizeCsv(CSV_HEADER, rows.map((r) => ({
|
|
22486
|
+
row: (r.input_indexes ?? []).map((i) => i + 1).join(";"),
|
|
22487
|
+
input: r.input ?? "",
|
|
22488
|
+
status: r.status,
|
|
22489
|
+
reason: r.status_reason ?? "",
|
|
22490
|
+
company: r.name ?? "",
|
|
22491
|
+
website: r.website ?? "",
|
|
22492
|
+
linkedin: r.linkedin ?? "",
|
|
22493
|
+
lead_id: r.lead_id ?? ""
|
|
22494
|
+
})));
|
|
22495
|
+
}
|
|
22496
|
+
async function saveIdentityFile(ctx, jobId, snapshot, items) {
|
|
22497
|
+
if (!ctx?.saveFile)
|
|
22498
|
+
return {};
|
|
22499
|
+
if (!TERMINAL_JOB_STATES.has(snapshot.job.state))
|
|
22500
|
+
return {};
|
|
22501
|
+
if (snapshot.items_truncated || items.length === 0)
|
|
22502
|
+
return {};
|
|
22503
|
+
try {
|
|
22504
|
+
const name = `leadbay-companies-${jobId.slice(0, 8)}.csv`;
|
|
22505
|
+
return { file: await ctx.saveFile(name, identityCsv(identityRows(items))) };
|
|
22506
|
+
} catch (e) {
|
|
22507
|
+
return { file_error: e instanceof Error ? e.message : String(e) };
|
|
22508
|
+
}
|
|
22509
|
+
}
|
|
22510
|
+
function readOffset(v) {
|
|
22511
|
+
const n = typeof v === "string" ? Number(v) : v;
|
|
22512
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
22513
|
+
}
|
|
22514
|
+
function identityAnswer(jobId, snapshot, items, offset) {
|
|
22515
|
+
const all = identityRows(items);
|
|
22516
|
+
const rows = all.slice(offset, offset + IDENTITY_ROWS_PER_RESULT);
|
|
22517
|
+
const end = offset + rows.length;
|
|
22518
|
+
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
22519
|
+
const truncated = snapshot.items_truncated ?? false;
|
|
22520
|
+
const nextOffset = !done || truncated ? 0 : end < all.length ? end : null;
|
|
22521
|
+
return {
|
|
22522
|
+
summary: {
|
|
22523
|
+
...identityCoverage(all),
|
|
22524
|
+
stop_reason: snapshot.funnel?.stop_reason ?? null
|
|
22525
|
+
},
|
|
22526
|
+
rows,
|
|
22527
|
+
rows_total: all.length,
|
|
22528
|
+
rows_offset: offset,
|
|
22529
|
+
items_truncated: truncated,
|
|
22530
|
+
scope_notes: snapshot.explain?.scope_notes ?? [],
|
|
22531
|
+
still_running: !done,
|
|
22532
|
+
next_poll: nextOffset === null ? null : {
|
|
22533
|
+
tool: "leadbay_lead_job_status",
|
|
22534
|
+
job_id: jobId,
|
|
22535
|
+
compact: true,
|
|
22536
|
+
offset: nextOffset,
|
|
22537
|
+
suggested_wait_seconds: done ? 0 : 60
|
|
22538
|
+
}
|
|
22539
|
+
};
|
|
22540
|
+
}
|
|
22541
|
+
var IDENTITY_ROWS_PER_RESULT, CSV_HEADER;
|
|
22542
|
+
var init_identity_rows = __esm({
|
|
22543
|
+
"../core/dist/composite/_identity-rows.js"() {
|
|
22544
|
+
"use strict";
|
|
22545
|
+
init_mcp_job_helpers();
|
|
22546
|
+
init_import_leads();
|
|
22547
|
+
IDENTITY_ROWS_PER_RESULT = 100;
|
|
22548
|
+
CSV_HEADER = [
|
|
22549
|
+
"row",
|
|
22550
|
+
"input",
|
|
22551
|
+
"status",
|
|
22552
|
+
"reason",
|
|
22553
|
+
"company",
|
|
22554
|
+
"website",
|
|
22555
|
+
"linkedin",
|
|
22556
|
+
"lead_id"
|
|
22557
|
+
];
|
|
22558
|
+
}
|
|
22559
|
+
});
|
|
22560
|
+
|
|
22351
22561
|
// ../core/dist/composite/qualify-leads.js
|
|
22352
22562
|
function normalizeLeadRefs(refs) {
|
|
22353
22563
|
if (!Array.isArray(refs))
|
|
@@ -22388,7 +22598,7 @@ function rejectMalformedLeadRefs(refs) {
|
|
|
22388
22598
|
hint: "Each ref is an object whose fields are STRINGS \u2014 {lead_id} | {website} | {name, location?} | {contact_id}. A bare string is accepted and reshaped; null, numbers, arrays and non-string field values are not. Fix or drop those entries and re-call."
|
|
22389
22599
|
};
|
|
22390
22600
|
}
|
|
22391
|
-
function
|
|
22601
|
+
function text2(value) {
|
|
22392
22602
|
if (typeof value !== "string")
|
|
22393
22603
|
return null;
|
|
22394
22604
|
const v = value.trim().toLowerCase();
|
|
@@ -22396,7 +22606,7 @@ function text(value) {
|
|
|
22396
22606
|
}
|
|
22397
22607
|
function derivedRequestId(params) {
|
|
22398
22608
|
const refs = canonicalSet((params.lead_refs ?? []).map((r) => {
|
|
22399
|
-
const website =
|
|
22609
|
+
const website = text2(r.website);
|
|
22400
22610
|
return [
|
|
22401
22611
|
// UUIDs are case-insensitive to the backend, so an uppercase id and
|
|
22402
22612
|
// its lowercase form are the same lead and must share a key.
|
|
@@ -22407,8 +22617,8 @@ function derivedRequestId(params) {
|
|
|
22407
22617
|
// AND to one key. Fall back to the trimmed/lowercased raw value when
|
|
22408
22618
|
// it is not domain-shaped, rather than dropping the field.
|
|
22409
22619
|
website ? normalizeDomain(website) ?? website : null,
|
|
22410
|
-
|
|
22411
|
-
|
|
22620
|
+
text2(r.name),
|
|
22621
|
+
text2(r.location)
|
|
22412
22622
|
];
|
|
22413
22623
|
}));
|
|
22414
22624
|
const shape = {
|
|
@@ -22452,6 +22662,7 @@ var init_qualify_leads = __esm({
|
|
|
22452
22662
|
"use strict";
|
|
22453
22663
|
init_mcp_job_helpers();
|
|
22454
22664
|
init_import_leads();
|
|
22665
|
+
init_identity_rows();
|
|
22455
22666
|
init_tool_descriptions_generated();
|
|
22456
22667
|
DEFAULT_WAIT_SECONDS2 = 45;
|
|
22457
22668
|
LEAD_REF_FIELDS = [
|
|
@@ -22569,6 +22780,7 @@ var init_qualify_leads = __esm({
|
|
|
22569
22780
|
const buysChannels = (params.channels?.length ?? 0) > 0;
|
|
22570
22781
|
const buysQualification = qualify !== false;
|
|
22571
22782
|
const isPaid = buysQualification || buysChannels;
|
|
22783
|
+
const identityOnly = !isPaid && (params.contact_titles?.length ?? 0) === 0 && params.prior_deliveries == null;
|
|
22572
22784
|
const vetoed = confirm === false;
|
|
22573
22785
|
const consented = !vetoed && confirm === true;
|
|
22574
22786
|
const requestId = presentRequestId(params.request_id) ?? (isPaid ? derivedRequestId(params) : void 0);
|
|
@@ -22617,6 +22829,25 @@ var init_qualify_leads = __esm({
|
|
|
22617
22829
|
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
22618
22830
|
const indexed = submit.duplicate ?? false ? remapInputIndexes(snapshot.items, params.lead_refs) : { items: snapshot.items, remapped: true };
|
|
22619
22831
|
const view = { ...snapshot, items: indexed.items };
|
|
22832
|
+
if (identityOnly) {
|
|
22833
|
+
const answer = identityAnswer(submit.job_id, snapshot, view.items, 0);
|
|
22834
|
+
return {
|
|
22835
|
+
job_id: submit.job_id,
|
|
22836
|
+
request_id: requestId ?? null,
|
|
22837
|
+
duplicate_submit: submit.duplicate ?? false,
|
|
22838
|
+
state: snapshot.job.state,
|
|
22839
|
+
done,
|
|
22840
|
+
...answer,
|
|
22841
|
+
summary: {
|
|
22842
|
+
refs_submitted: params.lead_refs?.length ?? 0,
|
|
22843
|
+
items_requested: submit.items_requested,
|
|
22844
|
+
...answer.summary
|
|
22845
|
+
},
|
|
22846
|
+
input_indexes_remapped: submit.duplicate ?? false ? indexed.remapped : null,
|
|
22847
|
+
...await saveIdentityFile(ctx, submit.job_id, snapshot, view.items),
|
|
22848
|
+
region: client.region
|
|
22849
|
+
};
|
|
22850
|
+
}
|
|
22620
22851
|
return {
|
|
22621
22852
|
job_id: submit.job_id,
|
|
22622
22853
|
// Echo the key actually sent, so a retry can reuse it verbatim.
|
|
@@ -22685,6 +22916,7 @@ var init_lead_job_status = __esm({
|
|
|
22685
22916
|
"../core/dist/composite/lead-job-status.js"() {
|
|
22686
22917
|
"use strict";
|
|
22687
22918
|
init_mcp_job_helpers();
|
|
22919
|
+
init_identity_rows();
|
|
22688
22920
|
init_tool_descriptions_generated();
|
|
22689
22921
|
leadJobStatus = {
|
|
22690
22922
|
name: "leadbay_lead_job_status",
|
|
@@ -22714,6 +22946,14 @@ var init_lead_job_status = __esm({
|
|
|
22714
22946
|
wait_seconds: {
|
|
22715
22947
|
type: "number",
|
|
22716
22948
|
description: "0 (default) = instant snapshot. >0 = keep polling up to this many seconds until the job is terminal \u2014 use ~60 when the user asked to wait for results."
|
|
22949
|
+
},
|
|
22950
|
+
compact: {
|
|
22951
|
+
type: "boolean",
|
|
22952
|
+
description: "true = one row per company (name, website, LinkedIn) in the order of the user's list, plus coverage counts, instead of full leads. Pass it when next_poll carries it (a leadbay_qualify_leads identity pass). Reads the whole job; since is ignored."
|
|
22953
|
+
},
|
|
22954
|
+
offset: {
|
|
22955
|
+
type: "number",
|
|
22956
|
+
description: "With compact: the first row to return (default 0). Each result carries at most 100 rows; take the next value from next_poll.offset."
|
|
22717
22957
|
}
|
|
22718
22958
|
},
|
|
22719
22959
|
required: ["job_id"],
|
|
@@ -22721,8 +22961,19 @@ var init_lead_job_status = __esm({
|
|
|
22721
22961
|
},
|
|
22722
22962
|
execute: async (client, params, ctx) => {
|
|
22723
22963
|
const waitSeconds = clampWaitSeconds(params.wait_seconds, 0);
|
|
22724
|
-
const
|
|
22964
|
+
const compact = params.compact === true || params.compact === "true";
|
|
22965
|
+
const since = compact ? void 0 : params.since;
|
|
22966
|
+
const snapshot = waitSeconds > 0 ? await waitForJob(client, params.job_id, waitSeconds, ctx, void 0, since, params.limit) : await collectJobSnapshot(client, params.job_id, since, params.limit, ctx?.signal);
|
|
22725
22967
|
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
22968
|
+
if (compact) {
|
|
22969
|
+
return {
|
|
22970
|
+
job_id: params.job_id,
|
|
22971
|
+
state: snapshot.job.state,
|
|
22972
|
+
done,
|
|
22973
|
+
...identityAnswer(params.job_id, snapshot, snapshot.items, readOffset(params.offset)),
|
|
22974
|
+
region: client.region
|
|
22975
|
+
};
|
|
22976
|
+
}
|
|
22726
22977
|
const { leads, skipped } = splitItems(snapshot);
|
|
22727
22978
|
return {
|
|
22728
22979
|
job_id: params.job_id,
|
|
@@ -24164,8 +24415,8 @@ var init_send_feedback = __esm({
|
|
|
24164
24415
|
}
|
|
24165
24416
|
},
|
|
24166
24417
|
execute: async (client, params, ctx) => {
|
|
24167
|
-
const
|
|
24168
|
-
if (!
|
|
24418
|
+
const text3 = typeof params.message === "string" ? params.message.trim() : "";
|
|
24419
|
+
if (!text3) {
|
|
24169
24420
|
return {
|
|
24170
24421
|
error: true,
|
|
24171
24422
|
code: "BAD_INPUT",
|
|
@@ -24173,7 +24424,7 @@ var init_send_feedback = __esm({
|
|
|
24173
24424
|
hint: "Ask the user what they'd like to tell the Leadbay team, then call again with their words in `message`."
|
|
24174
24425
|
};
|
|
24175
24426
|
}
|
|
24176
|
-
const message =
|
|
24427
|
+
const message = text3.length > MESSAGE_MAX2 ? `${text3.slice(0, MESSAGE_MAX2 - 1)}\u2026` : text3;
|
|
24177
24428
|
if (!ctx?.sendFeedback) {
|
|
24178
24429
|
return {
|
|
24179
24430
|
sent: false,
|
|
@@ -24774,7 +25025,7 @@ var init_dist = __esm({
|
|
|
24774
25025
|
init_dist();
|
|
24775
25026
|
import { realpathSync } from "fs";
|
|
24776
25027
|
import { fileURLToPath } from "url";
|
|
24777
|
-
import { basename } from "path";
|
|
25028
|
+
import { basename as basename2 } from "path";
|
|
24778
25029
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
24779
25030
|
|
|
24780
25031
|
// src/server.ts
|
|
@@ -27678,8 +27929,8 @@ var PROMPT_CATALOG_BULLETS = {
|
|
|
27678
27929
|
};
|
|
27679
27930
|
|
|
27680
27931
|
// src/prompts.ts
|
|
27681
|
-
function userMessage(
|
|
27682
|
-
return { role: "user", content: { type: "text", text:
|
|
27932
|
+
function userMessage(text3) {
|
|
27933
|
+
return { role: "user", content: { type: "text", text: text3 } };
|
|
27683
27934
|
}
|
|
27684
27935
|
function substitutePlaceholders(body, substitutions) {
|
|
27685
27936
|
let out = body;
|
|
@@ -29378,6 +29629,7 @@ ${url}
|
|
|
29378
29629
|
// RAW value: the friction redaction above is an ANALYTICS control, and
|
|
29379
29630
|
// must not change in-process tool behaviour.
|
|
29380
29631
|
triggered_by: rawTriggeredBy,
|
|
29632
|
+
saveFile: opts.saveFile,
|
|
29381
29633
|
// Route leadbay_send_feedback to Sentry's feedback inbox (same place
|
|
29382
29634
|
// the web app's form lands). NOOP_TELEMETRY returns false, so the
|
|
29383
29635
|
// tool reports honestly when telemetry is off.
|
|
@@ -29620,11 +29872,24 @@ ${url}
|
|
|
29620
29872
|
return server;
|
|
29621
29873
|
}
|
|
29622
29874
|
|
|
29875
|
+
// src/local-files.ts
|
|
29876
|
+
import { existsSync as existsSync2 } from "fs";
|
|
29877
|
+
import { writeFile } from "fs/promises";
|
|
29878
|
+
import { homedir } from "os";
|
|
29879
|
+
import { basename, join as join2 } from "path";
|
|
29880
|
+
async function saveToDownloads(name, content, home = homedir()) {
|
|
29881
|
+
const downloads = join2(home, "Downloads");
|
|
29882
|
+
const dir = existsSync2(downloads) ? downloads : home;
|
|
29883
|
+
const path = join2(dir, basename(name));
|
|
29884
|
+
await writeFile(path, content, "utf8");
|
|
29885
|
+
return path;
|
|
29886
|
+
}
|
|
29887
|
+
|
|
29623
29888
|
// installer/install-shared.ts
|
|
29624
29889
|
import { spawn } from "child_process";
|
|
29625
|
-
import { existsSync as
|
|
29626
|
-
import { join as
|
|
29627
|
-
import { homedir } from "os";
|
|
29890
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
29891
|
+
import { join as join3 } from "path";
|
|
29892
|
+
import { homedir as homedir2 } from "os";
|
|
29628
29893
|
var HOSTED_MCP_URL_CHATGPT = "https://mcp.leadbay.app/chatgpt/mcp";
|
|
29629
29894
|
function formatInstallOsLabel(platform = process.platform, arch = process.arch) {
|
|
29630
29895
|
const name = platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform === "linux" ? "Linux" : platform;
|
|
@@ -29632,15 +29897,15 @@ function formatInstallOsLabel(platform = process.platform, arch = process.arch)
|
|
|
29632
29897
|
}
|
|
29633
29898
|
function detectClaudeDesktopMode(claudeSupportDir) {
|
|
29634
29899
|
const markers = [];
|
|
29635
|
-
const legacy =
|
|
29636
|
-
if (
|
|
29900
|
+
const legacy = existsSync3(join3(claudeSupportDir, "claude_desktop_config.json"));
|
|
29901
|
+
if (existsSync3(join3(claudeSupportDir, "Claude Extensions"))) {
|
|
29637
29902
|
markers.push("Claude Extensions/");
|
|
29638
29903
|
}
|
|
29639
|
-
if (
|
|
29904
|
+
if (existsSync3(join3(claudeSupportDir, "extensions-installations.json"))) {
|
|
29640
29905
|
markers.push("extensions-installations.json");
|
|
29641
29906
|
}
|
|
29642
|
-
const cfgPath =
|
|
29643
|
-
if (
|
|
29907
|
+
const cfgPath = join3(claudeSupportDir, "config.json");
|
|
29908
|
+
if (existsSync3(cfgPath)) {
|
|
29644
29909
|
try {
|
|
29645
29910
|
const raw = readFileSync2(cfgPath, "utf8");
|
|
29646
29911
|
const parsed = JSON.parse(raw);
|
|
@@ -29680,8 +29945,8 @@ async function windowsStoreAppInstalled(packageName, appName) {
|
|
|
29680
29945
|
});
|
|
29681
29946
|
}
|
|
29682
29947
|
function isClaudeStorePackagePresent(localAppData) {
|
|
29683
|
-
const packagesDir =
|
|
29684
|
-
if (!
|
|
29948
|
+
const packagesDir = join3(localAppData, "Packages");
|
|
29949
|
+
if (!existsSync3(packagesDir)) return false;
|
|
29685
29950
|
try {
|
|
29686
29951
|
return readdirSync2(packagesDir).some((name) => /^Claude_/i.test(name));
|
|
29687
29952
|
} catch {
|
|
@@ -29690,7 +29955,7 @@ function isClaudeStorePackagePresent(localAppData) {
|
|
|
29690
29955
|
}
|
|
29691
29956
|
async function isClaudeDesktopInstalled(home) {
|
|
29692
29957
|
if (process.platform === "darwin") {
|
|
29693
|
-
return
|
|
29958
|
+
return existsSync3("/Applications/Claude.app") || existsSync3(home + "/Applications/Claude.app");
|
|
29694
29959
|
}
|
|
29695
29960
|
if (process.platform === "win32") {
|
|
29696
29961
|
const local = process.env.LOCALAPPDATA ?? home + "/AppData/Local";
|
|
@@ -29701,18 +29966,18 @@ async function isClaudeDesktopInstalled(home) {
|
|
|
29701
29966
|
local + "/Claude/Claude.exe",
|
|
29702
29967
|
programFiles ? programFiles + "/Claude/Claude.exe" : null,
|
|
29703
29968
|
programFilesX86 ? programFilesX86 + "/Claude/Claude.exe" : null
|
|
29704
|
-
].some((candidate) => candidate !== null &&
|
|
29969
|
+
].some((candidate) => candidate !== null && existsSync3(candidate));
|
|
29705
29970
|
if (exeInstalled) return true;
|
|
29706
29971
|
if (isClaudeStorePackagePresent(local)) return true;
|
|
29707
29972
|
return await windowsStoreAppInstalled("AnthropicPBC.Claude", "Claude");
|
|
29708
29973
|
}
|
|
29709
29974
|
const desktopBin = await findOnPath("claude-desktop");
|
|
29710
29975
|
if (desktopBin) return true;
|
|
29711
|
-
return
|
|
29976
|
+
return existsSync3(home + "/.local/share/applications/claude-desktop.desktop") || existsSync3("/usr/share/applications/claude-desktop.desktop") || existsSync3("/opt/Claude/Claude") || existsSync3("/opt/Claude/claude") || existsSync3("/opt/claude/claude");
|
|
29712
29977
|
}
|
|
29713
29978
|
async function isChatGptDesktopInstalled(home) {
|
|
29714
29979
|
if (process.platform === "darwin") {
|
|
29715
|
-
return
|
|
29980
|
+
return existsSync3("/Applications/ChatGPT.app") || existsSync3(home + "/Applications/ChatGPT.app");
|
|
29716
29981
|
}
|
|
29717
29982
|
if (process.platform === "win32") {
|
|
29718
29983
|
const local = process.env.LOCALAPPDATA ?? home + "/AppData/Local";
|
|
@@ -29725,7 +29990,7 @@ async function isChatGptDesktopInstalled(home) {
|
|
|
29725
29990
|
programFiles ? programFiles + "/ChatGPT/ChatGPT.exe" : null,
|
|
29726
29991
|
programFilesX86 ? programFilesX86 + "/OpenAI/ChatGPT/ChatGPT.exe" : null,
|
|
29727
29992
|
programFilesX86 ? programFilesX86 + "/ChatGPT/ChatGPT.exe" : null
|
|
29728
|
-
].some((candidate) => candidate !== null &&
|
|
29993
|
+
].some((candidate) => candidate !== null && existsSync3(candidate));
|
|
29729
29994
|
return exeInstalled || await windowsStoreAppInstalled("OpenAI.ChatGPT-Desktop", "ChatGPT");
|
|
29730
29995
|
}
|
|
29731
29996
|
return false;
|
|
@@ -29733,16 +29998,16 @@ async function isChatGptDesktopInstalled(home) {
|
|
|
29733
29998
|
async function isCursorInstalled(home) {
|
|
29734
29999
|
const cursorBin = await findOnPath("cursor");
|
|
29735
30000
|
if (cursorBin) return true;
|
|
29736
|
-
if (process.platform === "darwin") return
|
|
30001
|
+
if (process.platform === "darwin") return existsSync3("/Applications/Cursor.app");
|
|
29737
30002
|
if (process.platform === "win32") {
|
|
29738
30003
|
const local = process.env.LOCALAPPDATA ?? `${home}\\AppData\\Local`;
|
|
29739
|
-
return
|
|
30004
|
+
return existsSync3(`${local}\\Programs\\Cursor\\Cursor.exe`);
|
|
29740
30005
|
}
|
|
29741
|
-
return
|
|
30006
|
+
return existsSync3("/usr/share/applications/cursor.desktop") || existsSync3("/opt/Cursor/cursor");
|
|
29742
30007
|
}
|
|
29743
30008
|
async function detectClients() {
|
|
29744
30009
|
const out = [];
|
|
29745
|
-
const home =
|
|
30010
|
+
const home = homedir2();
|
|
29746
30011
|
const claudeBin = await findOnPath("claude");
|
|
29747
30012
|
if (claudeBin) {
|
|
29748
30013
|
out.push({ id: "claude-code", label: "Claude Code", detail: `${claudeBin} mcp add ...` });
|
|
@@ -29761,7 +30026,7 @@ async function detectClients() {
|
|
|
29761
30026
|
out.push({
|
|
29762
30027
|
id: "cursor",
|
|
29763
30028
|
label: "Cursor",
|
|
29764
|
-
detail:
|
|
30029
|
+
detail: existsSync3(cursorPath) ? cursorPath : `${cursorPath} (will be created)`,
|
|
29765
30030
|
configPath: cursorPath
|
|
29766
30031
|
});
|
|
29767
30032
|
}
|
|
@@ -29850,11 +30115,11 @@ async function uninstallFromClaudeCode() {
|
|
|
29850
30115
|
// installer/install-json-config.ts
|
|
29851
30116
|
async function installInJsonConfig(configPath, token, region, includeWrite, telemetryEnabled, localBinPath) {
|
|
29852
30117
|
try {
|
|
29853
|
-
const { readFileSync: readFileSync3, writeFileSync, existsSync:
|
|
30118
|
+
const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync5, mkdirSync, statSync } = await import("fs");
|
|
29854
30119
|
const { dirname: dirname2 } = await import("path");
|
|
29855
30120
|
let parsed = {};
|
|
29856
30121
|
let preserved = {};
|
|
29857
|
-
const existed =
|
|
30122
|
+
const existed = existsSync5(configPath);
|
|
29858
30123
|
if (existed) {
|
|
29859
30124
|
const raw = readFileSync3(configPath, "utf8");
|
|
29860
30125
|
try {
|
|
@@ -29904,8 +30169,8 @@ function stripJsonMcpEntry(existing) {
|
|
|
29904
30169
|
}
|
|
29905
30170
|
async function uninstallFromJsonConfig(configPath) {
|
|
29906
30171
|
try {
|
|
29907
|
-
const { existsSync:
|
|
29908
|
-
if (!
|
|
30172
|
+
const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync } = await import("fs");
|
|
30173
|
+
if (!existsSync5(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
|
|
29909
30174
|
const existing = readFileSync3(configPath, "utf8");
|
|
29910
30175
|
const { content, changed } = stripJsonMcpEntry(existing);
|
|
29911
30176
|
if (!changed) return { ok: true, message: "leadbay entry not present" };
|
|
@@ -29988,10 +30253,10 @@ function stripShellExportBlock(existing) {
|
|
|
29988
30253
|
}
|
|
29989
30254
|
async function installInCodexConfig(configPath, includeWrite, telemetryEnabled, localBinPath) {
|
|
29990
30255
|
try {
|
|
29991
|
-
const { readFileSync: readFileSync3, writeFileSync, existsSync:
|
|
30256
|
+
const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync5, mkdirSync, statSync, renameSync, chmodSync } = await import("fs");
|
|
29992
30257
|
const { dirname: dirname2 } = await import("path");
|
|
29993
30258
|
let existing = "";
|
|
29994
|
-
const existed =
|
|
30259
|
+
const existed = existsSync5(configPath);
|
|
29995
30260
|
if (existed) {
|
|
29996
30261
|
existing = readFileSync3(configPath, "utf8");
|
|
29997
30262
|
} else {
|
|
@@ -30037,15 +30302,15 @@ async function appendShellExports(token, region, includeWrite, telemetryEnabled)
|
|
|
30037
30302
|
}
|
|
30038
30303
|
return { ok: true, message: "env exported with setx; restart Codex/terminal" };
|
|
30039
30304
|
}
|
|
30040
|
-
const { existsSync:
|
|
30305
|
+
const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
|
|
30041
30306
|
const os = await import("os");
|
|
30042
30307
|
const home = os.homedir();
|
|
30043
|
-
const preferred = [`${home}/.zshrc`, `${home}/.bashrc`].filter((path) =>
|
|
30308
|
+
const preferred = [`${home}/.zshrc`, `${home}/.bashrc`].filter((path) => existsSync5(path));
|
|
30044
30309
|
const paths = preferred.length ? preferred : [`${home}/.profile`];
|
|
30045
30310
|
const block = buildShellExportBlock(token, region, includeWrite, telemetryEnabled);
|
|
30046
30311
|
const updated = [];
|
|
30047
30312
|
for (const path of paths) {
|
|
30048
|
-
const existing =
|
|
30313
|
+
const existing = existsSync5(path) ? readFileSync3(path, "utf8") : "";
|
|
30049
30314
|
const merged = mergeShellExportBlock(existing, block);
|
|
30050
30315
|
if (!merged.changed) continue;
|
|
30051
30316
|
const tmp = `${path}.leadbay.tmp`;
|
|
@@ -30063,8 +30328,8 @@ async function appendShellExports(token, region, includeWrite, telemetryEnabled)
|
|
|
30063
30328
|
}
|
|
30064
30329
|
async function uninstallFromCodexConfig(configPath) {
|
|
30065
30330
|
try {
|
|
30066
|
-
const { existsSync:
|
|
30067
|
-
if (!
|
|
30331
|
+
const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync } = await import("fs");
|
|
30332
|
+
if (!existsSync5(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
|
|
30068
30333
|
const existing = readFileSync3(configPath, "utf8");
|
|
30069
30334
|
const { content, changed } = stripCodexBlock(existing);
|
|
30070
30335
|
if (!changed) return { ok: true, message: "leadbay block not present" };
|
|
@@ -30076,11 +30341,11 @@ async function uninstallFromCodexConfig(configPath) {
|
|
|
30076
30341
|
}
|
|
30077
30342
|
async function uninstallShellExports() {
|
|
30078
30343
|
try {
|
|
30079
|
-
const { existsSync:
|
|
30344
|
+
const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
|
|
30080
30345
|
const os = await import("os");
|
|
30081
30346
|
const home = os.homedir();
|
|
30082
30347
|
const candidates = [`${home}/.zshrc`, `${home}/.bashrc`, `${home}/.profile`].filter(
|
|
30083
|
-
(p) =>
|
|
30348
|
+
(p) => existsSync5(p)
|
|
30084
30349
|
);
|
|
30085
30350
|
const updated = [];
|
|
30086
30351
|
for (const p of candidates) {
|
|
@@ -30105,17 +30370,17 @@ async function uninstallShellExports() {
|
|
|
30105
30370
|
var DXT_EXTENSION_ID = "local.dxt.leadbay.leadbay";
|
|
30106
30371
|
async function removeDxtExtension(claudeSupportDir) {
|
|
30107
30372
|
try {
|
|
30108
|
-
const { existsSync:
|
|
30109
|
-
const { join:
|
|
30110
|
-
const extensionDir =
|
|
30111
|
-
const registryPath =
|
|
30373
|
+
const { existsSync: existsSync5, readFileSync: readFileSync3, writeFileSync, rmSync } = await import("fs");
|
|
30374
|
+
const { join: join4 } = await import("path");
|
|
30375
|
+
const extensionDir = join4(claudeSupportDir, "Claude Extensions", DXT_EXTENSION_ID);
|
|
30376
|
+
const registryPath = join4(claudeSupportDir, "extensions-installations.json");
|
|
30112
30377
|
let removedDir = false;
|
|
30113
30378
|
let removedEntry = false;
|
|
30114
|
-
if (
|
|
30379
|
+
if (existsSync5(extensionDir)) {
|
|
30115
30380
|
rmSync(extensionDir, { recursive: true, force: true });
|
|
30116
30381
|
removedDir = true;
|
|
30117
30382
|
}
|
|
30118
|
-
if (
|
|
30383
|
+
if (existsSync5(registryPath)) {
|
|
30119
30384
|
try {
|
|
30120
30385
|
const raw = readFileSync3(registryPath, "utf8");
|
|
30121
30386
|
const parsed = JSON.parse(raw);
|
|
@@ -30142,8 +30407,8 @@ async function removeDxtExtension(claudeSupportDir) {
|
|
|
30142
30407
|
}
|
|
30143
30408
|
|
|
30144
30409
|
// installer/install-wizard.ts
|
|
30145
|
-
function ansi(
|
|
30146
|
-
return enabled ? `\x1B[${code}m${
|
|
30410
|
+
function ansi(text3, code, enabled) {
|
|
30411
|
+
return enabled ? `\x1B[${code}m${text3}\x1B[0m` : text3;
|
|
30147
30412
|
}
|
|
30148
30413
|
function parseInstallSelection(input, clientCount) {
|
|
30149
30414
|
const normalized = input.trim().toLowerCase();
|
|
@@ -30323,7 +30588,7 @@ import {
|
|
|
30323
30588
|
} from "fs/promises";
|
|
30324
30589
|
import { constants as fsConstants } from "fs";
|
|
30325
30590
|
import { dirname, resolve as resolvePath } from "path";
|
|
30326
|
-
import { homedir as
|
|
30591
|
+
import { homedir as homedir3 } from "os";
|
|
30327
30592
|
function emptyState() {
|
|
30328
30593
|
return {
|
|
30329
30594
|
last_check_time: 0,
|
|
@@ -30359,7 +30624,7 @@ var UpdateStateStore = class {
|
|
|
30359
30624
|
}
|
|
30360
30625
|
validatePath(p) {
|
|
30361
30626
|
if (this.allowUnsafePath) return;
|
|
30362
|
-
const home = resolvePath(
|
|
30627
|
+
const home = resolvePath(homedir3());
|
|
30363
30628
|
if (p !== home && !p.startsWith(home + "/") && !p.startsWith(home + "\\")) {
|
|
30364
30629
|
throw new Error(
|
|
30365
30630
|
`UpdateStateStore: path ${p} is outside $HOME (${home}). Set LEADBAY_UPDATE_STATE_PATH_UNSAFE=1 to override.`
|
|
@@ -30487,7 +30752,7 @@ async function openTmpFileExclusive(path) {
|
|
|
30487
30752
|
async function createDefaultUpdateStateStore(opts = {}) {
|
|
30488
30753
|
const env = opts.env ?? process.env;
|
|
30489
30754
|
const allowUnsafePath = env.LEADBAY_UPDATE_STATE_PATH_UNSAFE === "1";
|
|
30490
|
-
const path = env.LEADBAY_UPDATE_STATE_PATH ?? resolvePath(
|
|
30755
|
+
const path = env.LEADBAY_UPDATE_STATE_PATH ?? resolvePath(homedir3(), ".leadbay", "update-state.json");
|
|
30491
30756
|
try {
|
|
30492
30757
|
const store = new UpdateStateStore({
|
|
30493
30758
|
backend: "file",
|
|
@@ -30511,7 +30776,7 @@ import { createHash as createHash5, randomBytes } from "crypto";
|
|
|
30511
30776
|
import { createServer } from "http";
|
|
30512
30777
|
import { request as httpsRequestRaw } from "https";
|
|
30513
30778
|
import { spawn as spawn3 } from "child_process";
|
|
30514
|
-
import { readdirSync as readdirSync3, existsSync as
|
|
30779
|
+
import { readdirSync as readdirSync3, existsSync as existsSync4 } from "fs";
|
|
30515
30780
|
var LEADBAY_LOOPBACK_PORTS = [51789, 51790, 51791, 51792];
|
|
30516
30781
|
var BrowserOpenFailedError = class extends Error {
|
|
30517
30782
|
authorizeUrl;
|
|
@@ -30844,11 +31109,11 @@ function browserLaunchEnv(debug) {
|
|
|
30844
31109
|
const env = { ...process.env };
|
|
30845
31110
|
if (process.platform !== "linux") return env;
|
|
30846
31111
|
let runtimeDir = env.XDG_RUNTIME_DIR;
|
|
30847
|
-
if (!runtimeDir || !
|
|
31112
|
+
if (!runtimeDir || !existsSync4(runtimeDir)) {
|
|
30848
31113
|
try {
|
|
30849
31114
|
const uid = process.getuid?.();
|
|
30850
31115
|
const candidate = uid !== void 0 ? `/run/user/${uid}` : void 0;
|
|
30851
|
-
if (candidate &&
|
|
31116
|
+
if (candidate && existsSync4(candidate)) {
|
|
30852
31117
|
runtimeDir = candidate;
|
|
30853
31118
|
env.XDG_RUNTIME_DIR = candidate;
|
|
30854
31119
|
debug?.(`browserLaunchEnv: injected XDG_RUNTIME_DIR=${candidate}`);
|
|
@@ -30868,7 +31133,7 @@ function browserLaunchEnv(debug) {
|
|
|
30868
31133
|
}
|
|
30869
31134
|
if (!env.DBUS_SESSION_BUS_ADDRESS && runtimeDir) {
|
|
30870
31135
|
const busPath = `${runtimeDir}/bus`;
|
|
30871
|
-
if (
|
|
31136
|
+
if (existsSync4(busPath)) {
|
|
30872
31137
|
env.DBUS_SESSION_BUS_ADDRESS = `unix:path=${busPath}`;
|
|
30873
31138
|
debug?.(`browserLaunchEnv: injected DBUS_SESSION_BUS_ADDRESS=unix:path=${busPath}`);
|
|
30874
31139
|
}
|
|
@@ -30891,7 +31156,7 @@ function browserLaunchEnv(debug) {
|
|
|
30891
31156
|
);
|
|
30892
31157
|
if (cookie) xauth = `${runtimeDir}/${cookie}`;
|
|
30893
31158
|
}
|
|
30894
|
-
if (!xauth && env.HOME &&
|
|
31159
|
+
if (!xauth && env.HOME && existsSync4(`${env.HOME}/.Xauthority`)) {
|
|
30895
31160
|
xauth = `${env.HOME}/.Xauthority`;
|
|
30896
31161
|
}
|
|
30897
31162
|
if (xauth) {
|
|
@@ -31054,7 +31319,7 @@ var OAUTH_BASE_URLS = {
|
|
|
31054
31319
|
fr: "https://staging.api.leadbay.app"
|
|
31055
31320
|
}
|
|
31056
31321
|
};
|
|
31057
|
-
var VERSION = "0.
|
|
31322
|
+
var VERSION = "0.39.0";
|
|
31058
31323
|
var HELP = `
|
|
31059
31324
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
31060
31325
|
|
|
@@ -31154,9 +31419,9 @@ function exitWithTokenError() {
|
|
|
31154
31419
|
function hydrateEnvFromCredentialsFile() {
|
|
31155
31420
|
if (process.env.LEADBAY_TOKEN) return false;
|
|
31156
31421
|
try {
|
|
31157
|
-
const { existsSync:
|
|
31422
|
+
const { existsSync: existsSync5, readFileSync: readFileSync3 } = require_("node:fs");
|
|
31158
31423
|
const { path } = resolveOAuthBootstrapCredentialsPath();
|
|
31159
|
-
if (!
|
|
31424
|
+
if (!existsSync5(path)) return false;
|
|
31160
31425
|
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
31161
31426
|
const env = parsed?.mcpServers?.leadbay?.env;
|
|
31162
31427
|
if (!env || typeof env !== "object") return false;
|
|
@@ -31177,9 +31442,9 @@ function hydrateEnvFromCredentialsFile() {
|
|
|
31177
31442
|
function resolveOAuthBootstrapCredentialsPath() {
|
|
31178
31443
|
const resolved = resolveDefaultCredentialsPath();
|
|
31179
31444
|
if (process.env.LEADBAY_OAUTH_STAGING !== "1") return resolved;
|
|
31180
|
-
const { dirname: dirname2, join:
|
|
31445
|
+
const { dirname: dirname2, join: join4 } = require_("node:path");
|
|
31181
31446
|
return {
|
|
31182
|
-
path:
|
|
31447
|
+
path: join4(dirname2(resolved.path), "credentials.staging.json"),
|
|
31183
31448
|
legacy: resolved.legacy
|
|
31184
31449
|
};
|
|
31185
31450
|
}
|
|
@@ -31189,20 +31454,20 @@ var bootstrapFailureMessage;
|
|
|
31189
31454
|
function bootstrapDebug(msg) {
|
|
31190
31455
|
try {
|
|
31191
31456
|
const { appendFileSync, mkdirSync } = require_("node:fs");
|
|
31192
|
-
const { join:
|
|
31193
|
-
const { homedir:
|
|
31194
|
-
const dir =
|
|
31457
|
+
const { join: join4 } = require_("node:path");
|
|
31458
|
+
const { homedir: homedir4 } = require_("node:os");
|
|
31459
|
+
const dir = join4(homedir4(), ".leadbay");
|
|
31195
31460
|
mkdirSync(dir, { recursive: true });
|
|
31196
31461
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
31197
|
-
appendFileSync(
|
|
31462
|
+
appendFileSync(join4(dir, "oauth-bootstrap-debug.log"), `${ts} [pid ${process.pid}] ${msg}
|
|
31198
31463
|
`);
|
|
31199
31464
|
} catch {
|
|
31200
31465
|
}
|
|
31201
31466
|
}
|
|
31202
31467
|
function oauthClientCachePath() {
|
|
31203
|
-
const { join:
|
|
31204
|
-
const { homedir:
|
|
31205
|
-
return
|
|
31468
|
+
const { join: join4 } = require_("node:path");
|
|
31469
|
+
const { homedir: homedir4 } = require_("node:os");
|
|
31470
|
+
return join4(homedir4(), ".leadbay", "oauth-client.json");
|
|
31206
31471
|
}
|
|
31207
31472
|
function getCachedOAuthClientId(authServerBaseUrl, port) {
|
|
31208
31473
|
try {
|
|
@@ -31529,8 +31794,8 @@ function parseFlag(args, name) {
|
|
|
31529
31794
|
function hasFlag(args, name) {
|
|
31530
31795
|
return args.some((a) => a === `--${name}`);
|
|
31531
31796
|
}
|
|
31532
|
-
function ansi2(
|
|
31533
|
-
return enabled ? `\x1B[${code}m${
|
|
31797
|
+
function ansi2(text3, code, enabled) {
|
|
31798
|
+
return enabled ? `\x1B[${code}m${text3}\x1B[0m` : text3;
|
|
31534
31799
|
}
|
|
31535
31800
|
function isInteractiveInstall() {
|
|
31536
31801
|
return process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
@@ -31750,8 +32015,8 @@ Restart your MCP client to pick up the new server.
|
|
|
31750
32015
|
usingLegacyPath = resolved.legacy;
|
|
31751
32016
|
}
|
|
31752
32017
|
try {
|
|
31753
|
-
const { existsSync:
|
|
31754
|
-
if (
|
|
32018
|
+
const { existsSync: existsSync5, readFileSync: readFileSync3 } = await import("fs");
|
|
32019
|
+
if (existsSync5(targetPath) && !force) {
|
|
31755
32020
|
let existing;
|
|
31756
32021
|
try {
|
|
31757
32022
|
existing = JSON.parse(readFileSync3(targetPath, "utf8"));
|
|
@@ -32279,6 +32544,9 @@ async function main() {
|
|
|
32279
32544
|
version: VERSION,
|
|
32280
32545
|
telemetry,
|
|
32281
32546
|
updateStateStore,
|
|
32547
|
+
// This process runs on the user's machine, so a tool can save a file
|
|
32548
|
+
// they will find. The hosted server (http-server.ts) does not pass this.
|
|
32549
|
+
saveFile: saveToDownloads,
|
|
32282
32550
|
// Non-blocking OAuth bootstrap gate. Read per tool call: once the
|
|
32283
32551
|
// background flow lands the token (client.isAuthenticated → true) this
|
|
32284
32552
|
// reports done and tools execute. While waiting it surfaces the live
|
|
@@ -32361,7 +32629,7 @@ var isEntrypoint = (() => {
|
|
|
32361
32629
|
try {
|
|
32362
32630
|
const entry = process.argv[1];
|
|
32363
32631
|
if (!entry) return false;
|
|
32364
|
-
const entryName =
|
|
32632
|
+
const entryName = basename2(entry).toLowerCase();
|
|
32365
32633
|
if (entryName !== "bin.js" && entryName !== "leadbay-mcp" && entryName !== "index.js") return false;
|
|
32366
32634
|
const self = fileURLToPath(import.meta.url);
|
|
32367
32635
|
return realpathSync(self) === realpathSync(entry);
|