@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/http-server.js
CHANGED
|
@@ -2912,8 +2912,8 @@ var PROMPT_CATALOG_BULLETS = {
|
|
|
2912
2912
|
};
|
|
2913
2913
|
|
|
2914
2914
|
// src/prompts.ts
|
|
2915
|
-
function userMessage(
|
|
2916
|
-
return { role: "user", content: { type: "text", text:
|
|
2915
|
+
function userMessage(text3) {
|
|
2916
|
+
return { role: "user", content: { type: "text", text: text3 } };
|
|
2917
2917
|
}
|
|
2918
2918
|
function substitutePlaceholders(body, substitutions) {
|
|
2919
2919
|
let out = body;
|
|
@@ -5791,7 +5791,7 @@ measured:
|
|
|
5791
5791
|
\`employees_max: number\`. FLAT numbers \u2014 nested \`employees: {min, max}\`
|
|
5792
5792
|
exists only in RESULT payloads. \`example_lead.employees\` does not filter.
|
|
5793
5793
|
\`locations\` take city/state/region names ("Dallas, TX", "\xCEle-de-France");
|
|
5794
|
-
a country
|
|
5794
|
+
never a country: this workspace's own is dropped, any other is refused.
|
|
5795
5795
|
7. **Prefer \`example_lead\` over \`query\`.** Query matches topic *vocabulary*:
|
|
5796
5796
|
"gyms that need durable flooring" surfaced flooring VENDORS, 0 delivered.
|
|
5797
5797
|
Use \`query\` only for signal an example can't express.
|
|
@@ -6861,8 +6861,31 @@ server-side: past the 30-min wall clock a job reads \`completed_partial\`
|
|
|
6861
6861
|
delivered leads via leadbay_qualify_leads \`prior_deliveries\`). A 404 means
|
|
6862
6862
|
unknown job or another org's job.
|
|
6863
6863
|
|
|
6864
|
+
\`compact: true\` (pass it whenever \`next_poll\` carries it) returns the rows of
|
|
6865
|
+
a free identity pass, 100 at a time from \`offset\`, instead of leads.
|
|
6866
|
+
|
|
6864
6867
|
---
|
|
6865
6868
|
|
|
6869
|
+
## RENDERING \u2014 identity pass (\`rows[]\`)
|
|
6870
|
+
|
|
6871
|
+
\`rows[]\` replaces \`leads[]\`: one row per company, in the user's order. One
|
|
6872
|
+
table: **Company** (\`input\`, plus \`name\` when it differs) \xB7 **Website** \xB7
|
|
6873
|
+
**LinkedIn**. Skipped rows stay in it: \`not_in_universe\` \u2192 "not in Leadbay",
|
|
6874
|
+
\`low_confidence_identity\` \u2192 "several matches \u2014 add a city or website".
|
|
6875
|
+
|
|
6876
|
+
Then ONE coverage line from \`summary\`: found \`resolved\` of \`rows_total\` \xB7
|
|
6877
|
+
\`with_website\` with a website \xB7 \`with_linkedin\` with a LinkedIn \xB7
|
|
6878
|
+
\`ambiguous\` unclear \xB7 \`not_found\` not found. When most rows lack what the
|
|
6879
|
+
user asked for, that line is the answer: say so, and do not look the
|
|
6880
|
+
missing ones up one by one.
|
|
6881
|
+
|
|
6882
|
+
Done job with \`next_poll.offset\`: say "showing N of \`rows_total\`" and offer
|
|
6883
|
+
the rest via \`leadbay_lead_job_status(job_id, compact: true, offset)\`.
|
|
6884
|
+
|
|
6885
|
+
\`file\` set (local install only): every row is saved there as a CSV. Give the
|
|
6886
|
+
user that path.
|
|
6887
|
+
|
|
6888
|
+
|
|
6866
6889
|
## RENDERING \u2014 delivery table + honest funnel line
|
|
6867
6890
|
|
|
6868
6891
|
Render delivered leads (\`leads[]\`, i.e. items with status \`delivered\` or
|
|
@@ -8105,13 +8128,14 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
|
|
|
8105
8128
|
`;
|
|
8106
8129
|
var leadbay_qualify_leads = `## WHEN TO USE
|
|
8107
8130
|
|
|
8108
|
-
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".
|
|
8131
|
+
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".
|
|
8109
8132
|
|
|
8110
8133
|
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\`.
|
|
8111
8134
|
|
|
8112
8135
|
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.
|
|
8113
8136
|
|
|
8114
8137
|
Examples that SHOULD invoke this tool:
|
|
8138
|
+
- "For each of these 300 brokers, give me the website and LinkedIn, nothing else."
|
|
8115
8139
|
- "Here are 60 restaurant websites from my Austin sweep \u2014 which fit, and who's the owner?"
|
|
8116
8140
|
- "Re-qualify last week's delivery and get phone numbers for the good ones."
|
|
8117
8141
|
- "Vet these 12 accounts from my spreadsheet against our criteria."
|
|
@@ -8123,7 +8147,8 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
|
8123
8147
|
|
|
8124
8148
|
## RENDER (quick)
|
|
8125
8149
|
|
|
8126
|
-
|
|
8150
|
+
Identity pass (\`rows[]\`): one table Company / Website / LinkedIn, then the
|
|
8151
|
+
coverage line. Otherwise a 3-col table for delivered items (fit bar + company / why-fits \u226420 words /
|
|
8127
8152
|
contact + channels) in returned order, then a compact Ref \u2192 Outcome table
|
|
8128
8153
|
for skipped refs (not_in_universe, low_confidence_identity, ... in plain
|
|
8129
8154
|
words), then the honest funnel line. Full algorithm below.
|
|
@@ -8138,6 +8163,14 @@ when asked \u2014 enriched with verified channels. Answers arrive per-item from
|
|
|
8138
8163
|
job; this tool polls up to \`wait_seconds\` (default 45) and hands off to
|
|
8139
8164
|
\`leadbay_lead_job_status\` when the batch needs longer.
|
|
8140
8165
|
|
|
8166
|
+
**A list of companies is ONE call, never a loop** of
|
|
8167
|
+
\`leadbay_research_lead_by_name_fuzzy\`: "for each of these companies give me
|
|
8168
|
+
X", a pasted list, a file. Identity only (in Leadbay or not, website,
|
|
8169
|
+
LinkedIn) \u2192 \`qualify: false\`: free, no \`confirm\`, returns \`rows[]\` and
|
|
8170
|
+
coverage counts. A fit question over the list ("which of these do
|
|
8171
|
+
factoring") keeps \`qualify: true\` and its quote. Over 500: one call per 500,
|
|
8172
|
+
each with its own \`request_id\`.
|
|
8173
|
+
|
|
8141
8174
|
**Refs are flexible; outcomes are per-item.** \`lead_refs\` accepts any mix of
|
|
8142
8175
|
\`lead_id\`, \`website\`, \`name\`(+\`location\`), or a stable \`contact_id\` from a
|
|
8143
8176
|
prior result (enrichment then targets exactly that person, never a re-match).
|
|
@@ -8174,6 +8207,26 @@ Retry-After beyond \u2014 wait, don't hammer), 30-min job wall clock.
|
|
|
8174
8207
|
|
|
8175
8208
|
---
|
|
8176
8209
|
|
|
8210
|
+
## RENDERING \u2014 identity pass (\`rows[]\`)
|
|
8211
|
+
|
|
8212
|
+
\`rows[]\` replaces \`leads[]\`: one row per company, in the user's order. One
|
|
8213
|
+
table: **Company** (\`input\`, plus \`name\` when it differs) \xB7 **Website** \xB7
|
|
8214
|
+
**LinkedIn**. Skipped rows stay in it: \`not_in_universe\` \u2192 "not in Leadbay",
|
|
8215
|
+
\`low_confidence_identity\` \u2192 "several matches \u2014 add a city or website".
|
|
8216
|
+
|
|
8217
|
+
Then ONE coverage line from \`summary\`: found \`resolved\` of \`rows_total\` \xB7
|
|
8218
|
+
\`with_website\` with a website \xB7 \`with_linkedin\` with a LinkedIn \xB7
|
|
8219
|
+
\`ambiguous\` unclear \xB7 \`not_found\` not found. When most rows lack what the
|
|
8220
|
+
user asked for, that line is the answer: say so, and do not look the
|
|
8221
|
+
missing ones up one by one.
|
|
8222
|
+
|
|
8223
|
+
Done job with \`next_poll.offset\`: say "showing N of \`rows_total\`" and offer
|
|
8224
|
+
the rest via \`leadbay_lead_job_status(job_id, compact: true, offset)\`.
|
|
8225
|
+
|
|
8226
|
+
\`file\` set (local install only): every row is saved there as a CSV. Give the
|
|
8227
|
+
user that path.
|
|
8228
|
+
|
|
8229
|
+
|
|
8177
8230
|
## RENDERING \u2014 delivery table + honest funnel line
|
|
8178
8231
|
|
|
8179
8232
|
Render delivered leads (\`leads[]\`, i.e. items with status \`delivered\` or
|
|
@@ -8328,6 +8381,8 @@ Pick the 2-3 options that match what actually happened:
|
|
|
8328
8381
|
| Items skipped \`low_confidence_identity\` | "Pick the right match" (show \`resolution.alternatives\`) | leadbay_qualify_leads with the chosen lead_id |
|
|
8329
8382
|
| 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) |
|
|
8330
8383
|
| Disqualified with evidence | "Review why \u2014 adjust qualification questions if the criteria are off" | leadbay_get_qualification_questions |
|
|
8384
|
+
| Identity pass, list over 500 | "Run the next 500" | leadbay_qualify_leads(next 500, qualify: false) |
|
|
8385
|
+
| 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 |
|
|
8331
8386
|
`;
|
|
8332
8387
|
var 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:
|
|
8333
8388
|
|
|
@@ -8806,9 +8861,9 @@ var leadbay_research_lead_by_name_fuzzy = `## WHEN TO USE
|
|
|
8806
8861
|
|
|
8807
8862
|
Trigger phrases: "look up <Company>", "research <Company>", "what do we know about <Company>".
|
|
8808
8863
|
|
|
8809
|
-
Do NOT use for: "picked row with leadId" \u2192 \`leadbay_research_lead_by_id\`; "draft outreach for <Contact>" \u2192 \`leadbay_prepare_outreach\`.
|
|
8864
|
+
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\`.
|
|
8810
8865
|
|
|
8811
|
-
Prefer when:
|
|
8866
|
+
Prefer when: ONE company name or domain in prose, no Leadbay id yet \u2014 always pass \`website\` if a domain was mentioned
|
|
8812
8867
|
|
|
8813
8868
|
Examples that SHOULD invoke this tool:
|
|
8814
8869
|
- "Look up Acme Corp for me."
|
|
@@ -8818,7 +8873,7 @@ Examples that SHOULD invoke this tool:
|
|
|
8818
8873
|
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
8819
8874
|
- "Tell me about that lead I just picked."
|
|
8820
8875
|
- "Draft outreach to Acme's CTO."
|
|
8821
|
-
- "
|
|
8876
|
+
- "Websites and LinkedIn for these 200 companies."
|
|
8822
8877
|
|
|
8823
8878
|
---
|
|
8824
8879
|
|
|
@@ -8832,15 +8887,13 @@ list" into an answer. With only a contact email, pass \`email\`: the company
|
|
|
8832
8887
|
domain is derived from it, consumer mailboxes ignored.
|
|
8833
8888
|
|
|
8834
8889
|
When the registry cannot pick one company it returns \`{resolution:
|
|
8835
|
-
"ambiguous", query, candidates:[
|
|
8836
|
-
|
|
8890
|
+
"ambiguous", query, candidates:[\u2026]}\` instead of a card. Ask which one; never
|
|
8891
|
+
guess from \`score\`.
|
|
8837
8892
|
|
|
8838
8893
|
\`LEAD_NOT_FOUND\` is not a dead end: its hint names the field that would have
|
|
8839
8894
|
found it \u2014 \`website\` or \`registry_number\`, both params. Ask for it and call
|
|
8840
8895
|
again. Do not offer an import before asking.
|
|
8841
8896
|
|
|
8842
|
-
Offer \`_meta.match_candidates\` when present.
|
|
8843
|
-
|
|
8844
8897
|
---
|
|
8845
8898
|
|
|
8846
8899
|
## RENDERING \u2014 single-record research card, mode-adaptive
|
|
@@ -10147,7 +10200,7 @@ measured:
|
|
|
10147
10200
|
\`employees_max: number\`. FLAT numbers \u2014 nested \`employees: {min, max}\`
|
|
10148
10201
|
exists only in RESULT payloads. \`example_lead.employees\` does not filter.
|
|
10149
10202
|
\`locations\` take city/state/region names ("Dallas, TX", "\xCEle-de-France");
|
|
10150
|
-
a country
|
|
10203
|
+
never a country: this workspace's own is dropped, any other is refused.
|
|
10151
10204
|
7. **Prefer \`example_lead\` over \`query\`.** Query matches topic *vocabulary*:
|
|
10152
10205
|
"gyms that need durable flooring" surfaced flooring VENDORS, 0 delivered.
|
|
10153
10206
|
Use \`query\` only for signal an example can't express.
|
|
@@ -16931,12 +16984,12 @@ var CITY_ALIASES = {
|
|
|
16931
16984
|
vegas: "Las Vegas",
|
|
16932
16985
|
nola: "New Orleans"
|
|
16933
16986
|
};
|
|
16934
|
-
function expandAlias(
|
|
16935
|
-
const key =
|
|
16936
|
-
return CITY_ALIASES[key] ??
|
|
16987
|
+
function expandAlias(text3) {
|
|
16988
|
+
const key = text3.trim().toLowerCase();
|
|
16989
|
+
return CITY_ALIASES[key] ?? text3;
|
|
16937
16990
|
}
|
|
16938
|
-
function scoreMatch(
|
|
16939
|
-
const t =
|
|
16991
|
+
function scoreMatch(text3, match) {
|
|
16992
|
+
const t = text3.trim().toLowerCase();
|
|
16940
16993
|
const n = match.name.trim().toLowerCase();
|
|
16941
16994
|
if (n === t)
|
|
16942
16995
|
return 1;
|
|
@@ -16959,8 +17012,8 @@ async function resolveLocations(client, texts) {
|
|
|
16959
17012
|
const resolved = [...direct];
|
|
16960
17013
|
const ambiguities = [];
|
|
16961
17014
|
for (const originalText of free) {
|
|
16962
|
-
const
|
|
16963
|
-
const path = `/geo/search?q=${encodeURIComponent(
|
|
17015
|
+
const text3 = expandAlias(originalText);
|
|
17016
|
+
const path = `/geo/search?q=${encodeURIComponent(text3)}`;
|
|
16964
17017
|
let response;
|
|
16965
17018
|
try {
|
|
16966
17019
|
response = await client.request("GET", path);
|
|
@@ -16999,7 +17052,7 @@ async function resolveLocations(client, texts) {
|
|
|
16999
17052
|
name: r.name,
|
|
17000
17053
|
country: r.country,
|
|
17001
17054
|
level: r.level,
|
|
17002
|
-
score: scoreMatch(
|
|
17055
|
+
score: scoreMatch(text3, r)
|
|
17003
17056
|
})).sort((a, b) => {
|
|
17004
17057
|
if (b.score !== a.score)
|
|
17005
17058
|
return b.score - a.score;
|
|
@@ -18165,9 +18218,9 @@ ${firm.short_description}`);
|
|
|
18165
18218
|
out.push(`### ${sec.section_emoji ?? ""} ${label}`.trim());
|
|
18166
18219
|
const entries = Array.isArray(sec.entries) ? sec.entries : [];
|
|
18167
18220
|
for (const e of entries.slice(0, 5)) {
|
|
18168
|
-
const
|
|
18221
|
+
const text3 = e.text ?? e.summary ?? JSON.stringify(e).slice(0, 200);
|
|
18169
18222
|
const hot = e.hot === true ? " \u{1F525}" : "";
|
|
18170
|
-
out.push(`- ${
|
|
18223
|
+
out.push(`- ${text3}${hot}`);
|
|
18171
18224
|
}
|
|
18172
18225
|
if (entries.length > 5)
|
|
18173
18226
|
out.push(`- _${entries.length - 5} more \u2026_`);
|
|
@@ -22904,8 +22957,8 @@ function tokens(s) {
|
|
|
22904
22957
|
return [];
|
|
22905
22958
|
return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
22906
22959
|
}
|
|
22907
|
-
function bestMatches(
|
|
22908
|
-
const want = new Set(tokens(
|
|
22960
|
+
function bestMatches(text3, taxonomy) {
|
|
22961
|
+
const want = new Set(tokens(text3));
|
|
22909
22962
|
if (want.size === 0)
|
|
22910
22963
|
return [];
|
|
22911
22964
|
const ranked = taxonomy.map((s) => {
|
|
@@ -22934,12 +22987,12 @@ async function resolveSectors(client, texts, ctx) {
|
|
|
22934
22987
|
}
|
|
22935
22988
|
const resolved = [...direct];
|
|
22936
22989
|
const ambiguities = [];
|
|
22937
|
-
for (const
|
|
22938
|
-
const matches = bestMatches(
|
|
22990
|
+
for (const text3 of free) {
|
|
22991
|
+
const matches = bestMatches(text3, taxonomy);
|
|
22939
22992
|
if (matches.length === 1 || matches.length >= 2 && matches[0].score >= 0.66 && matches[0].score - matches[1].score >= 0.34) {
|
|
22940
22993
|
resolved.push(matches[0].id);
|
|
22941
22994
|
} else {
|
|
22942
|
-
ambiguities.push({ sector_text:
|
|
22995
|
+
ambiguities.push({ sector_text: text3, matches });
|
|
22943
22996
|
}
|
|
22944
22997
|
}
|
|
22945
22998
|
return { resolved, ambiguities };
|
|
@@ -24356,6 +24409,21 @@ var findNewLeads = {
|
|
|
24356
24409
|
"channels",
|
|
24357
24410
|
"exclude_lead_ids"
|
|
24358
24411
|
]);
|
|
24412
|
+
const homeCountry = detectCountryLocations(params.filters?.locations, "filters.locations", client.region).filter((hit) => hit.kind === "home_country");
|
|
24413
|
+
let countryNote;
|
|
24414
|
+
if (homeCountry.length > 0) {
|
|
24415
|
+
const dropped = new Set(homeCountry.map((hit) => hit.value));
|
|
24416
|
+
const raw = params.filters.locations;
|
|
24417
|
+
const rest = (Array.isArray(raw) ? raw : [raw]).filter((v) => !dropped.has(v));
|
|
24418
|
+
params = {
|
|
24419
|
+
...params,
|
|
24420
|
+
filters: { ...params.filters, locations: rest.length > 0 ? rest : void 0 }
|
|
24421
|
+
};
|
|
24422
|
+
const removed = [...dropped].map((v) => `"${v}"`).join(", ");
|
|
24423
|
+
countryNote = {
|
|
24424
|
+
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.`
|
|
24425
|
+
};
|
|
24426
|
+
}
|
|
24359
24427
|
rejectCountryLocations(params.filters?.locations, client.region);
|
|
24360
24428
|
rejectMalformedExclusions(params.exclude_lead_ids);
|
|
24361
24429
|
rejectOversizedExclusions(params.exclude_lead_ids);
|
|
@@ -24433,6 +24501,7 @@ var findNewLeads = {
|
|
|
24433
24501
|
return {
|
|
24434
24502
|
dry_run: true,
|
|
24435
24503
|
...forecast,
|
|
24504
|
+
...countryNote,
|
|
24436
24505
|
region: client.region
|
|
24437
24506
|
};
|
|
24438
24507
|
}
|
|
@@ -24453,6 +24522,7 @@ var findNewLeads = {
|
|
|
24453
24522
|
estimated_cost: forecast?.estimated_cost ?? null,
|
|
24454
24523
|
items_requested: forecast?.items_requested ?? null,
|
|
24455
24524
|
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.",
|
|
24525
|
+
...countryNote,
|
|
24456
24526
|
region: client.region
|
|
24457
24527
|
};
|
|
24458
24528
|
}
|
|
@@ -24507,11 +24577,142 @@ var findNewLeads = {
|
|
|
24507
24577
|
since: snapshot.next_since ?? null,
|
|
24508
24578
|
suggested_wait_seconds: done ? 0 : 60
|
|
24509
24579
|
},
|
|
24580
|
+
...countryNote,
|
|
24510
24581
|
region: client.region
|
|
24511
24582
|
};
|
|
24512
24583
|
}
|
|
24513
24584
|
};
|
|
24514
24585
|
|
|
24586
|
+
// ../core/dist/composite/_identity-rows.js
|
|
24587
|
+
var IDENTITY_ROWS_PER_RESULT = 100;
|
|
24588
|
+
function text(v) {
|
|
24589
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
24590
|
+
}
|
|
24591
|
+
function toRow(item) {
|
|
24592
|
+
const asked = item.ref?.requested_as ?? {};
|
|
24593
|
+
const row = {
|
|
24594
|
+
input_indexes: item.ref?.input_indexes ?? null,
|
|
24595
|
+
input: text(asked.name) ?? text(asked.website) ?? text(asked.lead_id) ?? text(asked.contact_id),
|
|
24596
|
+
status: item.status
|
|
24597
|
+
};
|
|
24598
|
+
if (item.status_reason)
|
|
24599
|
+
row.status_reason = item.status_reason;
|
|
24600
|
+
const lead = item.lead;
|
|
24601
|
+
if (!lead)
|
|
24602
|
+
return row;
|
|
24603
|
+
const company = lead.company ?? {};
|
|
24604
|
+
row.lead_id = lead.lead_id ?? item.ref?.lead_id ?? void 0;
|
|
24605
|
+
row.name = company.name;
|
|
24606
|
+
row.website = text(company.website);
|
|
24607
|
+
row.linkedin = text(company.socials?.linkedin);
|
|
24608
|
+
return row;
|
|
24609
|
+
}
|
|
24610
|
+
function identityRows(items) {
|
|
24611
|
+
const first = (i) => {
|
|
24612
|
+
const idx = i.ref?.input_indexes;
|
|
24613
|
+
return Array.isArray(idx) && idx.length > 0 ? Math.min(...idx) : Number.MAX_SAFE_INTEGER;
|
|
24614
|
+
};
|
|
24615
|
+
return [...items].sort((a, b) => first(a) - first(b) || a.seq - b.seq).map(toRow);
|
|
24616
|
+
}
|
|
24617
|
+
function identityCoverage(rows) {
|
|
24618
|
+
let resolved = 0;
|
|
24619
|
+
let ambiguous = 0;
|
|
24620
|
+
let notFound = 0;
|
|
24621
|
+
let otherSkipped = 0;
|
|
24622
|
+
let withWebsite = 0;
|
|
24623
|
+
let withLinkedin = 0;
|
|
24624
|
+
for (const r of rows) {
|
|
24625
|
+
if (r.status !== "skipped") {
|
|
24626
|
+
resolved += 1;
|
|
24627
|
+
if (r.website)
|
|
24628
|
+
withWebsite += 1;
|
|
24629
|
+
if (r.linkedin)
|
|
24630
|
+
withLinkedin += 1;
|
|
24631
|
+
} else if (r.status_reason === "low_confidence_identity")
|
|
24632
|
+
ambiguous += 1;
|
|
24633
|
+
else if (r.status_reason === "not_in_universe")
|
|
24634
|
+
notFound += 1;
|
|
24635
|
+
else
|
|
24636
|
+
otherSkipped += 1;
|
|
24637
|
+
}
|
|
24638
|
+
return {
|
|
24639
|
+
resolved,
|
|
24640
|
+
ambiguous,
|
|
24641
|
+
not_found: notFound,
|
|
24642
|
+
other_skipped: otherSkipped,
|
|
24643
|
+
with_website: withWebsite,
|
|
24644
|
+
with_linkedin: withLinkedin,
|
|
24645
|
+
without_website: resolved - withWebsite
|
|
24646
|
+
};
|
|
24647
|
+
}
|
|
24648
|
+
var CSV_HEADER = [
|
|
24649
|
+
"row",
|
|
24650
|
+
"input",
|
|
24651
|
+
"status",
|
|
24652
|
+
"reason",
|
|
24653
|
+
"company",
|
|
24654
|
+
"website",
|
|
24655
|
+
"linkedin",
|
|
24656
|
+
"lead_id"
|
|
24657
|
+
];
|
|
24658
|
+
function identityCsv(rows) {
|
|
24659
|
+
return synthesizeCsv(CSV_HEADER, rows.map((r) => ({
|
|
24660
|
+
row: (r.input_indexes ?? []).map((i) => i + 1).join(";"),
|
|
24661
|
+
input: r.input ?? "",
|
|
24662
|
+
status: r.status,
|
|
24663
|
+
reason: r.status_reason ?? "",
|
|
24664
|
+
company: r.name ?? "",
|
|
24665
|
+
website: r.website ?? "",
|
|
24666
|
+
linkedin: r.linkedin ?? "",
|
|
24667
|
+
lead_id: r.lead_id ?? ""
|
|
24668
|
+
})));
|
|
24669
|
+
}
|
|
24670
|
+
async function saveIdentityFile(ctx, jobId, snapshot, items) {
|
|
24671
|
+
if (!ctx?.saveFile)
|
|
24672
|
+
return {};
|
|
24673
|
+
if (!TERMINAL_JOB_STATES.has(snapshot.job.state))
|
|
24674
|
+
return {};
|
|
24675
|
+
if (snapshot.items_truncated || items.length === 0)
|
|
24676
|
+
return {};
|
|
24677
|
+
try {
|
|
24678
|
+
const name = `leadbay-companies-${jobId.slice(0, 8)}.csv`;
|
|
24679
|
+
return { file: await ctx.saveFile(name, identityCsv(identityRows(items))) };
|
|
24680
|
+
} catch (e) {
|
|
24681
|
+
return { file_error: e instanceof Error ? e.message : String(e) };
|
|
24682
|
+
}
|
|
24683
|
+
}
|
|
24684
|
+
function readOffset(v) {
|
|
24685
|
+
const n = typeof v === "string" ? Number(v) : v;
|
|
24686
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
24687
|
+
}
|
|
24688
|
+
function identityAnswer(jobId, snapshot, items, offset) {
|
|
24689
|
+
const all = identityRows(items);
|
|
24690
|
+
const rows = all.slice(offset, offset + IDENTITY_ROWS_PER_RESULT);
|
|
24691
|
+
const end = offset + rows.length;
|
|
24692
|
+
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
24693
|
+
const truncated = snapshot.items_truncated ?? false;
|
|
24694
|
+
const nextOffset = !done || truncated ? 0 : end < all.length ? end : null;
|
|
24695
|
+
return {
|
|
24696
|
+
summary: {
|
|
24697
|
+
...identityCoverage(all),
|
|
24698
|
+
stop_reason: snapshot.funnel?.stop_reason ?? null
|
|
24699
|
+
},
|
|
24700
|
+
rows,
|
|
24701
|
+
rows_total: all.length,
|
|
24702
|
+
rows_offset: offset,
|
|
24703
|
+
items_truncated: truncated,
|
|
24704
|
+
scope_notes: snapshot.explain?.scope_notes ?? [],
|
|
24705
|
+
still_running: !done,
|
|
24706
|
+
next_poll: nextOffset === null ? null : {
|
|
24707
|
+
tool: "leadbay_lead_job_status",
|
|
24708
|
+
job_id: jobId,
|
|
24709
|
+
compact: true,
|
|
24710
|
+
offset: nextOffset,
|
|
24711
|
+
suggested_wait_seconds: done ? 0 : 60
|
|
24712
|
+
}
|
|
24713
|
+
};
|
|
24714
|
+
}
|
|
24715
|
+
|
|
24515
24716
|
// ../core/dist/composite/qualify-leads.js
|
|
24516
24717
|
var DEFAULT_WAIT_SECONDS2 = 45;
|
|
24517
24718
|
function normalizeLeadRefs(refs) {
|
|
@@ -24560,7 +24761,7 @@ function rejectMalformedLeadRefs(refs) {
|
|
|
24560
24761
|
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."
|
|
24561
24762
|
};
|
|
24562
24763
|
}
|
|
24563
|
-
function
|
|
24764
|
+
function text2(value) {
|
|
24564
24765
|
if (typeof value !== "string")
|
|
24565
24766
|
return null;
|
|
24566
24767
|
const v = value.trim().toLowerCase();
|
|
@@ -24568,7 +24769,7 @@ function text(value) {
|
|
|
24568
24769
|
}
|
|
24569
24770
|
function derivedRequestId(params) {
|
|
24570
24771
|
const refs = canonicalSet((params.lead_refs ?? []).map((r) => {
|
|
24571
|
-
const website =
|
|
24772
|
+
const website = text2(r.website);
|
|
24572
24773
|
return [
|
|
24573
24774
|
// UUIDs are case-insensitive to the backend, so an uppercase id and
|
|
24574
24775
|
// its lowercase form are the same lead and must share a key.
|
|
@@ -24579,8 +24780,8 @@ function derivedRequestId(params) {
|
|
|
24579
24780
|
// AND to one key. Fall back to the trimmed/lowercased raw value when
|
|
24580
24781
|
// it is not domain-shaped, rather than dropping the field.
|
|
24581
24782
|
website ? normalizeDomain(website) ?? website : null,
|
|
24582
|
-
|
|
24583
|
-
|
|
24783
|
+
text2(r.name),
|
|
24784
|
+
text2(r.location)
|
|
24584
24785
|
];
|
|
24585
24786
|
}));
|
|
24586
24787
|
const shape = {
|
|
@@ -24726,6 +24927,7 @@ var qualifyLeads = {
|
|
|
24726
24927
|
const buysChannels = (params.channels?.length ?? 0) > 0;
|
|
24727
24928
|
const buysQualification = qualify !== false;
|
|
24728
24929
|
const isPaid = buysQualification || buysChannels;
|
|
24930
|
+
const identityOnly = !isPaid && (params.contact_titles?.length ?? 0) === 0 && params.prior_deliveries == null;
|
|
24729
24931
|
const vetoed = confirm === false;
|
|
24730
24932
|
const consented = !vetoed && confirm === true;
|
|
24731
24933
|
const requestId = presentRequestId(params.request_id) ?? (isPaid ? derivedRequestId(params) : void 0);
|
|
@@ -24774,6 +24976,25 @@ var qualifyLeads = {
|
|
|
24774
24976
|
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
24775
24977
|
const indexed = submit.duplicate ?? false ? remapInputIndexes(snapshot.items, params.lead_refs) : { items: snapshot.items, remapped: true };
|
|
24776
24978
|
const view = { ...snapshot, items: indexed.items };
|
|
24979
|
+
if (identityOnly) {
|
|
24980
|
+
const answer = identityAnswer(submit.job_id, snapshot, view.items, 0);
|
|
24981
|
+
return {
|
|
24982
|
+
job_id: submit.job_id,
|
|
24983
|
+
request_id: requestId ?? null,
|
|
24984
|
+
duplicate_submit: submit.duplicate ?? false,
|
|
24985
|
+
state: snapshot.job.state,
|
|
24986
|
+
done,
|
|
24987
|
+
...answer,
|
|
24988
|
+
summary: {
|
|
24989
|
+
refs_submitted: params.lead_refs?.length ?? 0,
|
|
24990
|
+
items_requested: submit.items_requested,
|
|
24991
|
+
...answer.summary
|
|
24992
|
+
},
|
|
24993
|
+
input_indexes_remapped: submit.duplicate ?? false ? indexed.remapped : null,
|
|
24994
|
+
...await saveIdentityFile(ctx, submit.job_id, snapshot, view.items),
|
|
24995
|
+
region: client.region
|
|
24996
|
+
};
|
|
24997
|
+
}
|
|
24777
24998
|
return {
|
|
24778
24999
|
job_id: submit.job_id,
|
|
24779
25000
|
// Echo the key actually sent, so a retry can reuse it verbatim.
|
|
@@ -24863,6 +25084,14 @@ var leadJobStatus = {
|
|
|
24863
25084
|
wait_seconds: {
|
|
24864
25085
|
type: "number",
|
|
24865
25086
|
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."
|
|
25087
|
+
},
|
|
25088
|
+
compact: {
|
|
25089
|
+
type: "boolean",
|
|
25090
|
+
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."
|
|
25091
|
+
},
|
|
25092
|
+
offset: {
|
|
25093
|
+
type: "number",
|
|
25094
|
+
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."
|
|
24866
25095
|
}
|
|
24867
25096
|
},
|
|
24868
25097
|
required: ["job_id"],
|
|
@@ -24870,8 +25099,19 @@ var leadJobStatus = {
|
|
|
24870
25099
|
},
|
|
24871
25100
|
execute: async (client, params, ctx) => {
|
|
24872
25101
|
const waitSeconds = clampWaitSeconds(params.wait_seconds, 0);
|
|
24873
|
-
const
|
|
25102
|
+
const compact = params.compact === true || params.compact === "true";
|
|
25103
|
+
const since = compact ? void 0 : params.since;
|
|
25104
|
+
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);
|
|
24874
25105
|
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
25106
|
+
if (compact) {
|
|
25107
|
+
return {
|
|
25108
|
+
job_id: params.job_id,
|
|
25109
|
+
state: snapshot.job.state,
|
|
25110
|
+
done,
|
|
25111
|
+
...identityAnswer(params.job_id, snapshot, snapshot.items, readOffset(params.offset)),
|
|
25112
|
+
region: client.region
|
|
25113
|
+
};
|
|
25114
|
+
}
|
|
24875
25115
|
const { leads, skipped } = splitItems(snapshot);
|
|
24876
25116
|
return {
|
|
24877
25117
|
job_id: params.job_id,
|
|
@@ -26251,8 +26491,8 @@ var sendFeedback = {
|
|
|
26251
26491
|
}
|
|
26252
26492
|
},
|
|
26253
26493
|
execute: async (client, params, ctx) => {
|
|
26254
|
-
const
|
|
26255
|
-
if (!
|
|
26494
|
+
const text3 = typeof params.message === "string" ? params.message.trim() : "";
|
|
26495
|
+
if (!text3) {
|
|
26256
26496
|
return {
|
|
26257
26497
|
error: true,
|
|
26258
26498
|
code: "BAD_INPUT",
|
|
@@ -26260,7 +26500,7 @@ var sendFeedback = {
|
|
|
26260
26500
|
hint: "Ask the user what they'd like to tell the Leadbay team, then call again with their words in `message`."
|
|
26261
26501
|
};
|
|
26262
26502
|
}
|
|
26263
|
-
const message =
|
|
26503
|
+
const message = text3.length > MESSAGE_MAX2 ? `${text3.slice(0, MESSAGE_MAX2 - 1)}\u2026` : text3;
|
|
26264
26504
|
if (!ctx?.sendFeedback) {
|
|
26265
26505
|
return {
|
|
26266
26506
|
sent: false,
|
|
@@ -27951,6 +28191,7 @@ ${url}
|
|
|
27951
28191
|
// RAW value: the friction redaction above is an ANALYTICS control, and
|
|
27952
28192
|
// must not change in-process tool behaviour.
|
|
27953
28193
|
triggered_by: rawTriggeredBy,
|
|
28194
|
+
saveFile: opts.saveFile,
|
|
27954
28195
|
// Route leadbay_send_feedback to Sentry's feedback inbox (same place
|
|
27955
28196
|
// the web app's form lands). NOOP_TELEMETRY returns false, so the
|
|
27956
28197
|
// tool reports honestly when telemetry is off.
|
|
@@ -28372,7 +28613,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
28372
28613
|
}
|
|
28373
28614
|
|
|
28374
28615
|
// src/http-server.ts
|
|
28375
|
-
var VERSION = true ? "0.
|
|
28616
|
+
var VERSION = true ? "0.39.0" : "0.0.0-dev";
|
|
28376
28617
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
28377
28618
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
28378
28619
|
var logger = {
|
package/dist/installer-gui.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leadbay/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.0",
|
|
4
4
|
"mcpName": "io.github.leadbay/leadbay-mcp",
|
|
5
5
|
"description": "Model Context Protocol (MCP) server for Leadbay — AI lead discovery, qualification, and enrichment for Claude Desktop, Cursor, and Claude Code.",
|
|
6
6
|
"type": "module",
|