@leadbay/mcp 0.38.2 → 0.39.1
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 +409 -124
- package/dist/http-server.js +302 -55
- 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.
|
|
@@ -10596,6 +10649,16 @@ var listLenses = {
|
|
|
10596
10649
|
}
|
|
10597
10650
|
};
|
|
10598
10651
|
|
|
10652
|
+
// ../core/dist/interactions.js
|
|
10653
|
+
function reportLeadInteractions(client, lensId, leadIds, types, logger2) {
|
|
10654
|
+
if (leadIds.length === 0 || types.length === 0)
|
|
10655
|
+
return;
|
|
10656
|
+
const events = leadIds.flatMap((leadId) => types.map((type) => ({ type, lead_id: leadId, lens_id: String(lensId) })));
|
|
10657
|
+
void client.request("POST", "/interactions", events).catch((err) => {
|
|
10658
|
+
logger2?.warn?.(`interactions: ${types.join("+")} for ${leadIds.length} lead(s) not recorded: ${err?.message ?? err?.code ?? err}`);
|
|
10659
|
+
});
|
|
10660
|
+
}
|
|
10661
|
+
|
|
10599
10662
|
// ../core/dist/tools/discover-leads.js
|
|
10600
10663
|
var discoverLeads = {
|
|
10601
10664
|
name: "leadbay_discover_leads",
|
|
@@ -10625,11 +10688,12 @@ var discoverLeads = {
|
|
|
10625
10688
|
},
|
|
10626
10689
|
additionalProperties: false
|
|
10627
10690
|
},
|
|
10628
|
-
execute: async (client, params) => {
|
|
10691
|
+
execute: async (client, params, ctx) => {
|
|
10629
10692
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
10630
10693
|
const page = params.page ?? 0;
|
|
10631
10694
|
const count = Math.min(params.count ?? 20, 50);
|
|
10632
10695
|
const res = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${count}&page=${page}&contacts=true`);
|
|
10696
|
+
reportLeadInteractions(client, lensId, res.items.map((lead) => lead.id), ["LEAD_SEEN"], ctx?.logger);
|
|
10633
10697
|
const totalPages = res.pagination?.pages ?? 0;
|
|
10634
10698
|
const currentPage = res.pagination?.page ?? page;
|
|
10635
10699
|
const hasMore = currentPage < totalPages - 1;
|
|
@@ -10718,13 +10782,9 @@ var getLeadProfile = {
|
|
|
10718
10782
|
},
|
|
10719
10783
|
required: ["lead", "contacts"]
|
|
10720
10784
|
},
|
|
10721
|
-
execute: async (client, params) => {
|
|
10785
|
+
execute: async (client, params, ctx) => {
|
|
10722
10786
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
10723
|
-
|
|
10724
|
-
{ type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
|
|
10725
|
-
{ type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
|
|
10726
|
-
]).catch(() => {
|
|
10727
|
-
});
|
|
10787
|
+
reportLeadInteractions(client, lensId, [params.leadId], ["LEAD_SEEN", "LEAD_CLICKED"], ctx?.logger);
|
|
10728
10788
|
const [leadResult, qualResult, contactsResult, paidContactsResult, webFetchResult] = await Promise.allSettled([
|
|
10729
10789
|
client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`),
|
|
10730
10790
|
client.request("GET", `/leads/${params.leadId}/ai_agent_responses`),
|
|
@@ -16818,6 +16878,9 @@ var pullLeads = {
|
|
|
16818
16878
|
return resolvedOrder.error;
|
|
16819
16879
|
const orderQs = resolvedOrder.order ? `&order=${encodeURIComponent(resolvedOrder.order)}` : "";
|
|
16820
16880
|
const res = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${count}&page=${page}&contacts=true${orderQs}`);
|
|
16881
|
+
if (params._reportSeen !== false) {
|
|
16882
|
+
reportLeadInteractions(client, lensId, res.items.map((lead) => lead.id), ["LEAD_SEEN"], ctx?.logger);
|
|
16883
|
+
}
|
|
16821
16884
|
const summaries = await Promise.all(res.items.map(async (lead) => {
|
|
16822
16885
|
try {
|
|
16823
16886
|
const r = await client.request("GET", `/leads/${lead.id}/ai_agent_responses`);
|
|
@@ -16931,12 +16994,12 @@ var CITY_ALIASES = {
|
|
|
16931
16994
|
vegas: "Las Vegas",
|
|
16932
16995
|
nola: "New Orleans"
|
|
16933
16996
|
};
|
|
16934
|
-
function expandAlias(
|
|
16935
|
-
const key =
|
|
16936
|
-
return CITY_ALIASES[key] ??
|
|
16997
|
+
function expandAlias(text3) {
|
|
16998
|
+
const key = text3.trim().toLowerCase();
|
|
16999
|
+
return CITY_ALIASES[key] ?? text3;
|
|
16937
17000
|
}
|
|
16938
|
-
function scoreMatch(
|
|
16939
|
-
const t =
|
|
17001
|
+
function scoreMatch(text3, match) {
|
|
17002
|
+
const t = text3.trim().toLowerCase();
|
|
16940
17003
|
const n = match.name.trim().toLowerCase();
|
|
16941
17004
|
if (n === t)
|
|
16942
17005
|
return 1;
|
|
@@ -16959,8 +17022,8 @@ async function resolveLocations(client, texts) {
|
|
|
16959
17022
|
const resolved = [...direct];
|
|
16960
17023
|
const ambiguities = [];
|
|
16961
17024
|
for (const originalText of free) {
|
|
16962
|
-
const
|
|
16963
|
-
const path = `/geo/search?q=${encodeURIComponent(
|
|
17025
|
+
const text3 = expandAlias(originalText);
|
|
17026
|
+
const path = `/geo/search?q=${encodeURIComponent(text3)}`;
|
|
16964
17027
|
let response;
|
|
16965
17028
|
try {
|
|
16966
17029
|
response = await client.request("GET", path);
|
|
@@ -16999,7 +17062,7 @@ async function resolveLocations(client, texts) {
|
|
|
16999
17062
|
name: r.name,
|
|
17000
17063
|
country: r.country,
|
|
17001
17064
|
level: r.level,
|
|
17002
|
-
score: scoreMatch(
|
|
17065
|
+
score: scoreMatch(text3, r)
|
|
17003
17066
|
})).sort((a, b) => {
|
|
17004
17067
|
if (b.score !== a.score)
|
|
17005
17068
|
return b.score - a.score;
|
|
@@ -17496,7 +17559,7 @@ var tourPlan = {
|
|
|
17496
17559
|
city_id: params.city_id,
|
|
17497
17560
|
count: followupsCount
|
|
17498
17561
|
}, ctx),
|
|
17499
|
-
pullLeads.execute(client, { count: DISCOVER_OVER_PULL }, ctx)
|
|
17562
|
+
pullLeads.execute(client, { count: DISCOVER_OVER_PULL, _reportSeen: false }, ctx)
|
|
17500
17563
|
]);
|
|
17501
17564
|
if (followupsResult.status === "fulfilled") {
|
|
17502
17565
|
const r = followupsResult.value;
|
|
@@ -17532,6 +17595,10 @@ var tourPlan = {
|
|
|
17532
17595
|
}
|
|
17533
17596
|
const filtered = rawDiscover.filter((l) => cityMatches(l, params.city));
|
|
17534
17597
|
const discoverLeads2 = filtered.slice(0, discoverCount);
|
|
17598
|
+
const pulledLensId = leadsResult.status === "fulfilled" ? leadsResult.value?.lens?.id : null;
|
|
17599
|
+
if (pulledLensId != null) {
|
|
17600
|
+
reportLeadInteractions(client, pulledLensId, discoverLeads2.map((l) => l.id), ["LEAD_SEEN"], ctx?.logger);
|
|
17601
|
+
}
|
|
17535
17602
|
const filterNote = params.city ? `Matched ${filtered.length}/${rawDiscover.length} Discover leads to '${params.city}'; returning top ${discoverLeads2.length}.` : `No city filter applied; returning top ${discoverLeads2.length} Discover leads.`;
|
|
17536
17603
|
return {
|
|
17537
17604
|
city: params.city ?? null,
|
|
@@ -18165,9 +18232,9 @@ ${firm.short_description}`);
|
|
|
18165
18232
|
out.push(`### ${sec.section_emoji ?? ""} ${label}`.trim());
|
|
18166
18233
|
const entries = Array.isArray(sec.entries) ? sec.entries : [];
|
|
18167
18234
|
for (const e of entries.slice(0, 5)) {
|
|
18168
|
-
const
|
|
18235
|
+
const text3 = e.text ?? e.summary ?? JSON.stringify(e).slice(0, 200);
|
|
18169
18236
|
const hot = e.hot === true ? " \u{1F525}" : "";
|
|
18170
|
-
out.push(`- ${
|
|
18237
|
+
out.push(`- ${text3}${hot}`);
|
|
18171
18238
|
}
|
|
18172
18239
|
if (entries.length > 5)
|
|
18173
18240
|
out.push(`- _${entries.length - 5} more \u2026_`);
|
|
@@ -18486,11 +18553,7 @@ var researchLeadById = {
|
|
|
18486
18553
|
}
|
|
18487
18554
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
18488
18555
|
const leadId = params.leadId;
|
|
18489
|
-
|
|
18490
|
-
{ type: "LEAD_SEEN", leadId, lensId: String(lensId) },
|
|
18491
|
-
{ type: "LEAD_CLICKED", leadId, lensId: String(lensId) }
|
|
18492
|
-
]).catch(() => {
|
|
18493
|
-
});
|
|
18556
|
+
reportLeadInteractions(client, lensId, [leadId], ["LEAD_SEEN", "LEAD_CLICKED"], _ctx?.logger);
|
|
18494
18557
|
const [profileR, qualR, contactsR, webFetchR, activitiesR, orgContactsR] = await Promise.allSettled([
|
|
18495
18558
|
client.request("GET", `/lenses/${lensId}/leads/${leadId}`),
|
|
18496
18559
|
client.request("GET", `/leads/${leadId}/ai_agent_responses`),
|
|
@@ -19532,11 +19595,7 @@ var getLeadCustomFields = {
|
|
|
19532
19595
|
},
|
|
19533
19596
|
execute: async (client, params, ctx) => {
|
|
19534
19597
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
19535
|
-
|
|
19536
|
-
{ type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
|
|
19537
|
-
{ type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
|
|
19538
|
-
]).catch(() => {
|
|
19539
|
-
});
|
|
19598
|
+
reportLeadInteractions(client, lensId, [params.leadId], ["LEAD_SEEN", "LEAD_CLICKED"], ctx?.logger);
|
|
19540
19599
|
const lead = await client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`);
|
|
19541
19600
|
const entries = lead.custom_fields ?? [];
|
|
19542
19601
|
const needsCatalog = entries.some((e) => !e?.field?.name);
|
|
@@ -22904,8 +22963,8 @@ function tokens(s) {
|
|
|
22904
22963
|
return [];
|
|
22905
22964
|
return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
22906
22965
|
}
|
|
22907
|
-
function bestMatches(
|
|
22908
|
-
const want = new Set(tokens(
|
|
22966
|
+
function bestMatches(text3, taxonomy) {
|
|
22967
|
+
const want = new Set(tokens(text3));
|
|
22909
22968
|
if (want.size === 0)
|
|
22910
22969
|
return [];
|
|
22911
22970
|
const ranked = taxonomy.map((s) => {
|
|
@@ -22934,12 +22993,12 @@ async function resolveSectors(client, texts, ctx) {
|
|
|
22934
22993
|
}
|
|
22935
22994
|
const resolved = [...direct];
|
|
22936
22995
|
const ambiguities = [];
|
|
22937
|
-
for (const
|
|
22938
|
-
const matches = bestMatches(
|
|
22996
|
+
for (const text3 of free) {
|
|
22997
|
+
const matches = bestMatches(text3, taxonomy);
|
|
22939
22998
|
if (matches.length === 1 || matches.length >= 2 && matches[0].score >= 0.66 && matches[0].score - matches[1].score >= 0.34) {
|
|
22940
22999
|
resolved.push(matches[0].id);
|
|
22941
23000
|
} else {
|
|
22942
|
-
ambiguities.push({ sector_text:
|
|
23001
|
+
ambiguities.push({ sector_text: text3, matches });
|
|
22943
23002
|
}
|
|
22944
23003
|
}
|
|
22945
23004
|
return { resolved, ambiguities };
|
|
@@ -24356,6 +24415,21 @@ var findNewLeads = {
|
|
|
24356
24415
|
"channels",
|
|
24357
24416
|
"exclude_lead_ids"
|
|
24358
24417
|
]);
|
|
24418
|
+
const homeCountry = detectCountryLocations(params.filters?.locations, "filters.locations", client.region).filter((hit) => hit.kind === "home_country");
|
|
24419
|
+
let countryNote;
|
|
24420
|
+
if (homeCountry.length > 0) {
|
|
24421
|
+
const dropped = new Set(homeCountry.map((hit) => hit.value));
|
|
24422
|
+
const raw = params.filters.locations;
|
|
24423
|
+
const rest = (Array.isArray(raw) ? raw : [raw]).filter((v) => !dropped.has(v));
|
|
24424
|
+
params = {
|
|
24425
|
+
...params,
|
|
24426
|
+
filters: { ...params.filters, locations: rest.length > 0 ? rest : void 0 }
|
|
24427
|
+
};
|
|
24428
|
+
const removed = [...dropped].map((v) => `"${v}"`).join(", ");
|
|
24429
|
+
countryNote = {
|
|
24430
|
+
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.`
|
|
24431
|
+
};
|
|
24432
|
+
}
|
|
24359
24433
|
rejectCountryLocations(params.filters?.locations, client.region);
|
|
24360
24434
|
rejectMalformedExclusions(params.exclude_lead_ids);
|
|
24361
24435
|
rejectOversizedExclusions(params.exclude_lead_ids);
|
|
@@ -24433,6 +24507,7 @@ var findNewLeads = {
|
|
|
24433
24507
|
return {
|
|
24434
24508
|
dry_run: true,
|
|
24435
24509
|
...forecast,
|
|
24510
|
+
...countryNote,
|
|
24436
24511
|
region: client.region
|
|
24437
24512
|
};
|
|
24438
24513
|
}
|
|
@@ -24453,6 +24528,7 @@ var findNewLeads = {
|
|
|
24453
24528
|
estimated_cost: forecast?.estimated_cost ?? null,
|
|
24454
24529
|
items_requested: forecast?.items_requested ?? null,
|
|
24455
24530
|
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.",
|
|
24531
|
+
...countryNote,
|
|
24456
24532
|
region: client.region
|
|
24457
24533
|
};
|
|
24458
24534
|
}
|
|
@@ -24507,11 +24583,142 @@ var findNewLeads = {
|
|
|
24507
24583
|
since: snapshot.next_since ?? null,
|
|
24508
24584
|
suggested_wait_seconds: done ? 0 : 60
|
|
24509
24585
|
},
|
|
24586
|
+
...countryNote,
|
|
24510
24587
|
region: client.region
|
|
24511
24588
|
};
|
|
24512
24589
|
}
|
|
24513
24590
|
};
|
|
24514
24591
|
|
|
24592
|
+
// ../core/dist/composite/_identity-rows.js
|
|
24593
|
+
var IDENTITY_ROWS_PER_RESULT = 100;
|
|
24594
|
+
function text(v) {
|
|
24595
|
+
return typeof v === "string" && v.trim() ? v.trim() : null;
|
|
24596
|
+
}
|
|
24597
|
+
function toRow(item) {
|
|
24598
|
+
const asked = item.ref?.requested_as ?? {};
|
|
24599
|
+
const row = {
|
|
24600
|
+
input_indexes: item.ref?.input_indexes ?? null,
|
|
24601
|
+
input: text(asked.name) ?? text(asked.website) ?? text(asked.lead_id) ?? text(asked.contact_id),
|
|
24602
|
+
status: item.status
|
|
24603
|
+
};
|
|
24604
|
+
if (item.status_reason)
|
|
24605
|
+
row.status_reason = item.status_reason;
|
|
24606
|
+
const lead = item.lead;
|
|
24607
|
+
if (!lead)
|
|
24608
|
+
return row;
|
|
24609
|
+
const company = lead.company ?? {};
|
|
24610
|
+
row.lead_id = lead.lead_id ?? item.ref?.lead_id ?? void 0;
|
|
24611
|
+
row.name = company.name;
|
|
24612
|
+
row.website = text(company.website);
|
|
24613
|
+
row.linkedin = text(company.socials?.linkedin);
|
|
24614
|
+
return row;
|
|
24615
|
+
}
|
|
24616
|
+
function identityRows(items) {
|
|
24617
|
+
const first = (i) => {
|
|
24618
|
+
const idx = i.ref?.input_indexes;
|
|
24619
|
+
return Array.isArray(idx) && idx.length > 0 ? Math.min(...idx) : Number.MAX_SAFE_INTEGER;
|
|
24620
|
+
};
|
|
24621
|
+
return [...items].sort((a, b) => first(a) - first(b) || a.seq - b.seq).map(toRow);
|
|
24622
|
+
}
|
|
24623
|
+
function identityCoverage(rows) {
|
|
24624
|
+
let resolved = 0;
|
|
24625
|
+
let ambiguous = 0;
|
|
24626
|
+
let notFound = 0;
|
|
24627
|
+
let otherSkipped = 0;
|
|
24628
|
+
let withWebsite = 0;
|
|
24629
|
+
let withLinkedin = 0;
|
|
24630
|
+
for (const r of rows) {
|
|
24631
|
+
if (r.status !== "skipped") {
|
|
24632
|
+
resolved += 1;
|
|
24633
|
+
if (r.website)
|
|
24634
|
+
withWebsite += 1;
|
|
24635
|
+
if (r.linkedin)
|
|
24636
|
+
withLinkedin += 1;
|
|
24637
|
+
} else if (r.status_reason === "low_confidence_identity")
|
|
24638
|
+
ambiguous += 1;
|
|
24639
|
+
else if (r.status_reason === "not_in_universe")
|
|
24640
|
+
notFound += 1;
|
|
24641
|
+
else
|
|
24642
|
+
otherSkipped += 1;
|
|
24643
|
+
}
|
|
24644
|
+
return {
|
|
24645
|
+
resolved,
|
|
24646
|
+
ambiguous,
|
|
24647
|
+
not_found: notFound,
|
|
24648
|
+
other_skipped: otherSkipped,
|
|
24649
|
+
with_website: withWebsite,
|
|
24650
|
+
with_linkedin: withLinkedin,
|
|
24651
|
+
without_website: resolved - withWebsite
|
|
24652
|
+
};
|
|
24653
|
+
}
|
|
24654
|
+
var CSV_HEADER = [
|
|
24655
|
+
"row",
|
|
24656
|
+
"input",
|
|
24657
|
+
"status",
|
|
24658
|
+
"reason",
|
|
24659
|
+
"company",
|
|
24660
|
+
"website",
|
|
24661
|
+
"linkedin",
|
|
24662
|
+
"lead_id"
|
|
24663
|
+
];
|
|
24664
|
+
function identityCsv(rows) {
|
|
24665
|
+
return synthesizeCsv(CSV_HEADER, rows.map((r) => ({
|
|
24666
|
+
row: (r.input_indexes ?? []).map((i) => i + 1).join(";"),
|
|
24667
|
+
input: r.input ?? "",
|
|
24668
|
+
status: r.status,
|
|
24669
|
+
reason: r.status_reason ?? "",
|
|
24670
|
+
company: r.name ?? "",
|
|
24671
|
+
website: r.website ?? "",
|
|
24672
|
+
linkedin: r.linkedin ?? "",
|
|
24673
|
+
lead_id: r.lead_id ?? ""
|
|
24674
|
+
})));
|
|
24675
|
+
}
|
|
24676
|
+
async function saveIdentityFile(ctx, jobId, snapshot, items) {
|
|
24677
|
+
if (!ctx?.saveFile)
|
|
24678
|
+
return {};
|
|
24679
|
+
if (!TERMINAL_JOB_STATES.has(snapshot.job.state))
|
|
24680
|
+
return {};
|
|
24681
|
+
if (snapshot.items_truncated || items.length === 0)
|
|
24682
|
+
return {};
|
|
24683
|
+
try {
|
|
24684
|
+
const name = `leadbay-companies-${jobId.slice(0, 8)}.csv`;
|
|
24685
|
+
return { file: await ctx.saveFile(name, identityCsv(identityRows(items))) };
|
|
24686
|
+
} catch (e) {
|
|
24687
|
+
return { file_error: e instanceof Error ? e.message : String(e) };
|
|
24688
|
+
}
|
|
24689
|
+
}
|
|
24690
|
+
function readOffset(v) {
|
|
24691
|
+
const n = typeof v === "string" ? Number(v) : v;
|
|
24692
|
+
return typeof n === "number" && Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
|
|
24693
|
+
}
|
|
24694
|
+
function identityAnswer(jobId, snapshot, items, offset) {
|
|
24695
|
+
const all = identityRows(items);
|
|
24696
|
+
const rows = all.slice(offset, offset + IDENTITY_ROWS_PER_RESULT);
|
|
24697
|
+
const end = offset + rows.length;
|
|
24698
|
+
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
24699
|
+
const truncated = snapshot.items_truncated ?? false;
|
|
24700
|
+
const nextOffset = !done || truncated ? 0 : end < all.length ? end : null;
|
|
24701
|
+
return {
|
|
24702
|
+
summary: {
|
|
24703
|
+
...identityCoverage(all),
|
|
24704
|
+
stop_reason: snapshot.funnel?.stop_reason ?? null
|
|
24705
|
+
},
|
|
24706
|
+
rows,
|
|
24707
|
+
rows_total: all.length,
|
|
24708
|
+
rows_offset: offset,
|
|
24709
|
+
items_truncated: truncated,
|
|
24710
|
+
scope_notes: snapshot.explain?.scope_notes ?? [],
|
|
24711
|
+
still_running: !done,
|
|
24712
|
+
next_poll: nextOffset === null ? null : {
|
|
24713
|
+
tool: "leadbay_lead_job_status",
|
|
24714
|
+
job_id: jobId,
|
|
24715
|
+
compact: true,
|
|
24716
|
+
offset: nextOffset,
|
|
24717
|
+
suggested_wait_seconds: done ? 0 : 60
|
|
24718
|
+
}
|
|
24719
|
+
};
|
|
24720
|
+
}
|
|
24721
|
+
|
|
24515
24722
|
// ../core/dist/composite/qualify-leads.js
|
|
24516
24723
|
var DEFAULT_WAIT_SECONDS2 = 45;
|
|
24517
24724
|
function normalizeLeadRefs(refs) {
|
|
@@ -24560,7 +24767,7 @@ function rejectMalformedLeadRefs(refs) {
|
|
|
24560
24767
|
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
24768
|
};
|
|
24562
24769
|
}
|
|
24563
|
-
function
|
|
24770
|
+
function text2(value) {
|
|
24564
24771
|
if (typeof value !== "string")
|
|
24565
24772
|
return null;
|
|
24566
24773
|
const v = value.trim().toLowerCase();
|
|
@@ -24568,7 +24775,7 @@ function text(value) {
|
|
|
24568
24775
|
}
|
|
24569
24776
|
function derivedRequestId(params) {
|
|
24570
24777
|
const refs = canonicalSet((params.lead_refs ?? []).map((r) => {
|
|
24571
|
-
const website =
|
|
24778
|
+
const website = text2(r.website);
|
|
24572
24779
|
return [
|
|
24573
24780
|
// UUIDs are case-insensitive to the backend, so an uppercase id and
|
|
24574
24781
|
// its lowercase form are the same lead and must share a key.
|
|
@@ -24579,8 +24786,8 @@ function derivedRequestId(params) {
|
|
|
24579
24786
|
// AND to one key. Fall back to the trimmed/lowercased raw value when
|
|
24580
24787
|
// it is not domain-shaped, rather than dropping the field.
|
|
24581
24788
|
website ? normalizeDomain(website) ?? website : null,
|
|
24582
|
-
|
|
24583
|
-
|
|
24789
|
+
text2(r.name),
|
|
24790
|
+
text2(r.location)
|
|
24584
24791
|
];
|
|
24585
24792
|
}));
|
|
24586
24793
|
const shape = {
|
|
@@ -24726,6 +24933,7 @@ var qualifyLeads = {
|
|
|
24726
24933
|
const buysChannels = (params.channels?.length ?? 0) > 0;
|
|
24727
24934
|
const buysQualification = qualify !== false;
|
|
24728
24935
|
const isPaid = buysQualification || buysChannels;
|
|
24936
|
+
const identityOnly = !isPaid && (params.contact_titles?.length ?? 0) === 0 && params.prior_deliveries == null;
|
|
24729
24937
|
const vetoed = confirm === false;
|
|
24730
24938
|
const consented = !vetoed && confirm === true;
|
|
24731
24939
|
const requestId = presentRequestId(params.request_id) ?? (isPaid ? derivedRequestId(params) : void 0);
|
|
@@ -24774,6 +24982,25 @@ var qualifyLeads = {
|
|
|
24774
24982
|
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
24775
24983
|
const indexed = submit.duplicate ?? false ? remapInputIndexes(snapshot.items, params.lead_refs) : { items: snapshot.items, remapped: true };
|
|
24776
24984
|
const view = { ...snapshot, items: indexed.items };
|
|
24985
|
+
if (identityOnly) {
|
|
24986
|
+
const answer = identityAnswer(submit.job_id, snapshot, view.items, 0);
|
|
24987
|
+
return {
|
|
24988
|
+
job_id: submit.job_id,
|
|
24989
|
+
request_id: requestId ?? null,
|
|
24990
|
+
duplicate_submit: submit.duplicate ?? false,
|
|
24991
|
+
state: snapshot.job.state,
|
|
24992
|
+
done,
|
|
24993
|
+
...answer,
|
|
24994
|
+
summary: {
|
|
24995
|
+
refs_submitted: params.lead_refs?.length ?? 0,
|
|
24996
|
+
items_requested: submit.items_requested,
|
|
24997
|
+
...answer.summary
|
|
24998
|
+
},
|
|
24999
|
+
input_indexes_remapped: submit.duplicate ?? false ? indexed.remapped : null,
|
|
25000
|
+
...await saveIdentityFile(ctx, submit.job_id, snapshot, view.items),
|
|
25001
|
+
region: client.region
|
|
25002
|
+
};
|
|
25003
|
+
}
|
|
24777
25004
|
return {
|
|
24778
25005
|
job_id: submit.job_id,
|
|
24779
25006
|
// Echo the key actually sent, so a retry can reuse it verbatim.
|
|
@@ -24863,6 +25090,14 @@ var leadJobStatus = {
|
|
|
24863
25090
|
wait_seconds: {
|
|
24864
25091
|
type: "number",
|
|
24865
25092
|
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."
|
|
25093
|
+
},
|
|
25094
|
+
compact: {
|
|
25095
|
+
type: "boolean",
|
|
25096
|
+
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."
|
|
25097
|
+
},
|
|
25098
|
+
offset: {
|
|
25099
|
+
type: "number",
|
|
25100
|
+
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
25101
|
}
|
|
24867
25102
|
},
|
|
24868
25103
|
required: ["job_id"],
|
|
@@ -24870,8 +25105,19 @@ var leadJobStatus = {
|
|
|
24870
25105
|
},
|
|
24871
25106
|
execute: async (client, params, ctx) => {
|
|
24872
25107
|
const waitSeconds = clampWaitSeconds(params.wait_seconds, 0);
|
|
24873
|
-
const
|
|
25108
|
+
const compact = params.compact === true || params.compact === "true";
|
|
25109
|
+
const since = compact ? void 0 : params.since;
|
|
25110
|
+
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
25111
|
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
25112
|
+
if (compact) {
|
|
25113
|
+
return {
|
|
25114
|
+
job_id: params.job_id,
|
|
25115
|
+
state: snapshot.job.state,
|
|
25116
|
+
done,
|
|
25117
|
+
...identityAnswer(params.job_id, snapshot, snapshot.items, readOffset(params.offset)),
|
|
25118
|
+
region: client.region
|
|
25119
|
+
};
|
|
25120
|
+
}
|
|
24875
25121
|
const { leads, skipped } = splitItems(snapshot);
|
|
24876
25122
|
return {
|
|
24877
25123
|
job_id: params.job_id,
|
|
@@ -26251,8 +26497,8 @@ var sendFeedback = {
|
|
|
26251
26497
|
}
|
|
26252
26498
|
},
|
|
26253
26499
|
execute: async (client, params, ctx) => {
|
|
26254
|
-
const
|
|
26255
|
-
if (!
|
|
26500
|
+
const text3 = typeof params.message === "string" ? params.message.trim() : "";
|
|
26501
|
+
if (!text3) {
|
|
26256
26502
|
return {
|
|
26257
26503
|
error: true,
|
|
26258
26504
|
code: "BAD_INPUT",
|
|
@@ -26260,7 +26506,7 @@ var sendFeedback = {
|
|
|
26260
26506
|
hint: "Ask the user what they'd like to tell the Leadbay team, then call again with their words in `message`."
|
|
26261
26507
|
};
|
|
26262
26508
|
}
|
|
26263
|
-
const message =
|
|
26509
|
+
const message = text3.length > MESSAGE_MAX2 ? `${text3.slice(0, MESSAGE_MAX2 - 1)}\u2026` : text3;
|
|
26264
26510
|
if (!ctx?.sendFeedback) {
|
|
26265
26511
|
return {
|
|
26266
26512
|
sent: false,
|
|
@@ -27951,6 +28197,7 @@ ${url}
|
|
|
27951
28197
|
// RAW value: the friction redaction above is an ANALYTICS control, and
|
|
27952
28198
|
// must not change in-process tool behaviour.
|
|
27953
28199
|
triggered_by: rawTriggeredBy,
|
|
28200
|
+
saveFile: opts.saveFile,
|
|
27954
28201
|
// Route leadbay_send_feedback to Sentry's feedback inbox (same place
|
|
27955
28202
|
// the web app's form lands). NOOP_TELEMETRY returns false, so the
|
|
27956
28203
|
// tool reports honestly when telemetry is off.
|
|
@@ -28372,7 +28619,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
28372
28619
|
}
|
|
28373
28620
|
|
|
28374
28621
|
// src/http-server.ts
|
|
28375
|
-
var VERSION = true ? "0.
|
|
28622
|
+
var VERSION = true ? "0.39.1" : "0.0.0-dev";
|
|
28376
28623
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
28377
28624
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
28378
28625
|
var logger = {
|