@leadbay/mcp 0.39.2 → 0.39.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js 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
@@ -20808,27 +20808,167 @@ var init_bulk_enrich_status = __esm({
20808
20808
  }
20809
20809
  });
20810
20810
 
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);
20811
+ // ../core/dist/composite/_sector-resolver.js
20812
+ function normalizeSectorText(value) {
20813
+ return (value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^0-9a-z]+/g, " ").trim();
20816
20814
  }
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));
20815
+ function singular(token) {
20816
+ if (token.length > 4 && token.endsWith("ies"))
20817
+ return `${token.slice(0, -3)}y`;
20818
+ return token.length > 3 && token.endsWith("s") ? token.slice(0, -1) : token;
20819
+ }
20820
+ function sectorTokens(value) {
20821
+ const all = normalizeSectorText(value).split(" ").filter(Boolean).map(singular);
20822
+ const core = all.filter((t) => !FILLER.has(t));
20823
+ return core.length > 0 ? core : all;
20824
+ }
20825
+ function sectorLabel(row) {
20826
+ if (!row)
20827
+ return "";
20828
+ const label = row.label;
20829
+ if (typeof label === "string" && label.length > 0)
20830
+ return label;
20831
+ return typeof row.name === "string" ? row.name : "";
20832
+ }
20833
+ function leadCount(row) {
20834
+ const n = row.number_of_leads;
20835
+ return typeof n === "number" && Number.isFinite(n) ? n : 0;
20836
+ }
20837
+ function rankCandidates(scored, tieBreak) {
20838
+ const out = [];
20839
+ const seen = /* @__PURE__ */ new Set();
20840
+ scored.sort((a, b) => b.score - a.score || (tieBreak === "breadth" ? leadCount(b.row) - leadCount(a.row) || b.closeness - a.closeness : b.closeness - a.closeness || leadCount(b.row) - leadCount(a.row)) || sectorLabel(a.row).length - sectorLabel(b.row).length).forEach(({ row, score }) => {
20841
+ const label = sectorLabel(row);
20842
+ if (!label || seen.has(label) || out.length >= MAX_CANDIDATES)
20843
+ return;
20844
+ seen.add(label);
20845
+ out.push({ id: row.id, name: label, score: Number(score.toFixed(2)) });
20846
+ });
20847
+ return out;
20848
+ }
20849
+ function matchSector(text3, taxonomy) {
20850
+ const wanted = normalizeSectorText(text3);
20851
+ if (!wanted)
20852
+ return { kind: "unresolved", candidates: [] };
20853
+ const exact = taxonomy.filter((row) => normalizeSectorText(sectorLabel(row)) === wanted);
20854
+ if (exact.length > 0) {
20855
+ const label = sectorLabel(exact.reduce((best, row) => leadCount(row) > leadCount(best) ? row : best));
20856
+ const ids = taxonomy.filter((row) => sectorLabel(row) === label).map((row) => row.id);
20857
+ return { kind: "resolved", label, ids, exact: true };
20858
+ }
20859
+ const want = new Set(sectorTokens(text3));
20860
+ const sameWords = taxonomy.filter((row) => {
20861
+ const have = new Set(sectorTokens(sectorLabel(row)));
20862
+ return have.size === want.size && [...want].every((token) => have.has(token));
20863
+ });
20864
+ const sameWordLabels = new Set(sameWords.map((row) => sectorLabel(row)).filter(Boolean));
20865
+ if (sameWordLabels.size === 1) {
20866
+ const label = [...sameWordLabels][0];
20867
+ const ids = taxonomy.filter((row) => sectorLabel(row) === label).map((row) => row.id);
20868
+ return { kind: "resolved", label, ids, exact: false };
20869
+ }
20870
+ const scored = [];
20871
+ for (const row of taxonomy) {
20872
+ const have = new Set(sectorTokens(sectorLabel(row)));
20873
+ if (have.size === 0)
20874
+ continue;
20823
20875
  let overlap = 0;
20824
- for (const t of want)
20825
- if (have.has(t))
20876
+ for (const token of want)
20877
+ if (have.has(token))
20826
20878
  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);
20879
+ if (overlap === 0)
20880
+ continue;
20881
+ scored.push({ row, score: overlap / want.size, closeness: overlap / have.size });
20882
+ }
20883
+ const full = scored.filter((s) => s.score >= 1);
20884
+ return {
20885
+ kind: "unresolved",
20886
+ candidates: full.length > 0 ? rankCandidates(full, "breadth") : rankCandidates(scored, "closeness").slice(0, MAX_PARTIAL_CANDIDATES)
20887
+ };
20888
+ }
20889
+ function sectorSections(taxonomy) {
20890
+ const roots = taxonomy.filter((row) => !row.parent);
20891
+ const labels = [];
20892
+ for (const row of roots.sort((a, b) => leadCount(b) - leadCount(a))) {
20893
+ const label = sectorLabel(row);
20894
+ if (label && !labels.includes(label))
20895
+ labels.push(label);
20896
+ }
20897
+ return labels;
20831
20898
  }
20899
+ async function fetchSectorTaxonomy(client, ctx) {
20900
+ const me = await client.resolveMe().catch(() => null);
20901
+ const lang = me?.language ?? "en";
20902
+ const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
20903
+ const unlabelled = taxonomy.filter((row) => !sectorLabel(row)).length;
20904
+ if (unlabelled > 0) {
20905
+ ctx?.logger?.warn?.(`sector taxonomy: ${unlabelled}/${taxonomy.length} row(s) carry no label`);
20906
+ }
20907
+ return taxonomy;
20908
+ }
20909
+ function resolveSectorValues(values, taxonomy) {
20910
+ const out = {
20911
+ values: [],
20912
+ rewritten: [],
20913
+ unresolved: [],
20914
+ sections: sectorSections(taxonomy)
20915
+ };
20916
+ for (const raw of values) {
20917
+ const asked = typeof raw === "string" ? raw : String(raw ?? "");
20918
+ if (NUMERIC_ID.test(asked.trim())) {
20919
+ out.values.push(asked.trim());
20920
+ continue;
20921
+ }
20922
+ const match = matchSector(asked, taxonomy);
20923
+ if (match.kind === "resolved") {
20924
+ out.values.push(match.label);
20925
+ if (match.label !== asked)
20926
+ out.rewritten.push({ asked, used: match.label });
20927
+ continue;
20928
+ }
20929
+ out.values.push(asked);
20930
+ out.unresolved.push({ asked, closest: match.candidates });
20931
+ }
20932
+ return out;
20933
+ }
20934
+ var FILLER, MAX_CANDIDATES, MAX_PARTIAL_CANDIDATES, NUMERIC_ID;
20935
+ var init_sector_resolver = __esm({
20936
+ "../core/dist/composite/_sector-resolver.js"() {
20937
+ "use strict";
20938
+ FILLER = /* @__PURE__ */ new Set([
20939
+ // en
20940
+ "and",
20941
+ "of",
20942
+ "the",
20943
+ "for",
20944
+ "or",
20945
+ "activity",
20946
+ // fr
20947
+ "et",
20948
+ "de",
20949
+ "des",
20950
+ "du",
20951
+ "d",
20952
+ "la",
20953
+ "le",
20954
+ "les",
20955
+ "l",
20956
+ "en",
20957
+ "aux",
20958
+ "au",
20959
+ "activite",
20960
+ "pour",
20961
+ "sur",
20962
+ "par",
20963
+ "a"
20964
+ ]);
20965
+ MAX_CANDIDATES = 8;
20966
+ MAX_PARTIAL_CANDIDATES = 4;
20967
+ NUMERIC_ID = /^\d+$/;
20968
+ }
20969
+ });
20970
+
20971
+ // ../core/dist/composite/adjust-audience.js
20832
20972
  async function resolveSectors(client, texts, ctx) {
20833
20973
  const looksLikeId2 = (s) => /^\d+$/.test(s);
20834
20974
  const direct = texts.filter(looksLikeId2);
@@ -20838,18 +20978,18 @@ async function resolveSectors(client, texts, ctx) {
20838
20978
  const me = await client.resolveMe().catch(() => null);
20839
20979
  const lang = me?.language ?? "en";
20840
20980
  const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
20841
- const nullNames = taxonomy.filter((s) => !s.name).length;
20981
+ const nullNames = taxonomy.filter((s) => !sectorLabel(s)).length;
20842
20982
  if (nullNames > 0) {
20843
20983
  ctx?.logger?.warn?.(`adjust_audience: /sectors/all returned ${nullNames}/${taxonomy.length} sector(s) with a null/missing name`);
20844
20984
  }
20845
20985
  const resolved = [...direct];
20846
20986
  const ambiguities = [];
20847
20987
  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);
20988
+ const match = matchSector(text3, taxonomy);
20989
+ if (match.kind === "resolved") {
20990
+ resolved.push(...match.ids);
20851
20991
  } else {
20852
- ambiguities.push({ sector_text: text3, matches });
20992
+ ambiguities.push({ sector_text: text3, matches: match.candidates });
20853
20993
  }
20854
20994
  }
20855
20995
  return { resolved, ambiguities };
@@ -20956,6 +21096,7 @@ var init_adjust_audience = __esm({
20956
21096
  "use strict";
20957
21097
  init_geo_helpers();
20958
21098
  init_country_guard();
21099
+ init_sector_resolver();
20959
21100
  init_tool_descriptions_generated();
20960
21101
  adjustAudience = {
20961
21102
  name: "leadbay_adjust_audience",
@@ -22173,12 +22314,68 @@ function sortFilterLists(filters) {
22173
22314
  }
22174
22315
  return out;
22175
22316
  }
22317
+ function isSectorRejection(err) {
22318
+ const e = err;
22319
+ return !!e && e.code === "BAD_INPUT" && typeof e.message === "string" && e.message.includes("filters.sectors");
22320
+ }
22321
+ async function submitSearch(client, body, opts, ctx) {
22322
+ try {
22323
+ return {
22324
+ ok: true,
22325
+ data: await client.request("POST", "/mcp/search", body, opts),
22326
+ rewritten: []
22327
+ };
22328
+ } catch (err) {
22329
+ const asked = body.filters?.sectors;
22330
+ if (!isSectorRejection(err) || !Array.isArray(asked) || asked.length === 0) {
22331
+ throw err;
22332
+ }
22333
+ const taxonomy = await fetchSectorTaxonomy(client, ctx).catch(() => {
22334
+ throw err;
22335
+ });
22336
+ const fix = resolveSectorValues(asked, taxonomy);
22337
+ if (fix.unresolved.length === 0 && fix.rewritten.length === 0)
22338
+ throw err;
22339
+ if (fix.unresolved.length > 0)
22340
+ return { ok: false, choice: sectorChoice(fix) };
22341
+ const retried = { ...body, filters: { ...body.filters, sectors: fix.values } };
22342
+ return {
22343
+ ok: true,
22344
+ data: await client.request("POST", "/mcp/search", retried, opts),
22345
+ rewritten: fix.rewritten
22346
+ };
22347
+ }
22348
+ }
22349
+ function sectorNote(rewritten) {
22350
+ if (rewritten.length === 0)
22351
+ return void 0;
22352
+ const pairs = rewritten.map((r) => `"${r.asked}" \u2192 "${r.used}"`).join(", ");
22353
+ return {
22354
+ sectors_note: `Leadbay's sector taxonomy spells these differently, so the search ran on ${pairs}. Tell the user which sector was searched.`,
22355
+ sectors_used: rewritten
22356
+ };
22357
+ }
22358
+ function sectorChoice(fix) {
22359
+ const names = fix.unresolved.map((u) => `"${u.asked}"`).join(", ");
22360
+ return {
22361
+ mode: "needs_sector_choice",
22362
+ submitted: false,
22363
+ unresolved_sectors: fix.unresolved.map((u) => ({
22364
+ asked: u.asked,
22365
+ closest: u.closest.map((c) => c.name)
22366
+ })),
22367
+ sector_sections: fix.sections,
22368
+ resolved_sectors: fix.rewritten,
22369
+ hint: `No sector in this workspace's taxonomy is named ${names || "that"}. Nothing was submitted and nothing was spent. Re-call with a label copied character for character from \`closest\` or \`sector_sections\` (a section covers everything under it), or drop \`filters.sectors\` and put the wording in \`query\` / \`example_lead.description\` instead \u2014 the search then RANKS on it rather than fencing on it, which is usually what the user meant. \`leadbay_list_sectors\` returns the full taxonomy if none of these fit.`
22370
+ };
22371
+ }
22176
22372
  var DEFAULT_WAIT_SECONDS, findNewLeads;
22177
22373
  var init_find_new_leads = __esm({
22178
22374
  "../core/dist/composite/find-new-leads.js"() {
22179
22375
  "use strict";
22180
22376
  init_mcp_job_helpers();
22181
22377
  init_country_guard();
22378
+ init_sector_resolver();
22182
22379
  init_tool_descriptions_generated();
22183
22380
  DEFAULT_WAIT_SECONDS = 45;
22184
22381
  findNewLeads = {
@@ -22219,7 +22416,7 @@ var init_find_new_leads = __esm({
22219
22416
  },
22220
22417
  filters: {
22221
22418
  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.",
22419
+ description: "HARD constraints (the seed only shapes ranking). Sector/location labels resolve at submit. `sectors` must be a label from Leadbay's own taxonomy (leadbay_list_sectors) \u2014 everyday wording like 'Professional Services' is not one; a spelling/plural slip is corrected for you, but a word with no label comes back as mode:'needs_sector_choice' with the labels to pick from. An unresolvable location is a 400 naming it.",
22223
22420
  properties: {
22224
22421
  sectors: { type: "array", items: { type: "string" } },
22225
22422
  locations: { type: "array", items: { type: "string" } },
@@ -22391,19 +22588,27 @@ var init_find_new_leads = __esm({
22391
22588
  dry_run: dryRun
22392
22589
  });
22393
22590
  if (dryRun === true) {
22394
- const forecast = await client.request("POST", "/mcp/search", body);
22591
+ const attempt2 = await submitSearch(client, body, void 0, ctx);
22592
+ if (!attempt2.ok)
22593
+ return { ...attempt2.choice, ...countryNote, region: client.region };
22395
22594
  return {
22396
22595
  dry_run: true,
22397
- ...forecast,
22596
+ ...attempt2.data,
22597
+ ...sectorNote(attempt2.rewritten),
22398
22598
  ...countryNote,
22399
22599
  region: client.region
22400
22600
  };
22401
22601
  }
22402
22602
  if (isPaid && !consented) {
22403
- const forecast = vetoed ? null : await client.request("POST", "/mcp/search", {
22404
- ...body,
22405
- dry_run: true
22406
- });
22603
+ let forecast = null;
22604
+ let quoteRewritten = [];
22605
+ if (!vetoed) {
22606
+ const attempt2 = await submitSearch(client, { ...body, dry_run: true }, void 0, ctx);
22607
+ if (!attempt2.ok)
22608
+ return { ...attempt2.choice, ...countryNote, region: client.region };
22609
+ forecast = attempt2.data;
22610
+ quoteRewritten = attempt2.rewritten;
22611
+ }
22407
22612
  return {
22408
22613
  mode: "needs_confirmation",
22409
22614
  submitted: false,
@@ -22416,11 +22621,15 @@ var init_find_new_leads = __esm({
22416
22621
  estimated_cost: forecast?.estimated_cost ?? null,
22417
22622
  items_requested: forecast?.items_requested ?? null,
22418
22623
  hint: vetoed ? "confirm:false vetoed the run \u2014 nothing was submitted. Re-call with confirm:true to proceed, or drop qualify/channels for a free search." : "Tell the user what will run and that it uses their plan's quota (no amounts, no money), get an explicit go-ahead, then re-call with confirm:true. For a free search instead: omit qualify and channels.",
22624
+ ...sectorNote(quoteRewritten),
22419
22625
  ...countryNote,
22420
22626
  region: client.region
22421
22627
  };
22422
22628
  }
22423
- const submit = await client.request("POST", "/mcp/search", body, { preSendSignal: ctx?.signal });
22629
+ const attempt = await submitSearch(client, body, { preSendSignal: ctx?.signal }, ctx);
22630
+ if (!attempt.ok)
22631
+ return { ...attempt.choice, ...countryNote, region: client.region };
22632
+ const submit = attempt.data;
22424
22633
  const mocked = mockedSubmitPreview(submit, "leadbay_find_new_leads", client.region);
22425
22634
  if (mocked)
22426
22635
  return mocked;
@@ -22471,6 +22680,7 @@ var init_find_new_leads = __esm({
22471
22680
  since: snapshot.next_since ?? null,
22472
22681
  suggested_wait_seconds: done ? 0 : 60
22473
22682
  },
22683
+ ...sectorNote(attempt.rewritten),
22474
22684
  ...countryNote,
22475
22685
  region: client.region
22476
22686
  };
@@ -24019,8 +24229,8 @@ var init_report_outreach = __esm({
24019
24229
  if (!params.dry_run && params.verification.source === "user_confirmed" && typeof ctx?.elicit === "function") {
24020
24230
  try {
24021
24231
  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.`;
24232
+ const leadCount2 = targetIds.length;
24233
+ const elicitMsg = leadCount2 === 1 ? `An AI agent wants to log outreach on lead ${targetIds[0]}: "${params.note}". The agent claims you confirmed this. Type your literal confirmation to proceed; cancel to reject.` : `An AI agent wants to log outreach on ${leadCount2} leads: "${params.note}". The agent claims you confirmed this. Type your literal confirmation to proceed; cancel to reject.`;
24024
24234
  const result = await ctx.elicit({
24025
24235
  message: elicitMsg,
24026
24236
  requestedSchema: {
@@ -31378,7 +31588,7 @@ var OAUTH_BASE_URLS = {
31378
31588
  fr: "https://staging.api.leadbay.app"
31379
31589
  }
31380
31590
  };
31381
- var VERSION = "0.39.2";
31591
+ var VERSION = "0.39.3";
31382
31592
  var HELP = `
31383
31593
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
31384
31594
 
@@ -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
@@ -22998,27 +22998,161 @@ var bulkEnrichStatus = {
22998
22998
  }
22999
22999
  };
23000
23000
 
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);
23001
+ // ../core/dist/composite/_sector-resolver.js
23002
+ var FILLER = /* @__PURE__ */ new Set([
23003
+ // en
23004
+ "and",
23005
+ "of",
23006
+ "the",
23007
+ "for",
23008
+ "or",
23009
+ "activity",
23010
+ // fr
23011
+ "et",
23012
+ "de",
23013
+ "des",
23014
+ "du",
23015
+ "d",
23016
+ "la",
23017
+ "le",
23018
+ "les",
23019
+ "l",
23020
+ "en",
23021
+ "aux",
23022
+ "au",
23023
+ "activite",
23024
+ "pour",
23025
+ "sur",
23026
+ "par",
23027
+ "a"
23028
+ ]);
23029
+ function normalizeSectorText(value) {
23030
+ return (value ?? "").normalize("NFD").replace(/[\u0300-\u036f]/g, "").toLowerCase().replace(/[^0-9a-z]+/g, " ").trim();
23006
23031
  }
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));
23032
+ function singular(token) {
23033
+ if (token.length > 4 && token.endsWith("ies"))
23034
+ return `${token.slice(0, -3)}y`;
23035
+ return token.length > 3 && token.endsWith("s") ? token.slice(0, -1) : token;
23036
+ }
23037
+ function sectorTokens(value) {
23038
+ const all = normalizeSectorText(value).split(" ").filter(Boolean).map(singular);
23039
+ const core = all.filter((t) => !FILLER.has(t));
23040
+ return core.length > 0 ? core : all;
23041
+ }
23042
+ function sectorLabel(row) {
23043
+ if (!row)
23044
+ return "";
23045
+ const label = row.label;
23046
+ if (typeof label === "string" && label.length > 0)
23047
+ return label;
23048
+ return typeof row.name === "string" ? row.name : "";
23049
+ }
23050
+ function leadCount(row) {
23051
+ const n = row.number_of_leads;
23052
+ return typeof n === "number" && Number.isFinite(n) ? n : 0;
23053
+ }
23054
+ var MAX_CANDIDATES = 8;
23055
+ var MAX_PARTIAL_CANDIDATES = 4;
23056
+ function rankCandidates(scored, tieBreak) {
23057
+ const out = [];
23058
+ const seen = /* @__PURE__ */ new Set();
23059
+ scored.sort((a, b) => b.score - a.score || (tieBreak === "breadth" ? leadCount(b.row) - leadCount(a.row) || b.closeness - a.closeness : b.closeness - a.closeness || leadCount(b.row) - leadCount(a.row)) || sectorLabel(a.row).length - sectorLabel(b.row).length).forEach(({ row, score }) => {
23060
+ const label = sectorLabel(row);
23061
+ if (!label || seen.has(label) || out.length >= MAX_CANDIDATES)
23062
+ return;
23063
+ seen.add(label);
23064
+ out.push({ id: row.id, name: label, score: Number(score.toFixed(2)) });
23065
+ });
23066
+ return out;
23067
+ }
23068
+ function matchSector(text3, taxonomy) {
23069
+ const wanted = normalizeSectorText(text3);
23070
+ if (!wanted)
23071
+ return { kind: "unresolved", candidates: [] };
23072
+ const exact = taxonomy.filter((row) => normalizeSectorText(sectorLabel(row)) === wanted);
23073
+ if (exact.length > 0) {
23074
+ const label = sectorLabel(exact.reduce((best, row) => leadCount(row) > leadCount(best) ? row : best));
23075
+ const ids = taxonomy.filter((row) => sectorLabel(row) === label).map((row) => row.id);
23076
+ return { kind: "resolved", label, ids, exact: true };
23077
+ }
23078
+ const want = new Set(sectorTokens(text3));
23079
+ const sameWords = taxonomy.filter((row) => {
23080
+ const have = new Set(sectorTokens(sectorLabel(row)));
23081
+ return have.size === want.size && [...want].every((token) => have.has(token));
23082
+ });
23083
+ const sameWordLabels = new Set(sameWords.map((row) => sectorLabel(row)).filter(Boolean));
23084
+ if (sameWordLabels.size === 1) {
23085
+ const label = [...sameWordLabels][0];
23086
+ const ids = taxonomy.filter((row) => sectorLabel(row) === label).map((row) => row.id);
23087
+ return { kind: "resolved", label, ids, exact: false };
23088
+ }
23089
+ const scored = [];
23090
+ for (const row of taxonomy) {
23091
+ const have = new Set(sectorTokens(sectorLabel(row)));
23092
+ if (have.size === 0)
23093
+ continue;
23013
23094
  let overlap = 0;
23014
- for (const t of want)
23015
- if (have.has(t))
23095
+ for (const token of want)
23096
+ if (have.has(token))
23016
23097
  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);
23098
+ if (overlap === 0)
23099
+ continue;
23100
+ scored.push({ row, score: overlap / want.size, closeness: overlap / have.size });
23101
+ }
23102
+ const full = scored.filter((s) => s.score >= 1);
23103
+ return {
23104
+ kind: "unresolved",
23105
+ candidates: full.length > 0 ? rankCandidates(full, "breadth") : rankCandidates(scored, "closeness").slice(0, MAX_PARTIAL_CANDIDATES)
23106
+ };
23107
+ }
23108
+ function sectorSections(taxonomy) {
23109
+ const roots = taxonomy.filter((row) => !row.parent);
23110
+ const labels = [];
23111
+ for (const row of roots.sort((a, b) => leadCount(b) - leadCount(a))) {
23112
+ const label = sectorLabel(row);
23113
+ if (label && !labels.includes(label))
23114
+ labels.push(label);
23115
+ }
23116
+ return labels;
23021
23117
  }
23118
+ async function fetchSectorTaxonomy(client, ctx) {
23119
+ const me = await client.resolveMe().catch(() => null);
23120
+ const lang = me?.language ?? "en";
23121
+ const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
23122
+ const unlabelled = taxonomy.filter((row) => !sectorLabel(row)).length;
23123
+ if (unlabelled > 0) {
23124
+ ctx?.logger?.warn?.(`sector taxonomy: ${unlabelled}/${taxonomy.length} row(s) carry no label`);
23125
+ }
23126
+ return taxonomy;
23127
+ }
23128
+ var NUMERIC_ID = /^\d+$/;
23129
+ function resolveSectorValues(values, taxonomy) {
23130
+ const out = {
23131
+ values: [],
23132
+ rewritten: [],
23133
+ unresolved: [],
23134
+ sections: sectorSections(taxonomy)
23135
+ };
23136
+ for (const raw of values) {
23137
+ const asked = typeof raw === "string" ? raw : String(raw ?? "");
23138
+ if (NUMERIC_ID.test(asked.trim())) {
23139
+ out.values.push(asked.trim());
23140
+ continue;
23141
+ }
23142
+ const match = matchSector(asked, taxonomy);
23143
+ if (match.kind === "resolved") {
23144
+ out.values.push(match.label);
23145
+ if (match.label !== asked)
23146
+ out.rewritten.push({ asked, used: match.label });
23147
+ continue;
23148
+ }
23149
+ out.values.push(asked);
23150
+ out.unresolved.push({ asked, closest: match.candidates });
23151
+ }
23152
+ return out;
23153
+ }
23154
+
23155
+ // ../core/dist/composite/adjust-audience.js
23022
23156
  async function resolveSectors(client, texts, ctx) {
23023
23157
  const looksLikeId2 = (s) => /^\d+$/.test(s);
23024
23158
  const direct = texts.filter(looksLikeId2);
@@ -23028,18 +23162,18 @@ async function resolveSectors(client, texts, ctx) {
23028
23162
  const me = await client.resolveMe().catch(() => null);
23029
23163
  const lang = me?.language ?? "en";
23030
23164
  const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
23031
- const nullNames = taxonomy.filter((s) => !s.name).length;
23165
+ const nullNames = taxonomy.filter((s) => !sectorLabel(s)).length;
23032
23166
  if (nullNames > 0) {
23033
23167
  ctx?.logger?.warn?.(`adjust_audience: /sectors/all returned ${nullNames}/${taxonomy.length} sector(s) with a null/missing name`);
23034
23168
  }
23035
23169
  const resolved = [...direct];
23036
23170
  const ambiguities = [];
23037
23171
  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);
23172
+ const match = matchSector(text3, taxonomy);
23173
+ if (match.kind === "resolved") {
23174
+ resolved.push(...match.ids);
23041
23175
  } else {
23042
- ambiguities.push({ sector_text: text3, matches });
23176
+ ambiguities.push({ sector_text: text3, matches: match.candidates });
23043
23177
  }
23044
23178
  }
23045
23179
  return { resolved, ambiguities };
@@ -24334,6 +24468,61 @@ function sortFilterLists(filters) {
24334
24468
  }
24335
24469
  return out;
24336
24470
  }
24471
+ function isSectorRejection(err) {
24472
+ const e = err;
24473
+ return !!e && e.code === "BAD_INPUT" && typeof e.message === "string" && e.message.includes("filters.sectors");
24474
+ }
24475
+ async function submitSearch(client, body, opts, ctx) {
24476
+ try {
24477
+ return {
24478
+ ok: true,
24479
+ data: await client.request("POST", "/mcp/search", body, opts),
24480
+ rewritten: []
24481
+ };
24482
+ } catch (err) {
24483
+ const asked = body.filters?.sectors;
24484
+ if (!isSectorRejection(err) || !Array.isArray(asked) || asked.length === 0) {
24485
+ throw err;
24486
+ }
24487
+ const taxonomy = await fetchSectorTaxonomy(client, ctx).catch(() => {
24488
+ throw err;
24489
+ });
24490
+ const fix = resolveSectorValues(asked, taxonomy);
24491
+ if (fix.unresolved.length === 0 && fix.rewritten.length === 0)
24492
+ throw err;
24493
+ if (fix.unresolved.length > 0)
24494
+ return { ok: false, choice: sectorChoice(fix) };
24495
+ const retried = { ...body, filters: { ...body.filters, sectors: fix.values } };
24496
+ return {
24497
+ ok: true,
24498
+ data: await client.request("POST", "/mcp/search", retried, opts),
24499
+ rewritten: fix.rewritten
24500
+ };
24501
+ }
24502
+ }
24503
+ function sectorNote(rewritten) {
24504
+ if (rewritten.length === 0)
24505
+ return void 0;
24506
+ const pairs = rewritten.map((r) => `"${r.asked}" \u2192 "${r.used}"`).join(", ");
24507
+ return {
24508
+ sectors_note: `Leadbay's sector taxonomy spells these differently, so the search ran on ${pairs}. Tell the user which sector was searched.`,
24509
+ sectors_used: rewritten
24510
+ };
24511
+ }
24512
+ function sectorChoice(fix) {
24513
+ const names = fix.unresolved.map((u) => `"${u.asked}"`).join(", ");
24514
+ return {
24515
+ mode: "needs_sector_choice",
24516
+ submitted: false,
24517
+ unresolved_sectors: fix.unresolved.map((u) => ({
24518
+ asked: u.asked,
24519
+ closest: u.closest.map((c) => c.name)
24520
+ })),
24521
+ sector_sections: fix.sections,
24522
+ resolved_sectors: fix.rewritten,
24523
+ hint: `No sector in this workspace's taxonomy is named ${names || "that"}. Nothing was submitted and nothing was spent. Re-call with a label copied character for character from \`closest\` or \`sector_sections\` (a section covers everything under it), or drop \`filters.sectors\` and put the wording in \`query\` / \`example_lead.description\` instead \u2014 the search then RANKS on it rather than fencing on it, which is usually what the user meant. \`leadbay_list_sectors\` returns the full taxonomy if none of these fit.`
24524
+ };
24525
+ }
24337
24526
  var findNewLeads = {
24338
24527
  name: "leadbay_find_new_leads",
24339
24528
  annotations: {
@@ -24372,7 +24561,7 @@ var findNewLeads = {
24372
24561
  },
24373
24562
  filters: {
24374
24563
  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.",
24564
+ description: "HARD constraints (the seed only shapes ranking). Sector/location labels resolve at submit. `sectors` must be a label from Leadbay's own taxonomy (leadbay_list_sectors) \u2014 everyday wording like 'Professional Services' is not one; a spelling/plural slip is corrected for you, but a word with no label comes back as mode:'needs_sector_choice' with the labels to pick from. An unresolvable location is a 400 naming it.",
24376
24565
  properties: {
24377
24566
  sectors: { type: "array", items: { type: "string" } },
24378
24567
  locations: { type: "array", items: { type: "string" } },
@@ -24544,19 +24733,27 @@ var findNewLeads = {
24544
24733
  dry_run: dryRun
24545
24734
  });
24546
24735
  if (dryRun === true) {
24547
- const forecast = await client.request("POST", "/mcp/search", body);
24736
+ const attempt2 = await submitSearch(client, body, void 0, ctx);
24737
+ if (!attempt2.ok)
24738
+ return { ...attempt2.choice, ...countryNote, region: client.region };
24548
24739
  return {
24549
24740
  dry_run: true,
24550
- ...forecast,
24741
+ ...attempt2.data,
24742
+ ...sectorNote(attempt2.rewritten),
24551
24743
  ...countryNote,
24552
24744
  region: client.region
24553
24745
  };
24554
24746
  }
24555
24747
  if (isPaid && !consented) {
24556
- const forecast = vetoed ? null : await client.request("POST", "/mcp/search", {
24557
- ...body,
24558
- dry_run: true
24559
- });
24748
+ let forecast = null;
24749
+ let quoteRewritten = [];
24750
+ if (!vetoed) {
24751
+ const attempt2 = await submitSearch(client, { ...body, dry_run: true }, void 0, ctx);
24752
+ if (!attempt2.ok)
24753
+ return { ...attempt2.choice, ...countryNote, region: client.region };
24754
+ forecast = attempt2.data;
24755
+ quoteRewritten = attempt2.rewritten;
24756
+ }
24560
24757
  return {
24561
24758
  mode: "needs_confirmation",
24562
24759
  submitted: false,
@@ -24569,11 +24766,15 @@ var findNewLeads = {
24569
24766
  estimated_cost: forecast?.estimated_cost ?? null,
24570
24767
  items_requested: forecast?.items_requested ?? null,
24571
24768
  hint: vetoed ? "confirm:false vetoed the run \u2014 nothing was submitted. Re-call with confirm:true to proceed, or drop qualify/channels for a free search." : "Tell the user what will run and that it uses their plan's quota (no amounts, no money), get an explicit go-ahead, then re-call with confirm:true. For a free search instead: omit qualify and channels.",
24769
+ ...sectorNote(quoteRewritten),
24572
24770
  ...countryNote,
24573
24771
  region: client.region
24574
24772
  };
24575
24773
  }
24576
- const submit = await client.request("POST", "/mcp/search", body, { preSendSignal: ctx?.signal });
24774
+ const attempt = await submitSearch(client, body, { preSendSignal: ctx?.signal }, ctx);
24775
+ if (!attempt.ok)
24776
+ return { ...attempt.choice, ...countryNote, region: client.region };
24777
+ const submit = attempt.data;
24577
24778
  const mocked = mockedSubmitPreview(submit, "leadbay_find_new_leads", client.region);
24578
24779
  if (mocked)
24579
24780
  return mocked;
@@ -24624,6 +24825,7 @@ var findNewLeads = {
24624
24825
  since: snapshot.next_since ?? null,
24625
24826
  suggested_wait_seconds: done ? 0 : 60
24626
24827
  },
24828
+ ...sectorNote(attempt.rewritten),
24627
24829
  ...countryNote,
24628
24830
  region: client.region
24629
24831
  };
@@ -26105,8 +26307,8 @@ var reportOutreach = {
26105
26307
  if (!params.dry_run && params.verification.source === "user_confirmed" && typeof ctx?.elicit === "function") {
26106
26308
  try {
26107
26309
  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.`;
26310
+ const leadCount2 = targetIds.length;
26311
+ const elicitMsg = leadCount2 === 1 ? `An AI agent wants to log outreach on lead ${targetIds[0]}: "${params.note}". The agent claims you confirmed this. Type your literal confirmation to proceed; cancel to reject.` : `An AI agent wants to log outreach on ${leadCount2} leads: "${params.note}". The agent claims you confirmed this. Type your literal confirmation to proceed; cancel to reject.`;
26110
26312
  const result = await ctx.elicit({
26111
26313
  message: elicitMsg,
26112
26314
  requestedSchema: {
@@ -28660,7 +28862,7 @@ function parseWriteEnv(env = process.env) {
28660
28862
  }
28661
28863
 
28662
28864
  // src/http-server.ts
28663
- var VERSION = true ? "0.39.2" : "0.0.0-dev";
28865
+ var VERSION = true ? "0.39.3" : "0.0.0-dev";
28664
28866
  var PORT = Number(process.env.PORT ?? 8080);
28665
28867
  var HOST = process.env.HOST ?? "0.0.0.0";
28666
28868
  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.3" : "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.3" : "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.3",
4
4
  "mcpName": "io.github.leadbay/leadbay-mcp",
5
5
  "description": "Model Context Protocol (MCP) server for Leadbay — AI lead discovery, qualification, and enrichment for Claude Desktop, Cursor, and Claude Code.",
6
6
  "type": "module",