@leadbay/mcp 0.39.5 → 0.39.8
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 +184 -57
- package/dist/http-server.js +182 -55
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -2996,7 +2996,7 @@ Pick the 2-3 options that match what actually happened \u2014 never all seven:
|
|
|
2996
2996
|
|
|
2997
2997
|
| Observation | Suggest | Calls |
|
|
2998
2998
|
|---|---|---|
|
|
2999
|
-
| Job still running (\`still_running: true\`) | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds:
|
|
2999
|
+
| Job still running (\`still_running: true\`) | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds: 45) |
|
|
3000
3000
|
| Free run delivered on-profile leads | "Qualify these N against your criteria (uses quota \u2014 \`dry_run\` first)" | leadbay_qualify_leads(prior_deliveries: {job_id}) |
|
|
3001
3001
|
| Delivered leads look right | "Draft outreach for the top ones" | leadbay_prepare_outreach |
|
|
3002
3002
|
| Delivered 0 or off-profile | "Reshape the example and retry" (name the fix from funnel + scope_notes) | leadbay_find_new_leads (NEW request_id) |
|
|
@@ -3992,7 +3992,7 @@ Trigger phrases: "is the lead search done", "any results yet on that job", "chec
|
|
|
3992
3992
|
|
|
3993
3993
|
Do NOT use for: "is the enrichment done" \u2192 \`leadbay_bulk_enrich_status\`; "is my import done" \u2192 \`leadbay_import_status\`; "is the top-N qualification done" \u2192 \`leadbay_qualify_status\`.
|
|
3994
3994
|
|
|
3995
|
-
Prefer when: a find_new_leads / qualify_leads result carried next_poll \u2014 pass its job_id; use wait_seconds
|
|
3995
|
+
Prefer when: a find_new_leads / qualify_leads result carried next_poll \u2014 pass its job_id; use wait_seconds 45 when the user asked to wait for results.
|
|
3996
3996
|
|
|
3997
3997
|
Examples that SHOULD invoke this tool:
|
|
3998
3998
|
- "Any leads yet from that search you started?"
|
|
@@ -4019,7 +4019,7 @@ usage counters (never render them), and the \`explain\` block
|
|
|
4019
4019
|
(basis, seed strategy, scope notes). Items are immutable once emitted \u2014
|
|
4020
4020
|
polling never re-reads live data, so numbers only ever grow.
|
|
4021
4021
|
|
|
4022
|
-
\`wait_seconds: 0\` (default) answers instantly; set
|
|
4022
|
+
\`wait_seconds: 0\` (default) answers instantly; set 45 (the maximum) to block-wait for
|
|
4023
4023
|
completion when the user asked for results "in this reply". \`since\` (from a
|
|
4024
4024
|
prior poll's \`next_since\`) pages only the new items. Jobs terminalize
|
|
4025
4025
|
server-side: past the 30-min wall clock a job reads \`completed_partial\`
|
|
@@ -4201,7 +4201,7 @@ is a status tool, keep it terse:
|
|
|
4201
4201
|
|
|
4202
4202
|
| Observation | Suggest | Calls |
|
|
4203
4203
|
|---|---|---|
|
|
4204
|
-
| Still running | "Keep waiting (~1 min) or leave it \u2014 results are kept 30 days" | leadbay_lead_job_status(job_id, wait_seconds:
|
|
4204
|
+
| Still running | "Keep waiting (~1 min) or leave it \u2014 results are kept 30 days" | leadbay_lead_job_status(job_id, wait_seconds: 45) |
|
|
4205
4205
|
| Terminal (completed / partial / failed) | Render the delivery per the RENDERING block, then offer the matching find_new_leads / qualify_leads NEXT STEPS | \u2014 |
|
|
4206
4206
|
| \`expired\` (past the 30-day window) | "Re-read the delivered leads from your delivery ledger" \u2014 there is nothing left to render: the job terminalized and its items are no longer listed, so do NOT present an empty delivery as a result | leadbay_qualify_leads(prior_deliveries: {job_id}) |
|
|
4207
4207
|
`;
|
|
@@ -5541,7 +5541,7 @@ Pick the 2-3 options that match what actually happened:
|
|
|
5541
5541
|
|
|
5542
5542
|
| Observation | Suggest | Calls |
|
|
5543
5543
|
|---|---|---|
|
|
5544
|
-
| Job still running | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds:
|
|
5544
|
+
| Job still running | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds: 45) |
|
|
5545
5545
|
| Fit leads with contacts delivered | "Draft outreach for the qualified ones" | leadbay_prepare_outreach |
|
|
5546
5546
|
| Items skipped \`not_in_universe\` | "Import those companies first, then re-qualify" | leadbay_import_leads \u2192 leadbay_qualify_leads |
|
|
5547
5547
|
| Items skipped \`low_confidence_identity\` | "Pick the right match" (show \`resolution.alternatives\`) | leadbay_qualify_leads with the chosen lead_id |
|
|
@@ -6169,13 +6169,11 @@ key. It survives a misspelled company name and is what turns "not in your
|
|
|
6169
6169
|
list" into an answer. With only a contact email, pass \`email\`: the company
|
|
6170
6170
|
domain is derived from it, consumer mailboxes ignored.
|
|
6171
6171
|
|
|
6172
|
-
|
|
6173
|
-
|
|
6174
|
-
|
|
6175
|
-
|
|
6176
|
-
|
|
6177
|
-
found it \u2014 \`website\` or \`registry_number\`, both params. Ask for it and call
|
|
6178
|
-
again. Do not offer an import before asking.
|
|
6172
|
+
Both \`resolution\` answers below are successes, not cards. \`"ambiguous"\`
|
|
6173
|
+
carries \`candidates[]\`: ask which one, never guess from \`score\`.
|
|
6174
|
+
\`"not_found"\` carries \`summary\` + \`next_step\`: nobody has it, so say that and
|
|
6175
|
+
do what \`next_step\` says \u2014 usually ask for the param \`would_help\` names, then
|
|
6176
|
+
call again. Do not offer an import before asking.
|
|
6179
6177
|
|
|
6180
6178
|
---
|
|
6181
6179
|
|
|
@@ -6316,7 +6314,7 @@ out?"\`
|
|
|
6316
6314
|
|
|
6317
6315
|
When \`resolution\` is \`"ambiguous"\`, render no card: use \`ask_user_input_v0\`,
|
|
6318
6316
|
ONE \`single_select\` question ("Which one?"), one short label per candidate
|
|
6319
|
-
combining \`name\` and \`location\`.
|
|
6317
|
+
combining \`name\` and \`location\`. \`"not_found"\` renders no card either.
|
|
6320
6318
|
|
|
6321
6319
|
When \`_meta.match_candidates\` is non-empty, prepend one extra NEXT STEPS row:
|
|
6322
6320
|
|
|
@@ -7088,17 +7086,18 @@ WHEN NOT TO USE: the user wants a lead list (leadbay_pull_leads / leadbay_pull_f
|
|
|
7088
7086
|
`;
|
|
7089
7087
|
leadbay_tour_plan = `## WHEN TO USE
|
|
7090
7088
|
|
|
7091
|
-
Trigger phrases: "visiting <city> in <N> days", "I'm in <city> next week / Tuesday \u2014 who's worth meeting", "I'm going to <city> \u2014 who should I see", "who's worth meeting in <city>", "field tour in <city>", "plan a tour in <city>", "who should I meet in <city>", "customers plus prospects in <city>", "tour itinerary".
|
|
7089
|
+
Trigger phrases: "visiting <city> in <N> days", "I'm in <city> next week / Tuesday \u2014 who's worth meeting", "I'm going to <city> \u2014 who should I see", "who's worth meeting in <city>", "field tour in <city>", "plan a tour in <city>", "who should I meet in <city>", "prospects within <N> km of <city>", "<city> and the surrounding area", "customers plus prospects in <city>", "tour itinerary".
|
|
7092
7090
|
|
|
7093
7091
|
Do NOT use for: "follow-ups only, no new prospects" \u2192 \`leadbay_followups_map\`; "new leads only" \u2192 \`leadbay_pull_leads\`; "research one account" \u2192 \`leadbay_research_lead_by_id\`.
|
|
7094
7092
|
|
|
7095
|
-
Prefer when:
|
|
7093
|
+
Prefer when: known accounts plus new discoveries in one itinerary; pass \`radius_km\` when they name a radius; NEVER a country name, and do NOT omit \`city\`: a city-less tour is arbitrary nationwide leads, so ask which city or region
|
|
7096
7094
|
|
|
7097
7095
|
Examples that SHOULD invoke this tool:
|
|
7098
7096
|
- "I'm flying to Limoges in 4 days \u2014 give me 3 customers, 3 qualified prospects, and 3 new high-potential."
|
|
7099
7097
|
- "I'm in San Francisco next Tuesday. Who's worth meeting?"
|
|
7100
7098
|
- "Plan my tour next Tuesday in Lyon: known accounts plus discoveries."
|
|
7101
7099
|
- "Build a mixed itinerary for Berlin \u2014 I want both follow-ups and fresh leads."
|
|
7100
|
+
- "J'ai un rdv le 19 ao\xFBt \xE0 Colmar \u2014 trouve-moi les prospects dans un rayon de 10km."
|
|
7102
7101
|
|
|
7103
7102
|
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
7104
7103
|
- "Show me my follow-ups for the SF trip."
|
|
@@ -7153,6 +7152,8 @@ same instruction in its \`hint\`.
|
|
|
7153
7152
|
|
|
7154
7153
|
**Counts**: \`followups_count\` (default 6 \u2014 generous so the agent can split into "customers + qualified" client-side) and \`discover_count\` (default 6 after client-side geo filter). The composite over-pulls Discover (30 raw) because the wishlist endpoint has no server-side geo filter \u2014 it then keeps the leads whose own \`location.city\` names the requested city, and falls back to \`location.state\` only when no city matched (which is what a regional ask like "Texas" or "\xCEle-de-France" looks like). \`location.country\` is never consulted. \`discover_filter_note\` reports the ratio and which field carried it, so the agent can be honest about coverage. **When it says no Discover lead is in the city, say that** \u2014 return the Monitor half and offer \`leadbay_find_new_leads\` for that city. Never fill the gap with leads from elsewhere.
|
|
7155
7154
|
|
|
7155
|
+
**The next town over**: a tour is a day of driving, so after the town's own leads are found the composite adds the Discover leads whose own coordinates put them within **\`radius_km\` (default 20)** of it \u2014 West Sacramento on a tour of Sacramento, Courbevoie and Ivry-sur-Seine on a tour of Paris. The town's own leads always come first. **Pass the user's own number when they give one** ("dans un rayon de 10km autour de Colmar" \u2192 \`radius_km: 10\`; "within 15 miles" \u2192 \`radius_km: 24\`), and \`radius_km: 0\` to keep the tour strictly inside the named town. The radius applies to Discover leads only; the Monitor half is scoped server-side and is untouched. When \`discover_filter_note\` splits the stops into "N in '<city>' and M in <other towns>", **repeat that split** \u2014 say which town each nearby stop is actually in rather than presenting every stop as being in the city the user named.
|
|
7156
|
+
|
|
7156
7157
|
**What \`tour_plan\` does NOT do**: it doesn't persist the tour as a campaign artifact. To do that \u2014 create a "Limoges Tour \u2013 May 24" campaign and attach the selected accounts \u2014 chain into \`leadbay_create_campaign({lead_ids: [...selected_ids], name: 'Limoges Tour \u2013 <date>'})\` after the user picks. See the \`leadbay_plan_tour_in_city\` prompt for the full end-to-end orchestrator.
|
|
7157
7158
|
|
|
7158
7159
|
---
|
|
@@ -7793,7 +7794,7 @@ Pick the 2-3 options that match what actually happened \u2014 never all seven:
|
|
|
7793
7794
|
|
|
7794
7795
|
| Observation | Suggest | Calls |
|
|
7795
7796
|
|---|---|---|
|
|
7796
|
-
| Job still running (\`still_running: true\`) | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds:
|
|
7797
|
+
| Job still running (\`still_running: true\`) | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds: 45) |
|
|
7797
7798
|
| Free run delivered on-profile leads | "Qualify these N against your criteria (uses quota \u2014 \`dry_run\` first)" | leadbay_qualify_leads(prior_deliveries: {job_id}) |
|
|
7798
7799
|
| Delivered leads look right | "Draft outreach for the top ones" | leadbay_prepare_outreach |
|
|
7799
7800
|
| Delivered 0 or off-profile | "Reshape the example and retry" (name the fix from funnel + scope_notes) | leadbay_find_new_leads (NEW request_id) |
|
|
@@ -15370,23 +15371,75 @@ function stateFieldOf(lead) {
|
|
|
15370
15371
|
const state = lead?.location?.state;
|
|
15371
15372
|
return typeof state === "string" ? normalizeGeo(state) : "";
|
|
15372
15373
|
}
|
|
15373
|
-
function
|
|
15374
|
+
function posOf(lead) {
|
|
15375
|
+
const pos = lead?.location?.pos;
|
|
15376
|
+
const valid = Array.isArray(pos) && pos.length === 2 && pos.every((n) => typeof n === "number" && Number.isFinite(n));
|
|
15377
|
+
return valid ? [pos[0], pos[1]] : null;
|
|
15378
|
+
}
|
|
15379
|
+
function distanceKm(a, b) {
|
|
15380
|
+
const toRad = (d) => d * Math.PI / 180;
|
|
15381
|
+
const dLat = toRad(b[0] - a[0]);
|
|
15382
|
+
const dLng = toRad(b[1] - a[1]);
|
|
15383
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(a[0])) * Math.cos(toRad(b[0])) * Math.sin(dLng / 2) ** 2;
|
|
15384
|
+
return 2 * 6371 * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
15385
|
+
}
|
|
15386
|
+
function withinRadius(leads, anchors, radiusKm) {
|
|
15387
|
+
if (anchors.length === 0 || !(radiusKm > 0))
|
|
15388
|
+
return [];
|
|
15389
|
+
return leads.filter((l) => {
|
|
15390
|
+
const p = posOf(l);
|
|
15391
|
+
return p !== null && anchors.some((a) => distanceKm(a, p) <= radiusKm);
|
|
15392
|
+
});
|
|
15393
|
+
}
|
|
15394
|
+
function filterDiscoverByCity(leads, cityHint, monitorLeads, radiusKm) {
|
|
15374
15395
|
const hint = cityHint ? cityHintCore(cityHint) : { name: "", isCity: false };
|
|
15375
15396
|
if (!hint.name)
|
|
15376
|
-
return { leads, matchedOn: null };
|
|
15397
|
+
return { leads, matchedOn: null, nearby: [], anchored: false };
|
|
15377
15398
|
const cityOf = (l) => typeof l?.location?.city === "string" ? townName(l.location.city) : "";
|
|
15399
|
+
const positions = (ls) => ls.map(posOf).filter((p) => p !== null);
|
|
15378
15400
|
const byCity = leads.filter((l) => cityOf(l) === hint.name);
|
|
15379
|
-
|
|
15380
|
-
|
|
15381
|
-
|
|
15382
|
-
|
|
15383
|
-
|
|
15384
|
-
|
|
15401
|
+
const anchors = positions([
|
|
15402
|
+
...byCity,
|
|
15403
|
+
...monitorLeads.filter((l) => cityOf(l) === hint.name)
|
|
15404
|
+
]);
|
|
15405
|
+
if (byCity.length > 0) {
|
|
15406
|
+
const inTown = new Set(byCity);
|
|
15407
|
+
const nearby2 = withinRadius(leads.filter((l) => !inTown.has(l)), anchors, radiusKm);
|
|
15408
|
+
return { leads: [...byCity, ...nearby2], matchedOn: "city", nearby: nearby2, anchored: true };
|
|
15409
|
+
}
|
|
15410
|
+
const anchored = anchors.length > 0;
|
|
15411
|
+
if (!hint.isCity) {
|
|
15412
|
+
const byState = leads.filter((l) => stateFieldOf(l) === hint.name);
|
|
15413
|
+
if (byState.length > 0)
|
|
15414
|
+
return { leads: byState, matchedOn: "state", nearby: [], anchored };
|
|
15415
|
+
}
|
|
15416
|
+
const nearby = withinRadius(leads, anchors, radiusKm);
|
|
15417
|
+
if (nearby.length > 0)
|
|
15418
|
+
return { leads: nearby, matchedOn: "city", nearby, anchored };
|
|
15419
|
+
return {
|
|
15420
|
+
leads: [],
|
|
15421
|
+
matchedOn: hint.isCity ? "city" : "state",
|
|
15422
|
+
nearby: [],
|
|
15423
|
+
anchored
|
|
15424
|
+
};
|
|
15425
|
+
}
|
|
15426
|
+
function townsOf(leads) {
|
|
15427
|
+
const seen = [];
|
|
15428
|
+
let unnamed = 0;
|
|
15429
|
+
for (const l of leads) {
|
|
15430
|
+
const city = typeof l?.location?.city === "string" ? l.location.city.trim() : "";
|
|
15431
|
+
const name = city.replace(/^(?:City|Town|Village|Borough|Township|Municipality) of /i, "");
|
|
15432
|
+
if (!name)
|
|
15433
|
+
unnamed += 1;
|
|
15434
|
+
else if (!seen.includes(name))
|
|
15435
|
+
seen.push(name);
|
|
15436
|
+
}
|
|
15437
|
+
const rest = unnamed > 0 ? `${unnamed} whose record names no town` : "";
|
|
15438
|
+
return [seen.join(", "), rest].filter(Boolean).join(", plus ");
|
|
15385
15439
|
}
|
|
15386
15440
|
function toMapLocation(lead, mode) {
|
|
15387
|
-
const pos = lead
|
|
15388
|
-
|
|
15389
|
-
if (!valid)
|
|
15441
|
+
const pos = posOf(lead);
|
|
15442
|
+
if (pos === null)
|
|
15390
15443
|
return null;
|
|
15391
15444
|
const loc = lead.location;
|
|
15392
15445
|
const c = lead.recommended_contact;
|
|
@@ -15431,7 +15484,7 @@ function buildMap(monitorLeads, discoverLeads2) {
|
|
|
15431
15484
|
}
|
|
15432
15485
|
};
|
|
15433
15486
|
}
|
|
15434
|
-
var DEFAULT_FOLLOWUPS_COUNT, DEFAULT_DISCOVER_COUNT, DISCOVER_OVER_PULL, ADMIN_PREFIX, tourPlan;
|
|
15487
|
+
var DEFAULT_FOLLOWUPS_COUNT, DEFAULT_DISCOVER_COUNT, DISCOVER_OVER_PULL, DEFAULT_RADIUS_KM, ADMIN_PREFIX, tourPlan;
|
|
15435
15488
|
var init_tour_plan = __esm({
|
|
15436
15489
|
"../core/dist/composite/tour-plan.js"() {
|
|
15437
15490
|
"use strict";
|
|
@@ -15444,6 +15497,7 @@ var init_tour_plan = __esm({
|
|
|
15444
15497
|
DEFAULT_FOLLOWUPS_COUNT = 6;
|
|
15445
15498
|
DEFAULT_DISCOVER_COUNT = 6;
|
|
15446
15499
|
DISCOVER_OVER_PULL = 30;
|
|
15500
|
+
DEFAULT_RADIUS_KM = 20;
|
|
15447
15501
|
ADMIN_PREFIX = /^(?:city|town|village|borough|township|municipality) of /;
|
|
15448
15502
|
tourPlan = {
|
|
15449
15503
|
name: "leadbay_tour_plan",
|
|
@@ -15473,6 +15527,10 @@ var init_tour_plan = __esm({
|
|
|
15473
15527
|
discover_count: {
|
|
15474
15528
|
type: "number",
|
|
15475
15529
|
description: `Top-N Discover leads (active lens wishlist) to return after client-side city filter. Default ${DEFAULT_DISCOVER_COUNT}.`
|
|
15530
|
+
},
|
|
15531
|
+
radius_km: {
|
|
15532
|
+
type: "number",
|
|
15533
|
+
description: `How far outside the named town a Discover stop may be, in km. Default ${DEFAULT_RADIUS_KM}, which is roughly a metro area \u2014 it is what puts West Sacramento on a tour of Sacramento and Courbevoie on a tour of Paris. Pass the user's own number when they name one ("dans un rayon de 10km", "zone 20km", "within 15 miles" \u2192 24). Pass 0 to keep the tour inside the town's own name. Leads in the town itself always come first; the radius only adds to them. It applies to Discover leads only \u2014 the Monitor half is scoped server-side and is untouched.`
|
|
15476
15534
|
}
|
|
15477
15535
|
},
|
|
15478
15536
|
additionalProperties: false
|
|
@@ -15489,12 +15547,12 @@ var init_tour_plan = __esm({
|
|
|
15489
15547
|
},
|
|
15490
15548
|
discover_leads: {
|
|
15491
15549
|
type: "array",
|
|
15492
|
-
description: "Fresh Discover leads from the active lens, filtered client-side to match the city. Pulls a larger candidate set internally to compensate for the missing server-side geo filter.",
|
|
15550
|
+
description: "Fresh Discover leads from the active lens, filtered client-side to match the city, then extended with the ones whose own coordinates put them within `radius_km` of it. The town's own leads come first. Pulls a larger candidate set internally to compensate for the missing server-side geo filter.",
|
|
15493
15551
|
items: { type: "object" }
|
|
15494
15552
|
},
|
|
15495
15553
|
discover_filter_note: {
|
|
15496
15554
|
type: "string",
|
|
15497
|
-
description: "Human-readable summary of the client-side geo filter applied to Discover leads
|
|
15555
|
+
description: "Human-readable summary of the client-side geo filter applied to Discover leads. Says how many stops are in the named town, how many are within `radius_km` of it, and which towns those are in \u2014 repeat that split to the user rather than presenting every stop as being in the city they named."
|
|
15498
15556
|
},
|
|
15499
15557
|
map_locations: {
|
|
15500
15558
|
type: "array",
|
|
@@ -15618,12 +15676,21 @@ var init_tour_plan = __esm({
|
|
|
15618
15676
|
const cityName = params.city && !/^\d+$/.test(params.city.trim()) ? params.city : void 0;
|
|
15619
15677
|
const knownId = params.city_id ?? (cityName ? void 0 : params.city);
|
|
15620
15678
|
const idOnly = Boolean(knownId) && !cityName;
|
|
15621
|
-
const
|
|
15679
|
+
const radiusKm = params.radius_km ?? DEFAULT_RADIUS_KM;
|
|
15680
|
+
const { leads: filtered, matchedOn, nearby, anchored } = idOnly ? {
|
|
15681
|
+
leads: [],
|
|
15682
|
+
matchedOn: null,
|
|
15683
|
+
nearby: [],
|
|
15684
|
+
anchored: false
|
|
15685
|
+
} : filterDiscoverByCity(rawDiscover, cityName, monitorLeads, radiusKm);
|
|
15622
15686
|
const discoverLeads2 = filtered.slice(0, discoverCount);
|
|
15623
15687
|
const pulledLensId = leadsResult.status === "fulfilled" ? leadsResult.value?.lens?.id : null;
|
|
15624
15688
|
if (pulledLensId != null) {
|
|
15625
15689
|
reportLeadInteractions(client, pulledLensId, discoverLeads2.map((l) => l.id), ["LEAD_SEEN"], ctx?.logger);
|
|
15626
15690
|
}
|
|
15691
|
+
const nearbySet = new Set(nearby);
|
|
15692
|
+
const nearShown = discoverLeads2.filter((l) => nearbySet.has(l));
|
|
15693
|
+
const inTownShown = discoverLeads2.length - nearShown.length;
|
|
15627
15694
|
let filterNote;
|
|
15628
15695
|
if (idOnly) {
|
|
15629
15696
|
filterNote = `Discover leads need the NAME of the place, and this call passed an area id (${knownId}) and no name. Re-call with \`city\` set to the name of that area, keeping \`city_id\` so the Monitor half stays on the area you picked. The follow-ups below are already scoped to it.`;
|
|
@@ -15631,10 +15698,16 @@ var init_tour_plan = __esm({
|
|
|
15631
15698
|
filterNote = `No city filter applied; returning top ${discoverLeads2.length} Discover leads.`;
|
|
15632
15699
|
} else if (matchedOn === null) {
|
|
15633
15700
|
filterNote = `No usable city filter in '${params.city}'; returning top ${discoverLeads2.length} Discover leads.`;
|
|
15701
|
+
} else if (filtered.length === 0 && anchored && radiusKm > 0) {
|
|
15702
|
+
filterNote = `No Discover lead in the active lens names '${params.city}' as its town, and none is within ${radiusKm} km of the leads that do (checked ${rawDiscover.length} candidates). Say so; do NOT present leads from elsewhere as if they were in '${params.city}'.`;
|
|
15634
15703
|
} else if (filtered.length === 0) {
|
|
15635
15704
|
filterNote = `No Discover lead in the active lens is in '${params.city}' (checked ${rawDiscover.length} candidates by city, then by state/region). Say so; do NOT present leads from elsewhere as if they were in '${params.city}'.`;
|
|
15705
|
+
} else if (nearShown.length > 0 && inTownShown === 0) {
|
|
15706
|
+
filterNote = `No Discover lead in the active lens names '${params.city}' as its town; ${nearby.length}/${rawDiscover.length} are within ${radiusKm} km of it. Returning top ${discoverLeads2.length}, in ${townsOf(nearShown)}. Name the town each stop is actually in rather than calling them all '${params.city}'.`;
|
|
15707
|
+
} else if (nearShown.length > 0) {
|
|
15708
|
+
filterNote = `Matched ${filtered.length - nearby.length}/${rawDiscover.length} Discover leads to '${params.city}' by city, plus ${nearby.length} within ${radiusKm} km of it. Returning top ${discoverLeads2.length}: ${inTownShown} in '${params.city}' and ${nearShown.length} in ${townsOf(nearShown)}. Name the town each nearby stop is in.`;
|
|
15636
15709
|
} else {
|
|
15637
|
-
filterNote = `Matched ${filtered.length}/${rawDiscover.length} Discover leads to '${params.city}' by ${matchedOn === "city" ? "city" : "state/region"}; returning top ${discoverLeads2.length}.`;
|
|
15710
|
+
filterNote = `Matched ${filtered.length - nearby.length}/${rawDiscover.length} Discover leads to '${params.city}' by ${matchedOn === "city" ? "city" : "state/region"}; returning top ${discoverLeads2.length}.`;
|
|
15638
15711
|
}
|
|
15639
15712
|
return {
|
|
15640
15713
|
city: params.city ?? null,
|
|
@@ -17122,7 +17195,7 @@ var init_research_lead_by_name_fuzzy = __esm({
|
|
|
17122
17195
|
// of a research card.
|
|
17123
17196
|
outputSchema: {
|
|
17124
17197
|
type: "object",
|
|
17125
|
-
description: "Same shape as leadbay_research_lead_by_id, with _meta.resolved_from='companyName'|'resolver', _meta.resolved_query='<needle>', _meta.resolved_matched_on=[...], and _meta.match_candidates=[{leadId,name,score}] populated. When the registry resolver cannot pick one company, returns {resolution:'ambiguous', query, candidates:[{leadId,name,website,location,registry_ids,score,matched_on}]} instead \u2014 ask the user which one, then call leadbay_research_lead_by_id.",
|
|
17198
|
+
description: "Same shape as leadbay_research_lead_by_id, with _meta.resolved_from='companyName'|'resolver', _meta.resolved_query='<needle>', _meta.resolved_matched_on=[...], and _meta.match_candidates=[{leadId,name,score}] populated. When the registry resolver cannot pick one company, returns {resolution:'ambiguous', query, candidates:[{leadId,name,website,location,registry_ids,score,matched_on}]} instead \u2014 ask the user which one, then call leadbay_research_lead_by_id. When neither the user's leads nor the registry hold the company, returns {resolution:'not_found', query, summary, would_help, next_step} \u2014 a successful answer, not an error: do what next_step says, which is normally to ask the user for the field would_help names ('website' or 'registry_number') and call again. would_help is empty when the search was scoped to one lens, where the remedy is to drop the scope, not to ask the user for a field.",
|
|
17126
17199
|
additionalProperties: true
|
|
17127
17200
|
},
|
|
17128
17201
|
execute: async (client, params, ctx) => {
|
|
@@ -17136,7 +17209,18 @@ var init_research_lead_by_name_fuzzy = __esm({
|
|
|
17136
17209
|
if (scoped.length > 0) {
|
|
17137
17210
|
return await delegate(scoped, params.lensId);
|
|
17138
17211
|
}
|
|
17139
|
-
|
|
17212
|
+
return {
|
|
17213
|
+
resolution: "not_found",
|
|
17214
|
+
query,
|
|
17215
|
+
summary: `No lead matching "${query}" in lens ${params.lensId}`,
|
|
17216
|
+
would_help: [],
|
|
17217
|
+
next_step: "This lookup was intentionally restricted to the supplied lens. Omit lensId to search your visible leads across Discover, Monitor, and Activate and then the Leadbay company registry.",
|
|
17218
|
+
_meta: {
|
|
17219
|
+
region: client.region,
|
|
17220
|
+
lens_id: params.lensId,
|
|
17221
|
+
resolved_query: query
|
|
17222
|
+
}
|
|
17223
|
+
};
|
|
17140
17224
|
}
|
|
17141
17225
|
async function delegate(matches, fallbackLens) {
|
|
17142
17226
|
const [primary, ...rest] = matches;
|
|
@@ -17231,7 +17315,23 @@ var init_research_lead_by_name_fuzzy = __esm({
|
|
|
17231
17315
|
const wanted = resolved.type === "none" && resolved.would_help.length > 0 ? resolved.would_help : ["website", "registry_number"];
|
|
17232
17316
|
const asks = wanted.map((f) => f === "registry_number" ? "a registry number (SIREN/SIRET) for `registry_number`" : f === "website" ? "the company website for `website`" : `\`${f}\``).join(" or ");
|
|
17233
17317
|
const hint = resolved.type === "unidentifiable" ? `The registry could not identify a company from this input (${resolved.reason}). Ask the user for ${asks}, then call this tool again with it.` : `The registry found no company for what was supplied. It would match on ${asks}. Ask the user for that \u2014 "what's their website?" usually settles it \u2014 then call this tool again. Do not offer an import before asking.`;
|
|
17234
|
-
|
|
17318
|
+
if (!corpusSearched) {
|
|
17319
|
+
throw client.makeError("LEAD_NOT_FOUND", `No company matching "${query}" ${searched}`, `${hint} But this lookup did not complete \u2014 the lead search route was unreachable, so their own leads were never checked. Retry once before telling the user the company is absent.`, "POST /leads/resolve");
|
|
17320
|
+
}
|
|
17321
|
+
return {
|
|
17322
|
+
resolution: "not_found",
|
|
17323
|
+
query,
|
|
17324
|
+
resolver_payload: payload,
|
|
17325
|
+
summary: `No company matching "${query}" ${searched}`,
|
|
17326
|
+
would_help: wanted,
|
|
17327
|
+
next_step: hint,
|
|
17328
|
+
_meta: {
|
|
17329
|
+
region: client.region,
|
|
17330
|
+
resolved_from: "resolver",
|
|
17331
|
+
resolved_query: query,
|
|
17332
|
+
endpoint: "POST /leads/resolve"
|
|
17333
|
+
}
|
|
17334
|
+
};
|
|
17235
17335
|
}
|
|
17236
17336
|
};
|
|
17237
17337
|
}
|
|
@@ -21457,6 +21557,21 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes, toAddLocati
|
|
|
21457
21557
|
const items = current?.lens_filter?.items ?? [];
|
|
21458
21558
|
const item = items[0] ?? { criteria: [] };
|
|
21459
21559
|
const criteria = item.criteria ? [...item.criteria] : [];
|
|
21560
|
+
const dropIds = (type, fromExcluded, ids) => {
|
|
21561
|
+
if (ids.length === 0)
|
|
21562
|
+
return;
|
|
21563
|
+
const idx = criteria.findIndex((c) => c.type === type && !!c.is_excluded === fromExcluded);
|
|
21564
|
+
if (idx < 0)
|
|
21565
|
+
return;
|
|
21566
|
+
const cur = criteria[idx];
|
|
21567
|
+
const key = type === "sector_ids" ? "sectors" : "locations";
|
|
21568
|
+
const held = cur[key] ?? [];
|
|
21569
|
+
const kept = held.filter((s) => !ids.includes(s));
|
|
21570
|
+
if (kept.length === 0)
|
|
21571
|
+
criteria.splice(idx, 1);
|
|
21572
|
+
else
|
|
21573
|
+
criteria[idx] = { ...cur, [key]: kept };
|
|
21574
|
+
};
|
|
21460
21575
|
if (toAddSectors.length > 0) {
|
|
21461
21576
|
const idx = criteria.findIndex((c) => c.type === "sector_ids" && !c.is_excluded);
|
|
21462
21577
|
if (idx >= 0) {
|
|
@@ -21471,6 +21586,7 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes, toAddLocati
|
|
|
21471
21586
|
});
|
|
21472
21587
|
}
|
|
21473
21588
|
}
|
|
21589
|
+
dropIds("sector_ids", true, toAddSectors);
|
|
21474
21590
|
if (toExcludeSectors.length > 0) {
|
|
21475
21591
|
const idx = criteria.findIndex((c) => c.type === "sector_ids" && c.is_excluded);
|
|
21476
21592
|
if (idx >= 0) {
|
|
@@ -21485,6 +21601,7 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes, toAddLocati
|
|
|
21485
21601
|
});
|
|
21486
21602
|
}
|
|
21487
21603
|
}
|
|
21604
|
+
dropIds("sector_ids", false, toExcludeSectors);
|
|
21488
21605
|
if (toAddLocations.length > 0) {
|
|
21489
21606
|
const idx = criteria.findIndex((c) => c.type === "location_ids" && !c.is_excluded);
|
|
21490
21607
|
if (idx >= 0) {
|
|
@@ -21499,6 +21616,7 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes, toAddLocati
|
|
|
21499
21616
|
});
|
|
21500
21617
|
}
|
|
21501
21618
|
}
|
|
21619
|
+
dropIds("location_ids", true, toAddLocations);
|
|
21502
21620
|
if (toExcludeLocations.length > 0) {
|
|
21503
21621
|
const idx = criteria.findIndex((c) => c.type === "location_ids" && c.is_excluded);
|
|
21504
21622
|
if (idx >= 0) {
|
|
@@ -21513,6 +21631,7 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes, toAddLocati
|
|
|
21513
21631
|
});
|
|
21514
21632
|
}
|
|
21515
21633
|
}
|
|
21634
|
+
dropIds("location_ids", false, toExcludeLocations);
|
|
21516
21635
|
if (sizes && sizes.length > 0) {
|
|
21517
21636
|
const normalizedSizes = sizes.map((s) => ({
|
|
21518
21637
|
min: s.min ?? 0,
|
|
@@ -22673,12 +22792,15 @@ function normalizeSearchFilters(filters) {
|
|
|
22673
22792
|
}
|
|
22674
22793
|
return out;
|
|
22675
22794
|
}
|
|
22676
|
-
function clampWaitSeconds(requested, fallback) {
|
|
22677
|
-
|
|
22678
|
-
|
|
22679
|
-
|
|
22795
|
+
function clampWaitSeconds(requested, fallback, startedAtMs) {
|
|
22796
|
+
const asked = requested == null || Number.isNaN(requested) ? fallback : requested;
|
|
22797
|
+
const bounded = Math.min(Math.max(asked, 0), MAX_WAIT_SECONDS);
|
|
22798
|
+
if (startedAtMs == null)
|
|
22799
|
+
return bounded;
|
|
22800
|
+
const spentSeconds = (Date.now() - startedAtMs) / 1e3;
|
|
22801
|
+
return Math.max(0, Math.min(bounded, MAX_WAIT_SECONDS - spentSeconds));
|
|
22680
22802
|
}
|
|
22681
|
-
var TERMINAL_JOB_STATES, MCP_JOB_POLL, SNAPSHOT_TIMEOUT_MS, PAGE_LIMIT, MAX_JOB_ITEMS, MIN_PAGES, maxPagesFor, JOB_ID_CHARSET, MAX_JOB_ID_LENGTH, UUID_RE2, LEAD_REASON_CHARS, SIGNAL_CHARS, MAX_SIGNALS, MAX_ALTERNATIVE_CONTACTS, RESULT_CHARS, LEAN_ROW_CHARS, COUNTRY_ALIASES, SUBNATIONAL_EXEMPTIONS, ALL_EXEMPTIONS, COUNTRY_LOCATION_VALUES, MAX_EXCLUDE_LEAD_IDS, MAX_LEAD_REFS;
|
|
22803
|
+
var TERMINAL_JOB_STATES, MCP_JOB_POLL, SNAPSHOT_TIMEOUT_MS, PAGE_LIMIT, MAX_JOB_ITEMS, MIN_PAGES, maxPagesFor, JOB_ID_CHARSET, MAX_JOB_ID_LENGTH, UUID_RE2, LEAD_REASON_CHARS, SIGNAL_CHARS, MAX_SIGNALS, MAX_ALTERNATIVE_CONTACTS, RESULT_CHARS, LEAN_ROW_CHARS, COUNTRY_ALIASES, SUBNATIONAL_EXEMPTIONS, ALL_EXEMPTIONS, COUNTRY_LOCATION_VALUES, MAX_EXCLUDE_LEAD_IDS, MAX_LEAD_REFS, MAX_WAIT_SECONDS;
|
|
22682
22804
|
var init_mcp_job_helpers = __esm({
|
|
22683
22805
|
"../core/dist/composite/_mcp-job-helpers.js"() {
|
|
22684
22806
|
"use strict";
|
|
@@ -22742,6 +22864,7 @@ var init_mcp_job_helpers = __esm({
|
|
|
22742
22864
|
COUNTRY_LOCATION_VALUES = buildCountryLocationValues();
|
|
22743
22865
|
MAX_EXCLUDE_LEAD_IDS = 500;
|
|
22744
22866
|
MAX_LEAD_REFS = 500;
|
|
22867
|
+
MAX_WAIT_SECONDS = 45;
|
|
22745
22868
|
}
|
|
22746
22869
|
});
|
|
22747
22870
|
|
|
@@ -22931,13 +23054,14 @@ var init_find_new_leads = __esm({
|
|
|
22931
23054
|
},
|
|
22932
23055
|
wait_seconds: {
|
|
22933
23056
|
type: "number",
|
|
22934
|
-
description: "How long to poll before returning (default 45
|
|
23057
|
+
description: "How long to poll before returning (default and maximum 45 \u2014 the whole call is bounded by it, so a slow submit shortens the poll; 0 = submit + one snapshot). Free searches usually finish inside the window; qualified exploration can take minutes \u2014 the result then carries still_running:true and the job_id to check with leadbay_lead_job_status."
|
|
22935
23058
|
}
|
|
22936
23059
|
},
|
|
22937
23060
|
required: ["count", "request_id"],
|
|
22938
23061
|
additionalProperties: false
|
|
22939
23062
|
},
|
|
22940
23063
|
execute: async (client, params, ctx) => {
|
|
23064
|
+
const startedAt = Date.now();
|
|
22941
23065
|
params = coerceArrayParams(params, [
|
|
22942
23066
|
"contact_titles",
|
|
22943
23067
|
"channels",
|
|
@@ -23076,7 +23200,7 @@ var init_find_new_leads = __esm({
|
|
|
23076
23200
|
const mocked = mockedSubmitPreview(submit, "leadbay_find_new_leads", client.region);
|
|
23077
23201
|
if (mocked)
|
|
23078
23202
|
return mocked;
|
|
23079
|
-
const waitSeconds = clampWaitSeconds(params.wait_seconds, DEFAULT_WAIT_SECONDS);
|
|
23203
|
+
const waitSeconds = clampWaitSeconds(params.wait_seconds, DEFAULT_WAIT_SECONDS, startedAt);
|
|
23080
23204
|
const snapshot = await snapshotAfterSubmit(client, submit.job_id, waitSeconds, ctx, params.count);
|
|
23081
23205
|
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
23082
23206
|
const { leads, skipped } = splitItems(snapshot);
|
|
@@ -23121,7 +23245,7 @@ var init_find_new_leads = __esm({
|
|
|
23121
23245
|
// INCREMENTALLY instead of re-reading (and re-rendering) the
|
|
23122
23246
|
// rows already delivered in this response.
|
|
23123
23247
|
since: snapshot.next_since ?? null,
|
|
23124
|
-
suggested_wait_seconds: done ? 0 :
|
|
23248
|
+
suggested_wait_seconds: done ? 0 : MAX_WAIT_SECONDS
|
|
23125
23249
|
},
|
|
23126
23250
|
...sectorNote(attempt.rewritten),
|
|
23127
23251
|
...countryNote,
|
|
@@ -23246,7 +23370,7 @@ function identityAnswer(jobId, snapshot, items, offset) {
|
|
|
23246
23370
|
job_id: jobId,
|
|
23247
23371
|
compact: true,
|
|
23248
23372
|
offset: nextOffset,
|
|
23249
|
-
suggested_wait_seconds: done ? 0 :
|
|
23373
|
+
suggested_wait_seconds: done ? 0 : MAX_WAIT_SECONDS
|
|
23250
23374
|
}
|
|
23251
23375
|
};
|
|
23252
23376
|
}
|
|
@@ -23471,12 +23595,13 @@ var init_qualify_leads = __esm({
|
|
|
23471
23595
|
},
|
|
23472
23596
|
wait_seconds: {
|
|
23473
23597
|
type: "number",
|
|
23474
|
-
description: "How long to poll before returning (default 45
|
|
23598
|
+
description: "How long to poll before returning (default and maximum 45 \u2014 the whole call is bounded by it, so a slow submit shortens the poll; 0 = submit + one snapshot). Large or research-heavy batches can take minutes \u2014 the result then carries still_running:true and the job_id for leadbay_lead_job_status."
|
|
23475
23599
|
}
|
|
23476
23600
|
},
|
|
23477
23601
|
additionalProperties: false
|
|
23478
23602
|
},
|
|
23479
23603
|
execute: async (client, params, ctx) => {
|
|
23604
|
+
const startedAt = Date.now();
|
|
23480
23605
|
params = coerceArrayParams(params, [
|
|
23481
23606
|
"lead_refs",
|
|
23482
23607
|
"contact_titles",
|
|
@@ -23536,7 +23661,7 @@ var init_qualify_leads = __esm({
|
|
|
23536
23661
|
const mocked = mockedSubmitPreview(submit, "leadbay_qualify_leads", client.region);
|
|
23537
23662
|
if (mocked)
|
|
23538
23663
|
return mocked;
|
|
23539
|
-
const waitSeconds = clampWaitSeconds(params.wait_seconds, DEFAULT_WAIT_SECONDS2);
|
|
23664
|
+
const waitSeconds = clampWaitSeconds(params.wait_seconds, DEFAULT_WAIT_SECONDS2, startedAt);
|
|
23540
23665
|
const snapshot = await snapshotAfterSubmit(client, submit.job_id, waitSeconds, ctx, submit.items_requested);
|
|
23541
23666
|
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
23542
23667
|
const indexed = submit.duplicate ?? false ? remapInputIndexes(snapshot.items, params.lead_refs) : { items: snapshot.items, remapped: true };
|
|
@@ -23613,7 +23738,7 @@ var init_qualify_leads = __esm({
|
|
|
23613
23738
|
// INCREMENTALLY instead of re-reading (and re-rendering) the
|
|
23614
23739
|
// rows already delivered in this response.
|
|
23615
23740
|
since: snapshot.next_since ?? null,
|
|
23616
|
-
suggested_wait_seconds: done ? 0 :
|
|
23741
|
+
suggested_wait_seconds: done ? 0 : MAX_WAIT_SECONDS
|
|
23617
23742
|
},
|
|
23618
23743
|
region: client.region
|
|
23619
23744
|
};
|
|
@@ -23657,7 +23782,7 @@ var init_lead_job_status = __esm({
|
|
|
23657
23782
|
},
|
|
23658
23783
|
wait_seconds: {
|
|
23659
23784
|
type: "number",
|
|
23660
|
-
description: "0 (default) = instant snapshot. >0 = keep polling up to this many seconds until the job is terminal \u2014 use
|
|
23785
|
+
description: "0 (default) = instant snapshot. >0 = keep polling up to this many seconds (maximum 45) until the job is terminal \u2014 use 45 when the user asked to wait for results."
|
|
23661
23786
|
},
|
|
23662
23787
|
compact: {
|
|
23663
23788
|
type: "boolean",
|
|
@@ -23709,7 +23834,7 @@ var init_lead_job_status = __esm({
|
|
|
23709
23834
|
// Same incremental handoff as the submit tools — following
|
|
23710
23835
|
// next_poll without the cursor re-reads the rows just returned.
|
|
23711
23836
|
since: snapshot.next_since ?? null,
|
|
23712
|
-
suggested_wait_seconds: done ? 0 :
|
|
23837
|
+
suggested_wait_seconds: done ? 0 : MAX_WAIT_SECONDS
|
|
23713
23838
|
},
|
|
23714
23839
|
region: client.region
|
|
23715
23840
|
};
|
|
@@ -27129,7 +27254,7 @@ searched. Render the delivery table and judge fit honestly: are these the
|
|
|
27129
27254
|
kind of companies I asked for?
|
|
27130
27255
|
|
|
27131
27256
|
- **\`still_running: true\`** \u2192 the job is ALIVE. Do not judge the seed and do
|
|
27132
|
-
not relaunch \u2014 poll \`leadbay_lead_job_status\` (\`wait_seconds:
|
|
27257
|
+
not relaunch \u2014 poll \`leadbay_lead_job_status\` (\`wait_seconds: 45\`) until
|
|
27133
27258
|
it goes terminal, reporting progress. Relaunching now burns an active-job
|
|
27134
27259
|
slot and rate-limit budget on a search that may be about to deliver.
|
|
27135
27260
|
- **On-profile** (terminal) \u2192 offer Phase 4.
|
|
@@ -27187,7 +27312,7 @@ When I want qualification evidence and/or reachable contacts:
|
|
|
27187
27312
|
\`exploration_cap\` can examine more than that, so send the most recent 500
|
|
27188
27313
|
rejects rather than an over-long list the tool refuses outright.
|
|
27189
27314
|
3. While the job runs, poll with \`leadbay_lead_job_status\`
|
|
27190
|
-
(\`wait_seconds:
|
|
27315
|
+
(\`wait_seconds: 45\`); report progress, not silence.
|
|
27191
27316
|
|
|
27192
27317
|
# PHASE 5 \u2014 DELIVER
|
|
27193
27318
|
|
|
@@ -27260,7 +27385,7 @@ Split the returned \`monitor_leads\` into two buckets client-side using their en
|
|
|
27260
27385
|
|
|
27261
27386
|
\`discover_leads\` are the **New** bucket.
|
|
27262
27387
|
|
|
27263
|
-
Aim for a 3+3+3 split if possible. If the customers bucket has fewer than 3, fill from qualified. If discover_filter_note indicates a low match ratio for the city, mention it: "Only N/30 fresh leads matched your city" \u2014 better honest than padded.
|
|
27388
|
+
Aim for a 3+3+3 split if possible. If the customers bucket has fewer than 3, fill from qualified. If discover_filter_note indicates a low match ratio for the city, mention it: "Only N/30 fresh leads matched your city" \u2014 better honest than padded. When it splits the stops into the ones in the city and the ones within \`radius_km\` of it, repeat that split and name the town each nearby stop is in \u2014 a company in Courbevoie is a stop on a Paris day, but it is not in Paris. If I named a radius ("within 10 km", "rayon de 20km"), pass it as \`radius_km\`.
|
|
27264
27389
|
|
|
27265
27390
|
# PHASE 3 \u2014 PRESENT THE ITINERARY + OFFER THE MAP
|
|
27266
27391
|
|
|
@@ -27839,10 +27964,12 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
27839
27964
|
|
|
27840
27965
|
|
|
27841
27966
|
# PHASE 2 \u2014 NOT FOUND
|
|
27842
|
-
If the resolver returns \`
|
|
27843
|
-
|
|
27844
|
-
|
|
27845
|
-
|
|
27967
|
+
If the resolver returns \`resolution: "not_found"\`, that call SUCCEEDED \u2014 it is
|
|
27968
|
+
the answer, not a failure. Do what its \`next_step\` says. Usually that is to ask
|
|
27969
|
+
for the field \`would_help\` names (normally \`website\`): **ask the user for it
|
|
27970
|
+
first** \u2014 "what's their website?" \u2014 and call the tool again with it. When
|
|
27971
|
+
\`would_help\` is empty the search was scoped to one lens, and \`next_step\` says
|
|
27972
|
+
to drop the scope rather than to ask the user for anything. Only when they cannot supply it should you say both their leads and
|
|
27846
27973
|
the Leadbay registry were searched.
|
|
27847
27974
|
**Do NOT call \`leadbay_import_and_qualify\` automatically.**
|
|
27848
27975
|
Offer to import and qualify as a separate, explicit next step; only call it
|
|
@@ -32035,7 +32162,7 @@ var OAUTH_BASE_URLS = {
|
|
|
32035
32162
|
fr: "https://staging.api.leadbay.app"
|
|
32036
32163
|
}
|
|
32037
32164
|
};
|
|
32038
|
-
var VERSION = "0.39.
|
|
32165
|
+
var VERSION = "0.39.8";
|
|
32039
32166
|
var HELP = `
|
|
32040
32167
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
32041
32168
|
|
package/dist/http-server.js
CHANGED
|
@@ -1400,7 +1400,7 @@ searched. Render the delivery table and judge fit honestly: are these the
|
|
|
1400
1400
|
kind of companies I asked for?
|
|
1401
1401
|
|
|
1402
1402
|
- **\`still_running: true\`** \u2192 the job is ALIVE. Do not judge the seed and do
|
|
1403
|
-
not relaunch \u2014 poll \`leadbay_lead_job_status\` (\`wait_seconds:
|
|
1403
|
+
not relaunch \u2014 poll \`leadbay_lead_job_status\` (\`wait_seconds: 45\`) until
|
|
1404
1404
|
it goes terminal, reporting progress. Relaunching now burns an active-job
|
|
1405
1405
|
slot and rate-limit budget on a search that may be about to deliver.
|
|
1406
1406
|
- **On-profile** (terminal) \u2192 offer Phase 4.
|
|
@@ -1458,7 +1458,7 @@ When I want qualification evidence and/or reachable contacts:
|
|
|
1458
1458
|
\`exploration_cap\` can examine more than that, so send the most recent 500
|
|
1459
1459
|
rejects rather than an over-long list the tool refuses outright.
|
|
1460
1460
|
3. While the job runs, poll with \`leadbay_lead_job_status\`
|
|
1461
|
-
(\`wait_seconds:
|
|
1461
|
+
(\`wait_seconds: 45\`); report progress, not silence.
|
|
1462
1462
|
|
|
1463
1463
|
# PHASE 5 \u2014 DELIVER
|
|
1464
1464
|
|
|
@@ -1531,7 +1531,7 @@ Split the returned \`monitor_leads\` into two buckets client-side using their en
|
|
|
1531
1531
|
|
|
1532
1532
|
\`discover_leads\` are the **New** bucket.
|
|
1533
1533
|
|
|
1534
|
-
Aim for a 3+3+3 split if possible. If the customers bucket has fewer than 3, fill from qualified. If discover_filter_note indicates a low match ratio for the city, mention it: "Only N/30 fresh leads matched your city" \u2014 better honest than padded.
|
|
1534
|
+
Aim for a 3+3+3 split if possible. If the customers bucket has fewer than 3, fill from qualified. If discover_filter_note indicates a low match ratio for the city, mention it: "Only N/30 fresh leads matched your city" \u2014 better honest than padded. When it splits the stops into the ones in the city and the ones within \`radius_km\` of it, repeat that split and name the town each nearby stop is in \u2014 a company in Courbevoie is a stop on a Paris day, but it is not in Paris. If I named a radius ("within 10 km", "rayon de 20km"), pass it as \`radius_km\`.
|
|
1535
1535
|
|
|
1536
1536
|
# PHASE 3 \u2014 PRESENT THE ITINERARY + OFFER THE MAP
|
|
1537
1537
|
|
|
@@ -2110,10 +2110,12 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
2110
2110
|
|
|
2111
2111
|
|
|
2112
2112
|
# PHASE 2 \u2014 NOT FOUND
|
|
2113
|
-
If the resolver returns \`
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2113
|
+
If the resolver returns \`resolution: "not_found"\`, that call SUCCEEDED \u2014 it is
|
|
2114
|
+
the answer, not a failure. Do what its \`next_step\` says. Usually that is to ask
|
|
2115
|
+
for the field \`would_help\` names (normally \`website\`): **ask the user for it
|
|
2116
|
+
first** \u2014 "what's their website?" \u2014 and call the tool again with it. When
|
|
2117
|
+
\`would_help\` is empty the search was scoped to one lens, and \`next_step\` says
|
|
2118
|
+
to drop the scope rather than to ask the user for anything. Only when they cannot supply it should you say both their leads and
|
|
2117
2119
|
the Leadbay registry were searched.
|
|
2118
2120
|
**Do NOT call \`leadbay_import_and_qualify\` automatically.**
|
|
2119
2121
|
Offer to import and qualify as a separate, explicit next step; only call it
|
|
@@ -5944,7 +5946,7 @@ Pick the 2-3 options that match what actually happened \u2014 never all seven:
|
|
|
5944
5946
|
|
|
5945
5947
|
| Observation | Suggest | Calls |
|
|
5946
5948
|
|---|---|---|
|
|
5947
|
-
| Job still running (\`still_running: true\`) | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds:
|
|
5949
|
+
| Job still running (\`still_running: true\`) | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds: 45) |
|
|
5948
5950
|
| Free run delivered on-profile leads | "Qualify these N against your criteria (uses quota \u2014 \`dry_run\` first)" | leadbay_qualify_leads(prior_deliveries: {job_id}) |
|
|
5949
5951
|
| Delivered leads look right | "Draft outreach for the top ones" | leadbay_prepare_outreach |
|
|
5950
5952
|
| Delivered 0 or off-profile | "Reshape the example and retry" (name the fix from funnel + scope_notes) | leadbay_find_new_leads (NEW request_id) |
|
|
@@ -6940,7 +6942,7 @@ Trigger phrases: "is the lead search done", "any results yet on that job", "chec
|
|
|
6940
6942
|
|
|
6941
6943
|
Do NOT use for: "is the enrichment done" \u2192 \`leadbay_bulk_enrich_status\`; "is my import done" \u2192 \`leadbay_import_status\`; "is the top-N qualification done" \u2192 \`leadbay_qualify_status\`.
|
|
6942
6944
|
|
|
6943
|
-
Prefer when: a find_new_leads / qualify_leads result carried next_poll \u2014 pass its job_id; use wait_seconds
|
|
6945
|
+
Prefer when: a find_new_leads / qualify_leads result carried next_poll \u2014 pass its job_id; use wait_seconds 45 when the user asked to wait for results.
|
|
6944
6946
|
|
|
6945
6947
|
Examples that SHOULD invoke this tool:
|
|
6946
6948
|
- "Any leads yet from that search you started?"
|
|
@@ -6967,7 +6969,7 @@ usage counters (never render them), and the \`explain\` block
|
|
|
6967
6969
|
(basis, seed strategy, scope notes). Items are immutable once emitted \u2014
|
|
6968
6970
|
polling never re-reads live data, so numbers only ever grow.
|
|
6969
6971
|
|
|
6970
|
-
\`wait_seconds: 0\` (default) answers instantly; set
|
|
6972
|
+
\`wait_seconds: 0\` (default) answers instantly; set 45 (the maximum) to block-wait for
|
|
6971
6973
|
completion when the user asked for results "in this reply". \`since\` (from a
|
|
6972
6974
|
prior poll's \`next_since\`) pages only the new items. Jobs terminalize
|
|
6973
6975
|
server-side: past the 30-min wall clock a job reads \`completed_partial\`
|
|
@@ -7149,7 +7151,7 @@ is a status tool, keep it terse:
|
|
|
7149
7151
|
|
|
7150
7152
|
| Observation | Suggest | Calls |
|
|
7151
7153
|
|---|---|---|
|
|
7152
|
-
| Still running | "Keep waiting (~1 min) or leave it \u2014 results are kept 30 days" | leadbay_lead_job_status(job_id, wait_seconds:
|
|
7154
|
+
| Still running | "Keep waiting (~1 min) or leave it \u2014 results are kept 30 days" | leadbay_lead_job_status(job_id, wait_seconds: 45) |
|
|
7153
7155
|
| Terminal (completed / partial / failed) | Render the delivery per the RENDERING block, then offer the matching find_new_leads / qualify_leads NEXT STEPS | \u2014 |
|
|
7154
7156
|
| \`expired\` (past the 30-day window) | "Re-read the delivered leads from your delivery ledger" \u2014 there is nothing left to render: the job terminalized and its items are no longer listed, so do NOT present an empty delivery as a result | leadbay_qualify_leads(prior_deliveries: {job_id}) |
|
|
7155
7157
|
`;
|
|
@@ -8489,7 +8491,7 @@ Pick the 2-3 options that match what actually happened:
|
|
|
8489
8491
|
|
|
8490
8492
|
| Observation | Suggest | Calls |
|
|
8491
8493
|
|---|---|---|
|
|
8492
|
-
| Job still running | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds:
|
|
8494
|
+
| Job still running | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds: 45) |
|
|
8493
8495
|
| Fit leads with contacts delivered | "Draft outreach for the qualified ones" | leadbay_prepare_outreach |
|
|
8494
8496
|
| Items skipped \`not_in_universe\` | "Import those companies first, then re-qualify" | leadbay_import_leads \u2192 leadbay_qualify_leads |
|
|
8495
8497
|
| Items skipped \`low_confidence_identity\` | "Pick the right match" (show \`resolution.alternatives\`) | leadbay_qualify_leads with the chosen lead_id |
|
|
@@ -9117,13 +9119,11 @@ key. It survives a misspelled company name and is what turns "not in your
|
|
|
9117
9119
|
list" into an answer. With only a contact email, pass \`email\`: the company
|
|
9118
9120
|
domain is derived from it, consumer mailboxes ignored.
|
|
9119
9121
|
|
|
9120
|
-
|
|
9121
|
-
|
|
9122
|
-
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
found it \u2014 \`website\` or \`registry_number\`, both params. Ask for it and call
|
|
9126
|
-
again. Do not offer an import before asking.
|
|
9122
|
+
Both \`resolution\` answers below are successes, not cards. \`"ambiguous"\`
|
|
9123
|
+
carries \`candidates[]\`: ask which one, never guess from \`score\`.
|
|
9124
|
+
\`"not_found"\` carries \`summary\` + \`next_step\`: nobody has it, so say that and
|
|
9125
|
+
do what \`next_step\` says \u2014 usually ask for the param \`would_help\` names, then
|
|
9126
|
+
call again. Do not offer an import before asking.
|
|
9127
9127
|
|
|
9128
9128
|
---
|
|
9129
9129
|
|
|
@@ -9264,7 +9264,7 @@ out?"\`
|
|
|
9264
9264
|
|
|
9265
9265
|
When \`resolution\` is \`"ambiguous"\`, render no card: use \`ask_user_input_v0\`,
|
|
9266
9266
|
ONE \`single_select\` question ("Which one?"), one short label per candidate
|
|
9267
|
-
combining \`name\` and \`location\`.
|
|
9267
|
+
combining \`name\` and \`location\`. \`"not_found"\` renders no card either.
|
|
9268
9268
|
|
|
9269
9269
|
When \`_meta.match_candidates\` is non-empty, prepend one extra NEXT STEPS row:
|
|
9270
9270
|
|
|
@@ -10036,17 +10036,18 @@ WHEN NOT TO USE: the user wants a lead list (leadbay_pull_leads / leadbay_pull_f
|
|
|
10036
10036
|
`;
|
|
10037
10037
|
var leadbay_tour_plan = `## WHEN TO USE
|
|
10038
10038
|
|
|
10039
|
-
Trigger phrases: "visiting <city> in <N> days", "I'm in <city> next week / Tuesday \u2014 who's worth meeting", "I'm going to <city> \u2014 who should I see", "who's worth meeting in <city>", "field tour in <city>", "plan a tour in <city>", "who should I meet in <city>", "customers plus prospects in <city>", "tour itinerary".
|
|
10039
|
+
Trigger phrases: "visiting <city> in <N> days", "I'm in <city> next week / Tuesday \u2014 who's worth meeting", "I'm going to <city> \u2014 who should I see", "who's worth meeting in <city>", "field tour in <city>", "plan a tour in <city>", "who should I meet in <city>", "prospects within <N> km of <city>", "<city> and the surrounding area", "customers plus prospects in <city>", "tour itinerary".
|
|
10040
10040
|
|
|
10041
10041
|
Do NOT use for: "follow-ups only, no new prospects" \u2192 \`leadbay_followups_map\`; "new leads only" \u2192 \`leadbay_pull_leads\`; "research one account" \u2192 \`leadbay_research_lead_by_id\`.
|
|
10042
10042
|
|
|
10043
|
-
Prefer when:
|
|
10043
|
+
Prefer when: known accounts plus new discoveries in one itinerary; pass \`radius_km\` when they name a radius; NEVER a country name, and do NOT omit \`city\`: a city-less tour is arbitrary nationwide leads, so ask which city or region
|
|
10044
10044
|
|
|
10045
10045
|
Examples that SHOULD invoke this tool:
|
|
10046
10046
|
- "I'm flying to Limoges in 4 days \u2014 give me 3 customers, 3 qualified prospects, and 3 new high-potential."
|
|
10047
10047
|
- "I'm in San Francisco next Tuesday. Who's worth meeting?"
|
|
10048
10048
|
- "Plan my tour next Tuesday in Lyon: known accounts plus discoveries."
|
|
10049
10049
|
- "Build a mixed itinerary for Berlin \u2014 I want both follow-ups and fresh leads."
|
|
10050
|
+
- "J'ai un rdv le 19 ao\xFBt \xE0 Colmar \u2014 trouve-moi les prospects dans un rayon de 10km."
|
|
10050
10051
|
|
|
10051
10052
|
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
10052
10053
|
- "Show me my follow-ups for the SF trip."
|
|
@@ -10101,6 +10102,8 @@ same instruction in its \`hint\`.
|
|
|
10101
10102
|
|
|
10102
10103
|
**Counts**: \`followups_count\` (default 6 \u2014 generous so the agent can split into "customers + qualified" client-side) and \`discover_count\` (default 6 after client-side geo filter). The composite over-pulls Discover (30 raw) because the wishlist endpoint has no server-side geo filter \u2014 it then keeps the leads whose own \`location.city\` names the requested city, and falls back to \`location.state\` only when no city matched (which is what a regional ask like "Texas" or "\xCEle-de-France" looks like). \`location.country\` is never consulted. \`discover_filter_note\` reports the ratio and which field carried it, so the agent can be honest about coverage. **When it says no Discover lead is in the city, say that** \u2014 return the Monitor half and offer \`leadbay_find_new_leads\` for that city. Never fill the gap with leads from elsewhere.
|
|
10103
10104
|
|
|
10105
|
+
**The next town over**: a tour is a day of driving, so after the town's own leads are found the composite adds the Discover leads whose own coordinates put them within **\`radius_km\` (default 20)** of it \u2014 West Sacramento on a tour of Sacramento, Courbevoie and Ivry-sur-Seine on a tour of Paris. The town's own leads always come first. **Pass the user's own number when they give one** ("dans un rayon de 10km autour de Colmar" \u2192 \`radius_km: 10\`; "within 15 miles" \u2192 \`radius_km: 24\`), and \`radius_km: 0\` to keep the tour strictly inside the named town. The radius applies to Discover leads only; the Monitor half is scoped server-side and is untouched. When \`discover_filter_note\` splits the stops into "N in '<city>' and M in <other towns>", **repeat that split** \u2014 say which town each nearby stop is actually in rather than presenting every stop as being in the city the user named.
|
|
10106
|
+
|
|
10104
10107
|
**What \`tour_plan\` does NOT do**: it doesn't persist the tour as a campaign artifact. To do that \u2014 create a "Limoges Tour \u2013 May 24" campaign and attach the selected accounts \u2014 chain into \`leadbay_create_campaign({lead_ids: [...selected_ids], name: 'Limoges Tour \u2013 <date>'})\` after the user picks. See the \`leadbay_plan_tour_in_city\` prompt for the full end-to-end orchestrator.
|
|
10105
10108
|
|
|
10106
10109
|
---
|
|
@@ -10741,7 +10744,7 @@ Pick the 2-3 options that match what actually happened \u2014 never all seven:
|
|
|
10741
10744
|
|
|
10742
10745
|
| Observation | Suggest | Calls |
|
|
10743
10746
|
|---|---|---|
|
|
10744
|
-
| Job still running (\`still_running: true\`) | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds:
|
|
10747
|
+
| Job still running (\`still_running: true\`) | "Check on it in ~1 min" | leadbay_lead_job_status(job_id, wait_seconds: 45) |
|
|
10745
10748
|
| Free run delivered on-profile leads | "Qualify these N against your criteria (uses quota \u2014 \`dry_run\` first)" | leadbay_qualify_leads(prior_deliveries: {job_id}) |
|
|
10746
10749
|
| Delivered leads look right | "Draft outreach for the top ones" | leadbay_prepare_outreach |
|
|
10747
10750
|
| Delivered 0 or off-profile | "Reshape the example and retry" (name the fix from funnel + scope_notes) | leadbay_find_new_leads (NEW request_id) |
|
|
@@ -17758,6 +17761,7 @@ var followupsMap = {
|
|
|
17758
17761
|
var DEFAULT_FOLLOWUPS_COUNT = 6;
|
|
17759
17762
|
var DEFAULT_DISCOVER_COUNT = 6;
|
|
17760
17763
|
var DISCOVER_OVER_PULL = 30;
|
|
17764
|
+
var DEFAULT_RADIUS_KM = 20;
|
|
17761
17765
|
function normalizeGeo(value) {
|
|
17762
17766
|
return value.normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
17763
17767
|
}
|
|
@@ -17782,23 +17786,75 @@ function stateFieldOf(lead) {
|
|
|
17782
17786
|
const state = lead?.location?.state;
|
|
17783
17787
|
return typeof state === "string" ? normalizeGeo(state) : "";
|
|
17784
17788
|
}
|
|
17785
|
-
function
|
|
17789
|
+
function posOf(lead) {
|
|
17790
|
+
const pos = lead?.location?.pos;
|
|
17791
|
+
const valid = Array.isArray(pos) && pos.length === 2 && pos.every((n) => typeof n === "number" && Number.isFinite(n));
|
|
17792
|
+
return valid ? [pos[0], pos[1]] : null;
|
|
17793
|
+
}
|
|
17794
|
+
function distanceKm(a, b) {
|
|
17795
|
+
const toRad = (d) => d * Math.PI / 180;
|
|
17796
|
+
const dLat = toRad(b[0] - a[0]);
|
|
17797
|
+
const dLng = toRad(b[1] - a[1]);
|
|
17798
|
+
const h = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(a[0])) * Math.cos(toRad(b[0])) * Math.sin(dLng / 2) ** 2;
|
|
17799
|
+
return 2 * 6371 * Math.asin(Math.min(1, Math.sqrt(h)));
|
|
17800
|
+
}
|
|
17801
|
+
function withinRadius(leads, anchors, radiusKm) {
|
|
17802
|
+
if (anchors.length === 0 || !(radiusKm > 0))
|
|
17803
|
+
return [];
|
|
17804
|
+
return leads.filter((l) => {
|
|
17805
|
+
const p = posOf(l);
|
|
17806
|
+
return p !== null && anchors.some((a) => distanceKm(a, p) <= radiusKm);
|
|
17807
|
+
});
|
|
17808
|
+
}
|
|
17809
|
+
function filterDiscoverByCity(leads, cityHint, monitorLeads, radiusKm) {
|
|
17786
17810
|
const hint = cityHint ? cityHintCore(cityHint) : { name: "", isCity: false };
|
|
17787
17811
|
if (!hint.name)
|
|
17788
|
-
return { leads, matchedOn: null };
|
|
17812
|
+
return { leads, matchedOn: null, nearby: [], anchored: false };
|
|
17789
17813
|
const cityOf = (l) => typeof l?.location?.city === "string" ? townName(l.location.city) : "";
|
|
17814
|
+
const positions = (ls) => ls.map(posOf).filter((p) => p !== null);
|
|
17790
17815
|
const byCity = leads.filter((l) => cityOf(l) === hint.name);
|
|
17791
|
-
|
|
17792
|
-
|
|
17793
|
-
|
|
17794
|
-
|
|
17795
|
-
|
|
17796
|
-
|
|
17816
|
+
const anchors = positions([
|
|
17817
|
+
...byCity,
|
|
17818
|
+
...monitorLeads.filter((l) => cityOf(l) === hint.name)
|
|
17819
|
+
]);
|
|
17820
|
+
if (byCity.length > 0) {
|
|
17821
|
+
const inTown = new Set(byCity);
|
|
17822
|
+
const nearby2 = withinRadius(leads.filter((l) => !inTown.has(l)), anchors, radiusKm);
|
|
17823
|
+
return { leads: [...byCity, ...nearby2], matchedOn: "city", nearby: nearby2, anchored: true };
|
|
17824
|
+
}
|
|
17825
|
+
const anchored = anchors.length > 0;
|
|
17826
|
+
if (!hint.isCity) {
|
|
17827
|
+
const byState = leads.filter((l) => stateFieldOf(l) === hint.name);
|
|
17828
|
+
if (byState.length > 0)
|
|
17829
|
+
return { leads: byState, matchedOn: "state", nearby: [], anchored };
|
|
17830
|
+
}
|
|
17831
|
+
const nearby = withinRadius(leads, anchors, radiusKm);
|
|
17832
|
+
if (nearby.length > 0)
|
|
17833
|
+
return { leads: nearby, matchedOn: "city", nearby, anchored };
|
|
17834
|
+
return {
|
|
17835
|
+
leads: [],
|
|
17836
|
+
matchedOn: hint.isCity ? "city" : "state",
|
|
17837
|
+
nearby: [],
|
|
17838
|
+
anchored
|
|
17839
|
+
};
|
|
17840
|
+
}
|
|
17841
|
+
function townsOf(leads) {
|
|
17842
|
+
const seen = [];
|
|
17843
|
+
let unnamed = 0;
|
|
17844
|
+
for (const l of leads) {
|
|
17845
|
+
const city = typeof l?.location?.city === "string" ? l.location.city.trim() : "";
|
|
17846
|
+
const name = city.replace(/^(?:City|Town|Village|Borough|Township|Municipality) of /i, "");
|
|
17847
|
+
if (!name)
|
|
17848
|
+
unnamed += 1;
|
|
17849
|
+
else if (!seen.includes(name))
|
|
17850
|
+
seen.push(name);
|
|
17851
|
+
}
|
|
17852
|
+
const rest = unnamed > 0 ? `${unnamed} whose record names no town` : "";
|
|
17853
|
+
return [seen.join(", "), rest].filter(Boolean).join(", plus ");
|
|
17797
17854
|
}
|
|
17798
17855
|
function toMapLocation(lead, mode) {
|
|
17799
|
-
const pos = lead
|
|
17800
|
-
|
|
17801
|
-
if (!valid)
|
|
17856
|
+
const pos = posOf(lead);
|
|
17857
|
+
if (pos === null)
|
|
17802
17858
|
return null;
|
|
17803
17859
|
const loc = lead.location;
|
|
17804
17860
|
const c = lead.recommended_contact;
|
|
@@ -17871,6 +17927,10 @@ var tourPlan = {
|
|
|
17871
17927
|
discover_count: {
|
|
17872
17928
|
type: "number",
|
|
17873
17929
|
description: `Top-N Discover leads (active lens wishlist) to return after client-side city filter. Default ${DEFAULT_DISCOVER_COUNT}.`
|
|
17930
|
+
},
|
|
17931
|
+
radius_km: {
|
|
17932
|
+
type: "number",
|
|
17933
|
+
description: `How far outside the named town a Discover stop may be, in km. Default ${DEFAULT_RADIUS_KM}, which is roughly a metro area \u2014 it is what puts West Sacramento on a tour of Sacramento and Courbevoie on a tour of Paris. Pass the user's own number when they name one ("dans un rayon de 10km", "zone 20km", "within 15 miles" \u2192 24). Pass 0 to keep the tour inside the town's own name. Leads in the town itself always come first; the radius only adds to them. It applies to Discover leads only \u2014 the Monitor half is scoped server-side and is untouched.`
|
|
17874
17934
|
}
|
|
17875
17935
|
},
|
|
17876
17936
|
additionalProperties: false
|
|
@@ -17887,12 +17947,12 @@ var tourPlan = {
|
|
|
17887
17947
|
},
|
|
17888
17948
|
discover_leads: {
|
|
17889
17949
|
type: "array",
|
|
17890
|
-
description: "Fresh Discover leads from the active lens, filtered client-side to match the city. Pulls a larger candidate set internally to compensate for the missing server-side geo filter.",
|
|
17950
|
+
description: "Fresh Discover leads from the active lens, filtered client-side to match the city, then extended with the ones whose own coordinates put them within `radius_km` of it. The town's own leads come first. Pulls a larger candidate set internally to compensate for the missing server-side geo filter.",
|
|
17891
17951
|
items: { type: "object" }
|
|
17892
17952
|
},
|
|
17893
17953
|
discover_filter_note: {
|
|
17894
17954
|
type: "string",
|
|
17895
|
-
description: "Human-readable summary of the client-side geo filter applied to Discover leads
|
|
17955
|
+
description: "Human-readable summary of the client-side geo filter applied to Discover leads. Says how many stops are in the named town, how many are within `radius_km` of it, and which towns those are in \u2014 repeat that split to the user rather than presenting every stop as being in the city they named."
|
|
17896
17956
|
},
|
|
17897
17957
|
map_locations: {
|
|
17898
17958
|
type: "array",
|
|
@@ -18016,12 +18076,21 @@ var tourPlan = {
|
|
|
18016
18076
|
const cityName = params.city && !/^\d+$/.test(params.city.trim()) ? params.city : void 0;
|
|
18017
18077
|
const knownId = params.city_id ?? (cityName ? void 0 : params.city);
|
|
18018
18078
|
const idOnly = Boolean(knownId) && !cityName;
|
|
18019
|
-
const
|
|
18079
|
+
const radiusKm = params.radius_km ?? DEFAULT_RADIUS_KM;
|
|
18080
|
+
const { leads: filtered, matchedOn, nearby, anchored } = idOnly ? {
|
|
18081
|
+
leads: [],
|
|
18082
|
+
matchedOn: null,
|
|
18083
|
+
nearby: [],
|
|
18084
|
+
anchored: false
|
|
18085
|
+
} : filterDiscoverByCity(rawDiscover, cityName, monitorLeads, radiusKm);
|
|
18020
18086
|
const discoverLeads2 = filtered.slice(0, discoverCount);
|
|
18021
18087
|
const pulledLensId = leadsResult.status === "fulfilled" ? leadsResult.value?.lens?.id : null;
|
|
18022
18088
|
if (pulledLensId != null) {
|
|
18023
18089
|
reportLeadInteractions(client, pulledLensId, discoverLeads2.map((l) => l.id), ["LEAD_SEEN"], ctx?.logger);
|
|
18024
18090
|
}
|
|
18091
|
+
const nearbySet = new Set(nearby);
|
|
18092
|
+
const nearShown = discoverLeads2.filter((l) => nearbySet.has(l));
|
|
18093
|
+
const inTownShown = discoverLeads2.length - nearShown.length;
|
|
18025
18094
|
let filterNote;
|
|
18026
18095
|
if (idOnly) {
|
|
18027
18096
|
filterNote = `Discover leads need the NAME of the place, and this call passed an area id (${knownId}) and no name. Re-call with \`city\` set to the name of that area, keeping \`city_id\` so the Monitor half stays on the area you picked. The follow-ups below are already scoped to it.`;
|
|
@@ -18029,10 +18098,16 @@ var tourPlan = {
|
|
|
18029
18098
|
filterNote = `No city filter applied; returning top ${discoverLeads2.length} Discover leads.`;
|
|
18030
18099
|
} else if (matchedOn === null) {
|
|
18031
18100
|
filterNote = `No usable city filter in '${params.city}'; returning top ${discoverLeads2.length} Discover leads.`;
|
|
18101
|
+
} else if (filtered.length === 0 && anchored && radiusKm > 0) {
|
|
18102
|
+
filterNote = `No Discover lead in the active lens names '${params.city}' as its town, and none is within ${radiusKm} km of the leads that do (checked ${rawDiscover.length} candidates). Say so; do NOT present leads from elsewhere as if they were in '${params.city}'.`;
|
|
18032
18103
|
} else if (filtered.length === 0) {
|
|
18033
18104
|
filterNote = `No Discover lead in the active lens is in '${params.city}' (checked ${rawDiscover.length} candidates by city, then by state/region). Say so; do NOT present leads from elsewhere as if they were in '${params.city}'.`;
|
|
18105
|
+
} else if (nearShown.length > 0 && inTownShown === 0) {
|
|
18106
|
+
filterNote = `No Discover lead in the active lens names '${params.city}' as its town; ${nearby.length}/${rawDiscover.length} are within ${radiusKm} km of it. Returning top ${discoverLeads2.length}, in ${townsOf(nearShown)}. Name the town each stop is actually in rather than calling them all '${params.city}'.`;
|
|
18107
|
+
} else if (nearShown.length > 0) {
|
|
18108
|
+
filterNote = `Matched ${filtered.length - nearby.length}/${rawDiscover.length} Discover leads to '${params.city}' by city, plus ${nearby.length} within ${radiusKm} km of it. Returning top ${discoverLeads2.length}: ${inTownShown} in '${params.city}' and ${nearShown.length} in ${townsOf(nearShown)}. Name the town each nearby stop is in.`;
|
|
18034
18109
|
} else {
|
|
18035
|
-
filterNote = `Matched ${filtered.length}/${rawDiscover.length} Discover leads to '${params.city}' by ${matchedOn === "city" ? "city" : "state/region"}; returning top ${discoverLeads2.length}.`;
|
|
18110
|
+
filterNote = `Matched ${filtered.length - nearby.length}/${rawDiscover.length} Discover leads to '${params.city}' by ${matchedOn === "city" ? "city" : "state/region"}; returning top ${discoverLeads2.length}.`;
|
|
18036
18111
|
}
|
|
18037
18112
|
return {
|
|
18038
18113
|
city: params.city ?? null,
|
|
@@ -19447,7 +19522,7 @@ var researchLeadByNameFuzzy = {
|
|
|
19447
19522
|
// of a research card.
|
|
19448
19523
|
outputSchema: {
|
|
19449
19524
|
type: "object",
|
|
19450
|
-
description: "Same shape as leadbay_research_lead_by_id, with _meta.resolved_from='companyName'|'resolver', _meta.resolved_query='<needle>', _meta.resolved_matched_on=[...], and _meta.match_candidates=[{leadId,name,score}] populated. When the registry resolver cannot pick one company, returns {resolution:'ambiguous', query, candidates:[{leadId,name,website,location,registry_ids,score,matched_on}]} instead \u2014 ask the user which one, then call leadbay_research_lead_by_id.",
|
|
19525
|
+
description: "Same shape as leadbay_research_lead_by_id, with _meta.resolved_from='companyName'|'resolver', _meta.resolved_query='<needle>', _meta.resolved_matched_on=[...], and _meta.match_candidates=[{leadId,name,score}] populated. When the registry resolver cannot pick one company, returns {resolution:'ambiguous', query, candidates:[{leadId,name,website,location,registry_ids,score,matched_on}]} instead \u2014 ask the user which one, then call leadbay_research_lead_by_id. When neither the user's leads nor the registry hold the company, returns {resolution:'not_found', query, summary, would_help, next_step} \u2014 a successful answer, not an error: do what next_step says, which is normally to ask the user for the field would_help names ('website' or 'registry_number') and call again. would_help is empty when the search was scoped to one lens, where the remedy is to drop the scope, not to ask the user for a field.",
|
|
19451
19526
|
additionalProperties: true
|
|
19452
19527
|
},
|
|
19453
19528
|
execute: async (client, params, ctx) => {
|
|
@@ -19461,7 +19536,18 @@ var researchLeadByNameFuzzy = {
|
|
|
19461
19536
|
if (scoped.length > 0) {
|
|
19462
19537
|
return await delegate(scoped, params.lensId);
|
|
19463
19538
|
}
|
|
19464
|
-
|
|
19539
|
+
return {
|
|
19540
|
+
resolution: "not_found",
|
|
19541
|
+
query,
|
|
19542
|
+
summary: `No lead matching "${query}" in lens ${params.lensId}`,
|
|
19543
|
+
would_help: [],
|
|
19544
|
+
next_step: "This lookup was intentionally restricted to the supplied lens. Omit lensId to search your visible leads across Discover, Monitor, and Activate and then the Leadbay company registry.",
|
|
19545
|
+
_meta: {
|
|
19546
|
+
region: client.region,
|
|
19547
|
+
lens_id: params.lensId,
|
|
19548
|
+
resolved_query: query
|
|
19549
|
+
}
|
|
19550
|
+
};
|
|
19465
19551
|
}
|
|
19466
19552
|
async function delegate(matches, fallbackLens) {
|
|
19467
19553
|
const [primary, ...rest] = matches;
|
|
@@ -19556,7 +19642,23 @@ var researchLeadByNameFuzzy = {
|
|
|
19556
19642
|
const wanted = resolved.type === "none" && resolved.would_help.length > 0 ? resolved.would_help : ["website", "registry_number"];
|
|
19557
19643
|
const asks = wanted.map((f) => f === "registry_number" ? "a registry number (SIREN/SIRET) for `registry_number`" : f === "website" ? "the company website for `website`" : `\`${f}\``).join(" or ");
|
|
19558
19644
|
const hint = resolved.type === "unidentifiable" ? `The registry could not identify a company from this input (${resolved.reason}). Ask the user for ${asks}, then call this tool again with it.` : `The registry found no company for what was supplied. It would match on ${asks}. Ask the user for that \u2014 "what's their website?" usually settles it \u2014 then call this tool again. Do not offer an import before asking.`;
|
|
19559
|
-
|
|
19645
|
+
if (!corpusSearched) {
|
|
19646
|
+
throw client.makeError("LEAD_NOT_FOUND", `No company matching "${query}" ${searched}`, `${hint} But this lookup did not complete \u2014 the lead search route was unreachable, so their own leads were never checked. Retry once before telling the user the company is absent.`, "POST /leads/resolve");
|
|
19647
|
+
}
|
|
19648
|
+
return {
|
|
19649
|
+
resolution: "not_found",
|
|
19650
|
+
query,
|
|
19651
|
+
resolver_payload: payload,
|
|
19652
|
+
summary: `No company matching "${query}" ${searched}`,
|
|
19653
|
+
would_help: wanted,
|
|
19654
|
+
next_step: hint,
|
|
19655
|
+
_meta: {
|
|
19656
|
+
region: client.region,
|
|
19657
|
+
resolved_from: "resolver",
|
|
19658
|
+
resolved_query: query,
|
|
19659
|
+
endpoint: "POST /leads/resolve"
|
|
19660
|
+
}
|
|
19661
|
+
};
|
|
19560
19662
|
}
|
|
19561
19663
|
};
|
|
19562
19664
|
|
|
@@ -23640,6 +23742,21 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes, toAddLocati
|
|
|
23640
23742
|
const items = current?.lens_filter?.items ?? [];
|
|
23641
23743
|
const item = items[0] ?? { criteria: [] };
|
|
23642
23744
|
const criteria = item.criteria ? [...item.criteria] : [];
|
|
23745
|
+
const dropIds = (type, fromExcluded, ids) => {
|
|
23746
|
+
if (ids.length === 0)
|
|
23747
|
+
return;
|
|
23748
|
+
const idx = criteria.findIndex((c) => c.type === type && !!c.is_excluded === fromExcluded);
|
|
23749
|
+
if (idx < 0)
|
|
23750
|
+
return;
|
|
23751
|
+
const cur = criteria[idx];
|
|
23752
|
+
const key = type === "sector_ids" ? "sectors" : "locations";
|
|
23753
|
+
const held = cur[key] ?? [];
|
|
23754
|
+
const kept = held.filter((s) => !ids.includes(s));
|
|
23755
|
+
if (kept.length === 0)
|
|
23756
|
+
criteria.splice(idx, 1);
|
|
23757
|
+
else
|
|
23758
|
+
criteria[idx] = { ...cur, [key]: kept };
|
|
23759
|
+
};
|
|
23643
23760
|
if (toAddSectors.length > 0) {
|
|
23644
23761
|
const idx = criteria.findIndex((c) => c.type === "sector_ids" && !c.is_excluded);
|
|
23645
23762
|
if (idx >= 0) {
|
|
@@ -23654,6 +23771,7 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes, toAddLocati
|
|
|
23654
23771
|
});
|
|
23655
23772
|
}
|
|
23656
23773
|
}
|
|
23774
|
+
dropIds("sector_ids", true, toAddSectors);
|
|
23657
23775
|
if (toExcludeSectors.length > 0) {
|
|
23658
23776
|
const idx = criteria.findIndex((c) => c.type === "sector_ids" && c.is_excluded);
|
|
23659
23777
|
if (idx >= 0) {
|
|
@@ -23668,6 +23786,7 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes, toAddLocati
|
|
|
23668
23786
|
});
|
|
23669
23787
|
}
|
|
23670
23788
|
}
|
|
23789
|
+
dropIds("sector_ids", false, toExcludeSectors);
|
|
23671
23790
|
if (toAddLocations.length > 0) {
|
|
23672
23791
|
const idx = criteria.findIndex((c) => c.type === "location_ids" && !c.is_excluded);
|
|
23673
23792
|
if (idx >= 0) {
|
|
@@ -23682,6 +23801,7 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes, toAddLocati
|
|
|
23682
23801
|
});
|
|
23683
23802
|
}
|
|
23684
23803
|
}
|
|
23804
|
+
dropIds("location_ids", true, toAddLocations);
|
|
23685
23805
|
if (toExcludeLocations.length > 0) {
|
|
23686
23806
|
const idx = criteria.findIndex((c) => c.type === "location_ids" && c.is_excluded);
|
|
23687
23807
|
if (idx >= 0) {
|
|
@@ -23696,6 +23816,7 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes, toAddLocati
|
|
|
23696
23816
|
});
|
|
23697
23817
|
}
|
|
23698
23818
|
}
|
|
23819
|
+
dropIds("location_ids", false, toExcludeLocations);
|
|
23699
23820
|
if (sizes && sizes.length > 0) {
|
|
23700
23821
|
const normalizedSizes = sizes.map((s) => ({
|
|
23701
23822
|
min: s.min ?? 0,
|
|
@@ -24891,10 +25012,14 @@ function normalizeSearchFilters(filters) {
|
|
|
24891
25012
|
}
|
|
24892
25013
|
return out;
|
|
24893
25014
|
}
|
|
24894
|
-
|
|
24895
|
-
|
|
24896
|
-
|
|
24897
|
-
|
|
25015
|
+
var MAX_WAIT_SECONDS = 45;
|
|
25016
|
+
function clampWaitSeconds(requested, fallback, startedAtMs) {
|
|
25017
|
+
const asked = requested == null || Number.isNaN(requested) ? fallback : requested;
|
|
25018
|
+
const bounded = Math.min(Math.max(asked, 0), MAX_WAIT_SECONDS);
|
|
25019
|
+
if (startedAtMs == null)
|
|
25020
|
+
return bounded;
|
|
25021
|
+
const spentSeconds = (Date.now() - startedAtMs) / 1e3;
|
|
25022
|
+
return Math.max(0, Math.min(bounded, MAX_WAIT_SECONDS - spentSeconds));
|
|
24898
25023
|
}
|
|
24899
25024
|
|
|
24900
25025
|
// ../core/dist/composite/find-new-leads.js
|
|
@@ -25075,13 +25200,14 @@ var findNewLeads = {
|
|
|
25075
25200
|
},
|
|
25076
25201
|
wait_seconds: {
|
|
25077
25202
|
type: "number",
|
|
25078
|
-
description: "How long to poll before returning (default 45
|
|
25203
|
+
description: "How long to poll before returning (default and maximum 45 \u2014 the whole call is bounded by it, so a slow submit shortens the poll; 0 = submit + one snapshot). Free searches usually finish inside the window; qualified exploration can take minutes \u2014 the result then carries still_running:true and the job_id to check with leadbay_lead_job_status."
|
|
25079
25204
|
}
|
|
25080
25205
|
},
|
|
25081
25206
|
required: ["count", "request_id"],
|
|
25082
25207
|
additionalProperties: false
|
|
25083
25208
|
},
|
|
25084
25209
|
execute: async (client, params, ctx) => {
|
|
25210
|
+
const startedAt = Date.now();
|
|
25085
25211
|
params = coerceArrayParams(params, [
|
|
25086
25212
|
"contact_titles",
|
|
25087
25213
|
"channels",
|
|
@@ -25220,7 +25346,7 @@ var findNewLeads = {
|
|
|
25220
25346
|
const mocked = mockedSubmitPreview(submit, "leadbay_find_new_leads", client.region);
|
|
25221
25347
|
if (mocked)
|
|
25222
25348
|
return mocked;
|
|
25223
|
-
const waitSeconds = clampWaitSeconds(params.wait_seconds, DEFAULT_WAIT_SECONDS);
|
|
25349
|
+
const waitSeconds = clampWaitSeconds(params.wait_seconds, DEFAULT_WAIT_SECONDS, startedAt);
|
|
25224
25350
|
const snapshot = await snapshotAfterSubmit(client, submit.job_id, waitSeconds, ctx, params.count);
|
|
25225
25351
|
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
25226
25352
|
const { leads, skipped } = splitItems(snapshot);
|
|
@@ -25265,7 +25391,7 @@ var findNewLeads = {
|
|
|
25265
25391
|
// INCREMENTALLY instead of re-reading (and re-rendering) the
|
|
25266
25392
|
// rows already delivered in this response.
|
|
25267
25393
|
since: snapshot.next_since ?? null,
|
|
25268
|
-
suggested_wait_seconds: done ? 0 :
|
|
25394
|
+
suggested_wait_seconds: done ? 0 : MAX_WAIT_SECONDS
|
|
25269
25395
|
},
|
|
25270
25396
|
...sectorNote(attempt.rewritten),
|
|
25271
25397
|
...countryNote,
|
|
@@ -25399,7 +25525,7 @@ function identityAnswer(jobId, snapshot, items, offset) {
|
|
|
25399
25525
|
job_id: jobId,
|
|
25400
25526
|
compact: true,
|
|
25401
25527
|
offset: nextOffset,
|
|
25402
|
-
suggested_wait_seconds: done ? 0 :
|
|
25528
|
+
suggested_wait_seconds: done ? 0 : MAX_WAIT_SECONDS
|
|
25403
25529
|
}
|
|
25404
25530
|
};
|
|
25405
25531
|
}
|
|
@@ -25597,12 +25723,13 @@ var qualifyLeads = {
|
|
|
25597
25723
|
},
|
|
25598
25724
|
wait_seconds: {
|
|
25599
25725
|
type: "number",
|
|
25600
|
-
description: "How long to poll before returning (default 45
|
|
25726
|
+
description: "How long to poll before returning (default and maximum 45 \u2014 the whole call is bounded by it, so a slow submit shortens the poll; 0 = submit + one snapshot). Large or research-heavy batches can take minutes \u2014 the result then carries still_running:true and the job_id for leadbay_lead_job_status."
|
|
25601
25727
|
}
|
|
25602
25728
|
},
|
|
25603
25729
|
additionalProperties: false
|
|
25604
25730
|
},
|
|
25605
25731
|
execute: async (client, params, ctx) => {
|
|
25732
|
+
const startedAt = Date.now();
|
|
25606
25733
|
params = coerceArrayParams(params, [
|
|
25607
25734
|
"lead_refs",
|
|
25608
25735
|
"contact_titles",
|
|
@@ -25662,7 +25789,7 @@ var qualifyLeads = {
|
|
|
25662
25789
|
const mocked = mockedSubmitPreview(submit, "leadbay_qualify_leads", client.region);
|
|
25663
25790
|
if (mocked)
|
|
25664
25791
|
return mocked;
|
|
25665
|
-
const waitSeconds = clampWaitSeconds(params.wait_seconds, DEFAULT_WAIT_SECONDS2);
|
|
25792
|
+
const waitSeconds = clampWaitSeconds(params.wait_seconds, DEFAULT_WAIT_SECONDS2, startedAt);
|
|
25666
25793
|
const snapshot = await snapshotAfterSubmit(client, submit.job_id, waitSeconds, ctx, submit.items_requested);
|
|
25667
25794
|
const done = TERMINAL_JOB_STATES.has(snapshot.job.state);
|
|
25668
25795
|
const indexed = submit.duplicate ?? false ? remapInputIndexes(snapshot.items, params.lead_refs) : { items: snapshot.items, remapped: true };
|
|
@@ -25739,7 +25866,7 @@ var qualifyLeads = {
|
|
|
25739
25866
|
// INCREMENTALLY instead of re-reading (and re-rendering) the
|
|
25740
25867
|
// rows already delivered in this response.
|
|
25741
25868
|
since: snapshot.next_since ?? null,
|
|
25742
|
-
suggested_wait_seconds: done ? 0 :
|
|
25869
|
+
suggested_wait_seconds: done ? 0 : MAX_WAIT_SECONDS
|
|
25743
25870
|
},
|
|
25744
25871
|
region: client.region
|
|
25745
25872
|
};
|
|
@@ -25774,7 +25901,7 @@ var leadJobStatus = {
|
|
|
25774
25901
|
},
|
|
25775
25902
|
wait_seconds: {
|
|
25776
25903
|
type: "number",
|
|
25777
|
-
description: "0 (default) = instant snapshot. >0 = keep polling up to this many seconds until the job is terminal \u2014 use
|
|
25904
|
+
description: "0 (default) = instant snapshot. >0 = keep polling up to this many seconds (maximum 45) until the job is terminal \u2014 use 45 when the user asked to wait for results."
|
|
25778
25905
|
},
|
|
25779
25906
|
compact: {
|
|
25780
25907
|
type: "boolean",
|
|
@@ -25826,7 +25953,7 @@ var leadJobStatus = {
|
|
|
25826
25953
|
// Same incremental handoff as the submit tools — following
|
|
25827
25954
|
// next_poll without the cursor re-reads the rows just returned.
|
|
25828
25955
|
since: snapshot.next_since ?? null,
|
|
25829
|
-
suggested_wait_seconds: done ? 0 :
|
|
25956
|
+
suggested_wait_seconds: done ? 0 : MAX_WAIT_SECONDS
|
|
25830
25957
|
},
|
|
25831
25958
|
region: client.region
|
|
25832
25959
|
};
|
|
@@ -29308,7 +29435,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
29308
29435
|
}
|
|
29309
29436
|
|
|
29310
29437
|
// src/http-server.ts
|
|
29311
|
-
var VERSION = true ? "0.39.
|
|
29438
|
+
var VERSION = true ? "0.39.8" : "0.0.0-dev";
|
|
29312
29439
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
29313
29440
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
29314
29441
|
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.8",
|
|
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",
|