@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/CHANGELOG.md +12 -0
- package/README.md +19 -53
- package/dist/bin.js +177 -98
- package/dist/http-server.js +569 -104
- package/dist/installer-electron.js +2 -2
- package/dist/installer-gui.js +2 -2
- package/package.json +1 -1
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
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
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"\`
|
|
8655
|
-
- \`_meta.resolved_query\`: the original
|
|
8656
|
-
- \`_meta.match_candidates[]\`: up to 4
|
|
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
|
-
|
|
8661
|
-
|
|
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:
|
|
8665
|
-
|
|
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:
|
|
8670
|
-
leadbay_research_lead_by_id directly.
|
|
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
|
|
8813
|
-
|
|
8814
|
-
|
|
|
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
|
|
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: "
|
|
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
|
|
16704
|
-
|
|
16705
|
-
|
|
16706
|
-
|
|
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
|
|
16709
|
-
|
|
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
|
|
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
|
|
24661
|
+
Research the company name or domain '{{arg:domain}}' for me using Leadbay.
|
|
24612
24662
|
|
|
24613
|
-
# PHASE 1 \u2014
|
|
24614
|
-
Call \`
|
|
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
|
-
|
|
24617
|
-
|
|
24618
|
-
|
|
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
|
|
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": "
|
|
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):
|
|
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: "
|
|
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
|
|
25345
|
-
flushInterval
|
|
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:
|
|
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
|
-
...
|
|
25586
|
-
...
|
|
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://
|
|
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.
|
|
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({
|
|
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);
|