@leadbay/mcp 0.24.0 → 0.24.1

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
@@ -8199,6 +8199,7 @@ Pick 2\u20133 items below based on what was actually observed in the response. T
8199
8199
  | Observation | Suggest | Calls |
8200
8200
  |------------------------------------------------------------|--------------------------------------------------------------|--------------------------------------------------------|
8201
8201
  | \u2265 5 leads returned (any batch) | "Build an interactive lead triage board for this batch" | emit antArtifact from data in hand (do NOT re-call leadbay_pull_leads) |
8202
+ | \u2265 1 lead returned (any batch) | "Enrich top leads" (reveal decision-maker email/phone on the top leads) | leadbay_enrich_titles({ leadIds: shown leads[].id, lensId }) \u2014 scope to the leads JUST shown; OMIT \`titles\` so it runs the no-spend discovery preview. Confirm titles + channels, then re-call with titles + confirm to launch |
8202
8203
  | \`has_more == true\` | "Pull the next page (page N+1 of M)" | leadbay_pull_leads(page = current + 1, lensId = pinned)|
8203
8204
  | \u2265 3 rows have \`qualification_summary.answered == 0\` | "Deepen AI qualification on the rows without \u2756 caps" | leadbay_bulk_qualify_leads(leadIds=[\u2026]) |
8204
8205
  | User points at a single row | "Research [Company] in depth" | leadbay_research_lead_by_id(leadId) |
@@ -8645,30 +8646,23 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
8645
8646
 
8646
8647
  ---
8647
8648
 
8648
- Thin wrapper around **leadbay_research_lead_by_id**. Accepts a \`companyName\`,
8649
- fuzzy-matches it against the top 50 of the active (or supplied) lens's
8650
- wishlist, picks the highest-scoring substring hit, and delegates the actual
8651
- research to \`leadbay_research_lead_by_id\`. Returns the **same payload shape**
8652
- as \`_by_id\`, with two additions on \`_meta\`:
8649
+ Resolves \`companyName\` across visible Discover, Monitor, and Activate leads,
8650
+ then delegates to **leadbay_research_lead_by_id**. Supplying \`lensId\`
8651
+ deliberately restricts the backend search to that lens. The result matches
8652
+ \`_by_id\`, plus:
8653
8653
 
8654
- - \`_meta.resolved_from\`: \`"companyName"\` (so the agent knows the entry point).
8655
- - \`_meta.resolved_query\`: the original \`companyName\` string.
8656
- - \`_meta.match_candidates[]\`: up to 4 next-best substring matches as
8657
- \`{leadId, name, score}\` \u2014 surface these when ambiguity is plausible so the
8658
- user can redirect.
8654
+ - \`_meta.resolved_from\`: \`"companyName"\`
8655
+ - \`_meta.resolved_query\`: the original query
8656
+ - \`_meta.match_candidates[]\`: up to 4 \`{leadId, name, score}\` alternatives
8659
8657
 
8660
- On zero matches, throws \`LEAD_NOT_FOUND\` with a \`nearest_names[]\` payload (the
8661
- top 5 by score from the lens, regardless of substring) so the agent can offer
8662
- "did you mean\u2026" disambiguation.
8658
+ \`LEAD_NOT_FOUND\` identifies whether the complete visible corpus, an explicit
8659
+ lens, or only a degraded active-lens fallback was searched.
8663
8660
 
8664
- WHEN TO USE: when the user references a company by
8665
- name and you don't yet have its \`lead_id\`. If \`_meta.match_candidates\` is
8666
- non-empty, offer the next-best matches in NEXT STEPS so the user can correct
8667
- a wrong fuzzy hit.
8661
+ WHEN TO USE: for a company/domain/contact reference
8662
+ without a \`lead_id\`. Offer \`_meta.match_candidates\` when present.
8668
8663
 
8669
- WHEN NOT TO USE: when you already have the UUID \u2014 use
8670
- leadbay_research_lead_by_id directly. This wrapper costs one extra
8671
- \`discoverLeads\` round-trip; skipping it when you can is cheaper.
8664
+ WHEN NOT TO USE: with a UUID; call
8665
+ leadbay_research_lead_by_id directly.
8672
8666
 
8673
8667
  ---
8674
8668
 
@@ -8809,9 +8803,9 @@ out?"\`
8809
8803
 
8810
8804
  When \`_meta.match_candidates\` is non-empty, prepend one extra NEXT STEPS row:
8811
8805
 
8812
- | Observation | Suggest | Calls |
8813
- |--------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------|
8814
- | \`_meta.match_candidates\` non-empty | "Did you mean **<next-best.name>**?" | leadbay_research_lead_by_id(leadId=<next-best.leadId>) |
8806
+ | Observation | Suggest | Calls |
8807
+ |---|---|---|
8808
+ | Alternatives found | "Did you mean **<next-best.name>**?" | leadbay_research_lead_by_id(leadId=<next-best.leadId>) |
8815
8809
  `;
8816
8810
  leadbay_resolve_import_rows = `Resolve messy CSV-shaped lead rows against Leadbay before file import. The tool sends each row's available identity signals to \`POST /leads/resolve\`, returns matched lead IDs or ambiguous candidate IDs, and produces \`records_for_import\` plus a SAFE identity-only \`mappings_for_import\` starting point for leadbay_import_leads / leadbay_import_and_qualify. This tool deliberately does not try to understand every CSV dialect; the agent should inspect the file, derive clean helper columns when useful, pass explicit \`identity_mappings\`, and build the final CRM mapping from \`mapping_guidance\`.
8817
8811
 
@@ -14675,6 +14669,11 @@ function buildPullLeadsNextSteps(args) {
14675
14669
  description: "Build an interactive lead triage board to sort and filter this batch.",
14676
14670
  kind: "build_artifact"
14677
14671
  });
14672
+ options.push({
14673
+ label: "Enrich top leads",
14674
+ description: "Enrich the top leads",
14675
+ kind: "enrich_top_leads"
14676
+ });
14678
14677
  options.push({
14679
14678
  label: "Deepen qualification",
14680
14679
  description: "Run deeper AI qualification on these leads.",
@@ -14766,7 +14765,7 @@ var init_pull_leads = __esm({
14766
14765
  },
14767
14766
  next_steps: {
14768
14767
  type: ["object", "null"],
14769
- description: "Ready-made NEXT STEPS for the host's choice widget. Each option has a SHORT `label` (\u22645 words, fits AskUserQuestion's label cap on Claude cowork/Claude Code) and a full `description`. For AskUserQuestion (cowork/Claude Code) pass each option as {label, description}. For ask_user_input_v0 (Claude chat/ChatGPT, string-only options) use the `description` as the option string. Use these VERBATIM, in order \u2014 do NOT re-derive, reword, or render as prose when a widget tool exists. options[0] is the artifact offer (build the lead triage board) whenever the batch is non-empty. When the batch is empty but the lens is still computing (computing_wishlist/computing_scores true), this carries a 'Re-pull in ~30s' option (kind:repull_computing) plus 'Refine audience' \u2014 render the widget so the user waits rather than seeing 'no leads.' null only when the batch is empty AND nothing is computing (a genuinely empty / over-narrow lens).",
14768
+ description: "Ready-made NEXT STEPS for the host's choice widget. Each option has a SHORT `label` (\u22645 words, fits AskUserQuestion's label cap on Claude cowork/Claude Code) and a full `description`. For AskUserQuestion (cowork/Claude Code) pass each option as {label, description}. For ask_user_input_v0 (Claude chat/ChatGPT, string-only options) use the `description` as the option string. Use these VERBATIM, in order \u2014 do NOT re-derive, reword, or render as prose when a widget tool exists. options[0] is the artifact offer (build the lead triage board) whenever the batch is non-empty; options[1] is the enrich offer (kind:enrich_top_leads \u2014 route it to leadbay_enrich_titles scoped to the leadIds JUST shown (pass leads[].id + the pinned lens.id) with NO titles, so it runs the no-spend discovery preview; quota is only spent after the user picks titles + confirms channels on a follow-up call). When the batch is empty but the lens is still computing (computing_wishlist/computing_scores true), this carries a 'Re-pull in ~30s' option (kind:repull_computing) plus 'Refine audience' \u2014 render the widget so the user waits rather than seeing 'no leads.' null only when the batch is empty AND nothing is computing (a genuinely empty / over-narrow lens).",
14770
14769
  properties: {
14771
14770
  question: { type: "string" },
14772
14771
  options: {
@@ -16646,11 +16645,45 @@ function rankSubstringMatches(needle, candidates) {
16646
16645
  });
16647
16646
  return hits;
16648
16647
  }
16648
+ function parseLensId(value) {
16649
+ const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
16650
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
16651
+ }
16652
+ function suggestionName(suggestion) {
16653
+ const companyName = (suggestion.company_name ?? suggestion.companyName)?.trim();
16654
+ return companyName || suggestion.text.trim();
16655
+ }
16656
+ function suggestionLeadId(suggestion) {
16657
+ return suggestion.lead_id ?? suggestion.leadId;
16658
+ }
16659
+ function isLeadbayError(error) {
16660
+ return typeof error === "object" && error !== null && error.error === true && typeof error.code === "string" && typeof error.message === "string" && typeof error.hint === "string";
16661
+ }
16662
+ async function resolveWithinLens(client, query, lensId) {
16663
+ const results = await client.request("GET", `/lenses/${lensId}/leads/wishlist?q=${encodeURIComponent(query)}&count=50&page=0&contacts=false`);
16664
+ return results.items.map((lead) => ({
16665
+ id: lead.id,
16666
+ name: lead.name,
16667
+ score: lead.score,
16668
+ lensId
16669
+ }));
16670
+ }
16671
+ async function resolveAcrossVisibleCorpus(client, query) {
16672
+ const suggestions = await client.request("GET", `/search/suggest?q=${encodeURIComponent(query)}`);
16673
+ return suggestions.map((suggestion) => {
16674
+ const id = suggestionLeadId(suggestion);
16675
+ return {
16676
+ id: id ?? "",
16677
+ name: suggestionName(suggestion),
16678
+ score: null,
16679
+ lensId: parseLensId(suggestion.lens_id ?? suggestion.lensId)
16680
+ };
16681
+ }).filter((suggestion) => suggestion.id !== "" && suggestion.name !== "");
16682
+ }
16649
16683
  var researchLeadByNameFuzzy;
16650
16684
  var init_research_lead_by_name_fuzzy = __esm({
16651
16685
  "../core/dist/composite/research-lead-by-name-fuzzy.js"() {
16652
16686
  "use strict";
16653
- init_discover_leads();
16654
16687
  init_research_lead_by_id();
16655
16688
  init_tool_descriptions_generated();
16656
16689
  researchLeadByNameFuzzy = {
@@ -16668,11 +16701,11 @@ var init_research_lead_by_name_fuzzy = __esm({
16668
16701
  properties: {
16669
16702
  companyName: {
16670
16703
  type: "string",
16671
- description: "Company name to look up. Substring fuzzy-match against the top 50 of the active lens's wishlist; ties broken by descending lead score."
16704
+ description: "Company name, domain, or contact name to resolve across visible Leadbay leads in Discover, Monitor, and Activate."
16672
16705
  },
16673
16706
  lensId: {
16674
16707
  type: "number",
16675
- description: "Lens id (escape hatch \u2014 normally omit; auto-resolves to the active lens)"
16708
+ description: "Optional strict scope. When supplied, search only this lens's wishlist; normally omit to search all visible Leadbay leads."
16676
16709
  },
16677
16710
  concise: {
16678
16711
  type: "boolean",
@@ -16697,18 +16730,34 @@ var init_research_lead_by_name_fuzzy = __esm({
16697
16730
  additionalProperties: true
16698
16731
  },
16699
16732
  execute: async (client, params, ctx) => {
16700
- if (!params.companyName || typeof params.companyName !== "string") {
16733
+ if (!params.companyName || typeof params.companyName !== "string" || params.companyName.trim() === "") {
16701
16734
  throw client.makeError("INVALID_PARAMS", "companyName is required", "Pass the company name as a string. If you already have the lead UUID, call leadbay_research_lead_by_id directly.");
16702
16735
  }
16703
- const lensId = params.lensId ?? await client.resolveDefaultLens();
16704
- const results = await discoverLeads.execute(client, { lensId, count: 50, page: 0 }, ctx);
16705
- const allLeads = results.leads;
16706
- const ranked = rankSubstringMatches(params.companyName, allLeads);
16736
+ const query = params.companyName.trim();
16737
+ let ranked;
16738
+ let lensId = params.lensId;
16739
+ let usedActiveLensFallback = false;
16740
+ if (lensId !== void 0) {
16741
+ ranked = await resolveWithinLens(client, query, lensId);
16742
+ } else {
16743
+ try {
16744
+ ranked = await resolveAcrossVisibleCorpus(client, query);
16745
+ } catch (error) {
16746
+ if (isLeadbayError(error))
16747
+ throw error;
16748
+ lensId = await client.resolveDefaultLens();
16749
+ usedActiveLensFallback = true;
16750
+ ctx?.logger?.warn?.("Cross-tab company search was unavailable; falling back to the active lens for this lookup.");
16751
+ ranked = rankSubstringMatches(query, await resolveWithinLens(client, query, lensId));
16752
+ }
16753
+ }
16707
16754
  if (ranked.length === 0) {
16708
- const nearestNames = [...allLeads].sort((a, b) => (b.score ?? -Infinity) - (a.score ?? -Infinity)).slice(0, 5).map((l) => ({ leadId: l.id, name: l.name, score: l.score }));
16709
- throw client.makeError("LEAD_NOT_FOUND", `No lead matching "${params.companyName}" in the current lens (top-50 wishlist)`, `Call leadbay_pull_leads to see what's available. Top-scoring leads currently in this lens: ${nearestNames.map((n) => n.name).join(", ")}.`);
16755
+ const scope = usedActiveLensFallback ? `in active lens ${lensId}; cross-tab search was unavailable` : params.lensId === void 0 ? "across your visible Leadbay leads" : `in lens ${params.lensId}`;
16756
+ const hint = usedActiveLensFallback ? `Only active lens ${lensId} was checked because cross-tab search was unavailable. Retry later before concluding the company is missing or offering to import it.` : params.lensId === void 0 ? "Search checks company names, domains, and contact names across Discover, Monitor, and Activate. Confirm the spelling or domain, or add/import the company first." : "This lookup was intentionally restricted to the supplied lens. Omit lensId to search visible leads across Discover, Monitor, and Activate.";
16757
+ throw client.makeError("LEAD_NOT_FOUND", `No lead matching "${query}" ${scope}`, hint);
16710
16758
  }
16711
16759
  const [primary, ...rest] = ranked;
16760
+ lensId = primary.lensId ?? lensId ?? await client.resolveDefaultLens();
16712
16761
  const candidates = rest.slice(0, 4).map((m) => ({
16713
16762
  leadId: m.id,
16714
16763
  name: m.name,
@@ -16721,7 +16770,7 @@ var init_research_lead_by_name_fuzzy = __esm({
16721
16770
  response_format: params.response_format,
16722
16771
  _resolved: {
16723
16772
  from: "companyName",
16724
- query: params.companyName,
16773
+ query,
16725
16774
  candidates
16726
16775
  }
16727
16776
  }, ctx);
@@ -23978,6 +24027,7 @@ Pick 2\u20133 items below based on what was actually observed in the response. T
23978
24027
  | Observation | Suggest | Calls |
23979
24028
  |------------------------------------------------------------|--------------------------------------------------------------|--------------------------------------------------------|
23980
24029
  | \u2265 5 leads returned (any batch) | "Build an interactive lead triage board for this batch" | emit antArtifact from data in hand (do NOT re-call leadbay_pull_leads) |
24030
+ | \u2265 1 lead returned (any batch) | "Enrich top leads" (reveal decision-maker email/phone on the top leads) | leadbay_enrich_titles({ leadIds: shown leads[].id, lensId }) \u2014 scope to the leads JUST shown; OMIT \`titles\` so it runs the no-spend discovery preview. Confirm titles + channels, then re-call with titles + confirm to launch |
23981
24031
  | \`has_more == true\` | "Pull the next page (page N+1 of M)" | leadbay_pull_leads(page = current + 1, lensId = pinned)|
23982
24032
  | \u2265 3 rows have \`qualification_summary.answered == 0\` | "Deepen AI qualification on the rows without \u2756 caps" | leadbay_bulk_qualify_leads(leadIds=[\u2026]) |
23983
24033
  | User points at a single row | "Research [Company] in depth" | leadbay_research_lead_by_id(leadId) |
@@ -24608,30 +24658,18 @@ GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ship
24608
24658
  If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.
24609
24659
 
24610
24660
 
24611
- Research the company with domain '{{arg:domain}}' for me using Leadbay.
24661
+ Research the company name or domain '{{arg:domain}}' for me using Leadbay.
24612
24662
 
24613
- # PHASE 1 \u2014 IMPORT + QUALIFY
24614
- Call \`leadbay_import_and_qualify\` with \`domains=[{domain:'{{arg:domain}}'}]\`. This imports the lead AND runs AI qualification in one call. If the response indicates \`quota_blocked\` or \`still_running\`, say so explicitly. Render the import status using the canonical terse single-record summary (never an enumeration of every imported lead):
24663
+ # PHASE 1 \u2014 RESOLVE + DEEP DIVE
24664
+ Call \`leadbay_research_lead_by_name_fuzzy\` with
24665
+ \`companyName:'{{arg:domain}}'\`. Omit \`lensId\`: the default search deliberately
24666
+ covers the user's visible Discover, Monitor, and Activate corpus, including
24667
+ other lenses and leads outside the active lens's first page. The composite
24668
+ resolves the lead and returns the full deep-research payload in one call.
24615
24669
 
24616
- ## RENDERING \u2014 import result summary (single-record, terse)
24617
-
24618
- The response carries either a completed result or an async handle. Render a brief summary; do NOT enumerate every imported lead.
24619
-
24620
- **Header \u2014 single line, choose by status:**
24621
-
24622
- - Completed: \`"\u2713 Import complete \u2014 N leads imported \xB7 M failed \xB7 P resolved-with-ambiguity"\`
24623
- - Running: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
24624
- - Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 qualify_id <id>"\`
24625
-
24626
- **When failures or ambiguous rows are non-empty**, follow the header with a small bulleted list (\u2264 5 items): \`<row identifier or domain> \xB7 <reason>\`. Then \`"*+N more \u2014 leadbay_import_status for full detail*"\`.
24627
-
24628
- **When the user's request implied a downstream use** ("import then prep outreach for them"), emit \`Imported leadIds: <up to 5 ids, then '+N more'>\` \u2014 just the ids. Let the next composite render the leads.
24629
-
24630
- Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_research_lead_by_id\` in NEXT STEPS.
24631
-
24632
-
24633
- # PHASE 2 \u2014 DEEP DIVE
24634
- When the import resolves, call \`leadbay_research_lead_by_id\` on the new leadId. Render the result using the canonical single-record card layout \u2014 detect MODE A (Discovery) since the user asked to "research" a domain rather than to prepare outreach:
24670
+ Render the result using the canonical single-record card layout \u2014 detect MODE A
24671
+ (Discovery) since the user asked to research a company rather than prepare
24672
+ outreach:
24635
24673
 
24636
24674
  ## RENDERING \u2014 single-record research card, mode-adaptive
24637
24675
 
@@ -24703,10 +24741,16 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
24703
24741
 
24704
24742
 
24705
24743
 
24744
+ # PHASE 2 \u2014 NOT FOUND
24745
+ If the resolver returns \`LEAD_NOT_FOUND\`, say that the existing visible corpus
24746
+ was searched. **Do NOT call \`leadbay_import_and_qualify\` automatically.** Offer
24747
+ to import and qualify the company as a separate, explicit next step; only call
24748
+ it after the user agrees.
24749
+
24706
24750
  # PHASE 3 \u2014 SUMMARY
24707
24751
  Place a 2\u20133 sentence summary ABOVE the card with:
24708
24752
  - Who is this company (1 sentence)
24709
- - Their fit (cite specific \`qualification_answers\` from the qualification response)
24753
+ - Their fit (cite specific qualification answers or signals from the research response)
24710
24754
  - Which contact would I email first (one short clause \u2014 the card's contacts table carries the rest)
24711
24755
 
24712
24756
  The card itself handles the signal callouts (\`\u{1F4C8} business signals\`, \`\u{1F4A1} prospecting clues\`). Do NOT re-narrate signals in prose above the card \u2014 that's what the card sections are for. Be honest about uncertainty: if any field is missing from tool responses, say "not surfaced by qualification" rather than guessing.
@@ -24882,7 +24926,7 @@ should I follow up on" to "I'll send via lemlist".
24882
24926
  `, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_pull_followups", "leadbay_research_lead_by_id", "leadbay_research_lead_by_name_fuzzy", "leadbay_prepare_outreach", "leadbay_report_outreach", "leadbay_set_pushback", "leadbay_remove_pushback", "leadbay_bulk_qualify_leads", "leadbay_enrich_titles", "leadbay_import_leads", "leadbay_add_note", "leadbay_adjust_audience"], "failure_modes": ['Drives outreach without asking the user "how did it go?" afterwards \u2014 leaving prospecting_actions and epilogue_status stale', 'Says "epilogue" in user-facing dialogue instead of "outcome"', 'Says "Monitor" in user-facing dialogue instead of "follow-ups"', 'Treats a "not now / next quarter" reply as a note instead of routing through the pushback mechanism', "Drafts outreach in a generic format when the user has a connected sequencer (lemlist, Outreach.io, etc.) that has its own idiom", "Re-pulls leads without passing the captured lensId, allowing a backend lens shift to discard prior work", "Skips the STOP byproduct in any multi-step workflow it triggers", "Calls leadbay_pull_leads (Discover wishlist) for a follow-up query, or leadbay_pull_followups (Monitor view) for a discovery query \u2014 the two entry points read from different backend tables; the right orchestrators are leadbay_daily_check_in (discovery) and leadbay_followup_check_in (follow-up)"] },
24883
24927
  leadbay_qualify_top_n: { "name": "leadbay_qualify_top_n", "short_description": "Bulk-qualify the top N un-qualified leads in the active lens. Uses\nleadbay_bulk_qualify_leads with a sensible default budget.\n", "arguments": [{ "name": "count", "description": "How many leads to qualify (default 10, max 25). Higher counts may take 5+ minutes.", "required": false }], "expected_calls": ["leadbay_bulk_qualify_leads", "leadbay_qualify_status", "leadbay_pull_leads", "leadbay_research_lead_by_id"], "failure_modes": ["Picks a count larger than the user asked for (or larger than the max 25)", "Glosses over still-running leads in the summary instead of naming them", "Recommends a lead from the existing qualified pool instead of one from this batch's actual results", 'Replaces the canonical pull_leads table with prose when rendering the newly-qualified batch (the per-tool RENDERING block is the structural contract; "standouts" commentary sits above it)', "Expands the qualify-status sentence into a card or table instead of the one-line status-inline render"] },
24884
24928
  leadbay_refine_audience: { "name": "leadbay_refine_audience", "short_description": "Refine the kind of leads Leadbay surfaces beyond firmographics, with a\nfree-text instruction. Handles the clarification round-trip if the new\nprompt is ambiguous.\n", "arguments": [{ "name": "instruction", "description": "The refinement (e.g. 'focus on hospitals running their own IT'). Set to plain English.", "required": true }], "expected_calls": ["leadbay_refine_prompt", "leadbay_account_status"], "failure_modes": ["Calls leadbay_answer_clarification on the user's behalf instead of surfacing the clarification verbatim", "Glosses over the clarification options instead of presenting them as offered", "Promises immediate effect when status='applied' actually triggers an async intelligence recompute"] },
24885
- leadbay_research_a_domain: { "name": "leadbay_research_a_domain", "short_description": "Import a company by domain and run deep qualification + research in one\npass. Use when a colleague mentions a name and you want everything Leadbay\nknows about it.\n", "arguments": [{ "name": "domain", "description": "The company's primary domain (e.g. 'acme.com'). Protocol/path are stripped.", "required": true }], "expected_calls": ["leadbay_import_and_qualify", "leadbay_research_lead_by_id"], "failure_modes": ["Fabricates qualification answers not present in any tool response", "Reports certainty about fit when qualification didn't actually run (e.g. quota_blocked)", "Skips the research step after import completes", "Renders the research_lead_by_id result as a freeform narrative instead of the canonical research-company-card layout (the card with header score bar, pill row, signal sections, contacts table is the structural contract; commentary belongs ABOVE or BELOW it)", "Enumerates every imported lead in prose instead of the terse single-record summary from the import-result rendering snippet"] },
24929
+ leadbay_research_a_domain: { "name": "leadbay_research_a_domain", "short_description": "Resolve a company by name or domain across the user's visible Discover,\nMonitor, and Activate corpus, then return everything Leadbay knows about it.\n", "arguments": [{ "name": "domain", "description": "Company name or domain (for example 'Acme Corporation' or 'acme.com'). The legacy argument key remains `domain` for client compatibility.", "required": true }], "expected_calls": ["leadbay_research_lead_by_name_fuzzy"], "failure_modes": ["Fabricates qualification answers not present in any tool response", "Calls leadbay_import_and_qualify before searching the existing visible corpus", "Treats the active lens as the entire search universe when the user did not request a lens scope", "Imports a missing company without the user's explicit permission", "Renders the research result as a freeform narrative instead of the canonical research-company-card layout (the card with header score bar, pill row, signal sections, contacts table is the structural contract; commentary belongs ABOVE or BELOW it)"] },
24886
24930
  leadbay_setup_team_prospecting: { "name": "leadbay_setup_team_prospecting", "short_description": "Manager-led prospecting setup: conversationally turn a natural-language\naudience ask into a Leadbay lens, validate the candidate leads, and\npersist them as one or more named campaigns the rep(s) can work\nthrough. Closes #3630 US3 end-to-end (within the current\ncreator-scoped campaign visibility model).\n", "arguments": [{ "name": "audience", "description": "Natural-language audience description (e.g. 'plumbing companies with 10-50 employees in Seine-Maritime'). The lens-creation step (`leadbay_refine_prompt` \u2192 `leadbay_create_lens`) interprets it.", "required": true }, { "name": "rep_split", "description": "Optional: how to split the validated leads into per-rep campaigns. Free text \u2014 e.g. 'split by city' or 'one campaign per rep: John gets Tulsa, Sarah gets OKC'.", "required": false }], "expected_calls": ["leadbay_refine_prompt", "leadbay_create_lens", "leadbay_promote_lens", "leadbay_pull_leads", "leadbay_research_lead_by_id", "leadbay_create_campaign", "leadbay_add_leads_to_campaign"], "failure_modes": ["Skips the validation step \u2014 creates a campaign of unvetted leads from a freshly-created lens without giving the manager a chance to drop weak fits", "Creates ONE campaign for all reps without asking about the split \u2014 the user explicitly mentioned per-rep distribution and the prompt should honor it", "Pretends the backend supports cross-user assignment \u2014 campaigns are owned by the caller (creator-scoped). Surface this honestly instead of fabricating an assignment model", "Asks ALL clarifying questions inline before tool calls \u2014 instead, run the lens refinement loop with `leadbay_refine_prompt` which handles the clarification protocol natively"] },
24887
24931
  leadbay_work_campaign: { "name": "leadbay_work_campaign", "short_description": "Work a campaign as a real outreach session: pick the campaign,\nassess what the user has (phones / emails / coords), then PROPOSE\nthe right session mode (call sheet, email sheet, enrich titles\nfirst, map). After they pick, render \u2014 and as they dictate\noutcomes per lead, record both note + epilogue via\n`leadbay_report_outreach` in one round trip.\n", "arguments": [{ "name": "campaign", "description": "Campaign name (fuzzy match against your own campaigns) or campaign UUID. Omit to list and pick interactively.", "required": false }, { "name": "mode", "description": "Optional: skip the readiness-assessment proposal and jump directly into 'call_sheet' / 'email_sheet' / 'map' / 'enrich_first'. Omit (recommended) and let the prompt propose based on the data.", "required": false }], "expected_calls": ["leadbay_list_campaigns", "leadbay_campaign_call_sheet", "leadbay_enrich_titles", "leadbay_report_outreach"], "failure_modes": ["Renders the call sheet immediately without proposing the right mode \u2014 if 60% of leads have no contacts, calling is futile; enrich first. Always assess `readiness` first.", "Auto-renders the map widget without asking \u2014 maps are intrusive when the user just wants to scroll a list. Map mode is a proposed option, not a default.", "Proposes map mode after the user has previously said they don't like maps \u2014 check conversation memory before adding 'View on a map' to the options list.", "Calls `leadbay_campaign_progression` instead of `leadbay_campaign_call_sheet` \u2014 progression has counts but no phones / LinkedIn / call-ready data; the user can't actually dial from progression rows.", "Renders contacts WITHOUT making the phone number a `[bare](tel:URL)` link \u2014 on mobile that breaks one-tap calling, which is the whole point of the cheat sheet.", "Records outreach WITHOUT epilogue_status \u2014 leaves the lead's pipeline state unchanged; the rep then sees the same lead surfaced again next session.", "Records outreach WITHOUT verification \u2014 verification.source/ref is REQUIRED. For calls, pass `{source: 'user_confirmed', ref: <user's exact words>}`.", "Loops through ALL leads in a 50-lead campaign before recording any outreach \u2014 the call-then-record loop must be per-lead, not batched."] }
24888
24932
  };
@@ -24898,7 +24942,7 @@ var PROMPT_CATALOG_BULLETS = {
24898
24942
  leadbay_prospecting_overview: `- \`leadbay_prospecting_overview\`: Orientation for working with Leadbay from any host \u2014 discovery vs. follow-up, the outreach loop, outcome recording, imports, pushback / snooze, and the connected-outreach-tool registry. Trigger when the conversation involves Leadbay leads, prospecting, pipeline, follow-up, outreach, or lens / ICP \u2014 anything from "show me my leads" to "what should I follow up on" to "I'll send via lemlist".`,
24899
24943
  leadbay_qualify_top_n: `- \`leadbay_qualify_top_n\` (optional args: count): Bulk-qualify the top N un-qualified leads in the active lens. Uses leadbay_bulk_qualify_leads with a sensible default budget.`,
24900
24944
  leadbay_refine_audience: `- \`leadbay_refine_audience\` (required args: instruction): Refine the kind of leads Leadbay surfaces beyond firmographics, with a free-text instruction. Handles the clarification round-trip if the new prompt is ambiguous.`,
24901
- leadbay_research_a_domain: `- \`leadbay_research_a_domain\` (required args: domain): Import a company by domain and run deep qualification + research in one pass. Use when a colleague mentions a name and you want everything Leadbay knows about it.`,
24945
+ leadbay_research_a_domain: `- \`leadbay_research_a_domain\` (required args: domain): Resolve a company by name or domain across the user's visible Discover, Monitor, and Activate corpus, then return everything Leadbay knows about it.`,
24902
24946
  leadbay_setup_team_prospecting: `- \`leadbay_setup_team_prospecting\` (required args: audience; optional args: rep_split): Manager-led prospecting setup: conversationally turn a natural-language audience ask into a Leadbay lens, validate the candidate leads, and persist them as one or more named campaigns the rep(s) can work through. Closes #3630 US3 end-to-end (within the current creator-scoped campaign visibility model).`,
24903
24947
  leadbay_work_campaign: `- \`leadbay_work_campaign\` (optional args: campaign, mode): Work a campaign as a real outreach session: pick the campaign, assess what the user has (phones / emails / coords), then PROPOSE the right session mode (call sheet, email sheet, enrich titles first, map). After they pick, render \u2014 and as they dictate outcomes per lead, record both note + epilogue via \`leadbay_report_outreach\` in one round trip.`
24904
24948
  };
@@ -24933,7 +24977,7 @@ var CATALOG = [
24933
24977
  arguments: [
24934
24978
  {
24935
24979
  name: "domain",
24936
- description: "The company's primary domain (e.g. 'acme.com'). Protocol/path are stripped.",
24980
+ description: "Company name or domain (for example 'Acme Corporation' or 'acme.com'). The legacy argument key remains `domain` for client compatibility.",
24937
24981
  required: true
24938
24982
  }
24939
24983
  ],
@@ -25286,15 +25330,15 @@ var EV_COMPOSITE_CALL = "mcp composite call";
25286
25330
  var NOOP_TELEMETRY = {
25287
25331
  identify: async () => {
25288
25332
  },
25289
- captureToolCall: () => {
25333
+ captureToolCall: (_props, _identity) => {
25290
25334
  },
25291
- captureCompositeCall: () => {
25335
+ captureCompositeCall: (_props, _identity) => {
25292
25336
  },
25293
- captureQuotaHit: () => {
25337
+ captureQuotaHit: (_props, _identity) => {
25294
25338
  },
25295
- captureTopupLink: () => {
25339
+ captureTopupLink: (_props, _identity) => {
25296
25340
  },
25297
- captureStartup: () => {
25341
+ captureStartup: (_props, _identity) => {
25298
25342
  },
25299
25343
  captureAgentMemoryCaptured: () => {
25300
25344
  },
@@ -25333,6 +25377,8 @@ function initTelemetry(opts) {
25333
25377
  const sentryDsn = process.env.LEADBAY_SENTRY_DSN ?? EMBEDDED_SENTRY_DSN;
25334
25378
  if (!posthogKey && !sentryDsn) return NOOP_TELEMETRY;
25335
25379
  const { version, logger } = opts;
25380
+ const flushAt = opts.flushAt ?? 20;
25381
+ const flushInterval = opts.flushInterval ?? 1e4;
25336
25382
  const environment = process.env.LEADBAY_ENV ?? (version.includes("-dev.") ? "dev" : "production");
25337
25383
  let posthog = null;
25338
25384
  let sentryReady = false;
@@ -25341,8 +25387,8 @@ function initTelemetry(opts) {
25341
25387
  if (posthogKey) {
25342
25388
  posthog = new PostHog(posthogKey, {
25343
25389
  host: process.env.LEADBAY_POSTHOG_HOST ?? EMBEDDED_POSTHOG_HOST,
25344
- flushAt: 20,
25345
- flushInterval: 1e4,
25390
+ flushAt,
25391
+ flushInterval,
25346
25392
  disableGeoip: false
25347
25393
  });
25348
25394
  }
@@ -25409,21 +25455,26 @@ function initTelemetry(opts) {
25409
25455
  const groupsFor = () => {
25410
25456
  return me?.organization?.id ? { organization: me.organization.id } : void 0;
25411
25457
  };
25412
- const doCapture = (event, properties) => {
25458
+ const doCapture = (event, properties, identity) => {
25413
25459
  if (!posthog) return;
25414
25460
  try {
25461
+ const props = identity?.region ? { ...baseProps(), region: identity.region, ...properties } : { ...baseProps(), ...properties };
25415
25462
  posthog.capture({
25416
- distinctId: distinctIdFor(),
25463
+ distinctId: identity ? identity.distinctId : distinctIdFor(),
25417
25464
  event,
25418
- properties: { ...baseProps(), ...properties },
25419
- groups: groupsFor()
25465
+ properties: props,
25466
+ groups: identity ? identity.groups : groupsFor()
25420
25467
  });
25421
25468
  } catch (err) {
25422
25469
  logger?.warn?.(`posthog capture failed: ${err?.message ?? err}`);
25423
25470
  }
25424
25471
  };
25425
- const emit = (event, properties) => {
25472
+ const emit = (event, properties, identity) => {
25426
25473
  if (!posthog) return;
25474
+ if (identity) {
25475
+ doCapture(event, properties, identity);
25476
+ return;
25477
+ }
25427
25478
  if (!me) {
25428
25479
  pendingEvents.push({ event, properties });
25429
25480
  return;
@@ -25486,32 +25537,32 @@ function initTelemetry(opts) {
25486
25537
  })();
25487
25538
  return identityPromise;
25488
25539
  },
25489
- captureToolCall(props) {
25490
- emit(EV_TOOL_CALL, { ...props });
25540
+ captureToolCall(props, identity) {
25541
+ emit(EV_TOOL_CALL, { ...props }, identity);
25491
25542
  },
25492
- captureCompositeCall(props) {
25493
- emit(EV_COMPOSITE_CALL, { ...props });
25543
+ captureCompositeCall(props, identity) {
25544
+ emit(EV_COMPOSITE_CALL, { ...props }, identity);
25494
25545
  },
25495
- captureQuotaHit(props) {
25496
- emit(EV_QUOTA_HIT, { ...props });
25546
+ captureQuotaHit(props, identity) {
25547
+ emit(EV_QUOTA_HIT, { ...props }, identity);
25497
25548
  },
25498
- captureTopupLink(props) {
25499
- emit(EV_TOPUP_LINK, { ...props });
25549
+ captureTopupLink(props, identity) {
25550
+ emit(EV_TOPUP_LINK, { ...props }, identity);
25500
25551
  },
25501
- captureStartup(props) {
25502
- emit(EV_STARTUP, { ...props });
25552
+ captureStartup(props, identity) {
25553
+ emit(EV_STARTUP, { ...props }, identity);
25503
25554
  },
25504
- captureAgentMemoryCaptured(props) {
25505
- emit(EV_AGENT_MEMORY_CAPTURED, { ...props });
25555
+ captureAgentMemoryCaptured(props, identity) {
25556
+ emit(EV_AGENT_MEMORY_CAPTURED, { ...props }, identity);
25506
25557
  },
25507
- captureAgentMemoryRecalled(props) {
25508
- emit(EV_AGENT_MEMORY_RECALLED, { ...props });
25558
+ captureAgentMemoryRecalled(props, identity) {
25559
+ emit(EV_AGENT_MEMORY_RECALLED, { ...props }, identity);
25509
25560
  },
25510
- captureAgentMemoryPruned(props) {
25511
- emit(EV_AGENT_MEMORY_PRUNED, { ...props });
25561
+ captureAgentMemoryPruned(props, identity) {
25562
+ emit(EV_AGENT_MEMORY_PRUNED, { ...props }, identity);
25512
25563
  },
25513
- captureFrictionReported(props) {
25514
- emit(EV_FRICTION_REPORTED, { ...props });
25564
+ captureFrictionReported(props, identity) {
25565
+ emit(EV_FRICTION_REPORTED, { ...props }, identity);
25515
25566
  },
25516
25567
  captureUpdateCheck(props) {
25517
25568
  emit(EV_MCP_UPDATE_CHECK, { ...props });
@@ -25561,11 +25612,11 @@ function initTelemetry(opts) {
25561
25612
  logger?.warn?.(`sentry captureException failed: ${e?.message ?? e}`);
25562
25613
  }
25563
25614
  },
25564
- async captureFeedback(message, opts2) {
25615
+ async captureFeedback(message, opts2, identity) {
25565
25616
  if (!sentryReady) return false;
25566
25617
  const trimmed = (message ?? "").trim();
25567
25618
  if (!trimmed) return false;
25568
- if (identityPromise) {
25619
+ if (!identity && identityPromise) {
25569
25620
  let waitTimer;
25570
25621
  try {
25571
25622
  await Promise.race([
@@ -25579,11 +25630,13 @@ function initTelemetry(opts) {
25579
25630
  if (waitTimer) clearTimeout(waitTimer);
25580
25631
  }
25581
25632
  }
25633
+ const fbName = identity?.name ?? me?.name;
25634
+ const fbEmail = identity?.email ?? me?.email;
25582
25635
  try {
25583
25636
  Sentry.captureFeedback({
25584
25637
  message: trimmed,
25585
- ...me?.name ? { name: me.name } : {},
25586
- ...me?.email ? { email: me.email } : {},
25638
+ ...fbName ? { name: fbName } : {},
25639
+ ...fbEmail ? { email: fbEmail } : {},
25587
25640
  ...opts2?.associatedEventId ? { associatedEventId: opts2.associatedEventId } : {}
25588
25641
  });
25589
25642
  const flushed = await Sentry.flush(4e3);
@@ -25597,6 +25650,27 @@ function initTelemetry(opts) {
25597
25650
  }
25598
25651
  },
25599
25652
  async shutdown() {
25653
+ if (!me && identityPromise) {
25654
+ let waitTimer;
25655
+ try {
25656
+ await Promise.race([
25657
+ identityPromise,
25658
+ new Promise((resolve) => {
25659
+ waitTimer = setTimeout(resolve, 1500);
25660
+ })
25661
+ ]);
25662
+ } catch {
25663
+ } finally {
25664
+ if (waitTimer) clearTimeout(waitTimer);
25665
+ }
25666
+ }
25667
+ if (!me) {
25668
+ me = {
25669
+ id: "unknown",
25670
+ organization: { id: "unknown", name: "unknown" }
25671
+ };
25672
+ flushPending();
25673
+ }
25600
25674
  const tasks = [];
25601
25675
  if (posthog) tasks.push(posthog.shutdown(2e3).catch(() => void 0));
25602
25676
  if (sentryReady) tasks.push(Sentry.close(2e3).catch(() => void 0));
@@ -26774,7 +26848,7 @@ import { spawn } from "child_process";
26774
26848
  import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
26775
26849
  import { join as join3 } from "path";
26776
26850
  import { homedir as homedir3 } from "os";
26777
- var HOSTED_MCP_URL = "https://leadbay-mcp-prod.fly.dev/mcp";
26851
+ var HOSTED_MCP_URL = "https://mcp.leadbay.app/mcp";
26778
26852
  function formatInstallOsLabel(platform2 = process.platform, arch = process.arch) {
26779
26853
  const name = platform2 === "darwin" ? "macOS" : platform2 === "win32" ? "Windows" : platform2 === "linux" ? "Linux" : platform2;
26780
26854
  return `${name} (${arch})`;
@@ -28203,7 +28277,7 @@ var OAUTH_BASE_URLS = {
28203
28277
  fr: "https://staging.api.leadbay.app"
28204
28278
  }
28205
28279
  };
28206
- var VERSION = "0.24.0";
28280
+ var VERSION = "0.24.1";
28207
28281
  var HELP = `
28208
28282
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
28209
28283
 
@@ -29373,7 +29447,12 @@ async function main() {
29373
29447
  }
29374
29448
  const logger = makeStderrLogger(parseLogLevel(process.env.LEADBAY_LOG_LEVEL));
29375
29449
  installStartupSafetyNets(logger);
29376
- const telemetry = initTelemetry({ version: VERSION, logger });
29450
+ const telemetry = initTelemetry({
29451
+ version: VERSION,
29452
+ logger,
29453
+ flushAt: 1,
29454
+ flushInterval: 5e3
29455
+ });
29377
29456
  const { client, authState } = await resolveClientFromEnv(logger);
29378
29457
  const bootstrapPending = authState === "pending";
29379
29458
  if (!bootstrapPending) telemetry.identify(client);