@leadbay/mcp 0.39.2 → 0.39.4

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 CHANGED
@@ -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 (leadCount <= 0) {
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 leadCount = res.items.length;
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 = leadCount === 0 && (res.pagination?.total ?? 0) === 0;
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
@@ -14970,8 +14970,16 @@ function townName(value) {
14970
14970
  }
14971
14971
  function cityHintCore(cityHint) {
14972
14972
  const head = cityHint.split(",")[0] ?? "";
14973
- const expanded = expandAlias(head);
14974
- return { name: townName(expanded), isCity: expanded !== head };
14973
+ const segments = cityHint.split(",").map((part) => part.trim()).filter(Boolean);
14974
+ for (let take = segments.length; take > 0; take -= 1) {
14975
+ const candidate = segments.slice(0, take).join(" ");
14976
+ const expanded = expandAlias(candidate);
14977
+ if (expanded !== candidate) {
14978
+ return { name: townName(expanded), isCity: true };
14979
+ }
14980
+ }
14981
+ const prefixed = ADMIN_PREFIX.test(normalizeGeo(head));
14982
+ return { name: townName(head), isCity: prefixed };
14975
14983
  }
14976
14984
  function stateFieldOf(lead) {
14977
14985
  const state = lead?.location?.state;
@@ -20808,27 +20816,167 @@ var init_bulk_enrich_status = __esm({
20808
20816
  }
20809
20817
  });
20810
20818
 
20811
- // ../core/dist/composite/adjust-audience.js
20812
- function tokens(s) {
20813
- if (!s)
20814
- return [];
20815
- return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
20819
+ // ../core/dist/composite/_sector-resolver.js
20820
+ function normalizeSectorText(value) {
20821
+ return (value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^0-9a-z]+/g, " ").trim();
20816
20822
  }
20817
- function bestMatches(text3, taxonomy) {
20818
- const want = new Set(tokens(text3));
20819
- if (want.size === 0)
20820
- return [];
20821
- const ranked = taxonomy.map((s) => {
20822
- const have = new Set(tokens(s.name));
20823
+ function singular(token) {
20824
+ if (token.length > 4 && token.endsWith("ies"))
20825
+ return `${token.slice(0, -3)}y`;
20826
+ return token.length > 3 && token.endsWith("s") ? token.slice(0, -1) : token;
20827
+ }
20828
+ function sectorTokens(value) {
20829
+ const all = normalizeSectorText(value).split(" ").filter(Boolean).map(singular);
20830
+ const core = all.filter((t) => !FILLER.has(t));
20831
+ return core.length > 0 ? core : all;
20832
+ }
20833
+ function sectorLabel(row) {
20834
+ if (!row)
20835
+ return "";
20836
+ const label = row.label;
20837
+ if (typeof label === "string" && label.length > 0)
20838
+ return label;
20839
+ return typeof row.name === "string" ? row.name : "";
20840
+ }
20841
+ function leadCount(row) {
20842
+ const n = row.number_of_leads;
20843
+ return typeof n === "number" && Number.isFinite(n) ? n : 0;
20844
+ }
20845
+ function rankCandidates(scored, tieBreak) {
20846
+ const out = [];
20847
+ const seen = /* @__PURE__ */ new Set();
20848
+ 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 }) => {
20849
+ const label = sectorLabel(row);
20850
+ if (!label || seen.has(label) || out.length >= MAX_CANDIDATES)
20851
+ return;
20852
+ seen.add(label);
20853
+ out.push({ id: row.id, name: label, score: Number(score.toFixed(2)) });
20854
+ });
20855
+ return out;
20856
+ }
20857
+ function matchSector(text3, taxonomy) {
20858
+ const wanted = normalizeSectorText(text3);
20859
+ if (!wanted)
20860
+ return { kind: "unresolved", candidates: [] };
20861
+ const exact = taxonomy.filter((row) => normalizeSectorText(sectorLabel(row)) === wanted);
20862
+ if (exact.length > 0) {
20863
+ const label = sectorLabel(exact.reduce((best, row) => leadCount(row) > leadCount(best) ? row : best));
20864
+ const ids = taxonomy.filter((row) => sectorLabel(row) === label).map((row) => row.id);
20865
+ return { kind: "resolved", label, ids, exact: true };
20866
+ }
20867
+ const want = new Set(sectorTokens(text3));
20868
+ const sameWords = taxonomy.filter((row) => {
20869
+ const have = new Set(sectorTokens(sectorLabel(row)));
20870
+ return have.size === want.size && [...want].every((token) => have.has(token));
20871
+ });
20872
+ const sameWordLabels = new Set(sameWords.map((row) => sectorLabel(row)).filter(Boolean));
20873
+ if (sameWordLabels.size === 1) {
20874
+ const label = [...sameWordLabels][0];
20875
+ const ids = taxonomy.filter((row) => sectorLabel(row) === label).map((row) => row.id);
20876
+ return { kind: "resolved", label, ids, exact: false };
20877
+ }
20878
+ const scored = [];
20879
+ for (const row of taxonomy) {
20880
+ const have = new Set(sectorTokens(sectorLabel(row)));
20881
+ if (have.size === 0)
20882
+ continue;
20823
20883
  let overlap = 0;
20824
- for (const t of want)
20825
- if (have.has(t))
20884
+ for (const token of want)
20885
+ if (have.has(token))
20826
20886
  overlap += 1;
20827
- const score = overlap / Math.max(want.size, 1);
20828
- return { id: s.id, name: s.name ?? "", score };
20829
- }).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
20830
- return ranked.slice(0, 5);
20887
+ if (overlap === 0)
20888
+ continue;
20889
+ scored.push({ row, score: overlap / want.size, closeness: overlap / have.size });
20890
+ }
20891
+ const full = scored.filter((s) => s.score >= 1);
20892
+ return {
20893
+ kind: "unresolved",
20894
+ candidates: full.length > 0 ? rankCandidates(full, "breadth") : rankCandidates(scored, "closeness").slice(0, MAX_PARTIAL_CANDIDATES)
20895
+ };
20896
+ }
20897
+ function sectorSections(taxonomy) {
20898
+ const roots = taxonomy.filter((row) => !row.parent);
20899
+ const labels = [];
20900
+ for (const row of roots.sort((a, b) => leadCount(b) - leadCount(a))) {
20901
+ const label = sectorLabel(row);
20902
+ if (label && !labels.includes(label))
20903
+ labels.push(label);
20904
+ }
20905
+ return labels;
20906
+ }
20907
+ async function fetchSectorTaxonomy(client, ctx) {
20908
+ const me = await client.resolveMe().catch(() => null);
20909
+ const lang = me?.language ?? "en";
20910
+ const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
20911
+ const unlabelled = taxonomy.filter((row) => !sectorLabel(row)).length;
20912
+ if (unlabelled > 0) {
20913
+ ctx?.logger?.warn?.(`sector taxonomy: ${unlabelled}/${taxonomy.length} row(s) carry no label`);
20914
+ }
20915
+ return taxonomy;
20916
+ }
20917
+ function resolveSectorValues(values, taxonomy) {
20918
+ const out = {
20919
+ values: [],
20920
+ rewritten: [],
20921
+ unresolved: [],
20922
+ sections: sectorSections(taxonomy)
20923
+ };
20924
+ for (const raw of values) {
20925
+ const asked = typeof raw === "string" ? raw : String(raw ?? "");
20926
+ if (NUMERIC_ID.test(asked.trim())) {
20927
+ out.values.push(asked.trim());
20928
+ continue;
20929
+ }
20930
+ const match = matchSector(asked, taxonomy);
20931
+ if (match.kind === "resolved") {
20932
+ out.values.push(match.label);
20933
+ if (match.label !== asked)
20934
+ out.rewritten.push({ asked, used: match.label });
20935
+ continue;
20936
+ }
20937
+ out.values.push(asked);
20938
+ out.unresolved.push({ asked, closest: match.candidates });
20939
+ }
20940
+ return out;
20831
20941
  }
20942
+ var FILLER, MAX_CANDIDATES, MAX_PARTIAL_CANDIDATES, NUMERIC_ID;
20943
+ var init_sector_resolver = __esm({
20944
+ "../core/dist/composite/_sector-resolver.js"() {
20945
+ "use strict";
20946
+ FILLER = /* @__PURE__ */ new Set([
20947
+ // en
20948
+ "and",
20949
+ "of",
20950
+ "the",
20951
+ "for",
20952
+ "or",
20953
+ "activity",
20954
+ // fr
20955
+ "et",
20956
+ "de",
20957
+ "des",
20958
+ "du",
20959
+ "d",
20960
+ "la",
20961
+ "le",
20962
+ "les",
20963
+ "l",
20964
+ "en",
20965
+ "aux",
20966
+ "au",
20967
+ "activite",
20968
+ "pour",
20969
+ "sur",
20970
+ "par",
20971
+ "a"
20972
+ ]);
20973
+ MAX_CANDIDATES = 8;
20974
+ MAX_PARTIAL_CANDIDATES = 4;
20975
+ NUMERIC_ID = /^\d+$/;
20976
+ }
20977
+ });
20978
+
20979
+ // ../core/dist/composite/adjust-audience.js
20832
20980
  async function resolveSectors(client, texts, ctx) {
20833
20981
  const looksLikeId2 = (s) => /^\d+$/.test(s);
20834
20982
  const direct = texts.filter(looksLikeId2);
@@ -20838,18 +20986,18 @@ async function resolveSectors(client, texts, ctx) {
20838
20986
  const me = await client.resolveMe().catch(() => null);
20839
20987
  const lang = me?.language ?? "en";
20840
20988
  const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
20841
- const nullNames = taxonomy.filter((s) => !s.name).length;
20989
+ const nullNames = taxonomy.filter((s) => !sectorLabel(s)).length;
20842
20990
  if (nullNames > 0) {
20843
20991
  ctx?.logger?.warn?.(`adjust_audience: /sectors/all returned ${nullNames}/${taxonomy.length} sector(s) with a null/missing name`);
20844
20992
  }
20845
20993
  const resolved = [...direct];
20846
20994
  const ambiguities = [];
20847
20995
  for (const text3 of free) {
20848
- const matches = bestMatches(text3, taxonomy);
20849
- if (matches.length === 1 || matches.length >= 2 && matches[0].score >= 0.66 && matches[0].score - matches[1].score >= 0.34) {
20850
- resolved.push(matches[0].id);
20996
+ const match = matchSector(text3, taxonomy);
20997
+ if (match.kind === "resolved") {
20998
+ resolved.push(...match.ids);
20851
20999
  } else {
20852
- ambiguities.push({ sector_text: text3, matches });
21000
+ ambiguities.push({ sector_text: text3, matches: match.candidates });
20853
21001
  }
20854
21002
  }
20855
21003
  return { resolved, ambiguities };
@@ -20956,6 +21104,7 @@ var init_adjust_audience = __esm({
20956
21104
  "use strict";
20957
21105
  init_geo_helpers();
20958
21106
  init_country_guard();
21107
+ init_sector_resolver();
20959
21108
  init_tool_descriptions_generated();
20960
21109
  adjustAudience = {
20961
21110
  name: "leadbay_adjust_audience",
@@ -22173,12 +22322,68 @@ function sortFilterLists(filters) {
22173
22322
  }
22174
22323
  return out;
22175
22324
  }
22325
+ function isSectorRejection(err) {
22326
+ const e = err;
22327
+ return !!e && e.code === "BAD_INPUT" && typeof e.message === "string" && e.message.includes("filters.sectors");
22328
+ }
22329
+ async function submitSearch(client, body, opts, ctx) {
22330
+ try {
22331
+ return {
22332
+ ok: true,
22333
+ data: await client.request("POST", "/mcp/search", body, opts),
22334
+ rewritten: []
22335
+ };
22336
+ } catch (err) {
22337
+ const asked = body.filters?.sectors;
22338
+ if (!isSectorRejection(err) || !Array.isArray(asked) || asked.length === 0) {
22339
+ throw err;
22340
+ }
22341
+ const taxonomy = await fetchSectorTaxonomy(client, ctx).catch(() => {
22342
+ throw err;
22343
+ });
22344
+ const fix = resolveSectorValues(asked, taxonomy);
22345
+ if (fix.unresolved.length === 0 && fix.rewritten.length === 0)
22346
+ throw err;
22347
+ if (fix.unresolved.length > 0)
22348
+ return { ok: false, choice: sectorChoice(fix) };
22349
+ const retried = { ...body, filters: { ...body.filters, sectors: fix.values } };
22350
+ return {
22351
+ ok: true,
22352
+ data: await client.request("POST", "/mcp/search", retried, opts),
22353
+ rewritten: fix.rewritten
22354
+ };
22355
+ }
22356
+ }
22357
+ function sectorNote(rewritten) {
22358
+ if (rewritten.length === 0)
22359
+ return void 0;
22360
+ const pairs = rewritten.map((r) => `"${r.asked}" \u2192 "${r.used}"`).join(", ");
22361
+ return {
22362
+ sectors_note: `Leadbay's sector taxonomy spells these differently, so the search ran on ${pairs}. Tell the user which sector was searched.`,
22363
+ sectors_used: rewritten
22364
+ };
22365
+ }
22366
+ function sectorChoice(fix) {
22367
+ const names = fix.unresolved.map((u) => `"${u.asked}"`).join(", ");
22368
+ return {
22369
+ mode: "needs_sector_choice",
22370
+ submitted: false,
22371
+ unresolved_sectors: fix.unresolved.map((u) => ({
22372
+ asked: u.asked,
22373
+ closest: u.closest.map((c) => c.name)
22374
+ })),
22375
+ sector_sections: fix.sections,
22376
+ resolved_sectors: fix.rewritten,
22377
+ 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.`
22378
+ };
22379
+ }
22176
22380
  var DEFAULT_WAIT_SECONDS, findNewLeads;
22177
22381
  var init_find_new_leads = __esm({
22178
22382
  "../core/dist/composite/find-new-leads.js"() {
22179
22383
  "use strict";
22180
22384
  init_mcp_job_helpers();
22181
22385
  init_country_guard();
22386
+ init_sector_resolver();
22182
22387
  init_tool_descriptions_generated();
22183
22388
  DEFAULT_WAIT_SECONDS = 45;
22184
22389
  findNewLeads = {
@@ -22219,7 +22424,7 @@ var init_find_new_leads = __esm({
22219
22424
  },
22220
22425
  filters: {
22221
22426
  type: "object",
22222
- description: "HARD constraints (the seed only shapes ranking). Sector/location labels resolve at submit; an unresolvable value is a 400 naming it.",
22427
+ 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.",
22223
22428
  properties: {
22224
22429
  sectors: { type: "array", items: { type: "string" } },
22225
22430
  locations: { type: "array", items: { type: "string" } },
@@ -22391,19 +22596,27 @@ var init_find_new_leads = __esm({
22391
22596
  dry_run: dryRun
22392
22597
  });
22393
22598
  if (dryRun === true) {
22394
- const forecast = await client.request("POST", "/mcp/search", body);
22599
+ const attempt2 = await submitSearch(client, body, void 0, ctx);
22600
+ if (!attempt2.ok)
22601
+ return { ...attempt2.choice, ...countryNote, region: client.region };
22395
22602
  return {
22396
22603
  dry_run: true,
22397
- ...forecast,
22604
+ ...attempt2.data,
22605
+ ...sectorNote(attempt2.rewritten),
22398
22606
  ...countryNote,
22399
22607
  region: client.region
22400
22608
  };
22401
22609
  }
22402
22610
  if (isPaid && !consented) {
22403
- const forecast = vetoed ? null : await client.request("POST", "/mcp/search", {
22404
- ...body,
22405
- dry_run: true
22406
- });
22611
+ let forecast = null;
22612
+ let quoteRewritten = [];
22613
+ if (!vetoed) {
22614
+ const attempt2 = await submitSearch(client, { ...body, dry_run: true }, void 0, ctx);
22615
+ if (!attempt2.ok)
22616
+ return { ...attempt2.choice, ...countryNote, region: client.region };
22617
+ forecast = attempt2.data;
22618
+ quoteRewritten = attempt2.rewritten;
22619
+ }
22407
22620
  return {
22408
22621
  mode: "needs_confirmation",
22409
22622
  submitted: false,
@@ -22416,11 +22629,15 @@ var init_find_new_leads = __esm({
22416
22629
  estimated_cost: forecast?.estimated_cost ?? null,
22417
22630
  items_requested: forecast?.items_requested ?? null,
22418
22631
  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.",
22632
+ ...sectorNote(quoteRewritten),
22419
22633
  ...countryNote,
22420
22634
  region: client.region
22421
22635
  };
22422
22636
  }
22423
- const submit = await client.request("POST", "/mcp/search", body, { preSendSignal: ctx?.signal });
22637
+ const attempt = await submitSearch(client, body, { preSendSignal: ctx?.signal }, ctx);
22638
+ if (!attempt.ok)
22639
+ return { ...attempt.choice, ...countryNote, region: client.region };
22640
+ const submit = attempt.data;
22424
22641
  const mocked = mockedSubmitPreview(submit, "leadbay_find_new_leads", client.region);
22425
22642
  if (mocked)
22426
22643
  return mocked;
@@ -22471,6 +22688,7 @@ var init_find_new_leads = __esm({
22471
22688
  since: snapshot.next_since ?? null,
22472
22689
  suggested_wait_seconds: done ? 0 : 60
22473
22690
  },
22691
+ ...sectorNote(attempt.rewritten),
22474
22692
  ...countryNote,
22475
22693
  region: client.region
22476
22694
  };
@@ -24019,8 +24237,8 @@ var init_report_outreach = __esm({
24019
24237
  if (!params.dry_run && params.verification.source === "user_confirmed" && typeof ctx?.elicit === "function") {
24020
24238
  try {
24021
24239
  const targetIds = params.lead_ids ?? [params.lead_id];
24022
- const leadCount = targetIds.length;
24023
- const elicitMsg = leadCount === 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 ${leadCount} leads: "${params.note}". The agent claims you confirmed this. Type your literal confirmation to proceed; cancel to reject.`;
24240
+ const leadCount2 = targetIds.length;
24241
+ 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.`;
24024
24242
  const result = await ctx.elicit({
24025
24243
  message: elicitMsg,
24026
24244
  requestedSchema: {
@@ -31378,7 +31596,7 @@ var OAUTH_BASE_URLS = {
31378
31596
  fr: "https://staging.api.leadbay.app"
31379
31597
  }
31380
31598
  };
31381
- var VERSION = "0.39.2";
31599
+ var VERSION = "0.39.4";
31382
31600
  var HELP = `
31383
31601
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
31384
31602
 
@@ -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 (leadCount <= 0) {
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 leadCount = res.items.length;
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 = leadCount === 0 && (res.pagination?.total ?? 0) === 0;
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
@@ -17382,8 +17382,16 @@ function townName(value) {
17382
17382
  }
17383
17383
  function cityHintCore(cityHint) {
17384
17384
  const head = cityHint.split(",")[0] ?? "";
17385
- const expanded = expandAlias(head);
17386
- return { name: townName(expanded), isCity: expanded !== head };
17385
+ const segments = cityHint.split(",").map((part) => part.trim()).filter(Boolean);
17386
+ for (let take = segments.length; take > 0; take -= 1) {
17387
+ const candidate = segments.slice(0, take).join(" ");
17388
+ const expanded = expandAlias(candidate);
17389
+ if (expanded !== candidate) {
17390
+ return { name: townName(expanded), isCity: true };
17391
+ }
17392
+ }
17393
+ const prefixed = ADMIN_PREFIX.test(normalizeGeo(head));
17394
+ return { name: townName(head), isCity: prefixed };
17387
17395
  }
17388
17396
  function stateFieldOf(lead) {
17389
17397
  const state = lead?.location?.state;
@@ -22998,27 +23006,161 @@ var bulkEnrichStatus = {
22998
23006
  }
22999
23007
  };
23000
23008
 
23001
- // ../core/dist/composite/adjust-audience.js
23002
- function tokens(s) {
23003
- if (!s)
23004
- return [];
23005
- return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
23009
+ // ../core/dist/composite/_sector-resolver.js
23010
+ var FILLER = /* @__PURE__ */ new Set([
23011
+ // en
23012
+ "and",
23013
+ "of",
23014
+ "the",
23015
+ "for",
23016
+ "or",
23017
+ "activity",
23018
+ // fr
23019
+ "et",
23020
+ "de",
23021
+ "des",
23022
+ "du",
23023
+ "d",
23024
+ "la",
23025
+ "le",
23026
+ "les",
23027
+ "l",
23028
+ "en",
23029
+ "aux",
23030
+ "au",
23031
+ "activite",
23032
+ "pour",
23033
+ "sur",
23034
+ "par",
23035
+ "a"
23036
+ ]);
23037
+ function normalizeSectorText(value) {
23038
+ return (value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^0-9a-z]+/g, " ").trim();
23006
23039
  }
23007
- function bestMatches(text3, taxonomy) {
23008
- const want = new Set(tokens(text3));
23009
- if (want.size === 0)
23010
- return [];
23011
- const ranked = taxonomy.map((s) => {
23012
- const have = new Set(tokens(s.name));
23040
+ function singular(token) {
23041
+ if (token.length > 4 && token.endsWith("ies"))
23042
+ return `${token.slice(0, -3)}y`;
23043
+ return token.length > 3 && token.endsWith("s") ? token.slice(0, -1) : token;
23044
+ }
23045
+ function sectorTokens(value) {
23046
+ const all = normalizeSectorText(value).split(" ").filter(Boolean).map(singular);
23047
+ const core = all.filter((t) => !FILLER.has(t));
23048
+ return core.length > 0 ? core : all;
23049
+ }
23050
+ function sectorLabel(row) {
23051
+ if (!row)
23052
+ return "";
23053
+ const label = row.label;
23054
+ if (typeof label === "string" && label.length > 0)
23055
+ return label;
23056
+ return typeof row.name === "string" ? row.name : "";
23057
+ }
23058
+ function leadCount(row) {
23059
+ const n = row.number_of_leads;
23060
+ return typeof n === "number" && Number.isFinite(n) ? n : 0;
23061
+ }
23062
+ var MAX_CANDIDATES = 8;
23063
+ var MAX_PARTIAL_CANDIDATES = 4;
23064
+ function rankCandidates(scored, tieBreak) {
23065
+ const out = [];
23066
+ const seen = /* @__PURE__ */ new Set();
23067
+ 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 }) => {
23068
+ const label = sectorLabel(row);
23069
+ if (!label || seen.has(label) || out.length >= MAX_CANDIDATES)
23070
+ return;
23071
+ seen.add(label);
23072
+ out.push({ id: row.id, name: label, score: Number(score.toFixed(2)) });
23073
+ });
23074
+ return out;
23075
+ }
23076
+ function matchSector(text3, taxonomy) {
23077
+ const wanted = normalizeSectorText(text3);
23078
+ if (!wanted)
23079
+ return { kind: "unresolved", candidates: [] };
23080
+ const exact = taxonomy.filter((row) => normalizeSectorText(sectorLabel(row)) === wanted);
23081
+ if (exact.length > 0) {
23082
+ const label = sectorLabel(exact.reduce((best, row) => leadCount(row) > leadCount(best) ? row : best));
23083
+ const ids = taxonomy.filter((row) => sectorLabel(row) === label).map((row) => row.id);
23084
+ return { kind: "resolved", label, ids, exact: true };
23085
+ }
23086
+ const want = new Set(sectorTokens(text3));
23087
+ const sameWords = taxonomy.filter((row) => {
23088
+ const have = new Set(sectorTokens(sectorLabel(row)));
23089
+ return have.size === want.size && [...want].every((token) => have.has(token));
23090
+ });
23091
+ const sameWordLabels = new Set(sameWords.map((row) => sectorLabel(row)).filter(Boolean));
23092
+ if (sameWordLabels.size === 1) {
23093
+ const label = [...sameWordLabels][0];
23094
+ const ids = taxonomy.filter((row) => sectorLabel(row) === label).map((row) => row.id);
23095
+ return { kind: "resolved", label, ids, exact: false };
23096
+ }
23097
+ const scored = [];
23098
+ for (const row of taxonomy) {
23099
+ const have = new Set(sectorTokens(sectorLabel(row)));
23100
+ if (have.size === 0)
23101
+ continue;
23013
23102
  let overlap = 0;
23014
- for (const t of want)
23015
- if (have.has(t))
23103
+ for (const token of want)
23104
+ if (have.has(token))
23016
23105
  overlap += 1;
23017
- const score = overlap / Math.max(want.size, 1);
23018
- return { id: s.id, name: s.name ?? "", score };
23019
- }).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
23020
- return ranked.slice(0, 5);
23106
+ if (overlap === 0)
23107
+ continue;
23108
+ scored.push({ row, score: overlap / want.size, closeness: overlap / have.size });
23109
+ }
23110
+ const full = scored.filter((s) => s.score >= 1);
23111
+ return {
23112
+ kind: "unresolved",
23113
+ candidates: full.length > 0 ? rankCandidates(full, "breadth") : rankCandidates(scored, "closeness").slice(0, MAX_PARTIAL_CANDIDATES)
23114
+ };
23115
+ }
23116
+ function sectorSections(taxonomy) {
23117
+ const roots = taxonomy.filter((row) => !row.parent);
23118
+ const labels = [];
23119
+ for (const row of roots.sort((a, b) => leadCount(b) - leadCount(a))) {
23120
+ const label = sectorLabel(row);
23121
+ if (label && !labels.includes(label))
23122
+ labels.push(label);
23123
+ }
23124
+ return labels;
23021
23125
  }
23126
+ async function fetchSectorTaxonomy(client, ctx) {
23127
+ const me = await client.resolveMe().catch(() => null);
23128
+ const lang = me?.language ?? "en";
23129
+ const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
23130
+ const unlabelled = taxonomy.filter((row) => !sectorLabel(row)).length;
23131
+ if (unlabelled > 0) {
23132
+ ctx?.logger?.warn?.(`sector taxonomy: ${unlabelled}/${taxonomy.length} row(s) carry no label`);
23133
+ }
23134
+ return taxonomy;
23135
+ }
23136
+ var NUMERIC_ID = /^\d+$/;
23137
+ function resolveSectorValues(values, taxonomy) {
23138
+ const out = {
23139
+ values: [],
23140
+ rewritten: [],
23141
+ unresolved: [],
23142
+ sections: sectorSections(taxonomy)
23143
+ };
23144
+ for (const raw of values) {
23145
+ const asked = typeof raw === "string" ? raw : String(raw ?? "");
23146
+ if (NUMERIC_ID.test(asked.trim())) {
23147
+ out.values.push(asked.trim());
23148
+ continue;
23149
+ }
23150
+ const match = matchSector(asked, taxonomy);
23151
+ if (match.kind === "resolved") {
23152
+ out.values.push(match.label);
23153
+ if (match.label !== asked)
23154
+ out.rewritten.push({ asked, used: match.label });
23155
+ continue;
23156
+ }
23157
+ out.values.push(asked);
23158
+ out.unresolved.push({ asked, closest: match.candidates });
23159
+ }
23160
+ return out;
23161
+ }
23162
+
23163
+ // ../core/dist/composite/adjust-audience.js
23022
23164
  async function resolveSectors(client, texts, ctx) {
23023
23165
  const looksLikeId2 = (s) => /^\d+$/.test(s);
23024
23166
  const direct = texts.filter(looksLikeId2);
@@ -23028,18 +23170,18 @@ async function resolveSectors(client, texts, ctx) {
23028
23170
  const me = await client.resolveMe().catch(() => null);
23029
23171
  const lang = me?.language ?? "en";
23030
23172
  const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
23031
- const nullNames = taxonomy.filter((s) => !s.name).length;
23173
+ const nullNames = taxonomy.filter((s) => !sectorLabel(s)).length;
23032
23174
  if (nullNames > 0) {
23033
23175
  ctx?.logger?.warn?.(`adjust_audience: /sectors/all returned ${nullNames}/${taxonomy.length} sector(s) with a null/missing name`);
23034
23176
  }
23035
23177
  const resolved = [...direct];
23036
23178
  const ambiguities = [];
23037
23179
  for (const text3 of free) {
23038
- const matches = bestMatches(text3, taxonomy);
23039
- if (matches.length === 1 || matches.length >= 2 && matches[0].score >= 0.66 && matches[0].score - matches[1].score >= 0.34) {
23040
- resolved.push(matches[0].id);
23180
+ const match = matchSector(text3, taxonomy);
23181
+ if (match.kind === "resolved") {
23182
+ resolved.push(...match.ids);
23041
23183
  } else {
23042
- ambiguities.push({ sector_text: text3, matches });
23184
+ ambiguities.push({ sector_text: text3, matches: match.candidates });
23043
23185
  }
23044
23186
  }
23045
23187
  return { resolved, ambiguities };
@@ -24334,6 +24476,61 @@ function sortFilterLists(filters) {
24334
24476
  }
24335
24477
  return out;
24336
24478
  }
24479
+ function isSectorRejection(err) {
24480
+ const e = err;
24481
+ return !!e && e.code === "BAD_INPUT" && typeof e.message === "string" && e.message.includes("filters.sectors");
24482
+ }
24483
+ async function submitSearch(client, body, opts, ctx) {
24484
+ try {
24485
+ return {
24486
+ ok: true,
24487
+ data: await client.request("POST", "/mcp/search", body, opts),
24488
+ rewritten: []
24489
+ };
24490
+ } catch (err) {
24491
+ const asked = body.filters?.sectors;
24492
+ if (!isSectorRejection(err) || !Array.isArray(asked) || asked.length === 0) {
24493
+ throw err;
24494
+ }
24495
+ const taxonomy = await fetchSectorTaxonomy(client, ctx).catch(() => {
24496
+ throw err;
24497
+ });
24498
+ const fix = resolveSectorValues(asked, taxonomy);
24499
+ if (fix.unresolved.length === 0 && fix.rewritten.length === 0)
24500
+ throw err;
24501
+ if (fix.unresolved.length > 0)
24502
+ return { ok: false, choice: sectorChoice(fix) };
24503
+ const retried = { ...body, filters: { ...body.filters, sectors: fix.values } };
24504
+ return {
24505
+ ok: true,
24506
+ data: await client.request("POST", "/mcp/search", retried, opts),
24507
+ rewritten: fix.rewritten
24508
+ };
24509
+ }
24510
+ }
24511
+ function sectorNote(rewritten) {
24512
+ if (rewritten.length === 0)
24513
+ return void 0;
24514
+ const pairs = rewritten.map((r) => `"${r.asked}" \u2192 "${r.used}"`).join(", ");
24515
+ return {
24516
+ sectors_note: `Leadbay's sector taxonomy spells these differently, so the search ran on ${pairs}. Tell the user which sector was searched.`,
24517
+ sectors_used: rewritten
24518
+ };
24519
+ }
24520
+ function sectorChoice(fix) {
24521
+ const names = fix.unresolved.map((u) => `"${u.asked}"`).join(", ");
24522
+ return {
24523
+ mode: "needs_sector_choice",
24524
+ submitted: false,
24525
+ unresolved_sectors: fix.unresolved.map((u) => ({
24526
+ asked: u.asked,
24527
+ closest: u.closest.map((c) => c.name)
24528
+ })),
24529
+ sector_sections: fix.sections,
24530
+ resolved_sectors: fix.rewritten,
24531
+ 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.`
24532
+ };
24533
+ }
24337
24534
  var findNewLeads = {
24338
24535
  name: "leadbay_find_new_leads",
24339
24536
  annotations: {
@@ -24372,7 +24569,7 @@ var findNewLeads = {
24372
24569
  },
24373
24570
  filters: {
24374
24571
  type: "object",
24375
- description: "HARD constraints (the seed only shapes ranking). Sector/location labels resolve at submit; an unresolvable value is a 400 naming it.",
24572
+ 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.",
24376
24573
  properties: {
24377
24574
  sectors: { type: "array", items: { type: "string" } },
24378
24575
  locations: { type: "array", items: { type: "string" } },
@@ -24544,19 +24741,27 @@ var findNewLeads = {
24544
24741
  dry_run: dryRun
24545
24742
  });
24546
24743
  if (dryRun === true) {
24547
- const forecast = await client.request("POST", "/mcp/search", body);
24744
+ const attempt2 = await submitSearch(client, body, void 0, ctx);
24745
+ if (!attempt2.ok)
24746
+ return { ...attempt2.choice, ...countryNote, region: client.region };
24548
24747
  return {
24549
24748
  dry_run: true,
24550
- ...forecast,
24749
+ ...attempt2.data,
24750
+ ...sectorNote(attempt2.rewritten),
24551
24751
  ...countryNote,
24552
24752
  region: client.region
24553
24753
  };
24554
24754
  }
24555
24755
  if (isPaid && !consented) {
24556
- const forecast = vetoed ? null : await client.request("POST", "/mcp/search", {
24557
- ...body,
24558
- dry_run: true
24559
- });
24756
+ let forecast = null;
24757
+ let quoteRewritten = [];
24758
+ if (!vetoed) {
24759
+ const attempt2 = await submitSearch(client, { ...body, dry_run: true }, void 0, ctx);
24760
+ if (!attempt2.ok)
24761
+ return { ...attempt2.choice, ...countryNote, region: client.region };
24762
+ forecast = attempt2.data;
24763
+ quoteRewritten = attempt2.rewritten;
24764
+ }
24560
24765
  return {
24561
24766
  mode: "needs_confirmation",
24562
24767
  submitted: false,
@@ -24569,11 +24774,15 @@ var findNewLeads = {
24569
24774
  estimated_cost: forecast?.estimated_cost ?? null,
24570
24775
  items_requested: forecast?.items_requested ?? null,
24571
24776
  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.",
24777
+ ...sectorNote(quoteRewritten),
24572
24778
  ...countryNote,
24573
24779
  region: client.region
24574
24780
  };
24575
24781
  }
24576
- const submit = await client.request("POST", "/mcp/search", body, { preSendSignal: ctx?.signal });
24782
+ const attempt = await submitSearch(client, body, { preSendSignal: ctx?.signal }, ctx);
24783
+ if (!attempt.ok)
24784
+ return { ...attempt.choice, ...countryNote, region: client.region };
24785
+ const submit = attempt.data;
24577
24786
  const mocked = mockedSubmitPreview(submit, "leadbay_find_new_leads", client.region);
24578
24787
  if (mocked)
24579
24788
  return mocked;
@@ -24624,6 +24833,7 @@ var findNewLeads = {
24624
24833
  since: snapshot.next_since ?? null,
24625
24834
  suggested_wait_seconds: done ? 0 : 60
24626
24835
  },
24836
+ ...sectorNote(attempt.rewritten),
24627
24837
  ...countryNote,
24628
24838
  region: client.region
24629
24839
  };
@@ -26105,8 +26315,8 @@ var reportOutreach = {
26105
26315
  if (!params.dry_run && params.verification.source === "user_confirmed" && typeof ctx?.elicit === "function") {
26106
26316
  try {
26107
26317
  const targetIds = params.lead_ids ?? [params.lead_id];
26108
- const leadCount = targetIds.length;
26109
- const elicitMsg = leadCount === 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 ${leadCount} leads: "${params.note}". The agent claims you confirmed this. Type your literal confirmation to proceed; cancel to reject.`;
26318
+ const leadCount2 = targetIds.length;
26319
+ 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.`;
26110
26320
  const result = await ctx.elicit({
26111
26321
  message: elicitMsg,
26112
26322
  requestedSchema: {
@@ -28660,7 +28870,7 @@ function parseWriteEnv(env = process.env) {
28660
28870
  }
28661
28871
 
28662
28872
  // src/http-server.ts
28663
- var VERSION = true ? "0.39.2" : "0.0.0-dev";
28873
+ var VERSION = true ? "0.39.4" : "0.0.0-dev";
28664
28874
  var PORT = Number(process.env.PORT ?? 8080);
28665
28875
  var HOST = process.env.HOST ?? "0.0.0.0";
28666
28876
  var logger = {
@@ -1805,7 +1805,7 @@ var init_installer_gui = __esm({
1805
1805
  init_install_dxt();
1806
1806
  init_install_shared();
1807
1807
  init_oauth();
1808
- VERSION = true ? "0.39.2" : "0.0.0-dev";
1808
+ VERSION = true ? "0.39.4" : "0.0.0-dev";
1809
1809
  MESSAGES = {
1810
1810
  en: {
1811
1811
  installer: {
@@ -1068,7 +1068,7 @@ async function oauthLogin(opts) {
1068
1068
  }
1069
1069
 
1070
1070
  // installer/installer-gui.ts
1071
- var VERSION = true ? "0.39.2" : "0.0.0-dev";
1071
+ var VERSION = true ? "0.39.4" : "0.0.0-dev";
1072
1072
  var MESSAGES = {
1073
1073
  en: {
1074
1074
  installer: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leadbay/mcp",
3
- "version": "0.39.2",
3
+ "version": "0.39.4",
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",