@leadbay/mcp 0.39.0 → 0.39.2
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 +93 -34
- package/dist/http-server.js +80 -33
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -6731,7 +6731,7 @@ prose paragraph. Full recipe below.
|
|
|
6731
6731
|
|
|
6732
6732
|
Build a single-call mixed-mode itinerary for a field sales tour. Combines \`leadbay_pull_followups\` (Monitor leads in the city \u2014 known accounts) with \`leadbay_pull_leads\` (Discover wishlist \u2014 new prospects, then client-side filtered by city) so the agent can answer the canonical #3630 US1 ask: *"I'm visiting Limoges in 4 days \u2014 propose 3 customers + 3 qualified prospects + 3 new high-potential discoveries."*
|
|
6733
6733
|
|
|
6734
|
-
**Geo resolution** is identical to \`leadbay_followups_map\`: pass \`city\` (any level from state down to neighborhood \u2014 state, *r\xE9gion*, county, city \u2014 the \`/geo/search\` resolver picks the best match), or a pre-resolved \`city_id\`. Ambiguous matches surface as \`status: "ambiguous_locations"\` + \`location_ambiguities[]\`; pick
|
|
6734
|
+
**Geo resolution** is identical to \`leadbay_followups_map\`: pass \`city\` (any level from state down to neighborhood \u2014 state, *r\xE9gion*, county, city \u2014 the \`/geo/search\` resolver picks the best match), or a pre-resolved \`city_id\`. Ambiguous matches surface as \`status: "ambiguous_locations"\` + \`location_ambiguities[]\`; re-call with \`city_id\` set to the id you pick AND \`city\` set to that candidate's \`name\`. Both are needed: the id scopes the follow-ups, the name scopes the Discover leads, and with \`city_id\` alone \`discover_leads\` comes back empty.
|
|
6735
6735
|
|
|
6736
6736
|
**One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
|
|
6737
6737
|
|
|
@@ -6761,7 +6761,7 @@ not an itinerary. So for ANY country-level \`city\` \u2014 this workspace's own
|
|
|
6761
6761
|
visiting and re-call with that. \`status: "country_level_location"\` carries the
|
|
6762
6762
|
same instruction in its \`hint\`.
|
|
6763
6763
|
|
|
6764
|
-
**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
|
|
6764
|
+
**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.
|
|
6765
6765
|
|
|
6766
6766
|
**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.
|
|
6767
6767
|
|
|
@@ -7718,12 +7718,28 @@ var init_list_lenses = __esm({
|
|
|
7718
7718
|
}
|
|
7719
7719
|
});
|
|
7720
7720
|
|
|
7721
|
+
// ../core/dist/interactions.js
|
|
7722
|
+
function reportLeadInteractions(client, lensId, leadIds, types, logger) {
|
|
7723
|
+
if (leadIds.length === 0 || types.length === 0)
|
|
7724
|
+
return;
|
|
7725
|
+
const events = leadIds.flatMap((leadId) => types.map((type) => ({ type, lead_id: leadId, lens_id: String(lensId) })));
|
|
7726
|
+
void client.request("POST", "/interactions", events).catch((err) => {
|
|
7727
|
+
logger?.warn?.(`interactions: ${types.join("+")} for ${leadIds.length} lead(s) not recorded: ${err?.message ?? err?.code ?? err}`);
|
|
7728
|
+
});
|
|
7729
|
+
}
|
|
7730
|
+
var init_interactions = __esm({
|
|
7731
|
+
"../core/dist/interactions.js"() {
|
|
7732
|
+
"use strict";
|
|
7733
|
+
}
|
|
7734
|
+
});
|
|
7735
|
+
|
|
7721
7736
|
// ../core/dist/tools/discover-leads.js
|
|
7722
7737
|
var discoverLeads;
|
|
7723
7738
|
var init_discover_leads = __esm({
|
|
7724
7739
|
"../core/dist/tools/discover-leads.js"() {
|
|
7725
7740
|
"use strict";
|
|
7726
7741
|
init_tool_descriptions_generated();
|
|
7742
|
+
init_interactions();
|
|
7727
7743
|
discoverLeads = {
|
|
7728
7744
|
name: "leadbay_discover_leads",
|
|
7729
7745
|
annotations: {
|
|
@@ -7752,11 +7768,12 @@ var init_discover_leads = __esm({
|
|
|
7752
7768
|
},
|
|
7753
7769
|
additionalProperties: false
|
|
7754
7770
|
},
|
|
7755
|
-
execute: async (client, params) => {
|
|
7771
|
+
execute: async (client, params, ctx) => {
|
|
7756
7772
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
7757
7773
|
const page = params.page ?? 0;
|
|
7758
7774
|
const count = Math.min(params.count ?? 20, 50);
|
|
7759
7775
|
const res = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${count}&page=${page}&contacts=true`);
|
|
7776
|
+
reportLeadInteractions(client, lensId, res.items.map((lead) => lead.id), ["LEAD_SEEN"], ctx?.logger);
|
|
7760
7777
|
const totalPages = res.pagination?.pages ?? 0;
|
|
7761
7778
|
const currentPage = res.pagination?.page ?? page;
|
|
7762
7779
|
const hasMore = currentPage < totalPages - 1;
|
|
@@ -7805,6 +7822,7 @@ var init_get_lead_profile = __esm({
|
|
|
7805
7822
|
"../core/dist/tools/get-lead-profile.js"() {
|
|
7806
7823
|
"use strict";
|
|
7807
7824
|
init_tool_descriptions_generated();
|
|
7825
|
+
init_interactions();
|
|
7808
7826
|
getLeadProfile = {
|
|
7809
7827
|
name: "leadbay_get_lead_profile",
|
|
7810
7828
|
annotations: {
|
|
@@ -7852,13 +7870,9 @@ var init_get_lead_profile = __esm({
|
|
|
7852
7870
|
},
|
|
7853
7871
|
required: ["lead", "contacts"]
|
|
7854
7872
|
},
|
|
7855
|
-
execute: async (client, params) => {
|
|
7873
|
+
execute: async (client, params, ctx) => {
|
|
7856
7874
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
7857
|
-
|
|
7858
|
-
{ type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
|
|
7859
|
-
{ type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
|
|
7860
|
-
]).catch(() => {
|
|
7861
|
-
});
|
|
7875
|
+
reportLeadInteractions(client, lensId, [params.leadId], ["LEAD_SEEN", "LEAD_CLICKED"], ctx?.logger);
|
|
7862
7876
|
const [leadResult, qualResult, contactsResult, paidContactsResult, webFetchResult] = await Promise.allSettled([
|
|
7863
7877
|
client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`),
|
|
7864
7878
|
client.request("GET", `/leads/${params.leadId}/ai_agent_responses`),
|
|
@@ -14291,6 +14305,7 @@ var init_pull_leads = __esm({
|
|
|
14291
14305
|
"../core/dist/composite/pull-leads.js"() {
|
|
14292
14306
|
"use strict";
|
|
14293
14307
|
init_lead_order();
|
|
14308
|
+
init_interactions();
|
|
14294
14309
|
init_empty_lens_reason();
|
|
14295
14310
|
init_tool_descriptions_generated();
|
|
14296
14311
|
pullLeads = {
|
|
@@ -14429,6 +14444,9 @@ var init_pull_leads = __esm({
|
|
|
14429
14444
|
return resolvedOrder.error;
|
|
14430
14445
|
const orderQs = resolvedOrder.order ? `&order=${encodeURIComponent(resolvedOrder.order)}` : "";
|
|
14431
14446
|
const res = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${count}&page=${page}&contacts=true${orderQs}`);
|
|
14447
|
+
if (params._reportSeen !== false) {
|
|
14448
|
+
reportLeadInteractions(client, lensId, res.items.map((lead) => lead.id), ["LEAD_SEEN"], ctx?.logger);
|
|
14449
|
+
}
|
|
14432
14450
|
const summaries = await Promise.all(res.items.map(async (lead) => {
|
|
14433
14451
|
try {
|
|
14434
14452
|
const r = await client.request("GET", `/leads/${lead.id}/ai_agent_responses`);
|
|
@@ -14944,13 +14962,33 @@ var init_followups_map = __esm({
|
|
|
14944
14962
|
});
|
|
14945
14963
|
|
|
14946
14964
|
// ../core/dist/composite/tour-plan.js
|
|
14947
|
-
function
|
|
14948
|
-
|
|
14949
|
-
|
|
14950
|
-
|
|
14951
|
-
|
|
14952
|
-
|
|
14953
|
-
|
|
14965
|
+
function normalizeGeo(value) {
|
|
14966
|
+
return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
14967
|
+
}
|
|
14968
|
+
function townName(value) {
|
|
14969
|
+
return normalizeGeo(value).replace(ADMIN_PREFIX, "");
|
|
14970
|
+
}
|
|
14971
|
+
function cityHintCore(cityHint) {
|
|
14972
|
+
const head = cityHint.split(",")[0] ?? "";
|
|
14973
|
+
const expanded = expandAlias(head);
|
|
14974
|
+
return { name: townName(expanded), isCity: expanded !== head };
|
|
14975
|
+
}
|
|
14976
|
+
function stateFieldOf(lead) {
|
|
14977
|
+
const state = lead?.location?.state;
|
|
14978
|
+
return typeof state === "string" ? normalizeGeo(state) : "";
|
|
14979
|
+
}
|
|
14980
|
+
function filterDiscoverByCity(leads, cityHint) {
|
|
14981
|
+
const hint = cityHint ? cityHintCore(cityHint) : { name: "", isCity: false };
|
|
14982
|
+
if (!hint.name)
|
|
14983
|
+
return { leads, matchedOn: null };
|
|
14984
|
+
const cityOf = (l) => typeof l?.location?.city === "string" ? townName(l.location.city) : "";
|
|
14985
|
+
const byCity = leads.filter((l) => cityOf(l) === hint.name);
|
|
14986
|
+
if (byCity.length > 0)
|
|
14987
|
+
return { leads: byCity, matchedOn: "city" };
|
|
14988
|
+
if (hint.isCity)
|
|
14989
|
+
return { leads: [], matchedOn: "city" };
|
|
14990
|
+
const byState = leads.filter((l) => stateFieldOf(l) === hint.name);
|
|
14991
|
+
return { leads: byState, matchedOn: "state" };
|
|
14954
14992
|
}
|
|
14955
14993
|
function toMapLocation(lead, mode) {
|
|
14956
14994
|
const pos = lead?.location?.pos;
|
|
@@ -15000,17 +15038,20 @@ function buildMap(monitorLeads, discoverLeads2) {
|
|
|
15000
15038
|
}
|
|
15001
15039
|
};
|
|
15002
15040
|
}
|
|
15003
|
-
var DEFAULT_FOLLOWUPS_COUNT, DEFAULT_DISCOVER_COUNT, DISCOVER_OVER_PULL, tourPlan;
|
|
15041
|
+
var DEFAULT_FOLLOWUPS_COUNT, DEFAULT_DISCOVER_COUNT, DISCOVER_OVER_PULL, ADMIN_PREFIX, tourPlan;
|
|
15004
15042
|
var init_tour_plan = __esm({
|
|
15005
15043
|
"../core/dist/composite/tour-plan.js"() {
|
|
15006
15044
|
"use strict";
|
|
15007
15045
|
init_pull_followups();
|
|
15008
15046
|
init_pull_leads();
|
|
15047
|
+
init_interactions();
|
|
15009
15048
|
init_country_guard();
|
|
15049
|
+
init_geo_helpers();
|
|
15010
15050
|
init_tool_descriptions_generated();
|
|
15011
15051
|
DEFAULT_FOLLOWUPS_COUNT = 6;
|
|
15012
15052
|
DEFAULT_DISCOVER_COUNT = 6;
|
|
15013
15053
|
DISCOVER_OVER_PULL = 30;
|
|
15054
|
+
ADMIN_PREFIX = /^(?:city|town|village|borough|township|municipality) of /;
|
|
15014
15055
|
tourPlan = {
|
|
15015
15056
|
name: "leadbay_tour_plan",
|
|
15016
15057
|
annotations: {
|
|
@@ -15030,7 +15071,7 @@ var init_tour_plan = __esm({
|
|
|
15030
15071
|
},
|
|
15031
15072
|
city_id: {
|
|
15032
15073
|
type: "string",
|
|
15033
|
-
description: "Pre-resolved admin_area id (numeric string). Bypasses the resolver."
|
|
15074
|
+
description: "Pre-resolved admin_area id (numeric string). Bypasses the resolver. Pass `city` alongside it with the NAME of the area you picked: the id scopes the Monitor half, and the name is the only thing that can scope the Discover half, which has no server-side geo filter. With `city_id` alone, `discover_leads` comes back empty."
|
|
15034
15075
|
},
|
|
15035
15076
|
followups_count: {
|
|
15036
15077
|
type: "number",
|
|
@@ -15137,11 +15178,17 @@ var init_tour_plan = __esm({
|
|
|
15137
15178
|
const discoverCount = params.discover_count ?? DEFAULT_DISCOVER_COUNT;
|
|
15138
15179
|
const [followupsResult, leadsResult] = await Promise.allSettled([
|
|
15139
15180
|
pullFollowups.execute(client, {
|
|
15140
|
-
|
|
15181
|
+
// A pre-resolved id bypasses the resolver, which is what its own
|
|
15182
|
+
// schema promises. Forwarding the free text alongside it sends the
|
|
15183
|
+
// name back through /geo/search, and the name is the thing that was
|
|
15184
|
+
// ambiguous — so an agent recovering from `ambiguous_locations` by
|
|
15185
|
+
// picking an id would be handed the same ambiguity again. Here the
|
|
15186
|
+
// free text stays behind and scopes the Discover half instead.
|
|
15187
|
+
city: params.city_id ? void 0 : params.city,
|
|
15141
15188
|
city_id: params.city_id,
|
|
15142
15189
|
count: followupsCount
|
|
15143
15190
|
}, ctx),
|
|
15144
|
-
pullLeads.execute(client, { count: DISCOVER_OVER_PULL }, ctx)
|
|
15191
|
+
pullLeads.execute(client, { count: DISCOVER_OVER_PULL, _reportSeen: false }, ctx)
|
|
15145
15192
|
]);
|
|
15146
15193
|
if (followupsResult.status === "fulfilled") {
|
|
15147
15194
|
const r = followupsResult.value;
|
|
@@ -15151,7 +15198,7 @@ var init_tour_plan = __esm({
|
|
|
15151
15198
|
location_ambiguities: r.location_ambiguities,
|
|
15152
15199
|
monitor_leads: [],
|
|
15153
15200
|
discover_leads: [],
|
|
15154
|
-
discover_filter_note: "City was ambiguous; pick
|
|
15201
|
+
discover_filter_note: "City was ambiguous; re-call with `city_id` set to the id you pick AND `city` set to that candidate's `name`. The id scopes the follow-ups; the name is what scopes the Discover leads.",
|
|
15155
15202
|
map_locations: [],
|
|
15156
15203
|
map_summary: {
|
|
15157
15204
|
total_leads: 0,
|
|
@@ -15175,9 +15222,27 @@ var init_tour_plan = __esm({
|
|
|
15175
15222
|
if (leadsResult.status === "rejected") {
|
|
15176
15223
|
ctx?.logger?.warn?.(`tour_plan: pull_leads failed: ${leadsResult.reason?.message ?? leadsResult.reason}`);
|
|
15177
15224
|
}
|
|
15178
|
-
const
|
|
15225
|
+
const cityName = params.city && !/^\d+$/.test(params.city.trim()) ? params.city : void 0;
|
|
15226
|
+
const knownId = params.city_id ?? (cityName ? void 0 : params.city);
|
|
15227
|
+
const idOnly = Boolean(knownId) && !cityName;
|
|
15228
|
+
const { leads: filtered, matchedOn } = idOnly ? { leads: [], matchedOn: null } : filterDiscoverByCity(rawDiscover, cityName);
|
|
15179
15229
|
const discoverLeads2 = filtered.slice(0, discoverCount);
|
|
15180
|
-
const
|
|
15230
|
+
const pulledLensId = leadsResult.status === "fulfilled" ? leadsResult.value?.lens?.id : null;
|
|
15231
|
+
if (pulledLensId != null) {
|
|
15232
|
+
reportLeadInteractions(client, pulledLensId, discoverLeads2.map((l) => l.id), ["LEAD_SEEN"], ctx?.logger);
|
|
15233
|
+
}
|
|
15234
|
+
let filterNote;
|
|
15235
|
+
if (idOnly) {
|
|
15236
|
+
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.`;
|
|
15237
|
+
} else if (!cityName) {
|
|
15238
|
+
filterNote = `No city filter applied; returning top ${discoverLeads2.length} Discover leads.`;
|
|
15239
|
+
} else if (matchedOn === null) {
|
|
15240
|
+
filterNote = `No usable city filter in '${params.city}'; returning top ${discoverLeads2.length} Discover leads.`;
|
|
15241
|
+
} else if (filtered.length === 0) {
|
|
15242
|
+
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}'.`;
|
|
15243
|
+
} else {
|
|
15244
|
+
filterNote = `Matched ${filtered.length}/${rawDiscover.length} Discover leads to '${params.city}' by ${matchedOn === "city" ? "city" : "state/region"}; returning top ${discoverLeads2.length}.`;
|
|
15245
|
+
}
|
|
15181
15246
|
return {
|
|
15182
15247
|
city: params.city ?? null,
|
|
15183
15248
|
city_id: params.city_id ?? null,
|
|
@@ -15943,6 +16008,7 @@ var init_research_lead_by_id = __esm({
|
|
|
15943
16008
|
"../core/dist/composite/research-lead-by-id.js"() {
|
|
15944
16009
|
"use strict";
|
|
15945
16010
|
init_web_fetch_helpers();
|
|
16011
|
+
init_interactions();
|
|
15946
16012
|
init_tool_descriptions_generated();
|
|
15947
16013
|
researchLeadById = {
|
|
15948
16014
|
name: "leadbay_research_lead_by_id",
|
|
@@ -16187,11 +16253,7 @@ var init_research_lead_by_id = __esm({
|
|
|
16187
16253
|
}
|
|
16188
16254
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
16189
16255
|
const leadId = params.leadId;
|
|
16190
|
-
|
|
16191
|
-
{ type: "LEAD_SEEN", leadId, lensId: String(lensId) },
|
|
16192
|
-
{ type: "LEAD_CLICKED", leadId, lensId: String(lensId) }
|
|
16193
|
-
]).catch(() => {
|
|
16194
|
-
});
|
|
16256
|
+
reportLeadInteractions(client, lensId, [leadId], ["LEAD_SEEN", "LEAD_CLICKED"], _ctx?.logger);
|
|
16195
16257
|
const [profileR, qualR, contactsR, webFetchR, activitiesR, orgContactsR] = await Promise.allSettled([
|
|
16196
16258
|
client.request("GET", `/lenses/${lensId}/leads/${leadId}`),
|
|
16197
16259
|
client.request("GET", `/leads/${leadId}/ai_agent_responses`),
|
|
@@ -17234,6 +17296,7 @@ var init_get_lead_custom_fields = __esm({
|
|
|
17234
17296
|
"../core/dist/composite/get-lead-custom-fields.js"() {
|
|
17235
17297
|
"use strict";
|
|
17236
17298
|
init_tool_descriptions_generated();
|
|
17299
|
+
init_interactions();
|
|
17237
17300
|
getLeadCustomFields = {
|
|
17238
17301
|
name: "leadbay_get_lead_custom_fields",
|
|
17239
17302
|
annotations: {
|
|
@@ -17277,11 +17340,7 @@ var init_get_lead_custom_fields = __esm({
|
|
|
17277
17340
|
},
|
|
17278
17341
|
execute: async (client, params, ctx) => {
|
|
17279
17342
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
17280
|
-
|
|
17281
|
-
{ type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
|
|
17282
|
-
{ type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
|
|
17283
|
-
]).catch(() => {
|
|
17284
|
-
});
|
|
17343
|
+
reportLeadInteractions(client, lensId, [params.leadId], ["LEAD_SEEN", "LEAD_CLICKED"], ctx?.logger);
|
|
17285
17344
|
const lead = await client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`);
|
|
17286
17345
|
const entries = lead.custom_fields ?? [];
|
|
17287
17346
|
const needsCatalog = entries.some((e) => !e?.field?.name);
|
|
@@ -31319,7 +31378,7 @@ var OAUTH_BASE_URLS = {
|
|
|
31319
31378
|
fr: "https://staging.api.leadbay.app"
|
|
31320
31379
|
}
|
|
31321
31380
|
};
|
|
31322
|
-
var VERSION = "0.39.
|
|
31381
|
+
var VERSION = "0.39.2";
|
|
31323
31382
|
var HELP = `
|
|
31324
31383
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
31325
31384
|
|
package/dist/http-server.js
CHANGED
|
@@ -9679,7 +9679,7 @@ prose paragraph. Full recipe below.
|
|
|
9679
9679
|
|
|
9680
9680
|
Build a single-call mixed-mode itinerary for a field sales tour. Combines \`leadbay_pull_followups\` (Monitor leads in the city \u2014 known accounts) with \`leadbay_pull_leads\` (Discover wishlist \u2014 new prospects, then client-side filtered by city) so the agent can answer the canonical #3630 US1 ask: *"I'm visiting Limoges in 4 days \u2014 propose 3 customers + 3 qualified prospects + 3 new high-potential discoveries."*
|
|
9681
9681
|
|
|
9682
|
-
**Geo resolution** is identical to \`leadbay_followups_map\`: pass \`city\` (any level from state down to neighborhood \u2014 state, *r\xE9gion*, county, city \u2014 the \`/geo/search\` resolver picks the best match), or a pre-resolved \`city_id\`. Ambiguous matches surface as \`status: "ambiguous_locations"\` + \`location_ambiguities[]\`; pick
|
|
9682
|
+
**Geo resolution** is identical to \`leadbay_followups_map\`: pass \`city\` (any level from state down to neighborhood \u2014 state, *r\xE9gion*, county, city \u2014 the \`/geo/search\` resolver picks the best match), or a pre-resolved \`city_id\`. Ambiguous matches surface as \`status: "ambiguous_locations"\` + \`location_ambiguities[]\`; re-call with \`city_id\` set to the id you pick AND \`city\` set to that candidate's \`name\`. Both are needed: the id scopes the follow-ups, the name scopes the Discover leads, and with \`city_id\` alone \`discover_leads\` comes back empty.
|
|
9683
9683
|
|
|
9684
9684
|
**One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
|
|
9685
9685
|
|
|
@@ -9709,7 +9709,7 @@ not an itinerary. So for ANY country-level \`city\` \u2014 this workspace's own
|
|
|
9709
9709
|
visiting and re-call with that. \`status: "country_level_location"\` carries the
|
|
9710
9710
|
same instruction in its \`hint\`.
|
|
9711
9711
|
|
|
9712
|
-
**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
|
|
9712
|
+
**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.
|
|
9713
9713
|
|
|
9714
9714
|
**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.
|
|
9715
9715
|
|
|
@@ -10649,6 +10649,16 @@ var listLenses = {
|
|
|
10649
10649
|
}
|
|
10650
10650
|
};
|
|
10651
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
|
+
|
|
10652
10662
|
// ../core/dist/tools/discover-leads.js
|
|
10653
10663
|
var discoverLeads = {
|
|
10654
10664
|
name: "leadbay_discover_leads",
|
|
@@ -10678,11 +10688,12 @@ var discoverLeads = {
|
|
|
10678
10688
|
},
|
|
10679
10689
|
additionalProperties: false
|
|
10680
10690
|
},
|
|
10681
|
-
execute: async (client, params) => {
|
|
10691
|
+
execute: async (client, params, ctx) => {
|
|
10682
10692
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
10683
10693
|
const page = params.page ?? 0;
|
|
10684
10694
|
const count = Math.min(params.count ?? 20, 50);
|
|
10685
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);
|
|
10686
10697
|
const totalPages = res.pagination?.pages ?? 0;
|
|
10687
10698
|
const currentPage = res.pagination?.page ?? page;
|
|
10688
10699
|
const hasMore = currentPage < totalPages - 1;
|
|
@@ -10771,13 +10782,9 @@ var getLeadProfile = {
|
|
|
10771
10782
|
},
|
|
10772
10783
|
required: ["lead", "contacts"]
|
|
10773
10784
|
},
|
|
10774
|
-
execute: async (client, params) => {
|
|
10785
|
+
execute: async (client, params, ctx) => {
|
|
10775
10786
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
10776
|
-
|
|
10777
|
-
{ type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
|
|
10778
|
-
{ type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
|
|
10779
|
-
]).catch(() => {
|
|
10780
|
-
});
|
|
10787
|
+
reportLeadInteractions(client, lensId, [params.leadId], ["LEAD_SEEN", "LEAD_CLICKED"], ctx?.logger);
|
|
10781
10788
|
const [leadResult, qualResult, contactsResult, paidContactsResult, webFetchResult] = await Promise.allSettled([
|
|
10782
10789
|
client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`),
|
|
10783
10790
|
client.request("GET", `/leads/${params.leadId}/ai_agent_responses`),
|
|
@@ -16871,6 +16878,9 @@ var pullLeads = {
|
|
|
16871
16878
|
return resolvedOrder.error;
|
|
16872
16879
|
const orderQs = resolvedOrder.order ? `&order=${encodeURIComponent(resolvedOrder.order)}` : "";
|
|
16873
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
|
+
}
|
|
16874
16884
|
const summaries = await Promise.all(res.items.map(async (lead) => {
|
|
16875
16885
|
try {
|
|
16876
16886
|
const r = await client.request("GET", `/leads/${lead.id}/ai_agent_responses`);
|
|
@@ -17363,13 +17373,34 @@ var followupsMap = {
|
|
|
17363
17373
|
var DEFAULT_FOLLOWUPS_COUNT = 6;
|
|
17364
17374
|
var DEFAULT_DISCOVER_COUNT = 6;
|
|
17365
17375
|
var DISCOVER_OVER_PULL = 30;
|
|
17366
|
-
function
|
|
17367
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17376
|
+
function normalizeGeo(value) {
|
|
17377
|
+
return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
17378
|
+
}
|
|
17379
|
+
var ADMIN_PREFIX = /^(?:city|town|village|borough|township|municipality) of /;
|
|
17380
|
+
function townName(value) {
|
|
17381
|
+
return normalizeGeo(value).replace(ADMIN_PREFIX, "");
|
|
17382
|
+
}
|
|
17383
|
+
function cityHintCore(cityHint) {
|
|
17384
|
+
const head = cityHint.split(",")[0] ?? "";
|
|
17385
|
+
const expanded = expandAlias(head);
|
|
17386
|
+
return { name: townName(expanded), isCity: expanded !== head };
|
|
17387
|
+
}
|
|
17388
|
+
function stateFieldOf(lead) {
|
|
17389
|
+
const state = lead?.location?.state;
|
|
17390
|
+
return typeof state === "string" ? normalizeGeo(state) : "";
|
|
17391
|
+
}
|
|
17392
|
+
function filterDiscoverByCity(leads, cityHint) {
|
|
17393
|
+
const hint = cityHint ? cityHintCore(cityHint) : { name: "", isCity: false };
|
|
17394
|
+
if (!hint.name)
|
|
17395
|
+
return { leads, matchedOn: null };
|
|
17396
|
+
const cityOf = (l) => typeof l?.location?.city === "string" ? townName(l.location.city) : "";
|
|
17397
|
+
const byCity = leads.filter((l) => cityOf(l) === hint.name);
|
|
17398
|
+
if (byCity.length > 0)
|
|
17399
|
+
return { leads: byCity, matchedOn: "city" };
|
|
17400
|
+
if (hint.isCity)
|
|
17401
|
+
return { leads: [], matchedOn: "city" };
|
|
17402
|
+
const byState = leads.filter((l) => stateFieldOf(l) === hint.name);
|
|
17403
|
+
return { leads: byState, matchedOn: "state" };
|
|
17373
17404
|
}
|
|
17374
17405
|
function toMapLocation(lead, mode) {
|
|
17375
17406
|
const pos = lead?.location?.pos;
|
|
@@ -17438,7 +17469,7 @@ var tourPlan = {
|
|
|
17438
17469
|
},
|
|
17439
17470
|
city_id: {
|
|
17440
17471
|
type: "string",
|
|
17441
|
-
description: "Pre-resolved admin_area id (numeric string). Bypasses the resolver."
|
|
17472
|
+
description: "Pre-resolved admin_area id (numeric string). Bypasses the resolver. Pass `city` alongside it with the NAME of the area you picked: the id scopes the Monitor half, and the name is the only thing that can scope the Discover half, which has no server-side geo filter. With `city_id` alone, `discover_leads` comes back empty."
|
|
17442
17473
|
},
|
|
17443
17474
|
followups_count: {
|
|
17444
17475
|
type: "number",
|
|
@@ -17545,11 +17576,17 @@ var tourPlan = {
|
|
|
17545
17576
|
const discoverCount = params.discover_count ?? DEFAULT_DISCOVER_COUNT;
|
|
17546
17577
|
const [followupsResult, leadsResult] = await Promise.allSettled([
|
|
17547
17578
|
pullFollowups.execute(client, {
|
|
17548
|
-
|
|
17579
|
+
// A pre-resolved id bypasses the resolver, which is what its own
|
|
17580
|
+
// schema promises. Forwarding the free text alongside it sends the
|
|
17581
|
+
// name back through /geo/search, and the name is the thing that was
|
|
17582
|
+
// ambiguous — so an agent recovering from `ambiguous_locations` by
|
|
17583
|
+
// picking an id would be handed the same ambiguity again. Here the
|
|
17584
|
+
// free text stays behind and scopes the Discover half instead.
|
|
17585
|
+
city: params.city_id ? void 0 : params.city,
|
|
17549
17586
|
city_id: params.city_id,
|
|
17550
17587
|
count: followupsCount
|
|
17551
17588
|
}, ctx),
|
|
17552
|
-
pullLeads.execute(client, { count: DISCOVER_OVER_PULL }, ctx)
|
|
17589
|
+
pullLeads.execute(client, { count: DISCOVER_OVER_PULL, _reportSeen: false }, ctx)
|
|
17553
17590
|
]);
|
|
17554
17591
|
if (followupsResult.status === "fulfilled") {
|
|
17555
17592
|
const r = followupsResult.value;
|
|
@@ -17559,7 +17596,7 @@ var tourPlan = {
|
|
|
17559
17596
|
location_ambiguities: r.location_ambiguities,
|
|
17560
17597
|
monitor_leads: [],
|
|
17561
17598
|
discover_leads: [],
|
|
17562
|
-
discover_filter_note: "City was ambiguous; pick
|
|
17599
|
+
discover_filter_note: "City was ambiguous; re-call with `city_id` set to the id you pick AND `city` set to that candidate's `name`. The id scopes the follow-ups; the name is what scopes the Discover leads.",
|
|
17563
17600
|
map_locations: [],
|
|
17564
17601
|
map_summary: {
|
|
17565
17602
|
total_leads: 0,
|
|
@@ -17583,9 +17620,27 @@ var tourPlan = {
|
|
|
17583
17620
|
if (leadsResult.status === "rejected") {
|
|
17584
17621
|
ctx?.logger?.warn?.(`tour_plan: pull_leads failed: ${leadsResult.reason?.message ?? leadsResult.reason}`);
|
|
17585
17622
|
}
|
|
17586
|
-
const
|
|
17623
|
+
const cityName = params.city && !/^\d+$/.test(params.city.trim()) ? params.city : void 0;
|
|
17624
|
+
const knownId = params.city_id ?? (cityName ? void 0 : params.city);
|
|
17625
|
+
const idOnly = Boolean(knownId) && !cityName;
|
|
17626
|
+
const { leads: filtered, matchedOn } = idOnly ? { leads: [], matchedOn: null } : filterDiscoverByCity(rawDiscover, cityName);
|
|
17587
17627
|
const discoverLeads2 = filtered.slice(0, discoverCount);
|
|
17588
|
-
const
|
|
17628
|
+
const pulledLensId = leadsResult.status === "fulfilled" ? leadsResult.value?.lens?.id : null;
|
|
17629
|
+
if (pulledLensId != null) {
|
|
17630
|
+
reportLeadInteractions(client, pulledLensId, discoverLeads2.map((l) => l.id), ["LEAD_SEEN"], ctx?.logger);
|
|
17631
|
+
}
|
|
17632
|
+
let filterNote;
|
|
17633
|
+
if (idOnly) {
|
|
17634
|
+
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.`;
|
|
17635
|
+
} else if (!cityName) {
|
|
17636
|
+
filterNote = `No city filter applied; returning top ${discoverLeads2.length} Discover leads.`;
|
|
17637
|
+
} else if (matchedOn === null) {
|
|
17638
|
+
filterNote = `No usable city filter in '${params.city}'; returning top ${discoverLeads2.length} Discover leads.`;
|
|
17639
|
+
} else if (filtered.length === 0) {
|
|
17640
|
+
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}'.`;
|
|
17641
|
+
} else {
|
|
17642
|
+
filterNote = `Matched ${filtered.length}/${rawDiscover.length} Discover leads to '${params.city}' by ${matchedOn === "city" ? "city" : "state/region"}; returning top ${discoverLeads2.length}.`;
|
|
17643
|
+
}
|
|
17589
17644
|
return {
|
|
17590
17645
|
city: params.city ?? null,
|
|
17591
17646
|
city_id: params.city_id ?? null,
|
|
@@ -18539,11 +18594,7 @@ var researchLeadById = {
|
|
|
18539
18594
|
}
|
|
18540
18595
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
18541
18596
|
const leadId = params.leadId;
|
|
18542
|
-
|
|
18543
|
-
{ type: "LEAD_SEEN", leadId, lensId: String(lensId) },
|
|
18544
|
-
{ type: "LEAD_CLICKED", leadId, lensId: String(lensId) }
|
|
18545
|
-
]).catch(() => {
|
|
18546
|
-
});
|
|
18597
|
+
reportLeadInteractions(client, lensId, [leadId], ["LEAD_SEEN", "LEAD_CLICKED"], _ctx?.logger);
|
|
18547
18598
|
const [profileR, qualR, contactsR, webFetchR, activitiesR, orgContactsR] = await Promise.allSettled([
|
|
18548
18599
|
client.request("GET", `/lenses/${lensId}/leads/${leadId}`),
|
|
18549
18600
|
client.request("GET", `/leads/${leadId}/ai_agent_responses`),
|
|
@@ -19585,11 +19636,7 @@ var getLeadCustomFields = {
|
|
|
19585
19636
|
},
|
|
19586
19637
|
execute: async (client, params, ctx) => {
|
|
19587
19638
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
19588
|
-
|
|
19589
|
-
{ type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
|
|
19590
|
-
{ type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
|
|
19591
|
-
]).catch(() => {
|
|
19592
|
-
});
|
|
19639
|
+
reportLeadInteractions(client, lensId, [params.leadId], ["LEAD_SEEN", "LEAD_CLICKED"], ctx?.logger);
|
|
19593
19640
|
const lead = await client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`);
|
|
19594
19641
|
const entries = lead.custom_fields ?? [];
|
|
19595
19642
|
const needsCatalog = entries.some((e) => !e?.field?.name);
|
|
@@ -28613,7 +28660,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
28613
28660
|
}
|
|
28614
28661
|
|
|
28615
28662
|
// src/http-server.ts
|
|
28616
|
-
var VERSION = true ? "0.39.
|
|
28663
|
+
var VERSION = true ? "0.39.2" : "0.0.0-dev";
|
|
28617
28664
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
28618
28665
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
28619
28666
|
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.2",
|
|
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",
|