@leadbay/mcp 0.39.1 → 0.39.3
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 +305 -53
- package/dist/http-server.js +295 -52
- 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
|
|
|
@@ -14249,8 +14249,8 @@ function summarise(responses) {
|
|
|
14249
14249
|
return { answered, total, avg_qualification_boost: avg, best_response_excerpt: excerpt };
|
|
14250
14250
|
}
|
|
14251
14251
|
function buildPullLeadsNextSteps(args) {
|
|
14252
|
-
const { leadCount, hasMore, nextPage, computingWishlist, computingScores } = args;
|
|
14253
|
-
if (
|
|
14252
|
+
const { leadCount: leadCount2, hasMore, nextPage, computingWishlist, computingScores } = args;
|
|
14253
|
+
if (leadCount2 <= 0) {
|
|
14254
14254
|
if (computingWishlist || computingScores) {
|
|
14255
14255
|
return {
|
|
14256
14256
|
question: "This lens is still warming up. What next?",
|
|
@@ -14503,15 +14503,15 @@ var init_pull_leads = __esm({
|
|
|
14503
14503
|
const currentPage = res.pagination?.page ?? page;
|
|
14504
14504
|
const hasMore = currentPage < totalPages - 1;
|
|
14505
14505
|
const nextPage = hasMore ? currentPage + 1 : null;
|
|
14506
|
-
const
|
|
14506
|
+
const leadCount2 = res.items.length;
|
|
14507
14507
|
const nextSteps = buildPullLeadsNextSteps({
|
|
14508
|
-
leadCount,
|
|
14508
|
+
leadCount: leadCount2,
|
|
14509
14509
|
hasMore,
|
|
14510
14510
|
nextPage,
|
|
14511
14511
|
computingWishlist: res.computing_wishlist,
|
|
14512
14512
|
computingScores: res.computing_scores
|
|
14513
14513
|
});
|
|
14514
|
-
const lensIsEmpty =
|
|
14514
|
+
const lensIsEmpty = leadCount2 === 0 && (res.pagination?.total ?? 0) === 0;
|
|
14515
14515
|
const emptyReason = lensIsEmpty ? await diagnoseEmptyLens(client, lensId, {
|
|
14516
14516
|
wishlist: res.computing_wishlist,
|
|
14517
14517
|
scores: res.computing_scores
|
|
@@ -14962,13 +14962,33 @@ var init_followups_map = __esm({
|
|
|
14962
14962
|
});
|
|
14963
14963
|
|
|
14964
14964
|
// ../core/dist/composite/tour-plan.js
|
|
14965
|
-
function
|
|
14966
|
-
|
|
14967
|
-
|
|
14968
|
-
|
|
14969
|
-
|
|
14970
|
-
|
|
14971
|
-
|
|
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" };
|
|
14972
14992
|
}
|
|
14973
14993
|
function toMapLocation(lead, mode) {
|
|
14974
14994
|
const pos = lead?.location?.pos;
|
|
@@ -15018,7 +15038,7 @@ function buildMap(monitorLeads, discoverLeads2) {
|
|
|
15018
15038
|
}
|
|
15019
15039
|
};
|
|
15020
15040
|
}
|
|
15021
|
-
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;
|
|
15022
15042
|
var init_tour_plan = __esm({
|
|
15023
15043
|
"../core/dist/composite/tour-plan.js"() {
|
|
15024
15044
|
"use strict";
|
|
@@ -15026,10 +15046,12 @@ var init_tour_plan = __esm({
|
|
|
15026
15046
|
init_pull_leads();
|
|
15027
15047
|
init_interactions();
|
|
15028
15048
|
init_country_guard();
|
|
15049
|
+
init_geo_helpers();
|
|
15029
15050
|
init_tool_descriptions_generated();
|
|
15030
15051
|
DEFAULT_FOLLOWUPS_COUNT = 6;
|
|
15031
15052
|
DEFAULT_DISCOVER_COUNT = 6;
|
|
15032
15053
|
DISCOVER_OVER_PULL = 30;
|
|
15054
|
+
ADMIN_PREFIX = /^(?:city|town|village|borough|township|municipality) of /;
|
|
15033
15055
|
tourPlan = {
|
|
15034
15056
|
name: "leadbay_tour_plan",
|
|
15035
15057
|
annotations: {
|
|
@@ -15049,7 +15071,7 @@ var init_tour_plan = __esm({
|
|
|
15049
15071
|
},
|
|
15050
15072
|
city_id: {
|
|
15051
15073
|
type: "string",
|
|
15052
|
-
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."
|
|
15053
15075
|
},
|
|
15054
15076
|
followups_count: {
|
|
15055
15077
|
type: "number",
|
|
@@ -15156,7 +15178,13 @@ var init_tour_plan = __esm({
|
|
|
15156
15178
|
const discoverCount = params.discover_count ?? DEFAULT_DISCOVER_COUNT;
|
|
15157
15179
|
const [followupsResult, leadsResult] = await Promise.allSettled([
|
|
15158
15180
|
pullFollowups.execute(client, {
|
|
15159
|
-
|
|
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,
|
|
15160
15188
|
city_id: params.city_id,
|
|
15161
15189
|
count: followupsCount
|
|
15162
15190
|
}, ctx),
|
|
@@ -15170,7 +15198,7 @@ var init_tour_plan = __esm({
|
|
|
15170
15198
|
location_ambiguities: r.location_ambiguities,
|
|
15171
15199
|
monitor_leads: [],
|
|
15172
15200
|
discover_leads: [],
|
|
15173
|
-
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.",
|
|
15174
15202
|
map_locations: [],
|
|
15175
15203
|
map_summary: {
|
|
15176
15204
|
total_leads: 0,
|
|
@@ -15194,13 +15222,27 @@ var init_tour_plan = __esm({
|
|
|
15194
15222
|
if (leadsResult.status === "rejected") {
|
|
15195
15223
|
ctx?.logger?.warn?.(`tour_plan: pull_leads failed: ${leadsResult.reason?.message ?? leadsResult.reason}`);
|
|
15196
15224
|
}
|
|
15197
|
-
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);
|
|
15198
15229
|
const discoverLeads2 = filtered.slice(0, discoverCount);
|
|
15199
15230
|
const pulledLensId = leadsResult.status === "fulfilled" ? leadsResult.value?.lens?.id : null;
|
|
15200
15231
|
if (pulledLensId != null) {
|
|
15201
15232
|
reportLeadInteractions(client, pulledLensId, discoverLeads2.map((l) => l.id), ["LEAD_SEEN"], ctx?.logger);
|
|
15202
15233
|
}
|
|
15203
|
-
|
|
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
|
+
}
|
|
15204
15246
|
return {
|
|
15205
15247
|
city: params.city ?? null,
|
|
15206
15248
|
city_id: params.city_id ?? null,
|
|
@@ -20766,27 +20808,167 @@ var init_bulk_enrich_status = __esm({
|
|
|
20766
20808
|
}
|
|
20767
20809
|
});
|
|
20768
20810
|
|
|
20769
|
-
// ../core/dist/composite/
|
|
20770
|
-
function
|
|
20771
|
-
|
|
20772
|
-
return [];
|
|
20773
|
-
return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
20811
|
+
// ../core/dist/composite/_sector-resolver.js
|
|
20812
|
+
function normalizeSectorText(value) {
|
|
20813
|
+
return (value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^0-9a-z]+/g, " ").trim();
|
|
20774
20814
|
}
|
|
20775
|
-
function
|
|
20776
|
-
|
|
20777
|
-
|
|
20778
|
-
|
|
20779
|
-
|
|
20780
|
-
|
|
20815
|
+
function singular(token) {
|
|
20816
|
+
if (token.length > 4 && token.endsWith("ies"))
|
|
20817
|
+
return `${token.slice(0, -3)}y`;
|
|
20818
|
+
return token.length > 3 && token.endsWith("s") ? token.slice(0, -1) : token;
|
|
20819
|
+
}
|
|
20820
|
+
function sectorTokens(value) {
|
|
20821
|
+
const all = normalizeSectorText(value).split(" ").filter(Boolean).map(singular);
|
|
20822
|
+
const core = all.filter((t) => !FILLER.has(t));
|
|
20823
|
+
return core.length > 0 ? core : all;
|
|
20824
|
+
}
|
|
20825
|
+
function sectorLabel(row) {
|
|
20826
|
+
if (!row)
|
|
20827
|
+
return "";
|
|
20828
|
+
const label = row.label;
|
|
20829
|
+
if (typeof label === "string" && label.length > 0)
|
|
20830
|
+
return label;
|
|
20831
|
+
return typeof row.name === "string" ? row.name : "";
|
|
20832
|
+
}
|
|
20833
|
+
function leadCount(row) {
|
|
20834
|
+
const n = row.number_of_leads;
|
|
20835
|
+
return typeof n === "number" && Number.isFinite(n) ? n : 0;
|
|
20836
|
+
}
|
|
20837
|
+
function rankCandidates(scored, tieBreak) {
|
|
20838
|
+
const out = [];
|
|
20839
|
+
const seen = /* @__PURE__ */ new Set();
|
|
20840
|
+
scored.sort((a, b) => b.score - a.score || (tieBreak === "breadth" ? leadCount(b.row) - leadCount(a.row) || b.closeness - a.closeness : b.closeness - a.closeness || leadCount(b.row) - leadCount(a.row)) || sectorLabel(a.row).length - sectorLabel(b.row).length).forEach(({ row, score }) => {
|
|
20841
|
+
const label = sectorLabel(row);
|
|
20842
|
+
if (!label || seen.has(label) || out.length >= MAX_CANDIDATES)
|
|
20843
|
+
return;
|
|
20844
|
+
seen.add(label);
|
|
20845
|
+
out.push({ id: row.id, name: label, score: Number(score.toFixed(2)) });
|
|
20846
|
+
});
|
|
20847
|
+
return out;
|
|
20848
|
+
}
|
|
20849
|
+
function matchSector(text3, taxonomy) {
|
|
20850
|
+
const wanted = normalizeSectorText(text3);
|
|
20851
|
+
if (!wanted)
|
|
20852
|
+
return { kind: "unresolved", candidates: [] };
|
|
20853
|
+
const exact = taxonomy.filter((row) => normalizeSectorText(sectorLabel(row)) === wanted);
|
|
20854
|
+
if (exact.length > 0) {
|
|
20855
|
+
const label = sectorLabel(exact.reduce((best, row) => leadCount(row) > leadCount(best) ? row : best));
|
|
20856
|
+
const ids = taxonomy.filter((row) => sectorLabel(row) === label).map((row) => row.id);
|
|
20857
|
+
return { kind: "resolved", label, ids, exact: true };
|
|
20858
|
+
}
|
|
20859
|
+
const want = new Set(sectorTokens(text3));
|
|
20860
|
+
const sameWords = taxonomy.filter((row) => {
|
|
20861
|
+
const have = new Set(sectorTokens(sectorLabel(row)));
|
|
20862
|
+
return have.size === want.size && [...want].every((token) => have.has(token));
|
|
20863
|
+
});
|
|
20864
|
+
const sameWordLabels = new Set(sameWords.map((row) => sectorLabel(row)).filter(Boolean));
|
|
20865
|
+
if (sameWordLabels.size === 1) {
|
|
20866
|
+
const label = [...sameWordLabels][0];
|
|
20867
|
+
const ids = taxonomy.filter((row) => sectorLabel(row) === label).map((row) => row.id);
|
|
20868
|
+
return { kind: "resolved", label, ids, exact: false };
|
|
20869
|
+
}
|
|
20870
|
+
const scored = [];
|
|
20871
|
+
for (const row of taxonomy) {
|
|
20872
|
+
const have = new Set(sectorTokens(sectorLabel(row)));
|
|
20873
|
+
if (have.size === 0)
|
|
20874
|
+
continue;
|
|
20781
20875
|
let overlap = 0;
|
|
20782
|
-
for (const
|
|
20783
|
-
if (have.has(
|
|
20876
|
+
for (const token of want)
|
|
20877
|
+
if (have.has(token))
|
|
20784
20878
|
overlap += 1;
|
|
20785
|
-
|
|
20786
|
-
|
|
20787
|
-
|
|
20788
|
-
|
|
20879
|
+
if (overlap === 0)
|
|
20880
|
+
continue;
|
|
20881
|
+
scored.push({ row, score: overlap / want.size, closeness: overlap / have.size });
|
|
20882
|
+
}
|
|
20883
|
+
const full = scored.filter((s) => s.score >= 1);
|
|
20884
|
+
return {
|
|
20885
|
+
kind: "unresolved",
|
|
20886
|
+
candidates: full.length > 0 ? rankCandidates(full, "breadth") : rankCandidates(scored, "closeness").slice(0, MAX_PARTIAL_CANDIDATES)
|
|
20887
|
+
};
|
|
20888
|
+
}
|
|
20889
|
+
function sectorSections(taxonomy) {
|
|
20890
|
+
const roots = taxonomy.filter((row) => !row.parent);
|
|
20891
|
+
const labels = [];
|
|
20892
|
+
for (const row of roots.sort((a, b) => leadCount(b) - leadCount(a))) {
|
|
20893
|
+
const label = sectorLabel(row);
|
|
20894
|
+
if (label && !labels.includes(label))
|
|
20895
|
+
labels.push(label);
|
|
20896
|
+
}
|
|
20897
|
+
return labels;
|
|
20898
|
+
}
|
|
20899
|
+
async function fetchSectorTaxonomy(client, ctx) {
|
|
20900
|
+
const me = await client.resolveMe().catch(() => null);
|
|
20901
|
+
const lang = me?.language ?? "en";
|
|
20902
|
+
const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
|
|
20903
|
+
const unlabelled = taxonomy.filter((row) => !sectorLabel(row)).length;
|
|
20904
|
+
if (unlabelled > 0) {
|
|
20905
|
+
ctx?.logger?.warn?.(`sector taxonomy: ${unlabelled}/${taxonomy.length} row(s) carry no label`);
|
|
20906
|
+
}
|
|
20907
|
+
return taxonomy;
|
|
20908
|
+
}
|
|
20909
|
+
function resolveSectorValues(values, taxonomy) {
|
|
20910
|
+
const out = {
|
|
20911
|
+
values: [],
|
|
20912
|
+
rewritten: [],
|
|
20913
|
+
unresolved: [],
|
|
20914
|
+
sections: sectorSections(taxonomy)
|
|
20915
|
+
};
|
|
20916
|
+
for (const raw of values) {
|
|
20917
|
+
const asked = typeof raw === "string" ? raw : String(raw ?? "");
|
|
20918
|
+
if (NUMERIC_ID.test(asked.trim())) {
|
|
20919
|
+
out.values.push(asked.trim());
|
|
20920
|
+
continue;
|
|
20921
|
+
}
|
|
20922
|
+
const match = matchSector(asked, taxonomy);
|
|
20923
|
+
if (match.kind === "resolved") {
|
|
20924
|
+
out.values.push(match.label);
|
|
20925
|
+
if (match.label !== asked)
|
|
20926
|
+
out.rewritten.push({ asked, used: match.label });
|
|
20927
|
+
continue;
|
|
20928
|
+
}
|
|
20929
|
+
out.values.push(asked);
|
|
20930
|
+
out.unresolved.push({ asked, closest: match.candidates });
|
|
20931
|
+
}
|
|
20932
|
+
return out;
|
|
20789
20933
|
}
|
|
20934
|
+
var FILLER, MAX_CANDIDATES, MAX_PARTIAL_CANDIDATES, NUMERIC_ID;
|
|
20935
|
+
var init_sector_resolver = __esm({
|
|
20936
|
+
"../core/dist/composite/_sector-resolver.js"() {
|
|
20937
|
+
"use strict";
|
|
20938
|
+
FILLER = /* @__PURE__ */ new Set([
|
|
20939
|
+
// en
|
|
20940
|
+
"and",
|
|
20941
|
+
"of",
|
|
20942
|
+
"the",
|
|
20943
|
+
"for",
|
|
20944
|
+
"or",
|
|
20945
|
+
"activity",
|
|
20946
|
+
// fr
|
|
20947
|
+
"et",
|
|
20948
|
+
"de",
|
|
20949
|
+
"des",
|
|
20950
|
+
"du",
|
|
20951
|
+
"d",
|
|
20952
|
+
"la",
|
|
20953
|
+
"le",
|
|
20954
|
+
"les",
|
|
20955
|
+
"l",
|
|
20956
|
+
"en",
|
|
20957
|
+
"aux",
|
|
20958
|
+
"au",
|
|
20959
|
+
"activite",
|
|
20960
|
+
"pour",
|
|
20961
|
+
"sur",
|
|
20962
|
+
"par",
|
|
20963
|
+
"a"
|
|
20964
|
+
]);
|
|
20965
|
+
MAX_CANDIDATES = 8;
|
|
20966
|
+
MAX_PARTIAL_CANDIDATES = 4;
|
|
20967
|
+
NUMERIC_ID = /^\d+$/;
|
|
20968
|
+
}
|
|
20969
|
+
});
|
|
20970
|
+
|
|
20971
|
+
// ../core/dist/composite/adjust-audience.js
|
|
20790
20972
|
async function resolveSectors(client, texts, ctx) {
|
|
20791
20973
|
const looksLikeId2 = (s) => /^\d+$/.test(s);
|
|
20792
20974
|
const direct = texts.filter(looksLikeId2);
|
|
@@ -20796,18 +20978,18 @@ async function resolveSectors(client, texts, ctx) {
|
|
|
20796
20978
|
const me = await client.resolveMe().catch(() => null);
|
|
20797
20979
|
const lang = me?.language ?? "en";
|
|
20798
20980
|
const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
|
|
20799
|
-
const nullNames = taxonomy.filter((s) => !s
|
|
20981
|
+
const nullNames = taxonomy.filter((s) => !sectorLabel(s)).length;
|
|
20800
20982
|
if (nullNames > 0) {
|
|
20801
20983
|
ctx?.logger?.warn?.(`adjust_audience: /sectors/all returned ${nullNames}/${taxonomy.length} sector(s) with a null/missing name`);
|
|
20802
20984
|
}
|
|
20803
20985
|
const resolved = [...direct];
|
|
20804
20986
|
const ambiguities = [];
|
|
20805
20987
|
for (const text3 of free) {
|
|
20806
|
-
const
|
|
20807
|
-
if (
|
|
20808
|
-
resolved.push(
|
|
20988
|
+
const match = matchSector(text3, taxonomy);
|
|
20989
|
+
if (match.kind === "resolved") {
|
|
20990
|
+
resolved.push(...match.ids);
|
|
20809
20991
|
} else {
|
|
20810
|
-
ambiguities.push({ sector_text: text3, matches });
|
|
20992
|
+
ambiguities.push({ sector_text: text3, matches: match.candidates });
|
|
20811
20993
|
}
|
|
20812
20994
|
}
|
|
20813
20995
|
return { resolved, ambiguities };
|
|
@@ -20914,6 +21096,7 @@ var init_adjust_audience = __esm({
|
|
|
20914
21096
|
"use strict";
|
|
20915
21097
|
init_geo_helpers();
|
|
20916
21098
|
init_country_guard();
|
|
21099
|
+
init_sector_resolver();
|
|
20917
21100
|
init_tool_descriptions_generated();
|
|
20918
21101
|
adjustAudience = {
|
|
20919
21102
|
name: "leadbay_adjust_audience",
|
|
@@ -22131,12 +22314,68 @@ function sortFilterLists(filters) {
|
|
|
22131
22314
|
}
|
|
22132
22315
|
return out;
|
|
22133
22316
|
}
|
|
22317
|
+
function isSectorRejection(err) {
|
|
22318
|
+
const e = err;
|
|
22319
|
+
return !!e && e.code === "BAD_INPUT" && typeof e.message === "string" && e.message.includes("filters.sectors");
|
|
22320
|
+
}
|
|
22321
|
+
async function submitSearch(client, body, opts, ctx) {
|
|
22322
|
+
try {
|
|
22323
|
+
return {
|
|
22324
|
+
ok: true,
|
|
22325
|
+
data: await client.request("POST", "/mcp/search", body, opts),
|
|
22326
|
+
rewritten: []
|
|
22327
|
+
};
|
|
22328
|
+
} catch (err) {
|
|
22329
|
+
const asked = body.filters?.sectors;
|
|
22330
|
+
if (!isSectorRejection(err) || !Array.isArray(asked) || asked.length === 0) {
|
|
22331
|
+
throw err;
|
|
22332
|
+
}
|
|
22333
|
+
const taxonomy = await fetchSectorTaxonomy(client, ctx).catch(() => {
|
|
22334
|
+
throw err;
|
|
22335
|
+
});
|
|
22336
|
+
const fix = resolveSectorValues(asked, taxonomy);
|
|
22337
|
+
if (fix.unresolved.length === 0 && fix.rewritten.length === 0)
|
|
22338
|
+
throw err;
|
|
22339
|
+
if (fix.unresolved.length > 0)
|
|
22340
|
+
return { ok: false, choice: sectorChoice(fix) };
|
|
22341
|
+
const retried = { ...body, filters: { ...body.filters, sectors: fix.values } };
|
|
22342
|
+
return {
|
|
22343
|
+
ok: true,
|
|
22344
|
+
data: await client.request("POST", "/mcp/search", retried, opts),
|
|
22345
|
+
rewritten: fix.rewritten
|
|
22346
|
+
};
|
|
22347
|
+
}
|
|
22348
|
+
}
|
|
22349
|
+
function sectorNote(rewritten) {
|
|
22350
|
+
if (rewritten.length === 0)
|
|
22351
|
+
return void 0;
|
|
22352
|
+
const pairs = rewritten.map((r) => `"${r.asked}" \u2192 "${r.used}"`).join(", ");
|
|
22353
|
+
return {
|
|
22354
|
+
sectors_note: `Leadbay's sector taxonomy spells these differently, so the search ran on ${pairs}. Tell the user which sector was searched.`,
|
|
22355
|
+
sectors_used: rewritten
|
|
22356
|
+
};
|
|
22357
|
+
}
|
|
22358
|
+
function sectorChoice(fix) {
|
|
22359
|
+
const names = fix.unresolved.map((u) => `"${u.asked}"`).join(", ");
|
|
22360
|
+
return {
|
|
22361
|
+
mode: "needs_sector_choice",
|
|
22362
|
+
submitted: false,
|
|
22363
|
+
unresolved_sectors: fix.unresolved.map((u) => ({
|
|
22364
|
+
asked: u.asked,
|
|
22365
|
+
closest: u.closest.map((c) => c.name)
|
|
22366
|
+
})),
|
|
22367
|
+
sector_sections: fix.sections,
|
|
22368
|
+
resolved_sectors: fix.rewritten,
|
|
22369
|
+
hint: `No sector in this workspace's taxonomy is named ${names || "that"}. Nothing was submitted and nothing was spent. Re-call with a label copied character for character from \`closest\` or \`sector_sections\` (a section covers everything under it), or drop \`filters.sectors\` and put the wording in \`query\` / \`example_lead.description\` instead \u2014 the search then RANKS on it rather than fencing on it, which is usually what the user meant. \`leadbay_list_sectors\` returns the full taxonomy if none of these fit.`
|
|
22370
|
+
};
|
|
22371
|
+
}
|
|
22134
22372
|
var DEFAULT_WAIT_SECONDS, findNewLeads;
|
|
22135
22373
|
var init_find_new_leads = __esm({
|
|
22136
22374
|
"../core/dist/composite/find-new-leads.js"() {
|
|
22137
22375
|
"use strict";
|
|
22138
22376
|
init_mcp_job_helpers();
|
|
22139
22377
|
init_country_guard();
|
|
22378
|
+
init_sector_resolver();
|
|
22140
22379
|
init_tool_descriptions_generated();
|
|
22141
22380
|
DEFAULT_WAIT_SECONDS = 45;
|
|
22142
22381
|
findNewLeads = {
|
|
@@ -22177,7 +22416,7 @@ var init_find_new_leads = __esm({
|
|
|
22177
22416
|
},
|
|
22178
22417
|
filters: {
|
|
22179
22418
|
type: "object",
|
|
22180
|
-
description: "HARD constraints (the seed only shapes ranking). Sector/location labels resolve at submit;
|
|
22419
|
+
description: "HARD constraints (the seed only shapes ranking). Sector/location labels resolve at submit. `sectors` must be a label from Leadbay's own taxonomy (leadbay_list_sectors) \u2014 everyday wording like 'Professional Services' is not one; a spelling/plural slip is corrected for you, but a word with no label comes back as mode:'needs_sector_choice' with the labels to pick from. An unresolvable location is a 400 naming it.",
|
|
22181
22420
|
properties: {
|
|
22182
22421
|
sectors: { type: "array", items: { type: "string" } },
|
|
22183
22422
|
locations: { type: "array", items: { type: "string" } },
|
|
@@ -22349,19 +22588,27 @@ var init_find_new_leads = __esm({
|
|
|
22349
22588
|
dry_run: dryRun
|
|
22350
22589
|
});
|
|
22351
22590
|
if (dryRun === true) {
|
|
22352
|
-
const
|
|
22591
|
+
const attempt2 = await submitSearch(client, body, void 0, ctx);
|
|
22592
|
+
if (!attempt2.ok)
|
|
22593
|
+
return { ...attempt2.choice, ...countryNote, region: client.region };
|
|
22353
22594
|
return {
|
|
22354
22595
|
dry_run: true,
|
|
22355
|
-
...
|
|
22596
|
+
...attempt2.data,
|
|
22597
|
+
...sectorNote(attempt2.rewritten),
|
|
22356
22598
|
...countryNote,
|
|
22357
22599
|
region: client.region
|
|
22358
22600
|
};
|
|
22359
22601
|
}
|
|
22360
22602
|
if (isPaid && !consented) {
|
|
22361
|
-
|
|
22362
|
-
|
|
22363
|
-
|
|
22364
|
-
|
|
22603
|
+
let forecast = null;
|
|
22604
|
+
let quoteRewritten = [];
|
|
22605
|
+
if (!vetoed) {
|
|
22606
|
+
const attempt2 = await submitSearch(client, { ...body, dry_run: true }, void 0, ctx);
|
|
22607
|
+
if (!attempt2.ok)
|
|
22608
|
+
return { ...attempt2.choice, ...countryNote, region: client.region };
|
|
22609
|
+
forecast = attempt2.data;
|
|
22610
|
+
quoteRewritten = attempt2.rewritten;
|
|
22611
|
+
}
|
|
22365
22612
|
return {
|
|
22366
22613
|
mode: "needs_confirmation",
|
|
22367
22614
|
submitted: false,
|
|
@@ -22374,11 +22621,15 @@ var init_find_new_leads = __esm({
|
|
|
22374
22621
|
estimated_cost: forecast?.estimated_cost ?? null,
|
|
22375
22622
|
items_requested: forecast?.items_requested ?? null,
|
|
22376
22623
|
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.",
|
|
22624
|
+
...sectorNote(quoteRewritten),
|
|
22377
22625
|
...countryNote,
|
|
22378
22626
|
region: client.region
|
|
22379
22627
|
};
|
|
22380
22628
|
}
|
|
22381
|
-
const
|
|
22629
|
+
const attempt = await submitSearch(client, body, { preSendSignal: ctx?.signal }, ctx);
|
|
22630
|
+
if (!attempt.ok)
|
|
22631
|
+
return { ...attempt.choice, ...countryNote, region: client.region };
|
|
22632
|
+
const submit = attempt.data;
|
|
22382
22633
|
const mocked = mockedSubmitPreview(submit, "leadbay_find_new_leads", client.region);
|
|
22383
22634
|
if (mocked)
|
|
22384
22635
|
return mocked;
|
|
@@ -22429,6 +22680,7 @@ var init_find_new_leads = __esm({
|
|
|
22429
22680
|
since: snapshot.next_since ?? null,
|
|
22430
22681
|
suggested_wait_seconds: done ? 0 : 60
|
|
22431
22682
|
},
|
|
22683
|
+
...sectorNote(attempt.rewritten),
|
|
22432
22684
|
...countryNote,
|
|
22433
22685
|
region: client.region
|
|
22434
22686
|
};
|
|
@@ -23977,8 +24229,8 @@ var init_report_outreach = __esm({
|
|
|
23977
24229
|
if (!params.dry_run && params.verification.source === "user_confirmed" && typeof ctx?.elicit === "function") {
|
|
23978
24230
|
try {
|
|
23979
24231
|
const targetIds = params.lead_ids ?? [params.lead_id];
|
|
23980
|
-
const
|
|
23981
|
-
const elicitMsg =
|
|
24232
|
+
const leadCount2 = targetIds.length;
|
|
24233
|
+
const elicitMsg = leadCount2 === 1 ? `An AI agent wants to log outreach on lead ${targetIds[0]}: "${params.note}". The agent claims you confirmed this. Type your literal confirmation to proceed; cancel to reject.` : `An AI agent wants to log outreach on ${leadCount2} leads: "${params.note}". The agent claims you confirmed this. Type your literal confirmation to proceed; cancel to reject.`;
|
|
23982
24234
|
const result = await ctx.elicit({
|
|
23983
24235
|
message: elicitMsg,
|
|
23984
24236
|
requestedSchema: {
|
|
@@ -31336,7 +31588,7 @@ var OAUTH_BASE_URLS = {
|
|
|
31336
31588
|
fr: "https://staging.api.leadbay.app"
|
|
31337
31589
|
}
|
|
31338
31590
|
};
|
|
31339
|
-
var VERSION = "0.39.
|
|
31591
|
+
var VERSION = "0.39.3";
|
|
31340
31592
|
var HELP = `
|
|
31341
31593
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
31342
31594
|
|
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
|
|
|
@@ -16691,8 +16691,8 @@ function summarise(responses) {
|
|
|
16691
16691
|
return { answered, total, avg_qualification_boost: avg, best_response_excerpt: excerpt };
|
|
16692
16692
|
}
|
|
16693
16693
|
function buildPullLeadsNextSteps(args) {
|
|
16694
|
-
const { leadCount, hasMore, nextPage, computingWishlist, computingScores } = args;
|
|
16695
|
-
if (
|
|
16694
|
+
const { leadCount: leadCount2, hasMore, nextPage, computingWishlist, computingScores } = args;
|
|
16695
|
+
if (leadCount2 <= 0) {
|
|
16696
16696
|
if (computingWishlist || computingScores) {
|
|
16697
16697
|
return {
|
|
16698
16698
|
question: "This lens is still warming up. What next?",
|
|
@@ -16937,15 +16937,15 @@ var pullLeads = {
|
|
|
16937
16937
|
const currentPage = res.pagination?.page ?? page;
|
|
16938
16938
|
const hasMore = currentPage < totalPages - 1;
|
|
16939
16939
|
const nextPage = hasMore ? currentPage + 1 : null;
|
|
16940
|
-
const
|
|
16940
|
+
const leadCount2 = res.items.length;
|
|
16941
16941
|
const nextSteps = buildPullLeadsNextSteps({
|
|
16942
|
-
leadCount,
|
|
16942
|
+
leadCount: leadCount2,
|
|
16943
16943
|
hasMore,
|
|
16944
16944
|
nextPage,
|
|
16945
16945
|
computingWishlist: res.computing_wishlist,
|
|
16946
16946
|
computingScores: res.computing_scores
|
|
16947
16947
|
});
|
|
16948
|
-
const lensIsEmpty =
|
|
16948
|
+
const lensIsEmpty = leadCount2 === 0 && (res.pagination?.total ?? 0) === 0;
|
|
16949
16949
|
const emptyReason = lensIsEmpty ? await diagnoseEmptyLens(client, lensId, {
|
|
16950
16950
|
wishlist: res.computing_wishlist,
|
|
16951
16951
|
scores: res.computing_scores
|
|
@@ -17373,13 +17373,34 @@ var followupsMap = {
|
|
|
17373
17373
|
var DEFAULT_FOLLOWUPS_COUNT = 6;
|
|
17374
17374
|
var DEFAULT_DISCOVER_COUNT = 6;
|
|
17375
17375
|
var DISCOVER_OVER_PULL = 30;
|
|
17376
|
-
function
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17381
|
-
|
|
17382
|
-
|
|
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" };
|
|
17383
17404
|
}
|
|
17384
17405
|
function toMapLocation(lead, mode) {
|
|
17385
17406
|
const pos = lead?.location?.pos;
|
|
@@ -17448,7 +17469,7 @@ var tourPlan = {
|
|
|
17448
17469
|
},
|
|
17449
17470
|
city_id: {
|
|
17450
17471
|
type: "string",
|
|
17451
|
-
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."
|
|
17452
17473
|
},
|
|
17453
17474
|
followups_count: {
|
|
17454
17475
|
type: "number",
|
|
@@ -17555,7 +17576,13 @@ var tourPlan = {
|
|
|
17555
17576
|
const discoverCount = params.discover_count ?? DEFAULT_DISCOVER_COUNT;
|
|
17556
17577
|
const [followupsResult, leadsResult] = await Promise.allSettled([
|
|
17557
17578
|
pullFollowups.execute(client, {
|
|
17558
|
-
|
|
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,
|
|
17559
17586
|
city_id: params.city_id,
|
|
17560
17587
|
count: followupsCount
|
|
17561
17588
|
}, ctx),
|
|
@@ -17569,7 +17596,7 @@ var tourPlan = {
|
|
|
17569
17596
|
location_ambiguities: r.location_ambiguities,
|
|
17570
17597
|
monitor_leads: [],
|
|
17571
17598
|
discover_leads: [],
|
|
17572
|
-
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.",
|
|
17573
17600
|
map_locations: [],
|
|
17574
17601
|
map_summary: {
|
|
17575
17602
|
total_leads: 0,
|
|
@@ -17593,13 +17620,27 @@ var tourPlan = {
|
|
|
17593
17620
|
if (leadsResult.status === "rejected") {
|
|
17594
17621
|
ctx?.logger?.warn?.(`tour_plan: pull_leads failed: ${leadsResult.reason?.message ?? leadsResult.reason}`);
|
|
17595
17622
|
}
|
|
17596
|
-
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);
|
|
17597
17627
|
const discoverLeads2 = filtered.slice(0, discoverCount);
|
|
17598
17628
|
const pulledLensId = leadsResult.status === "fulfilled" ? leadsResult.value?.lens?.id : null;
|
|
17599
17629
|
if (pulledLensId != null) {
|
|
17600
17630
|
reportLeadInteractions(client, pulledLensId, discoverLeads2.map((l) => l.id), ["LEAD_SEEN"], ctx?.logger);
|
|
17601
17631
|
}
|
|
17602
|
-
|
|
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
|
+
}
|
|
17603
17644
|
return {
|
|
17604
17645
|
city: params.city ?? null,
|
|
17605
17646
|
city_id: params.city_id ?? null,
|
|
@@ -22957,27 +22998,161 @@ var bulkEnrichStatus = {
|
|
|
22957
22998
|
}
|
|
22958
22999
|
};
|
|
22959
23000
|
|
|
22960
|
-
// ../core/dist/composite/
|
|
22961
|
-
|
|
22962
|
-
|
|
22963
|
-
|
|
22964
|
-
|
|
23001
|
+
// ../core/dist/composite/_sector-resolver.js
|
|
23002
|
+
var FILLER = /* @__PURE__ */ new Set([
|
|
23003
|
+
// en
|
|
23004
|
+
"and",
|
|
23005
|
+
"of",
|
|
23006
|
+
"the",
|
|
23007
|
+
"for",
|
|
23008
|
+
"or",
|
|
23009
|
+
"activity",
|
|
23010
|
+
// fr
|
|
23011
|
+
"et",
|
|
23012
|
+
"de",
|
|
23013
|
+
"des",
|
|
23014
|
+
"du",
|
|
23015
|
+
"d",
|
|
23016
|
+
"la",
|
|
23017
|
+
"le",
|
|
23018
|
+
"les",
|
|
23019
|
+
"l",
|
|
23020
|
+
"en",
|
|
23021
|
+
"aux",
|
|
23022
|
+
"au",
|
|
23023
|
+
"activite",
|
|
23024
|
+
"pour",
|
|
23025
|
+
"sur",
|
|
23026
|
+
"par",
|
|
23027
|
+
"a"
|
|
23028
|
+
]);
|
|
23029
|
+
function normalizeSectorText(value) {
|
|
23030
|
+
return (value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^0-9a-z]+/g, " ").trim();
|
|
22965
23031
|
}
|
|
22966
|
-
function
|
|
22967
|
-
|
|
22968
|
-
|
|
22969
|
-
|
|
22970
|
-
|
|
22971
|
-
|
|
23032
|
+
function singular(token) {
|
|
23033
|
+
if (token.length > 4 && token.endsWith("ies"))
|
|
23034
|
+
return `${token.slice(0, -3)}y`;
|
|
23035
|
+
return token.length > 3 && token.endsWith("s") ? token.slice(0, -1) : token;
|
|
23036
|
+
}
|
|
23037
|
+
function sectorTokens(value) {
|
|
23038
|
+
const all = normalizeSectorText(value).split(" ").filter(Boolean).map(singular);
|
|
23039
|
+
const core = all.filter((t) => !FILLER.has(t));
|
|
23040
|
+
return core.length > 0 ? core : all;
|
|
23041
|
+
}
|
|
23042
|
+
function sectorLabel(row) {
|
|
23043
|
+
if (!row)
|
|
23044
|
+
return "";
|
|
23045
|
+
const label = row.label;
|
|
23046
|
+
if (typeof label === "string" && label.length > 0)
|
|
23047
|
+
return label;
|
|
23048
|
+
return typeof row.name === "string" ? row.name : "";
|
|
23049
|
+
}
|
|
23050
|
+
function leadCount(row) {
|
|
23051
|
+
const n = row.number_of_leads;
|
|
23052
|
+
return typeof n === "number" && Number.isFinite(n) ? n : 0;
|
|
23053
|
+
}
|
|
23054
|
+
var MAX_CANDIDATES = 8;
|
|
23055
|
+
var MAX_PARTIAL_CANDIDATES = 4;
|
|
23056
|
+
function rankCandidates(scored, tieBreak) {
|
|
23057
|
+
const out = [];
|
|
23058
|
+
const seen = /* @__PURE__ */ new Set();
|
|
23059
|
+
scored.sort((a, b) => b.score - a.score || (tieBreak === "breadth" ? leadCount(b.row) - leadCount(a.row) || b.closeness - a.closeness : b.closeness - a.closeness || leadCount(b.row) - leadCount(a.row)) || sectorLabel(a.row).length - sectorLabel(b.row).length).forEach(({ row, score }) => {
|
|
23060
|
+
const label = sectorLabel(row);
|
|
23061
|
+
if (!label || seen.has(label) || out.length >= MAX_CANDIDATES)
|
|
23062
|
+
return;
|
|
23063
|
+
seen.add(label);
|
|
23064
|
+
out.push({ id: row.id, name: label, score: Number(score.toFixed(2)) });
|
|
23065
|
+
});
|
|
23066
|
+
return out;
|
|
23067
|
+
}
|
|
23068
|
+
function matchSector(text3, taxonomy) {
|
|
23069
|
+
const wanted = normalizeSectorText(text3);
|
|
23070
|
+
if (!wanted)
|
|
23071
|
+
return { kind: "unresolved", candidates: [] };
|
|
23072
|
+
const exact = taxonomy.filter((row) => normalizeSectorText(sectorLabel(row)) === wanted);
|
|
23073
|
+
if (exact.length > 0) {
|
|
23074
|
+
const label = sectorLabel(exact.reduce((best, row) => leadCount(row) > leadCount(best) ? row : best));
|
|
23075
|
+
const ids = taxonomy.filter((row) => sectorLabel(row) === label).map((row) => row.id);
|
|
23076
|
+
return { kind: "resolved", label, ids, exact: true };
|
|
23077
|
+
}
|
|
23078
|
+
const want = new Set(sectorTokens(text3));
|
|
23079
|
+
const sameWords = taxonomy.filter((row) => {
|
|
23080
|
+
const have = new Set(sectorTokens(sectorLabel(row)));
|
|
23081
|
+
return have.size === want.size && [...want].every((token) => have.has(token));
|
|
23082
|
+
});
|
|
23083
|
+
const sameWordLabels = new Set(sameWords.map((row) => sectorLabel(row)).filter(Boolean));
|
|
23084
|
+
if (sameWordLabels.size === 1) {
|
|
23085
|
+
const label = [...sameWordLabels][0];
|
|
23086
|
+
const ids = taxonomy.filter((row) => sectorLabel(row) === label).map((row) => row.id);
|
|
23087
|
+
return { kind: "resolved", label, ids, exact: false };
|
|
23088
|
+
}
|
|
23089
|
+
const scored = [];
|
|
23090
|
+
for (const row of taxonomy) {
|
|
23091
|
+
const have = new Set(sectorTokens(sectorLabel(row)));
|
|
23092
|
+
if (have.size === 0)
|
|
23093
|
+
continue;
|
|
22972
23094
|
let overlap = 0;
|
|
22973
|
-
for (const
|
|
22974
|
-
if (have.has(
|
|
23095
|
+
for (const token of want)
|
|
23096
|
+
if (have.has(token))
|
|
22975
23097
|
overlap += 1;
|
|
22976
|
-
|
|
22977
|
-
|
|
22978
|
-
|
|
22979
|
-
|
|
23098
|
+
if (overlap === 0)
|
|
23099
|
+
continue;
|
|
23100
|
+
scored.push({ row, score: overlap / want.size, closeness: overlap / have.size });
|
|
23101
|
+
}
|
|
23102
|
+
const full = scored.filter((s) => s.score >= 1);
|
|
23103
|
+
return {
|
|
23104
|
+
kind: "unresolved",
|
|
23105
|
+
candidates: full.length > 0 ? rankCandidates(full, "breadth") : rankCandidates(scored, "closeness").slice(0, MAX_PARTIAL_CANDIDATES)
|
|
23106
|
+
};
|
|
23107
|
+
}
|
|
23108
|
+
function sectorSections(taxonomy) {
|
|
23109
|
+
const roots = taxonomy.filter((row) => !row.parent);
|
|
23110
|
+
const labels = [];
|
|
23111
|
+
for (const row of roots.sort((a, b) => leadCount(b) - leadCount(a))) {
|
|
23112
|
+
const label = sectorLabel(row);
|
|
23113
|
+
if (label && !labels.includes(label))
|
|
23114
|
+
labels.push(label);
|
|
23115
|
+
}
|
|
23116
|
+
return labels;
|
|
23117
|
+
}
|
|
23118
|
+
async function fetchSectorTaxonomy(client, ctx) {
|
|
23119
|
+
const me = await client.resolveMe().catch(() => null);
|
|
23120
|
+
const lang = me?.language ?? "en";
|
|
23121
|
+
const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
|
|
23122
|
+
const unlabelled = taxonomy.filter((row) => !sectorLabel(row)).length;
|
|
23123
|
+
if (unlabelled > 0) {
|
|
23124
|
+
ctx?.logger?.warn?.(`sector taxonomy: ${unlabelled}/${taxonomy.length} row(s) carry no label`);
|
|
23125
|
+
}
|
|
23126
|
+
return taxonomy;
|
|
23127
|
+
}
|
|
23128
|
+
var NUMERIC_ID = /^\d+$/;
|
|
23129
|
+
function resolveSectorValues(values, taxonomy) {
|
|
23130
|
+
const out = {
|
|
23131
|
+
values: [],
|
|
23132
|
+
rewritten: [],
|
|
23133
|
+
unresolved: [],
|
|
23134
|
+
sections: sectorSections(taxonomy)
|
|
23135
|
+
};
|
|
23136
|
+
for (const raw of values) {
|
|
23137
|
+
const asked = typeof raw === "string" ? raw : String(raw ?? "");
|
|
23138
|
+
if (NUMERIC_ID.test(asked.trim())) {
|
|
23139
|
+
out.values.push(asked.trim());
|
|
23140
|
+
continue;
|
|
23141
|
+
}
|
|
23142
|
+
const match = matchSector(asked, taxonomy);
|
|
23143
|
+
if (match.kind === "resolved") {
|
|
23144
|
+
out.values.push(match.label);
|
|
23145
|
+
if (match.label !== asked)
|
|
23146
|
+
out.rewritten.push({ asked, used: match.label });
|
|
23147
|
+
continue;
|
|
23148
|
+
}
|
|
23149
|
+
out.values.push(asked);
|
|
23150
|
+
out.unresolved.push({ asked, closest: match.candidates });
|
|
23151
|
+
}
|
|
23152
|
+
return out;
|
|
22980
23153
|
}
|
|
23154
|
+
|
|
23155
|
+
// ../core/dist/composite/adjust-audience.js
|
|
22981
23156
|
async function resolveSectors(client, texts, ctx) {
|
|
22982
23157
|
const looksLikeId2 = (s) => /^\d+$/.test(s);
|
|
22983
23158
|
const direct = texts.filter(looksLikeId2);
|
|
@@ -22987,18 +23162,18 @@ async function resolveSectors(client, texts, ctx) {
|
|
|
22987
23162
|
const me = await client.resolveMe().catch(() => null);
|
|
22988
23163
|
const lang = me?.language ?? "en";
|
|
22989
23164
|
const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
|
|
22990
|
-
const nullNames = taxonomy.filter((s) => !s
|
|
23165
|
+
const nullNames = taxonomy.filter((s) => !sectorLabel(s)).length;
|
|
22991
23166
|
if (nullNames > 0) {
|
|
22992
23167
|
ctx?.logger?.warn?.(`adjust_audience: /sectors/all returned ${nullNames}/${taxonomy.length} sector(s) with a null/missing name`);
|
|
22993
23168
|
}
|
|
22994
23169
|
const resolved = [...direct];
|
|
22995
23170
|
const ambiguities = [];
|
|
22996
23171
|
for (const text3 of free) {
|
|
22997
|
-
const
|
|
22998
|
-
if (
|
|
22999
|
-
resolved.push(
|
|
23172
|
+
const match = matchSector(text3, taxonomy);
|
|
23173
|
+
if (match.kind === "resolved") {
|
|
23174
|
+
resolved.push(...match.ids);
|
|
23000
23175
|
} else {
|
|
23001
|
-
ambiguities.push({ sector_text: text3, matches });
|
|
23176
|
+
ambiguities.push({ sector_text: text3, matches: match.candidates });
|
|
23002
23177
|
}
|
|
23003
23178
|
}
|
|
23004
23179
|
return { resolved, ambiguities };
|
|
@@ -24293,6 +24468,61 @@ function sortFilterLists(filters) {
|
|
|
24293
24468
|
}
|
|
24294
24469
|
return out;
|
|
24295
24470
|
}
|
|
24471
|
+
function isSectorRejection(err) {
|
|
24472
|
+
const e = err;
|
|
24473
|
+
return !!e && e.code === "BAD_INPUT" && typeof e.message === "string" && e.message.includes("filters.sectors");
|
|
24474
|
+
}
|
|
24475
|
+
async function submitSearch(client, body, opts, ctx) {
|
|
24476
|
+
try {
|
|
24477
|
+
return {
|
|
24478
|
+
ok: true,
|
|
24479
|
+
data: await client.request("POST", "/mcp/search", body, opts),
|
|
24480
|
+
rewritten: []
|
|
24481
|
+
};
|
|
24482
|
+
} catch (err) {
|
|
24483
|
+
const asked = body.filters?.sectors;
|
|
24484
|
+
if (!isSectorRejection(err) || !Array.isArray(asked) || asked.length === 0) {
|
|
24485
|
+
throw err;
|
|
24486
|
+
}
|
|
24487
|
+
const taxonomy = await fetchSectorTaxonomy(client, ctx).catch(() => {
|
|
24488
|
+
throw err;
|
|
24489
|
+
});
|
|
24490
|
+
const fix = resolveSectorValues(asked, taxonomy);
|
|
24491
|
+
if (fix.unresolved.length === 0 && fix.rewritten.length === 0)
|
|
24492
|
+
throw err;
|
|
24493
|
+
if (fix.unresolved.length > 0)
|
|
24494
|
+
return { ok: false, choice: sectorChoice(fix) };
|
|
24495
|
+
const retried = { ...body, filters: { ...body.filters, sectors: fix.values } };
|
|
24496
|
+
return {
|
|
24497
|
+
ok: true,
|
|
24498
|
+
data: await client.request("POST", "/mcp/search", retried, opts),
|
|
24499
|
+
rewritten: fix.rewritten
|
|
24500
|
+
};
|
|
24501
|
+
}
|
|
24502
|
+
}
|
|
24503
|
+
function sectorNote(rewritten) {
|
|
24504
|
+
if (rewritten.length === 0)
|
|
24505
|
+
return void 0;
|
|
24506
|
+
const pairs = rewritten.map((r) => `"${r.asked}" \u2192 "${r.used}"`).join(", ");
|
|
24507
|
+
return {
|
|
24508
|
+
sectors_note: `Leadbay's sector taxonomy spells these differently, so the search ran on ${pairs}. Tell the user which sector was searched.`,
|
|
24509
|
+
sectors_used: rewritten
|
|
24510
|
+
};
|
|
24511
|
+
}
|
|
24512
|
+
function sectorChoice(fix) {
|
|
24513
|
+
const names = fix.unresolved.map((u) => `"${u.asked}"`).join(", ");
|
|
24514
|
+
return {
|
|
24515
|
+
mode: "needs_sector_choice",
|
|
24516
|
+
submitted: false,
|
|
24517
|
+
unresolved_sectors: fix.unresolved.map((u) => ({
|
|
24518
|
+
asked: u.asked,
|
|
24519
|
+
closest: u.closest.map((c) => c.name)
|
|
24520
|
+
})),
|
|
24521
|
+
sector_sections: fix.sections,
|
|
24522
|
+
resolved_sectors: fix.rewritten,
|
|
24523
|
+
hint: `No sector in this workspace's taxonomy is named ${names || "that"}. Nothing was submitted and nothing was spent. Re-call with a label copied character for character from \`closest\` or \`sector_sections\` (a section covers everything under it), or drop \`filters.sectors\` and put the wording in \`query\` / \`example_lead.description\` instead \u2014 the search then RANKS on it rather than fencing on it, which is usually what the user meant. \`leadbay_list_sectors\` returns the full taxonomy if none of these fit.`
|
|
24524
|
+
};
|
|
24525
|
+
}
|
|
24296
24526
|
var findNewLeads = {
|
|
24297
24527
|
name: "leadbay_find_new_leads",
|
|
24298
24528
|
annotations: {
|
|
@@ -24331,7 +24561,7 @@ var findNewLeads = {
|
|
|
24331
24561
|
},
|
|
24332
24562
|
filters: {
|
|
24333
24563
|
type: "object",
|
|
24334
|
-
description: "HARD constraints (the seed only shapes ranking). Sector/location labels resolve at submit;
|
|
24564
|
+
description: "HARD constraints (the seed only shapes ranking). Sector/location labels resolve at submit. `sectors` must be a label from Leadbay's own taxonomy (leadbay_list_sectors) \u2014 everyday wording like 'Professional Services' is not one; a spelling/plural slip is corrected for you, but a word with no label comes back as mode:'needs_sector_choice' with the labels to pick from. An unresolvable location is a 400 naming it.",
|
|
24335
24565
|
properties: {
|
|
24336
24566
|
sectors: { type: "array", items: { type: "string" } },
|
|
24337
24567
|
locations: { type: "array", items: { type: "string" } },
|
|
@@ -24503,19 +24733,27 @@ var findNewLeads = {
|
|
|
24503
24733
|
dry_run: dryRun
|
|
24504
24734
|
});
|
|
24505
24735
|
if (dryRun === true) {
|
|
24506
|
-
const
|
|
24736
|
+
const attempt2 = await submitSearch(client, body, void 0, ctx);
|
|
24737
|
+
if (!attempt2.ok)
|
|
24738
|
+
return { ...attempt2.choice, ...countryNote, region: client.region };
|
|
24507
24739
|
return {
|
|
24508
24740
|
dry_run: true,
|
|
24509
|
-
...
|
|
24741
|
+
...attempt2.data,
|
|
24742
|
+
...sectorNote(attempt2.rewritten),
|
|
24510
24743
|
...countryNote,
|
|
24511
24744
|
region: client.region
|
|
24512
24745
|
};
|
|
24513
24746
|
}
|
|
24514
24747
|
if (isPaid && !consented) {
|
|
24515
|
-
|
|
24516
|
-
|
|
24517
|
-
|
|
24518
|
-
|
|
24748
|
+
let forecast = null;
|
|
24749
|
+
let quoteRewritten = [];
|
|
24750
|
+
if (!vetoed) {
|
|
24751
|
+
const attempt2 = await submitSearch(client, { ...body, dry_run: true }, void 0, ctx);
|
|
24752
|
+
if (!attempt2.ok)
|
|
24753
|
+
return { ...attempt2.choice, ...countryNote, region: client.region };
|
|
24754
|
+
forecast = attempt2.data;
|
|
24755
|
+
quoteRewritten = attempt2.rewritten;
|
|
24756
|
+
}
|
|
24519
24757
|
return {
|
|
24520
24758
|
mode: "needs_confirmation",
|
|
24521
24759
|
submitted: false,
|
|
@@ -24528,11 +24766,15 @@ var findNewLeads = {
|
|
|
24528
24766
|
estimated_cost: forecast?.estimated_cost ?? null,
|
|
24529
24767
|
items_requested: forecast?.items_requested ?? null,
|
|
24530
24768
|
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.",
|
|
24769
|
+
...sectorNote(quoteRewritten),
|
|
24531
24770
|
...countryNote,
|
|
24532
24771
|
region: client.region
|
|
24533
24772
|
};
|
|
24534
24773
|
}
|
|
24535
|
-
const
|
|
24774
|
+
const attempt = await submitSearch(client, body, { preSendSignal: ctx?.signal }, ctx);
|
|
24775
|
+
if (!attempt.ok)
|
|
24776
|
+
return { ...attempt.choice, ...countryNote, region: client.region };
|
|
24777
|
+
const submit = attempt.data;
|
|
24536
24778
|
const mocked = mockedSubmitPreview(submit, "leadbay_find_new_leads", client.region);
|
|
24537
24779
|
if (mocked)
|
|
24538
24780
|
return mocked;
|
|
@@ -24583,6 +24825,7 @@ var findNewLeads = {
|
|
|
24583
24825
|
since: snapshot.next_since ?? null,
|
|
24584
24826
|
suggested_wait_seconds: done ? 0 : 60
|
|
24585
24827
|
},
|
|
24828
|
+
...sectorNote(attempt.rewritten),
|
|
24586
24829
|
...countryNote,
|
|
24587
24830
|
region: client.region
|
|
24588
24831
|
};
|
|
@@ -26064,8 +26307,8 @@ var reportOutreach = {
|
|
|
26064
26307
|
if (!params.dry_run && params.verification.source === "user_confirmed" && typeof ctx?.elicit === "function") {
|
|
26065
26308
|
try {
|
|
26066
26309
|
const targetIds = params.lead_ids ?? [params.lead_id];
|
|
26067
|
-
const
|
|
26068
|
-
const elicitMsg =
|
|
26310
|
+
const leadCount2 = targetIds.length;
|
|
26311
|
+
const elicitMsg = leadCount2 === 1 ? `An AI agent wants to log outreach on lead ${targetIds[0]}: "${params.note}". The agent claims you confirmed this. Type your literal confirmation to proceed; cancel to reject.` : `An AI agent wants to log outreach on ${leadCount2} leads: "${params.note}". The agent claims you confirmed this. Type your literal confirmation to proceed; cancel to reject.`;
|
|
26069
26312
|
const result = await ctx.elicit({
|
|
26070
26313
|
message: elicitMsg,
|
|
26071
26314
|
requestedSchema: {
|
|
@@ -28619,7 +28862,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
28619
28862
|
}
|
|
28620
28863
|
|
|
28621
28864
|
// src/http-server.ts
|
|
28622
|
-
var VERSION = true ? "0.39.
|
|
28865
|
+
var VERSION = true ? "0.39.3" : "0.0.0-dev";
|
|
28623
28866
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
28624
28867
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
28625
28868
|
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.3",
|
|
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",
|