@leadbay/mcp 0.39.5 → 0.39.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +137 -35
- package/dist/http-server.js +136 -34
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -6169,13 +6169,11 @@ key. It survives a misspelled company name and is what turns "not in your
|
|
|
6169
6169
|
list" into an answer. With only a contact email, pass \`email\`: the company
|
|
6170
6170
|
domain is derived from it, consumer mailboxes ignored.
|
|
6171
6171
|
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
found it \u2014 \`website\` or \`registry_number\`, both params. Ask for it and call
|
|
6178
|
-
again. Do not offer an import before asking.
|
|
6172
|
+
Both \`resolution\` answers below are successes, not cards. \`"ambiguous"\`
|
|
6173
|
+
carries \`candidates[]\`: ask which one, never guess from \`score\`.
|
|
6174
|
+
\`"not_found"\` carries \`summary\` + \`next_step\`: nobody has it, so say that and
|
|
6175
|
+
do what \`next_step\` says \u2014 usually ask for the param \`would_help\` names, then
|
|
6176
|
+
call again. Do not offer an import before asking.
|
|
6179
6177
|
|
|
6180
6178
|
---
|
|
6181
6179
|
|
|
@@ -6316,7 +6314,7 @@ out?"\`
|
|
|
6316
6314
|
|
|
6317
6315
|
When \`resolution\` is \`"ambiguous"\`, render no card: use \`ask_user_input_v0\`,
|
|
6318
6316
|
ONE \`single_select\` question ("Which one?"), one short label per candidate
|
|
6319
|
-
combining \`name\` and \`location\`.
|
|
6317
|
+
combining \`name\` and \`location\`. \`"not_found"\` renders no card either.
|
|
6320
6318
|
|
|
6321
6319
|
When \`_meta.match_candidates\` is non-empty, prepend one extra NEXT STEPS row:
|
|
6322
6320
|
|
|
@@ -7088,17 +7086,18 @@ WHEN NOT TO USE: the user wants a lead list (leadbay_pull_leads / leadbay_pull_f
|
|
|
7088
7086
|
`;
|
|
7089
7087
|
leadbay_tour_plan = `## WHEN TO USE
|
|
7090
7088
|
|
|
7091
|
-
Trigger phrases: "visiting <city> in <N> days", "I'm in <city> next week / Tuesday \u2014 who's worth meeting", "I'm going to <city> \u2014 who should I see", "who's worth meeting in <city>", "field tour in <city>", "plan a tour in <city>", "who should I meet in <city>", "customers plus prospects in <city>", "tour itinerary".
|
|
7089
|
+
Trigger phrases: "visiting <city> in <N> days", "I'm in <city> next week / Tuesday \u2014 who's worth meeting", "I'm going to <city> \u2014 who should I see", "who's worth meeting in <city>", "field tour in <city>", "plan a tour in <city>", "who should I meet in <city>", "prospects within <N> km of <city>", "<city> and the surrounding area", "customers plus prospects in <city>", "tour itinerary".
|
|
7092
7090
|
|
|
7093
7091
|
Do NOT use for: "follow-ups only, no new prospects" \u2192 \`leadbay_followups_map\`; "new leads only" \u2192 \`leadbay_pull_leads\`; "research one account" \u2192 \`leadbay_research_lead_by_id\`.
|
|
7094
7092
|
|
|
7095
|
-
Prefer when:
|
|
7093
|
+
Prefer when: known accounts plus new discoveries in one itinerary; pass \`radius_km\` when they name a radius; NEVER a country name, and do NOT omit \`city\`: a city-less tour is arbitrary nationwide leads, so ask which city or region
|
|
7096
7094
|
|
|
7097
7095
|
Examples that SHOULD invoke this tool:
|
|
7098
7096
|
- "I'm flying to Limoges in 4 days \u2014 give me 3 customers, 3 qualified prospects, and 3 new high-potential."
|
|
7099
7097
|
- "I'm in San Francisco next Tuesday. Who's worth meeting?"
|
|
7100
7098
|
- "Plan my tour next Tuesday in Lyon: known accounts plus discoveries."
|
|
7101
7099
|
- "Build a mixed itinerary for Berlin \u2014 I want both follow-ups and fresh leads."
|
|
7100
|
+
- "J'ai un rdv le 19 ao\xFBt \xE0 Colmar \u2014 trouve-moi les prospects dans un rayon de 10km."
|
|
7102
7101
|
|
|
7103
7102
|
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
7104
7103
|
- "Show me my follow-ups for the SF trip."
|
|
@@ -7153,6 +7152,8 @@ same instruction in its \`hint\`.
|
|
|
7153
7152
|
|
|
7154
7153
|
**Counts**: \`followups_count\` (default 6 \u2014 generous so the agent can split into "customers + qualified" client-side) and \`discover_count\` (default 6 after client-side geo filter). The composite over-pulls Discover (30 raw) because the wishlist endpoint has no server-side geo filter \u2014 it then keeps the leads whose own \`location.city\` names the requested city, and falls back to \`location.state\` only when no city matched (which is what a regional ask like "Texas" or "\xCEle-de-France" looks like). \`location.country\` is never consulted. \`discover_filter_note\` reports the ratio and which field carried it, so the agent can be honest about coverage. **When it says no Discover lead is in the city, say that** \u2014 return the Monitor half and offer \`leadbay_find_new_leads\` for that city. Never fill the gap with leads from elsewhere.
|
|
7155
7154
|
|
|
7155
|
+
**The next town over**: a tour is a day of driving, so after the town's own leads are found the composite adds the Discover leads whose own coordinates put them within **\`radius_km\` (default 20)** of it \u2014 West Sacramento on a tour of Sacramento, Courbevoie and Ivry-sur-Seine on a tour of Paris. The town's own leads always come first. **Pass the user's own number when they give one** ("dans un rayon de 10km autour de Colmar" \u2192 \`radius_km: 10\`; "within 15 miles" \u2192 \`radius_km: 24\`), and \`radius_km: 0\` to keep the tour strictly inside the named town. The radius applies to Discover leads only; the Monitor half is scoped server-side and is untouched. When \`discover_filter_note\` splits the stops into "N in '<city>' and M in <other towns>", **repeat that split** \u2014 say which town each nearby stop is actually in rather than presenting every stop as being in the city the user named.
|
|
7156
|
+
|
|
7156
7157
|
**What \`tour_plan\` does NOT do**: it doesn't persist the tour as a campaign artifact. To do that \u2014 create a "Limoges Tour \u2013 May 24" campaign and attach the selected accounts \u2014 chain into \`leadbay_create_campaign({lead_ids: [...selected_ids], name: 'Limoges Tour \u2013 <date>'})\` after the user picks. See the \`leadbay_plan_tour_in_city\` prompt for the full end-to-end orchestrator.
|
|
7157
7158
|
|
|
7158
7159
|
---
|
|
@@ -15370,23 +15371,75 @@ function stateFieldOf(lead) {
|
|
|
15370
15371
|
const state = lead?.location?.state;
|
|
15371
15372
|
return typeof state === "string" ? normalizeGeo(state) : "";
|
|
15372
15373
|
}
|
|
15373
|
-
function
|
|
15374
|
+
function posOf(lead) {
|
|
15375
|
+
const pos = lead?.location?.pos;
|
|
15376
|
+
const valid = Array.isArray(pos) && pos.length === 2 && pos.every((n) => typeof n === "number" && Number.isFinite(n));
|
|
15377
|
+
return valid ? [pos[0], pos[1]] : null;
|
|
15378
|
+
}
|
|
15379
|
+
function distanceKm(a, b) {
|
|
15380
|
+
const toRad = (d) => d * Math.PI / 180;
|
|
15381
|
+
const dLat = toRad(b[0] - a[0]);
|
|
15382
|
+
const dLng = toRad(b[1] - a[1]);
|
|
15383
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(a[0])) * Math.cos(toRad(b[0])) * Math.sin(dLng / 2) ** 2;
|
|
15384
|
+
return 2 * 6371 * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
15385
|
+
}
|
|
15386
|
+
function withinRadius(leads, anchors, radiusKm) {
|
|
15387
|
+
if (anchors.length === 0 || !(radiusKm > 0))
|
|
15388
|
+
return [];
|
|
15389
|
+
return leads.filter((l) => {
|
|
15390
|
+
const p = posOf(l);
|
|
15391
|
+
return p !== null && anchors.some((a) => distanceKm(a, p) <= radiusKm);
|
|
15392
|
+
});
|
|
15393
|
+
}
|
|
15394
|
+
function filterDiscoverByCity(leads, cityHint, monitorLeads, radiusKm) {
|
|
15374
15395
|
const hint = cityHint ? cityHintCore(cityHint) : { name: "", isCity: false };
|
|
15375
15396
|
if (!hint.name)
|
|
15376
|
-
return { leads, matchedOn: null };
|
|
15397
|
+
return { leads, matchedOn: null, nearby: [], anchored: false };
|
|
15377
15398
|
const cityOf = (l) => typeof l?.location?.city === "string" ? townName(l.location.city) : "";
|
|
15399
|
+
const positions = (ls) => ls.map(posOf).filter((p) => p !== null);
|
|
15378
15400
|
const byCity = leads.filter((l) => cityOf(l) === hint.name);
|
|
15379
|
-
|
|
15380
|
-
|
|
15381
|
-
|
|
15382
|
-
|
|
15383
|
-
|
|
15384
|
-
|
|
15401
|
+
const anchors = positions([
|
|
15402
|
+
...byCity,
|
|
15403
|
+
...monitorLeads.filter((l) => cityOf(l) === hint.name)
|
|
15404
|
+
]);
|
|
15405
|
+
if (byCity.length > 0) {
|
|
15406
|
+
const inTown = new Set(byCity);
|
|
15407
|
+
const nearby2 = withinRadius(leads.filter((l) => !inTown.has(l)), anchors, radiusKm);
|
|
15408
|
+
return { leads: [...byCity, ...nearby2], matchedOn: "city", nearby: nearby2, anchored: true };
|
|
15409
|
+
}
|
|
15410
|
+
const anchored = anchors.length > 0;
|
|
15411
|
+
if (!hint.isCity) {
|
|
15412
|
+
const byState = leads.filter((l) => stateFieldOf(l) === hint.name);
|
|
15413
|
+
if (byState.length > 0)
|
|
15414
|
+
return { leads: byState, matchedOn: "state", nearby: [], anchored };
|
|
15415
|
+
}
|
|
15416
|
+
const nearby = withinRadius(leads, anchors, radiusKm);
|
|
15417
|
+
if (nearby.length > 0)
|
|
15418
|
+
return { leads: nearby, matchedOn: "city", nearby, anchored };
|
|
15419
|
+
return {
|
|
15420
|
+
leads: [],
|
|
15421
|
+
matchedOn: hint.isCity ? "city" : "state",
|
|
15422
|
+
nearby: [],
|
|
15423
|
+
anchored
|
|
15424
|
+
};
|
|
15425
|
+
}
|
|
15426
|
+
function townsOf(leads) {
|
|
15427
|
+
const seen = [];
|
|
15428
|
+
let unnamed = 0;
|
|
15429
|
+
for (const l of leads) {
|
|
15430
|
+
const city = typeof l?.location?.city === "string" ? l.location.city.trim() : "";
|
|
15431
|
+
const name = city.replace(/^(?:City|Town|Village|Borough|Township|Municipality) of /i, "");
|
|
15432
|
+
if (!name)
|
|
15433
|
+
unnamed += 1;
|
|
15434
|
+
else if (!seen.includes(name))
|
|
15435
|
+
seen.push(name);
|
|
15436
|
+
}
|
|
15437
|
+
const rest = unnamed > 0 ? `${unnamed} whose record names no town` : "";
|
|
15438
|
+
return [seen.join(", "), rest].filter(Boolean).join(", plus ");
|
|
15385
15439
|
}
|
|
15386
15440
|
function toMapLocation(lead, mode) {
|
|
15387
|
-
const pos = lead
|
|
15388
|
-
|
|
15389
|
-
if (!valid)
|
|
15441
|
+
const pos = posOf(lead);
|
|
15442
|
+
if (pos === null)
|
|
15390
15443
|
return null;
|
|
15391
15444
|
const loc = lead.location;
|
|
15392
15445
|
const c = lead.recommended_contact;
|
|
@@ -15431,7 +15484,7 @@ function buildMap(monitorLeads, discoverLeads2) {
|
|
|
15431
15484
|
}
|
|
15432
15485
|
};
|
|
15433
15486
|
}
|
|
15434
|
-
var DEFAULT_FOLLOWUPS_COUNT, DEFAULT_DISCOVER_COUNT, DISCOVER_OVER_PULL, ADMIN_PREFIX, tourPlan;
|
|
15487
|
+
var DEFAULT_FOLLOWUPS_COUNT, DEFAULT_DISCOVER_COUNT, DISCOVER_OVER_PULL, DEFAULT_RADIUS_KM, ADMIN_PREFIX, tourPlan;
|
|
15435
15488
|
var init_tour_plan = __esm({
|
|
15436
15489
|
"../core/dist/composite/tour-plan.js"() {
|
|
15437
15490
|
"use strict";
|
|
@@ -15444,6 +15497,7 @@ var init_tour_plan = __esm({
|
|
|
15444
15497
|
DEFAULT_FOLLOWUPS_COUNT = 6;
|
|
15445
15498
|
DEFAULT_DISCOVER_COUNT = 6;
|
|
15446
15499
|
DISCOVER_OVER_PULL = 30;
|
|
15500
|
+
DEFAULT_RADIUS_KM = 20;
|
|
15447
15501
|
ADMIN_PREFIX = /^(?:city|town|village|borough|township|municipality) of /;
|
|
15448
15502
|
tourPlan = {
|
|
15449
15503
|
name: "leadbay_tour_plan",
|
|
@@ -15473,6 +15527,10 @@ var init_tour_plan = __esm({
|
|
|
15473
15527
|
discover_count: {
|
|
15474
15528
|
type: "number",
|
|
15475
15529
|
description: `Top-N Discover leads (active lens wishlist) to return after client-side city filter. Default ${DEFAULT_DISCOVER_COUNT}.`
|
|
15530
|
+
},
|
|
15531
|
+
radius_km: {
|
|
15532
|
+
type: "number",
|
|
15533
|
+
description: `How far outside the named town a Discover stop may be, in km. Default ${DEFAULT_RADIUS_KM}, which is roughly a metro area \u2014 it is what puts West Sacramento on a tour of Sacramento and Courbevoie on a tour of Paris. Pass the user's own number when they name one ("dans un rayon de 10km", "zone 20km", "within 15 miles" \u2192 24). Pass 0 to keep the tour inside the town's own name. Leads in the town itself always come first; the radius only adds to them. It applies to Discover leads only \u2014 the Monitor half is scoped server-side and is untouched.`
|
|
15476
15534
|
}
|
|
15477
15535
|
},
|
|
15478
15536
|
additionalProperties: false
|
|
@@ -15489,12 +15547,12 @@ var init_tour_plan = __esm({
|
|
|
15489
15547
|
},
|
|
15490
15548
|
discover_leads: {
|
|
15491
15549
|
type: "array",
|
|
15492
|
-
description: "Fresh Discover leads from the active lens, filtered client-side to match the city. Pulls a larger candidate set internally to compensate for the missing server-side geo filter.",
|
|
15550
|
+
description: "Fresh Discover leads from the active lens, filtered client-side to match the city, then extended with the ones whose own coordinates put them within `radius_km` of it. The town's own leads come first. Pulls a larger candidate set internally to compensate for the missing server-side geo filter.",
|
|
15493
15551
|
items: { type: "object" }
|
|
15494
15552
|
},
|
|
15495
15553
|
discover_filter_note: {
|
|
15496
15554
|
type: "string",
|
|
15497
|
-
description: "Human-readable summary of the client-side geo filter applied to Discover leads
|
|
15555
|
+
description: "Human-readable summary of the client-side geo filter applied to Discover leads. Says how many stops are in the named town, how many are within `radius_km` of it, and which towns those are in \u2014 repeat that split to the user rather than presenting every stop as being in the city they named."
|
|
15498
15556
|
},
|
|
15499
15557
|
map_locations: {
|
|
15500
15558
|
type: "array",
|
|
@@ -15618,12 +15676,21 @@ var init_tour_plan = __esm({
|
|
|
15618
15676
|
const cityName = params.city && !/^\d+$/.test(params.city.trim()) ? params.city : void 0;
|
|
15619
15677
|
const knownId = params.city_id ?? (cityName ? void 0 : params.city);
|
|
15620
15678
|
const idOnly = Boolean(knownId) && !cityName;
|
|
15621
|
-
const
|
|
15679
|
+
const radiusKm = params.radius_km ?? DEFAULT_RADIUS_KM;
|
|
15680
|
+
const { leads: filtered, matchedOn, nearby, anchored } = idOnly ? {
|
|
15681
|
+
leads: [],
|
|
15682
|
+
matchedOn: null,
|
|
15683
|
+
nearby: [],
|
|
15684
|
+
anchored: false
|
|
15685
|
+
} : filterDiscoverByCity(rawDiscover, cityName, monitorLeads, radiusKm);
|
|
15622
15686
|
const discoverLeads2 = filtered.slice(0, discoverCount);
|
|
15623
15687
|
const pulledLensId = leadsResult.status === "fulfilled" ? leadsResult.value?.lens?.id : null;
|
|
15624
15688
|
if (pulledLensId != null) {
|
|
15625
15689
|
reportLeadInteractions(client, pulledLensId, discoverLeads2.map((l) => l.id), ["LEAD_SEEN"], ctx?.logger);
|
|
15626
15690
|
}
|
|
15691
|
+
const nearbySet = new Set(nearby);
|
|
15692
|
+
const nearShown = discoverLeads2.filter((l) => nearbySet.has(l));
|
|
15693
|
+
const inTownShown = discoverLeads2.length - nearShown.length;
|
|
15627
15694
|
let filterNote;
|
|
15628
15695
|
if (idOnly) {
|
|
15629
15696
|
filterNote = `Discover leads need the NAME of the place, and this call passed an area id (${knownId}) and no name. Re-call with \`city\` set to the name of that area, keeping \`city_id\` so the Monitor half stays on the area you picked. The follow-ups below are already scoped to it.`;
|
|
@@ -15631,10 +15698,16 @@ var init_tour_plan = __esm({
|
|
|
15631
15698
|
filterNote = `No city filter applied; returning top ${discoverLeads2.length} Discover leads.`;
|
|
15632
15699
|
} else if (matchedOn === null) {
|
|
15633
15700
|
filterNote = `No usable city filter in '${params.city}'; returning top ${discoverLeads2.length} Discover leads.`;
|
|
15701
|
+
} else if (filtered.length === 0 && anchored && radiusKm > 0) {
|
|
15702
|
+
filterNote = `No Discover lead in the active lens names '${params.city}' as its town, and none is within ${radiusKm} km of the leads that do (checked ${rawDiscover.length} candidates). Say so; do NOT present leads from elsewhere as if they were in '${params.city}'.`;
|
|
15634
15703
|
} else if (filtered.length === 0) {
|
|
15635
15704
|
filterNote = `No Discover lead in the active lens is in '${params.city}' (checked ${rawDiscover.length} candidates by city, then by state/region). Say so; do NOT present leads from elsewhere as if they were in '${params.city}'.`;
|
|
15705
|
+
} else if (nearShown.length > 0 && inTownShown === 0) {
|
|
15706
|
+
filterNote = `No Discover lead in the active lens names '${params.city}' as its town; ${nearby.length}/${rawDiscover.length} are within ${radiusKm} km of it. Returning top ${discoverLeads2.length}, in ${townsOf(nearShown)}. Name the town each stop is actually in rather than calling them all '${params.city}'.`;
|
|
15707
|
+
} else if (nearShown.length > 0) {
|
|
15708
|
+
filterNote = `Matched ${filtered.length - nearby.length}/${rawDiscover.length} Discover leads to '${params.city}' by city, plus ${nearby.length} within ${radiusKm} km of it. Returning top ${discoverLeads2.length}: ${inTownShown} in '${params.city}' and ${nearShown.length} in ${townsOf(nearShown)}. Name the town each nearby stop is in.`;
|
|
15636
15709
|
} else {
|
|
15637
|
-
filterNote = `Matched ${filtered.length}/${rawDiscover.length} Discover leads to '${params.city}' by ${matchedOn === "city" ? "city" : "state/region"}; returning top ${discoverLeads2.length}.`;
|
|
15710
|
+
filterNote = `Matched ${filtered.length - nearby.length}/${rawDiscover.length} Discover leads to '${params.city}' by ${matchedOn === "city" ? "city" : "state/region"}; returning top ${discoverLeads2.length}.`;
|
|
15638
15711
|
}
|
|
15639
15712
|
return {
|
|
15640
15713
|
city: params.city ?? null,
|
|
@@ -17122,7 +17195,7 @@ var init_research_lead_by_name_fuzzy = __esm({
|
|
|
17122
17195
|
// of a research card.
|
|
17123
17196
|
outputSchema: {
|
|
17124
17197
|
type: "object",
|
|
17125
|
-
description: "Same shape as leadbay_research_lead_by_id, with _meta.resolved_from='companyName'|'resolver', _meta.resolved_query='<needle>', _meta.resolved_matched_on=[...], and _meta.match_candidates=[{leadId,name,score}] populated. When the registry resolver cannot pick one company, returns {resolution:'ambiguous', query, candidates:[{leadId,name,website,location,registry_ids,score,matched_on}]} instead \u2014 ask the user which one, then call leadbay_research_lead_by_id.",
|
|
17198
|
+
description: "Same shape as leadbay_research_lead_by_id, with _meta.resolved_from='companyName'|'resolver', _meta.resolved_query='<needle>', _meta.resolved_matched_on=[...], and _meta.match_candidates=[{leadId,name,score}] populated. When the registry resolver cannot pick one company, returns {resolution:'ambiguous', query, candidates:[{leadId,name,website,location,registry_ids,score,matched_on}]} instead \u2014 ask the user which one, then call leadbay_research_lead_by_id. When neither the user's leads nor the registry hold the company, returns {resolution:'not_found', query, summary, would_help, next_step} \u2014 a successful answer, not an error: do what next_step says, which is normally to ask the user for the field would_help names ('website' or 'registry_number') and call again. would_help is empty when the search was scoped to one lens, where the remedy is to drop the scope, not to ask the user for a field.",
|
|
17126
17199
|
additionalProperties: true
|
|
17127
17200
|
},
|
|
17128
17201
|
execute: async (client, params, ctx) => {
|
|
@@ -17136,7 +17209,18 @@ var init_research_lead_by_name_fuzzy = __esm({
|
|
|
17136
17209
|
if (scoped.length > 0) {
|
|
17137
17210
|
return await delegate(scoped, params.lensId);
|
|
17138
17211
|
}
|
|
17139
|
-
|
|
17212
|
+
return {
|
|
17213
|
+
resolution: "not_found",
|
|
17214
|
+
query,
|
|
17215
|
+
summary: `No lead matching "${query}" in lens ${params.lensId}`,
|
|
17216
|
+
would_help: [],
|
|
17217
|
+
next_step: "This lookup was intentionally restricted to the supplied lens. Omit lensId to search your visible leads across Discover, Monitor, and Activate and then the Leadbay company registry.",
|
|
17218
|
+
_meta: {
|
|
17219
|
+
region: client.region,
|
|
17220
|
+
lens_id: params.lensId,
|
|
17221
|
+
resolved_query: query
|
|
17222
|
+
}
|
|
17223
|
+
};
|
|
17140
17224
|
}
|
|
17141
17225
|
async function delegate(matches, fallbackLens) {
|
|
17142
17226
|
const [primary, ...rest] = matches;
|
|
@@ -17231,7 +17315,23 @@ var init_research_lead_by_name_fuzzy = __esm({
|
|
|
17231
17315
|
const wanted = resolved.type === "none" && resolved.would_help.length > 0 ? resolved.would_help : ["website", "registry_number"];
|
|
17232
17316
|
const asks = wanted.map((f) => f === "registry_number" ? "a registry number (SIREN/SIRET) for `registry_number`" : f === "website" ? "the company website for `website`" : `\`${f}\``).join(" or ");
|
|
17233
17317
|
const hint = resolved.type === "unidentifiable" ? `The registry could not identify a company from this input (${resolved.reason}). Ask the user for ${asks}, then call this tool again with it.` : `The registry found no company for what was supplied. It would match on ${asks}. Ask the user for that \u2014 "what's their website?" usually settles it \u2014 then call this tool again. Do not offer an import before asking.`;
|
|
17234
|
-
|
|
17318
|
+
if (!corpusSearched) {
|
|
17319
|
+
throw client.makeError("LEAD_NOT_FOUND", `No company matching "${query}" ${searched}`, `${hint} But this lookup did not complete \u2014 the lead search route was unreachable, so their own leads were never checked. Retry once before telling the user the company is absent.`, "POST /leads/resolve");
|
|
17320
|
+
}
|
|
17321
|
+
return {
|
|
17322
|
+
resolution: "not_found",
|
|
17323
|
+
query,
|
|
17324
|
+
resolver_payload: payload,
|
|
17325
|
+
summary: `No company matching "${query}" ${searched}`,
|
|
17326
|
+
would_help: wanted,
|
|
17327
|
+
next_step: hint,
|
|
17328
|
+
_meta: {
|
|
17329
|
+
region: client.region,
|
|
17330
|
+
resolved_from: "resolver",
|
|
17331
|
+
resolved_query: query,
|
|
17332
|
+
endpoint: "POST /leads/resolve"
|
|
17333
|
+
}
|
|
17334
|
+
};
|
|
17235
17335
|
}
|
|
17236
17336
|
};
|
|
17237
17337
|
}
|
|
@@ -27260,7 +27360,7 @@ Split the returned \`monitor_leads\` into two buckets client-side using their en
|
|
|
27260
27360
|
|
|
27261
27361
|
\`discover_leads\` are the **New** bucket.
|
|
27262
27362
|
|
|
27263
|
-
Aim for a 3+3+3 split if possible. If the customers bucket has fewer than 3, fill from qualified. If discover_filter_note indicates a low match ratio for the city, mention it: "Only N/30 fresh leads matched your city" \u2014 better honest than padded.
|
|
27363
|
+
Aim for a 3+3+3 split if possible. If the customers bucket has fewer than 3, fill from qualified. If discover_filter_note indicates a low match ratio for the city, mention it: "Only N/30 fresh leads matched your city" \u2014 better honest than padded. When it splits the stops into the ones in the city and the ones within \`radius_km\` of it, repeat that split and name the town each nearby stop is in \u2014 a company in Courbevoie is a stop on a Paris day, but it is not in Paris. If I named a radius ("within 10 km", "rayon de 20km"), pass it as \`radius_km\`.
|
|
27264
27364
|
|
|
27265
27365
|
# PHASE 3 \u2014 PRESENT THE ITINERARY + OFFER THE MAP
|
|
27266
27366
|
|
|
@@ -27839,10 +27939,12 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
27839
27939
|
|
|
27840
27940
|
|
|
27841
27941
|
# PHASE 2 \u2014 NOT FOUND
|
|
27842
|
-
If the resolver returns \`
|
|
27843
|
-
|
|
27844
|
-
|
|
27845
|
-
|
|
27942
|
+
If the resolver returns \`resolution: "not_found"\`, that call SUCCEEDED \u2014 it is
|
|
27943
|
+
the answer, not a failure. Do what its \`next_step\` says. Usually that is to ask
|
|
27944
|
+
for the field \`would_help\` names (normally \`website\`): **ask the user for it
|
|
27945
|
+
first** \u2014 "what's their website?" \u2014 and call the tool again with it. When
|
|
27946
|
+
\`would_help\` is empty the search was scoped to one lens, and \`next_step\` says
|
|
27947
|
+
to drop the scope rather than to ask the user for anything. Only when they cannot supply it should you say both their leads and
|
|
27846
27948
|
the Leadbay registry were searched.
|
|
27847
27949
|
**Do NOT call \`leadbay_import_and_qualify\` automatically.**
|
|
27848
27950
|
Offer to import and qualify as a separate, explicit next step; only call it
|
|
@@ -32035,7 +32137,7 @@ var OAUTH_BASE_URLS = {
|
|
|
32035
32137
|
fr: "https://staging.api.leadbay.app"
|
|
32036
32138
|
}
|
|
32037
32139
|
};
|
|
32038
|
-
var VERSION = "0.39.
|
|
32140
|
+
var VERSION = "0.39.7";
|
|
32039
32141
|
var HELP = `
|
|
32040
32142
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
32041
32143
|
|
package/dist/http-server.js
CHANGED
|
@@ -1531,7 +1531,7 @@ Split the returned \`monitor_leads\` into two buckets client-side using their en
|
|
|
1531
1531
|
|
|
1532
1532
|
\`discover_leads\` are the **New** bucket.
|
|
1533
1533
|
|
|
1534
|
-
Aim for a 3+3+3 split if possible. If the customers bucket has fewer than 3, fill from qualified. If discover_filter_note indicates a low match ratio for the city, mention it: "Only N/30 fresh leads matched your city" \u2014 better honest than padded.
|
|
1534
|
+
Aim for a 3+3+3 split if possible. If the customers bucket has fewer than 3, fill from qualified. If discover_filter_note indicates a low match ratio for the city, mention it: "Only N/30 fresh leads matched your city" \u2014 better honest than padded. When it splits the stops into the ones in the city and the ones within \`radius_km\` of it, repeat that split and name the town each nearby stop is in \u2014 a company in Courbevoie is a stop on a Paris day, but it is not in Paris. If I named a radius ("within 10 km", "rayon de 20km"), pass it as \`radius_km\`.
|
|
1535
1535
|
|
|
1536
1536
|
# PHASE 3 \u2014 PRESENT THE ITINERARY + OFFER THE MAP
|
|
1537
1537
|
|
|
@@ -2110,10 +2110,12 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
2110
2110
|
|
|
2111
2111
|
|
|
2112
2112
|
# PHASE 2 \u2014 NOT FOUND
|
|
2113
|
-
If the resolver returns \`
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2113
|
+
If the resolver returns \`resolution: "not_found"\`, that call SUCCEEDED \u2014 it is
|
|
2114
|
+
the answer, not a failure. Do what its \`next_step\` says. Usually that is to ask
|
|
2115
|
+
for the field \`would_help\` names (normally \`website\`): **ask the user for it
|
|
2116
|
+
first** \u2014 "what's their website?" \u2014 and call the tool again with it. When
|
|
2117
|
+
\`would_help\` is empty the search was scoped to one lens, and \`next_step\` says
|
|
2118
|
+
to drop the scope rather than to ask the user for anything. Only when they cannot supply it should you say both their leads and
|
|
2117
2119
|
the Leadbay registry were searched.
|
|
2118
2120
|
**Do NOT call \`leadbay_import_and_qualify\` automatically.**
|
|
2119
2121
|
Offer to import and qualify as a separate, explicit next step; only call it
|
|
@@ -9117,13 +9119,11 @@ key. It survives a misspelled company name and is what turns "not in your
|
|
|
9117
9119
|
list" into an answer. With only a contact email, pass \`email\`: the company
|
|
9118
9120
|
domain is derived from it, consumer mailboxes ignored.
|
|
9119
9121
|
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
found it \u2014 \`website\` or \`registry_number\`, both params. Ask for it and call
|
|
9126
|
-
again. Do not offer an import before asking.
|
|
9122
|
+
Both \`resolution\` answers below are successes, not cards. \`"ambiguous"\`
|
|
9123
|
+
carries \`candidates[]\`: ask which one, never guess from \`score\`.
|
|
9124
|
+
\`"not_found"\` carries \`summary\` + \`next_step\`: nobody has it, so say that and
|
|
9125
|
+
do what \`next_step\` says \u2014 usually ask for the param \`would_help\` names, then
|
|
9126
|
+
call again. Do not offer an import before asking.
|
|
9127
9127
|
|
|
9128
9128
|
---
|
|
9129
9129
|
|
|
@@ -9264,7 +9264,7 @@ out?"\`
|
|
|
9264
9264
|
|
|
9265
9265
|
When \`resolution\` is \`"ambiguous"\`, render no card: use \`ask_user_input_v0\`,
|
|
9266
9266
|
ONE \`single_select\` question ("Which one?"), one short label per candidate
|
|
9267
|
-
combining \`name\` and \`location\`.
|
|
9267
|
+
combining \`name\` and \`location\`. \`"not_found"\` renders no card either.
|
|
9268
9268
|
|
|
9269
9269
|
When \`_meta.match_candidates\` is non-empty, prepend one extra NEXT STEPS row:
|
|
9270
9270
|
|
|
@@ -10036,17 +10036,18 @@ WHEN NOT TO USE: the user wants a lead list (leadbay_pull_leads / leadbay_pull_f
|
|
|
10036
10036
|
`;
|
|
10037
10037
|
var leadbay_tour_plan = `## WHEN TO USE
|
|
10038
10038
|
|
|
10039
|
-
Trigger phrases: "visiting <city> in <N> days", "I'm in <city> next week / Tuesday \u2014 who's worth meeting", "I'm going to <city> \u2014 who should I see", "who's worth meeting in <city>", "field tour in <city>", "plan a tour in <city>", "who should I meet in <city>", "customers plus prospects in <city>", "tour itinerary".
|
|
10039
|
+
Trigger phrases: "visiting <city> in <N> days", "I'm in <city> next week / Tuesday \u2014 who's worth meeting", "I'm going to <city> \u2014 who should I see", "who's worth meeting in <city>", "field tour in <city>", "plan a tour in <city>", "who should I meet in <city>", "prospects within <N> km of <city>", "<city> and the surrounding area", "customers plus prospects in <city>", "tour itinerary".
|
|
10040
10040
|
|
|
10041
10041
|
Do NOT use for: "follow-ups only, no new prospects" \u2192 \`leadbay_followups_map\`; "new leads only" \u2192 \`leadbay_pull_leads\`; "research one account" \u2192 \`leadbay_research_lead_by_id\`.
|
|
10042
10042
|
|
|
10043
|
-
Prefer when:
|
|
10043
|
+
Prefer when: known accounts plus new discoveries in one itinerary; pass \`radius_km\` when they name a radius; NEVER a country name, and do NOT omit \`city\`: a city-less tour is arbitrary nationwide leads, so ask which city or region
|
|
10044
10044
|
|
|
10045
10045
|
Examples that SHOULD invoke this tool:
|
|
10046
10046
|
- "I'm flying to Limoges in 4 days \u2014 give me 3 customers, 3 qualified prospects, and 3 new high-potential."
|
|
10047
10047
|
- "I'm in San Francisco next Tuesday. Who's worth meeting?"
|
|
10048
10048
|
- "Plan my tour next Tuesday in Lyon: known accounts plus discoveries."
|
|
10049
10049
|
- "Build a mixed itinerary for Berlin \u2014 I want both follow-ups and fresh leads."
|
|
10050
|
+
- "J'ai un rdv le 19 ao\xFBt \xE0 Colmar \u2014 trouve-moi les prospects dans un rayon de 10km."
|
|
10050
10051
|
|
|
10051
10052
|
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
10052
10053
|
- "Show me my follow-ups for the SF trip."
|
|
@@ -10101,6 +10102,8 @@ same instruction in its \`hint\`.
|
|
|
10101
10102
|
|
|
10102
10103
|
**Counts**: \`followups_count\` (default 6 \u2014 generous so the agent can split into "customers + qualified" client-side) and \`discover_count\` (default 6 after client-side geo filter). The composite over-pulls Discover (30 raw) because the wishlist endpoint has no server-side geo filter \u2014 it then keeps the leads whose own \`location.city\` names the requested city, and falls back to \`location.state\` only when no city matched (which is what a regional ask like "Texas" or "\xCEle-de-France" looks like). \`location.country\` is never consulted. \`discover_filter_note\` reports the ratio and which field carried it, so the agent can be honest about coverage. **When it says no Discover lead is in the city, say that** \u2014 return the Monitor half and offer \`leadbay_find_new_leads\` for that city. Never fill the gap with leads from elsewhere.
|
|
10103
10104
|
|
|
10105
|
+
**The next town over**: a tour is a day of driving, so after the town's own leads are found the composite adds the Discover leads whose own coordinates put them within **\`radius_km\` (default 20)** of it \u2014 West Sacramento on a tour of Sacramento, Courbevoie and Ivry-sur-Seine on a tour of Paris. The town's own leads always come first. **Pass the user's own number when they give one** ("dans un rayon de 10km autour de Colmar" \u2192 \`radius_km: 10\`; "within 15 miles" \u2192 \`radius_km: 24\`), and \`radius_km: 0\` to keep the tour strictly inside the named town. The radius applies to Discover leads only; the Monitor half is scoped server-side and is untouched. When \`discover_filter_note\` splits the stops into "N in '<city>' and M in <other towns>", **repeat that split** \u2014 say which town each nearby stop is actually in rather than presenting every stop as being in the city the user named.
|
|
10106
|
+
|
|
10104
10107
|
**What \`tour_plan\` does NOT do**: it doesn't persist the tour as a campaign artifact. To do that \u2014 create a "Limoges Tour \u2013 May 24" campaign and attach the selected accounts \u2014 chain into \`leadbay_create_campaign({lead_ids: [...selected_ids], name: 'Limoges Tour \u2013 <date>'})\` after the user picks. See the \`leadbay_plan_tour_in_city\` prompt for the full end-to-end orchestrator.
|
|
10105
10108
|
|
|
10106
10109
|
---
|
|
@@ -17758,6 +17761,7 @@ var followupsMap = {
|
|
|
17758
17761
|
var DEFAULT_FOLLOWUPS_COUNT = 6;
|
|
17759
17762
|
var DEFAULT_DISCOVER_COUNT = 6;
|
|
17760
17763
|
var DISCOVER_OVER_PULL = 30;
|
|
17764
|
+
var DEFAULT_RADIUS_KM = 20;
|
|
17761
17765
|
function normalizeGeo(value) {
|
|
17762
17766
|
return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
17763
17767
|
}
|
|
@@ -17782,23 +17786,75 @@ function stateFieldOf(lead) {
|
|
|
17782
17786
|
const state = lead?.location?.state;
|
|
17783
17787
|
return typeof state === "string" ? normalizeGeo(state) : "";
|
|
17784
17788
|
}
|
|
17785
|
-
function
|
|
17789
|
+
function posOf(lead) {
|
|
17790
|
+
const pos = lead?.location?.pos;
|
|
17791
|
+
const valid = Array.isArray(pos) && pos.length === 2 && pos.every((n) => typeof n === "number" && Number.isFinite(n));
|
|
17792
|
+
return valid ? [pos[0], pos[1]] : null;
|
|
17793
|
+
}
|
|
17794
|
+
function distanceKm(a, b) {
|
|
17795
|
+
const toRad = (d) => d * Math.PI / 180;
|
|
17796
|
+
const dLat = toRad(b[0] - a[0]);
|
|
17797
|
+
const dLng = toRad(b[1] - a[1]);
|
|
17798
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(a[0])) * Math.cos(toRad(b[0])) * Math.sin(dLng / 2) ** 2;
|
|
17799
|
+
return 2 * 6371 * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
17800
|
+
}
|
|
17801
|
+
function withinRadius(leads, anchors, radiusKm) {
|
|
17802
|
+
if (anchors.length === 0 || !(radiusKm > 0))
|
|
17803
|
+
return [];
|
|
17804
|
+
return leads.filter((l) => {
|
|
17805
|
+
const p = posOf(l);
|
|
17806
|
+
return p !== null && anchors.some((a) => distanceKm(a, p) <= radiusKm);
|
|
17807
|
+
});
|
|
17808
|
+
}
|
|
17809
|
+
function filterDiscoverByCity(leads, cityHint, monitorLeads, radiusKm) {
|
|
17786
17810
|
const hint = cityHint ? cityHintCore(cityHint) : { name: "", isCity: false };
|
|
17787
17811
|
if (!hint.name)
|
|
17788
|
-
return { leads, matchedOn: null };
|
|
17812
|
+
return { leads, matchedOn: null, nearby: [], anchored: false };
|
|
17789
17813
|
const cityOf = (l) => typeof l?.location?.city === "string" ? townName(l.location.city) : "";
|
|
17814
|
+
const positions = (ls) => ls.map(posOf).filter((p) => p !== null);
|
|
17790
17815
|
const byCity = leads.filter((l) => cityOf(l) === hint.name);
|
|
17791
|
-
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
|
|
17816
|
+
const anchors = positions([
|
|
17817
|
+
...byCity,
|
|
17818
|
+
...monitorLeads.filter((l) => cityOf(l) === hint.name)
|
|
17819
|
+
]);
|
|
17820
|
+
if (byCity.length > 0) {
|
|
17821
|
+
const inTown = new Set(byCity);
|
|
17822
|
+
const nearby2 = withinRadius(leads.filter((l) => !inTown.has(l)), anchors, radiusKm);
|
|
17823
|
+
return { leads: [...byCity, ...nearby2], matchedOn: "city", nearby: nearby2, anchored: true };
|
|
17824
|
+
}
|
|
17825
|
+
const anchored = anchors.length > 0;
|
|
17826
|
+
if (!hint.isCity) {
|
|
17827
|
+
const byState = leads.filter((l) => stateFieldOf(l) === hint.name);
|
|
17828
|
+
if (byState.length > 0)
|
|
17829
|
+
return { leads: byState, matchedOn: "state", nearby: [], anchored };
|
|
17830
|
+
}
|
|
17831
|
+
const nearby = withinRadius(leads, anchors, radiusKm);
|
|
17832
|
+
if (nearby.length > 0)
|
|
17833
|
+
return { leads: nearby, matchedOn: "city", nearby, anchored };
|
|
17834
|
+
return {
|
|
17835
|
+
leads: [],
|
|
17836
|
+
matchedOn: hint.isCity ? "city" : "state",
|
|
17837
|
+
nearby: [],
|
|
17838
|
+
anchored
|
|
17839
|
+
};
|
|
17840
|
+
}
|
|
17841
|
+
function townsOf(leads) {
|
|
17842
|
+
const seen = [];
|
|
17843
|
+
let unnamed = 0;
|
|
17844
|
+
for (const l of leads) {
|
|
17845
|
+
const city = typeof l?.location?.city === "string" ? l.location.city.trim() : "";
|
|
17846
|
+
const name = city.replace(/^(?:City|Town|Village|Borough|Township|Municipality) of /i, "");
|
|
17847
|
+
if (!name)
|
|
17848
|
+
unnamed += 1;
|
|
17849
|
+
else if (!seen.includes(name))
|
|
17850
|
+
seen.push(name);
|
|
17851
|
+
}
|
|
17852
|
+
const rest = unnamed > 0 ? `${unnamed} whose record names no town` : "";
|
|
17853
|
+
return [seen.join(", "), rest].filter(Boolean).join(", plus ");
|
|
17797
17854
|
}
|
|
17798
17855
|
function toMapLocation(lead, mode) {
|
|
17799
|
-
const pos = lead
|
|
17800
|
-
|
|
17801
|
-
if (!valid)
|
|
17856
|
+
const pos = posOf(lead);
|
|
17857
|
+
if (pos === null)
|
|
17802
17858
|
return null;
|
|
17803
17859
|
const loc = lead.location;
|
|
17804
17860
|
const c = lead.recommended_contact;
|
|
@@ -17871,6 +17927,10 @@ var tourPlan = {
|
|
|
17871
17927
|
discover_count: {
|
|
17872
17928
|
type: "number",
|
|
17873
17929
|
description: `Top-N Discover leads (active lens wishlist) to return after client-side city filter. Default ${DEFAULT_DISCOVER_COUNT}.`
|
|
17930
|
+
},
|
|
17931
|
+
radius_km: {
|
|
17932
|
+
type: "number",
|
|
17933
|
+
description: `How far outside the named town a Discover stop may be, in km. Default ${DEFAULT_RADIUS_KM}, which is roughly a metro area \u2014 it is what puts West Sacramento on a tour of Sacramento and Courbevoie on a tour of Paris. Pass the user's own number when they name one ("dans un rayon de 10km", "zone 20km", "within 15 miles" \u2192 24). Pass 0 to keep the tour inside the town's own name. Leads in the town itself always come first; the radius only adds to them. It applies to Discover leads only \u2014 the Monitor half is scoped server-side and is untouched.`
|
|
17874
17934
|
}
|
|
17875
17935
|
},
|
|
17876
17936
|
additionalProperties: false
|
|
@@ -17887,12 +17947,12 @@ var tourPlan = {
|
|
|
17887
17947
|
},
|
|
17888
17948
|
discover_leads: {
|
|
17889
17949
|
type: "array",
|
|
17890
|
-
description: "Fresh Discover leads from the active lens, filtered client-side to match the city. Pulls a larger candidate set internally to compensate for the missing server-side geo filter.",
|
|
17950
|
+
description: "Fresh Discover leads from the active lens, filtered client-side to match the city, then extended with the ones whose own coordinates put them within `radius_km` of it. The town's own leads come first. Pulls a larger candidate set internally to compensate for the missing server-side geo filter.",
|
|
17891
17951
|
items: { type: "object" }
|
|
17892
17952
|
},
|
|
17893
17953
|
discover_filter_note: {
|
|
17894
17954
|
type: "string",
|
|
17895
|
-
description: "Human-readable summary of the client-side geo filter applied to Discover leads
|
|
17955
|
+
description: "Human-readable summary of the client-side geo filter applied to Discover leads. Says how many stops are in the named town, how many are within `radius_km` of it, and which towns those are in \u2014 repeat that split to the user rather than presenting every stop as being in the city they named."
|
|
17896
17956
|
},
|
|
17897
17957
|
map_locations: {
|
|
17898
17958
|
type: "array",
|
|
@@ -18016,12 +18076,21 @@ var tourPlan = {
|
|
|
18016
18076
|
const cityName = params.city && !/^\d+$/.test(params.city.trim()) ? params.city : void 0;
|
|
18017
18077
|
const knownId = params.city_id ?? (cityName ? void 0 : params.city);
|
|
18018
18078
|
const idOnly = Boolean(knownId) && !cityName;
|
|
18019
|
-
const
|
|
18079
|
+
const radiusKm = params.radius_km ?? DEFAULT_RADIUS_KM;
|
|
18080
|
+
const { leads: filtered, matchedOn, nearby, anchored } = idOnly ? {
|
|
18081
|
+
leads: [],
|
|
18082
|
+
matchedOn: null,
|
|
18083
|
+
nearby: [],
|
|
18084
|
+
anchored: false
|
|
18085
|
+
} : filterDiscoverByCity(rawDiscover, cityName, monitorLeads, radiusKm);
|
|
18020
18086
|
const discoverLeads2 = filtered.slice(0, discoverCount);
|
|
18021
18087
|
const pulledLensId = leadsResult.status === "fulfilled" ? leadsResult.value?.lens?.id : null;
|
|
18022
18088
|
if (pulledLensId != null) {
|
|
18023
18089
|
reportLeadInteractions(client, pulledLensId, discoverLeads2.map((l) => l.id), ["LEAD_SEEN"], ctx?.logger);
|
|
18024
18090
|
}
|
|
18091
|
+
const nearbySet = new Set(nearby);
|
|
18092
|
+
const nearShown = discoverLeads2.filter((l) => nearbySet.has(l));
|
|
18093
|
+
const inTownShown = discoverLeads2.length - nearShown.length;
|
|
18025
18094
|
let filterNote;
|
|
18026
18095
|
if (idOnly) {
|
|
18027
18096
|
filterNote = `Discover leads need the NAME of the place, and this call passed an area id (${knownId}) and no name. Re-call with \`city\` set to the name of that area, keeping \`city_id\` so the Monitor half stays on the area you picked. The follow-ups below are already scoped to it.`;
|
|
@@ -18029,10 +18098,16 @@ var tourPlan = {
|
|
|
18029
18098
|
filterNote = `No city filter applied; returning top ${discoverLeads2.length} Discover leads.`;
|
|
18030
18099
|
} else if (matchedOn === null) {
|
|
18031
18100
|
filterNote = `No usable city filter in '${params.city}'; returning top ${discoverLeads2.length} Discover leads.`;
|
|
18101
|
+
} else if (filtered.length === 0 && anchored && radiusKm > 0) {
|
|
18102
|
+
filterNote = `No Discover lead in the active lens names '${params.city}' as its town, and none is within ${radiusKm} km of the leads that do (checked ${rawDiscover.length} candidates). Say so; do NOT present leads from elsewhere as if they were in '${params.city}'.`;
|
|
18032
18103
|
} else if (filtered.length === 0) {
|
|
18033
18104
|
filterNote = `No Discover lead in the active lens is in '${params.city}' (checked ${rawDiscover.length} candidates by city, then by state/region). Say so; do NOT present leads from elsewhere as if they were in '${params.city}'.`;
|
|
18105
|
+
} else if (nearShown.length > 0 && inTownShown === 0) {
|
|
18106
|
+
filterNote = `No Discover lead in the active lens names '${params.city}' as its town; ${nearby.length}/${rawDiscover.length} are within ${radiusKm} km of it. Returning top ${discoverLeads2.length}, in ${townsOf(nearShown)}. Name the town each stop is actually in rather than calling them all '${params.city}'.`;
|
|
18107
|
+
} else if (nearShown.length > 0) {
|
|
18108
|
+
filterNote = `Matched ${filtered.length - nearby.length}/${rawDiscover.length} Discover leads to '${params.city}' by city, plus ${nearby.length} within ${radiusKm} km of it. Returning top ${discoverLeads2.length}: ${inTownShown} in '${params.city}' and ${nearShown.length} in ${townsOf(nearShown)}. Name the town each nearby stop is in.`;
|
|
18034
18109
|
} else {
|
|
18035
|
-
filterNote = `Matched ${filtered.length}/${rawDiscover.length} Discover leads to '${params.city}' by ${matchedOn === "city" ? "city" : "state/region"}; returning top ${discoverLeads2.length}.`;
|
|
18110
|
+
filterNote = `Matched ${filtered.length - nearby.length}/${rawDiscover.length} Discover leads to '${params.city}' by ${matchedOn === "city" ? "city" : "state/region"}; returning top ${discoverLeads2.length}.`;
|
|
18036
18111
|
}
|
|
18037
18112
|
return {
|
|
18038
18113
|
city: params.city ?? null,
|
|
@@ -19447,7 +19522,7 @@ var researchLeadByNameFuzzy = {
|
|
|
19447
19522
|
// of a research card.
|
|
19448
19523
|
outputSchema: {
|
|
19449
19524
|
type: "object",
|
|
19450
|
-
description: "Same shape as leadbay_research_lead_by_id, with _meta.resolved_from='companyName'|'resolver', _meta.resolved_query='<needle>', _meta.resolved_matched_on=[...], and _meta.match_candidates=[{leadId,name,score}] populated. When the registry resolver cannot pick one company, returns {resolution:'ambiguous', query, candidates:[{leadId,name,website,location,registry_ids,score,matched_on}]} instead \u2014 ask the user which one, then call leadbay_research_lead_by_id.",
|
|
19525
|
+
description: "Same shape as leadbay_research_lead_by_id, with _meta.resolved_from='companyName'|'resolver', _meta.resolved_query='<needle>', _meta.resolved_matched_on=[...], and _meta.match_candidates=[{leadId,name,score}] populated. When the registry resolver cannot pick one company, returns {resolution:'ambiguous', query, candidates:[{leadId,name,website,location,registry_ids,score,matched_on}]} instead \u2014 ask the user which one, then call leadbay_research_lead_by_id. When neither the user's leads nor the registry hold the company, returns {resolution:'not_found', query, summary, would_help, next_step} \u2014 a successful answer, not an error: do what next_step says, which is normally to ask the user for the field would_help names ('website' or 'registry_number') and call again. would_help is empty when the search was scoped to one lens, where the remedy is to drop the scope, not to ask the user for a field.",
|
|
19451
19526
|
additionalProperties: true
|
|
19452
19527
|
},
|
|
19453
19528
|
execute: async (client, params, ctx) => {
|
|
@@ -19461,7 +19536,18 @@ var researchLeadByNameFuzzy = {
|
|
|
19461
19536
|
if (scoped.length > 0) {
|
|
19462
19537
|
return await delegate(scoped, params.lensId);
|
|
19463
19538
|
}
|
|
19464
|
-
|
|
19539
|
+
return {
|
|
19540
|
+
resolution: "not_found",
|
|
19541
|
+
query,
|
|
19542
|
+
summary: `No lead matching "${query}" in lens ${params.lensId}`,
|
|
19543
|
+
would_help: [],
|
|
19544
|
+
next_step: "This lookup was intentionally restricted to the supplied lens. Omit lensId to search your visible leads across Discover, Monitor, and Activate and then the Leadbay company registry.",
|
|
19545
|
+
_meta: {
|
|
19546
|
+
region: client.region,
|
|
19547
|
+
lens_id: params.lensId,
|
|
19548
|
+
resolved_query: query
|
|
19549
|
+
}
|
|
19550
|
+
};
|
|
19465
19551
|
}
|
|
19466
19552
|
async function delegate(matches, fallbackLens) {
|
|
19467
19553
|
const [primary, ...rest] = matches;
|
|
@@ -19556,7 +19642,23 @@ var researchLeadByNameFuzzy = {
|
|
|
19556
19642
|
const wanted = resolved.type === "none" && resolved.would_help.length > 0 ? resolved.would_help : ["website", "registry_number"];
|
|
19557
19643
|
const asks = wanted.map((f) => f === "registry_number" ? "a registry number (SIREN/SIRET) for `registry_number`" : f === "website" ? "the company website for `website`" : `\`${f}\``).join(" or ");
|
|
19558
19644
|
const hint = resolved.type === "unidentifiable" ? `The registry could not identify a company from this input (${resolved.reason}). Ask the user for ${asks}, then call this tool again with it.` : `The registry found no company for what was supplied. It would match on ${asks}. Ask the user for that \u2014 "what's their website?" usually settles it \u2014 then call this tool again. Do not offer an import before asking.`;
|
|
19559
|
-
|
|
19645
|
+
if (!corpusSearched) {
|
|
19646
|
+
throw client.makeError("LEAD_NOT_FOUND", `No company matching "${query}" ${searched}`, `${hint} But this lookup did not complete \u2014 the lead search route was unreachable, so their own leads were never checked. Retry once before telling the user the company is absent.`, "POST /leads/resolve");
|
|
19647
|
+
}
|
|
19648
|
+
return {
|
|
19649
|
+
resolution: "not_found",
|
|
19650
|
+
query,
|
|
19651
|
+
resolver_payload: payload,
|
|
19652
|
+
summary: `No company matching "${query}" ${searched}`,
|
|
19653
|
+
would_help: wanted,
|
|
19654
|
+
next_step: hint,
|
|
19655
|
+
_meta: {
|
|
19656
|
+
region: client.region,
|
|
19657
|
+
resolved_from: "resolver",
|
|
19658
|
+
resolved_query: query,
|
|
19659
|
+
endpoint: "POST /leads/resolve"
|
|
19660
|
+
}
|
|
19661
|
+
};
|
|
19560
19662
|
}
|
|
19561
19663
|
};
|
|
19562
19664
|
|
|
@@ -29308,7 +29410,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
29308
29410
|
}
|
|
29309
29411
|
|
|
29310
29412
|
// src/http-server.ts
|
|
29311
|
-
var VERSION = true ? "0.39.
|
|
29413
|
+
var VERSION = true ? "0.39.7" : "0.0.0-dev";
|
|
29312
29414
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
29313
29415
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
29314
29416
|
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.39.
|
|
3
|
+
"version": "0.39.7",
|
|
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",
|