@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.
@@ -408,6 +408,7 @@ Pick 2\u20133 items below based on what was actually observed in the response. T
408
408
  | Observation | Suggest | Calls |
409
409
  |------------------------------------------------------------|--------------------------------------------------------------|--------------------------------------------------------|
410
410
  | \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) |
411
+ | \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 |
411
412
  | \`has_more == true\` | "Pull the next page (page N+1 of M)" | leadbay_pull_leads(page = current + 1, lensId = pinned)|
412
413
  | \u2265 3 rows have \`qualification_summary.answered == 0\` | "Deepen AI qualification on the rows without \u2756 caps" | leadbay_bulk_qualify_leads(leadIds=[\u2026]) |
413
414
  | User points at a single row | "Research [Company] in depth" | leadbay_research_lead_by_id(leadId) |
@@ -1038,30 +1039,18 @@ GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ship
1038
1039
  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.
1039
1040
 
1040
1041
 
1041
- Research the company with domain '{{arg:domain}}' for me using Leadbay.
1042
+ Research the company name or domain '{{arg:domain}}' for me using Leadbay.
1042
1043
 
1043
- # PHASE 1 \u2014 IMPORT + QUALIFY
1044
- 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):
1044
+ # PHASE 1 \u2014 RESOLVE + DEEP DIVE
1045
+ Call \`leadbay_research_lead_by_name_fuzzy\` with
1046
+ \`companyName:'{{arg:domain}}'\`. Omit \`lensId\`: the default search deliberately
1047
+ covers the user's visible Discover, Monitor, and Activate corpus, including
1048
+ other lenses and leads outside the active lens's first page. The composite
1049
+ resolves the lead and returns the full deep-research payload in one call.
1045
1050
 
1046
- ## RENDERING \u2014 import result summary (single-record, terse)
1047
-
1048
- The response carries either a completed result or an async handle. Render a brief summary; do NOT enumerate every imported lead.
1049
-
1050
- **Header \u2014 single line, choose by status:**
1051
-
1052
- - Completed: \`"\u2713 Import complete \u2014 N leads imported \xB7 M failed \xB7 P resolved-with-ambiguity"\`
1053
- - Running: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
1054
- - Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 qualify_id <id>"\`
1055
-
1056
- **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*"\`.
1057
-
1058
- **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.
1059
-
1060
- Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_research_lead_by_id\` in NEXT STEPS.
1061
-
1062
-
1063
- # PHASE 2 \u2014 DEEP DIVE
1064
- 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:
1051
+ Render the result using the canonical single-record card layout \u2014 detect MODE A
1052
+ (Discovery) since the user asked to research a company rather than prepare
1053
+ outreach:
1065
1054
 
1066
1055
  ## RENDERING \u2014 single-record research card, mode-adaptive
1067
1056
 
@@ -1133,10 +1122,16 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
1133
1122
 
1134
1123
 
1135
1124
 
1125
+ # PHASE 2 \u2014 NOT FOUND
1126
+ If the resolver returns \`LEAD_NOT_FOUND\`, say that the existing visible corpus
1127
+ was searched. **Do NOT call \`leadbay_import_and_qualify\` automatically.** Offer
1128
+ to import and qualify the company as a separate, explicit next step; only call
1129
+ it after the user agrees.
1130
+
1136
1131
  # PHASE 3 \u2014 SUMMARY
1137
1132
  Place a 2\u20133 sentence summary ABOVE the card with:
1138
1133
  - Who is this company (1 sentence)
1139
- - Their fit (cite specific \`qualification_answers\` from the qualification response)
1134
+ - Their fit (cite specific qualification answers or signals from the research response)
1140
1135
  - Which contact would I email first (one short clause \u2014 the card's contacts table carries the rest)
1141
1136
 
1142
1137
  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.
@@ -1312,7 +1307,7 @@ should I follow up on" to "I'll send via lemlist".
1312
1307
  `, "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)"] },
1313
1308
  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"] },
1314
1309
  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"] },
1315
- 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"] },
1310
+ 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)"] },
1316
1311
  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"] },
1317
1312
  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."] }
1318
1313
  };
@@ -1328,7 +1323,7 @@ var PROMPT_CATALOG_BULLETS = {
1328
1323
  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".`,
1329
1324
  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.`,
1330
1325
  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.`,
1331
- 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.`,
1326
+ 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.`,
1332
1327
  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).`,
1333
1328
  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.`
1334
1329
  };
@@ -1363,7 +1358,7 @@ var CATALOG = [
1363
1358
  arguments: [
1364
1359
  {
1365
1360
  name: "domain",
1366
- description: "The company's primary domain (e.g. 'acme.com'). Protocol/path are stripped.",
1361
+ description: "Company name or domain (for example 'Acme Corporation' or 'acme.com'). The legacy argument key remains `domain` for client compatibility.",
1367
1362
  required: true
1368
1363
  }
1369
1364
  ],
@@ -9333,6 +9328,7 @@ Pick 2\u20133 items below based on what was actually observed in the response. T
9333
9328
  | Observation | Suggest | Calls |
9334
9329
  |------------------------------------------------------------|--------------------------------------------------------------|--------------------------------------------------------|
9335
9330
  | \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) |
9331
+ | \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 |
9336
9332
  | \`has_more == true\` | "Pull the next page (page N+1 of M)" | leadbay_pull_leads(page = current + 1, lensId = pinned)|
9337
9333
  | \u2265 3 rows have \`qualification_summary.answered == 0\` | "Deepen AI qualification on the rows without \u2756 caps" | leadbay_bulk_qualify_leads(leadIds=[\u2026]) |
9338
9334
  | User points at a single row | "Research [Company] in depth" | leadbay_research_lead_by_id(leadId) |
@@ -9779,30 +9775,23 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
9779
9775
 
9780
9776
  ---
9781
9777
 
9782
- Thin wrapper around **leadbay_research_lead_by_id**. Accepts a \`companyName\`,
9783
- fuzzy-matches it against the top 50 of the active (or supplied) lens's
9784
- wishlist, picks the highest-scoring substring hit, and delegates the actual
9785
- research to \`leadbay_research_lead_by_id\`. Returns the **same payload shape**
9786
- as \`_by_id\`, with two additions on \`_meta\`:
9778
+ Resolves \`companyName\` across visible Discover, Monitor, and Activate leads,
9779
+ then delegates to **leadbay_research_lead_by_id**. Supplying \`lensId\`
9780
+ deliberately restricts the backend search to that lens. The result matches
9781
+ \`_by_id\`, plus:
9787
9782
 
9788
- - \`_meta.resolved_from\`: \`"companyName"\` (so the agent knows the entry point).
9789
- - \`_meta.resolved_query\`: the original \`companyName\` string.
9790
- - \`_meta.match_candidates[]\`: up to 4 next-best substring matches as
9791
- \`{leadId, name, score}\` \u2014 surface these when ambiguity is plausible so the
9792
- user can redirect.
9783
+ - \`_meta.resolved_from\`: \`"companyName"\`
9784
+ - \`_meta.resolved_query\`: the original query
9785
+ - \`_meta.match_candidates[]\`: up to 4 \`{leadId, name, score}\` alternatives
9793
9786
 
9794
- On zero matches, throws \`LEAD_NOT_FOUND\` with a \`nearest_names[]\` payload (the
9795
- top 5 by score from the lens, regardless of substring) so the agent can offer
9796
- "did you mean\u2026" disambiguation.
9787
+ \`LEAD_NOT_FOUND\` identifies whether the complete visible corpus, an explicit
9788
+ lens, or only a degraded active-lens fallback was searched.
9797
9789
 
9798
- WHEN TO USE: when the user references a company by
9799
- name and you don't yet have its \`lead_id\`. If \`_meta.match_candidates\` is
9800
- non-empty, offer the next-best matches in NEXT STEPS so the user can correct
9801
- a wrong fuzzy hit.
9790
+ WHEN TO USE: for a company/domain/contact reference
9791
+ without a \`lead_id\`. Offer \`_meta.match_candidates\` when present.
9802
9792
 
9803
- WHEN NOT TO USE: when you already have the UUID \u2014 use
9804
- leadbay_research_lead_by_id directly. This wrapper costs one extra
9805
- \`discoverLeads\` round-trip; skipping it when you can is cheaper.
9793
+ WHEN NOT TO USE: with a UUID; call
9794
+ leadbay_research_lead_by_id directly.
9806
9795
 
9807
9796
  ---
9808
9797
 
@@ -9943,9 +9932,9 @@ out?"\`
9943
9932
 
9944
9933
  When \`_meta.match_candidates\` is non-empty, prepend one extra NEXT STEPS row:
9945
9934
 
9946
- | Observation | Suggest | Calls |
9947
- |--------------------------------------------------------|----------------------------------------------------------------------|------------------------------------------------------------------------|
9948
- | \`_meta.match_candidates\` non-empty | "Did you mean **&lt;next-best.name&gt;**?" | leadbay_research_lead_by_id(leadId=&lt;next-best.leadId&gt;) |
9935
+ | Observation | Suggest | Calls |
9936
+ |---|---|---|
9937
+ | Alternatives found | "Did you mean **&lt;next-best.name&gt;**?" | leadbay_research_lead_by_id(leadId=&lt;next-best.leadId&gt;) |
9949
9938
  `;
9950
9939
  var 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\`.
9951
9940
 
@@ -15350,6 +15339,11 @@ function buildPullLeadsNextSteps(args) {
15350
15339
  description: "Build an interactive lead triage board to sort and filter this batch.",
15351
15340
  kind: "build_artifact"
15352
15341
  });
15342
+ options.push({
15343
+ label: "Enrich top leads",
15344
+ description: "Enrich the top leads",
15345
+ kind: "enrich_top_leads"
15346
+ });
15353
15347
  options.push({
15354
15348
  label: "Deepen qualification",
15355
15349
  description: "Run deeper AI qualification on these leads.",
@@ -15435,7 +15429,7 @@ var pullLeads = {
15435
15429
  },
15436
15430
  next_steps: {
15437
15431
  type: ["object", "null"],
15438
- 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).",
15432
+ 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).",
15439
15433
  properties: {
15440
15434
  question: { type: "string" },
15441
15435
  options: {
@@ -17224,6 +17218,41 @@ function rankSubstringMatches(needle, candidates) {
17224
17218
  });
17225
17219
  return hits;
17226
17220
  }
17221
+ function parseLensId(value) {
17222
+ const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
17223
+ return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
17224
+ }
17225
+ function suggestionName(suggestion) {
17226
+ const companyName = (suggestion.company_name ?? suggestion.companyName)?.trim();
17227
+ return companyName || suggestion.text.trim();
17228
+ }
17229
+ function suggestionLeadId(suggestion) {
17230
+ return suggestion.lead_id ?? suggestion.leadId;
17231
+ }
17232
+ function isLeadbayError(error) {
17233
+ return typeof error === "object" && error !== null && error.error === true && typeof error.code === "string" && typeof error.message === "string" && typeof error.hint === "string";
17234
+ }
17235
+ async function resolveWithinLens(client, query, lensId) {
17236
+ const results = await client.request("GET", `/lenses/${lensId}/leads/wishlist?q=${encodeURIComponent(query)}&count=50&page=0&contacts=false`);
17237
+ return results.items.map((lead) => ({
17238
+ id: lead.id,
17239
+ name: lead.name,
17240
+ score: lead.score,
17241
+ lensId
17242
+ }));
17243
+ }
17244
+ async function resolveAcrossVisibleCorpus(client, query) {
17245
+ const suggestions = await client.request("GET", `/search/suggest?q=${encodeURIComponent(query)}`);
17246
+ return suggestions.map((suggestion) => {
17247
+ const id = suggestionLeadId(suggestion);
17248
+ return {
17249
+ id: id ?? "",
17250
+ name: suggestionName(suggestion),
17251
+ score: null,
17252
+ lensId: parseLensId(suggestion.lens_id ?? suggestion.lensId)
17253
+ };
17254
+ }).filter((suggestion) => suggestion.id !== "" && suggestion.name !== "");
17255
+ }
17227
17256
  var researchLeadByNameFuzzy = {
17228
17257
  name: "leadbay_research_lead_by_name_fuzzy",
17229
17258
  annotations: {
@@ -17239,11 +17268,11 @@ var researchLeadByNameFuzzy = {
17239
17268
  properties: {
17240
17269
  companyName: {
17241
17270
  type: "string",
17242
- 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."
17271
+ description: "Company name, domain, or contact name to resolve across visible Leadbay leads in Discover, Monitor, and Activate."
17243
17272
  },
17244
17273
  lensId: {
17245
17274
  type: "number",
17246
- description: "Lens id (escape hatch \u2014 normally omit; auto-resolves to the active lens)"
17275
+ description: "Optional strict scope. When supplied, search only this lens's wishlist; normally omit to search all visible Leadbay leads."
17247
17276
  },
17248
17277
  concise: {
17249
17278
  type: "boolean",
@@ -17268,18 +17297,34 @@ var researchLeadByNameFuzzy = {
17268
17297
  additionalProperties: true
17269
17298
  },
17270
17299
  execute: async (client, params, ctx) => {
17271
- if (!params.companyName || typeof params.companyName !== "string") {
17300
+ if (!params.companyName || typeof params.companyName !== "string" || params.companyName.trim() === "") {
17272
17301
  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.");
17273
17302
  }
17274
- const lensId = params.lensId ?? await client.resolveDefaultLens();
17275
- const results = await discoverLeads.execute(client, { lensId, count: 50, page: 0 }, ctx);
17276
- const allLeads = results.leads;
17277
- const ranked = rankSubstringMatches(params.companyName, allLeads);
17303
+ const query = params.companyName.trim();
17304
+ let ranked;
17305
+ let lensId = params.lensId;
17306
+ let usedActiveLensFallback = false;
17307
+ if (lensId !== void 0) {
17308
+ ranked = await resolveWithinLens(client, query, lensId);
17309
+ } else {
17310
+ try {
17311
+ ranked = await resolveAcrossVisibleCorpus(client, query);
17312
+ } catch (error) {
17313
+ if (isLeadbayError(error))
17314
+ throw error;
17315
+ lensId = await client.resolveDefaultLens();
17316
+ usedActiveLensFallback = true;
17317
+ ctx?.logger?.warn?.("Cross-tab company search was unavailable; falling back to the active lens for this lookup.");
17318
+ ranked = rankSubstringMatches(query, await resolveWithinLens(client, query, lensId));
17319
+ }
17320
+ }
17278
17321
  if (ranked.length === 0) {
17279
- 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 }));
17280
- 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(", ")}.`);
17322
+ 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}`;
17323
+ 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.";
17324
+ throw client.makeError("LEAD_NOT_FOUND", `No lead matching "${query}" ${scope}`, hint);
17281
17325
  }
17282
17326
  const [primary, ...rest] = ranked;
17327
+ lensId = primary.lensId ?? lensId ?? await client.resolveDefaultLens();
17283
17328
  const candidates = rest.slice(0, 4).map((m) => ({
17284
17329
  leadId: m.id,
17285
17330
  name: m.name,
@@ -17292,7 +17337,7 @@ var researchLeadByNameFuzzy = {
17292
17337
  response_format: params.response_format,
17293
17338
  _resolved: {
17294
17339
  from: "companyName",
17295
- query: params.companyName,
17340
+ query,
17296
17341
  candidates
17297
17342
  }
17298
17343
  }, ctx);
@@ -23154,18 +23199,41 @@ var BUILTIN_WIDGETS_PARAGRAPH = 'Prefer host-native widgets over inline markdown
23154
23199
  // src/telemetry.ts
23155
23200
  import { PostHog } from "posthog-node";
23156
23201
  import * as Sentry from "@sentry/node";
23202
+
23203
+ // src/telemetry-constants.ts
23204
+ var EMBEDDED_POSTHOG_KEY = "phc_N9SnA7OULuAlXReQJZ0Y3rPI4eC0mJLpMRbzgqamhHR";
23205
+ var EMBEDDED_POSTHOG_HOST = "https://eu.i.posthog.com";
23206
+ var EMBEDDED_SENTRY_DSN = "https://301f1c433433b76132956ed5415bea19@o4505874436849664.ingest.us.sentry.io/4511419984248832";
23207
+
23208
+ // src/telemetry-events.ts
23209
+ var EV_TOOL_CALL = "mcp tool called";
23210
+ var EV_QUOTA_HIT = "mcp quota hit";
23211
+ var EV_TOPUP_LINK = "mcp topup link created";
23212
+ var EV_STARTUP = "mcp startup";
23213
+ var EV_MCP_UPDATE_CHECK = "mcp update check";
23214
+ var EV_MCP_UPDATE_PROMPTED = "mcp update prompted";
23215
+ var EV_MCP_UPDATE_INSTALL_CLICKED = "mcp update install_clicked";
23216
+ var EV_MCP_UPDATE_DISMISSED = "mcp update dismissed";
23217
+ var EV_MCP_VERSION_UPDATED = "mcp version updated";
23218
+ var EV_AGENT_MEMORY_CAPTURED = "agent_memory_captured";
23219
+ var EV_AGENT_MEMORY_RECALLED = "agent_memory_recalled";
23220
+ var EV_AGENT_MEMORY_PRUNED = "agent_memory_pruned";
23221
+ var EV_FRICTION_REPORTED = "mcp friction reported";
23222
+ var EV_COMPOSITE_CALL = "mcp composite call";
23223
+
23224
+ // src/telemetry.ts
23157
23225
  var NOOP_TELEMETRY = {
23158
23226
  identify: async () => {
23159
23227
  },
23160
- captureToolCall: () => {
23228
+ captureToolCall: (_props, _identity) => {
23161
23229
  },
23162
- captureCompositeCall: () => {
23230
+ captureCompositeCall: (_props, _identity) => {
23163
23231
  },
23164
- captureQuotaHit: () => {
23232
+ captureQuotaHit: (_props, _identity) => {
23165
23233
  },
23166
- captureTopupLink: () => {
23234
+ captureTopupLink: (_props, _identity) => {
23167
23235
  },
23168
- captureStartup: () => {
23236
+ captureStartup: (_props, _identity) => {
23169
23237
  },
23170
23238
  captureAgentMemoryCaptured: () => {
23171
23239
  },
@@ -23191,6 +23259,320 @@ var NOOP_TELEMETRY = {
23191
23259
  shutdown: async () => {
23192
23260
  }
23193
23261
  };
23262
+ function parseTelemetryEnv(raw) {
23263
+ if (raw === void 0 || raw === "") return true;
23264
+ const v = raw.trim().toLowerCase();
23265
+ if (v === "false" || v === "0" || v === "no" || v === "off") return false;
23266
+ return true;
23267
+ }
23268
+ function initTelemetry(opts) {
23269
+ if (!parseTelemetryEnv(process.env.LEADBAY_TELEMETRY_ENABLED)) return NOOP_TELEMETRY;
23270
+ if (process.env.NODE_ENV === "test") return NOOP_TELEMETRY;
23271
+ const posthogKey = process.env.LEADBAY_POSTHOG_KEY ?? EMBEDDED_POSTHOG_KEY;
23272
+ const sentryDsn = process.env.LEADBAY_SENTRY_DSN ?? EMBEDDED_SENTRY_DSN;
23273
+ if (!posthogKey && !sentryDsn) return NOOP_TELEMETRY;
23274
+ const { version, logger: logger2 } = opts;
23275
+ const flushAt = opts.flushAt ?? 20;
23276
+ const flushInterval = opts.flushInterval ?? 1e4;
23277
+ const environment = process.env.LEADBAY_ENV ?? (version.includes("-dev.") ? "dev" : "production");
23278
+ let posthog = null;
23279
+ let sentryReady = false;
23280
+ let initError = null;
23281
+ try {
23282
+ if (posthogKey) {
23283
+ posthog = new PostHog(posthogKey, {
23284
+ host: process.env.LEADBAY_POSTHOG_HOST ?? EMBEDDED_POSTHOG_HOST,
23285
+ flushAt,
23286
+ flushInterval,
23287
+ disableGeoip: false
23288
+ });
23289
+ }
23290
+ } catch (err) {
23291
+ initError = err;
23292
+ posthog = null;
23293
+ }
23294
+ try {
23295
+ if (sentryDsn) {
23296
+ Sentry.init({
23297
+ dsn: sentryDsn,
23298
+ release: `@leadbay/mcp@${version}`,
23299
+ environment,
23300
+ tracesSampleRate: 0,
23301
+ profilesSampleRate: 0,
23302
+ defaultIntegrations: false,
23303
+ integrations: [Sentry.httpIntegration()],
23304
+ sendDefaultPii: false,
23305
+ // Tag every captured event with the surface so Sentry views can
23306
+ // split MCP issues from web-app issues without per-call work.
23307
+ // Version is also encoded in `release` above, but a dedicated
23308
+ // `mcp_version` tag is filterable from Sentry's issue list without
23309
+ // expanding the release dropdown — load-bearing when triaging
23310
+ // "errors at reinstall on @0.13" vs older clients still on @0.11.
23311
+ initialScope: {
23312
+ tags: {
23313
+ source: "mcp",
23314
+ mcp_version: version,
23315
+ node_version: process.versions.node,
23316
+ platform: process.platform
23317
+ }
23318
+ }
23319
+ });
23320
+ sentryReady = true;
23321
+ }
23322
+ } catch (err) {
23323
+ initError = initError ?? err;
23324
+ sentryReady = false;
23325
+ }
23326
+ if (initError) {
23327
+ logger2?.warn?.(`telemetry init failed: ${initError.message ?? initError}`);
23328
+ }
23329
+ if (!posthog && !sentryReady) return NOOP_TELEMETRY;
23330
+ let me = null;
23331
+ let identityPromise = null;
23332
+ const pendingEvents = [];
23333
+ let region = "unknown";
23334
+ const baseProps = () => ({
23335
+ // Always tag MCP-originated events so PostHog dashboards can split
23336
+ // MCP usage from the web app and any future surfaces. The value
23337
+ // ("mcp") is the canonical source identifier — match it in any
23338
+ // PostHog filter or insight that should isolate the MCP surface.
23339
+ source: "mcp",
23340
+ mcp_version: version,
23341
+ node_version: process.versions.node,
23342
+ platform: process.platform,
23343
+ region
23344
+ });
23345
+ const distinctIdFor = () => {
23346
+ if (me?.email) return me.email;
23347
+ if (me?.id) return `mcp:user-${me.id}`;
23348
+ return "mcp:unknown";
23349
+ };
23350
+ const groupsFor = () => {
23351
+ return me?.organization?.id ? { organization: me.organization.id } : void 0;
23352
+ };
23353
+ const doCapture = (event, properties, identity) => {
23354
+ if (!posthog) return;
23355
+ try {
23356
+ const props = identity?.region ? { ...baseProps(), region: identity.region, ...properties } : { ...baseProps(), ...properties };
23357
+ posthog.capture({
23358
+ distinctId: identity ? identity.distinctId : distinctIdFor(),
23359
+ event,
23360
+ properties: props,
23361
+ groups: identity ? identity.groups : groupsFor()
23362
+ });
23363
+ } catch (err) {
23364
+ logger2?.warn?.(`posthog capture failed: ${err?.message ?? err}`);
23365
+ }
23366
+ };
23367
+ const emit = (event, properties, identity) => {
23368
+ if (!posthog) return;
23369
+ if (identity) {
23370
+ doCapture(event, properties, identity);
23371
+ return;
23372
+ }
23373
+ if (!me) {
23374
+ pendingEvents.push({ event, properties });
23375
+ return;
23376
+ }
23377
+ doCapture(event, properties);
23378
+ };
23379
+ const flushPending = () => {
23380
+ if (!posthog || pendingEvents.length === 0) return;
23381
+ const buf = pendingEvents.splice(0, pendingEvents.length);
23382
+ for (const { event, properties } of buf) {
23383
+ doCapture(event, properties);
23384
+ }
23385
+ };
23386
+ return {
23387
+ identify(client) {
23388
+ if (identityPromise) return identityPromise;
23389
+ region = client.region;
23390
+ identityPromise = (async () => {
23391
+ try {
23392
+ const resolved = await client.resolveMe();
23393
+ me = resolved;
23394
+ if (posthog && resolved.email) {
23395
+ try {
23396
+ posthog.identify({
23397
+ distinctId: resolved.email,
23398
+ properties: {
23399
+ email: resolved.email,
23400
+ leadbay_id: resolved.id,
23401
+ leadbay_name: resolved.name,
23402
+ leadbay_organization: resolved.organization?.name,
23403
+ leadbay_organization_id: resolved.organization?.id
23404
+ }
23405
+ });
23406
+ } catch (err) {
23407
+ logger2?.warn?.(`posthog identify failed: ${err?.message ?? err}`);
23408
+ }
23409
+ }
23410
+ if (sentryReady) {
23411
+ try {
23412
+ Sentry.setUser({
23413
+ id: resolved.id,
23414
+ email: resolved.email,
23415
+ username: resolved.name
23416
+ });
23417
+ } catch (err) {
23418
+ logger2?.warn?.(`sentry setUser failed: ${err?.message ?? err}`);
23419
+ }
23420
+ }
23421
+ flushPending();
23422
+ } catch (err) {
23423
+ logger2?.warn?.(
23424
+ `telemetry identify failed (${err?.message ?? err}); flushing events anonymously`
23425
+ );
23426
+ me = {
23427
+ id: "unknown",
23428
+ organization: { id: "unknown", name: "unknown" }
23429
+ };
23430
+ flushPending();
23431
+ }
23432
+ })();
23433
+ return identityPromise;
23434
+ },
23435
+ captureToolCall(props, identity) {
23436
+ emit(EV_TOOL_CALL, { ...props }, identity);
23437
+ },
23438
+ captureCompositeCall(props, identity) {
23439
+ emit(EV_COMPOSITE_CALL, { ...props }, identity);
23440
+ },
23441
+ captureQuotaHit(props, identity) {
23442
+ emit(EV_QUOTA_HIT, { ...props }, identity);
23443
+ },
23444
+ captureTopupLink(props, identity) {
23445
+ emit(EV_TOPUP_LINK, { ...props }, identity);
23446
+ },
23447
+ captureStartup(props, identity) {
23448
+ emit(EV_STARTUP, { ...props }, identity);
23449
+ },
23450
+ captureAgentMemoryCaptured(props, identity) {
23451
+ emit(EV_AGENT_MEMORY_CAPTURED, { ...props }, identity);
23452
+ },
23453
+ captureAgentMemoryRecalled(props, identity) {
23454
+ emit(EV_AGENT_MEMORY_RECALLED, { ...props }, identity);
23455
+ },
23456
+ captureAgentMemoryPruned(props, identity) {
23457
+ emit(EV_AGENT_MEMORY_PRUNED, { ...props }, identity);
23458
+ },
23459
+ captureFrictionReported(props, identity) {
23460
+ emit(EV_FRICTION_REPORTED, { ...props }, identity);
23461
+ },
23462
+ captureUpdateCheck(props) {
23463
+ emit(EV_MCP_UPDATE_CHECK, { ...props });
23464
+ },
23465
+ captureUpdatePrompted(props) {
23466
+ emit(EV_MCP_UPDATE_PROMPTED, { ...props });
23467
+ },
23468
+ captureUpdateInstallClicked(props) {
23469
+ emit(EV_MCP_UPDATE_INSTALL_CLICKED, { ...props });
23470
+ },
23471
+ captureUpdateDismissed(props) {
23472
+ emit(EV_MCP_UPDATE_DISMISSED, { ...props });
23473
+ },
23474
+ captureVersionUpdated(props) {
23475
+ emit(EV_MCP_VERSION_UPDATED, { ...props });
23476
+ },
23477
+ captureException(err, ctx) {
23478
+ if (!sentryReady) return;
23479
+ try {
23480
+ Sentry.withScope((scope) => {
23481
+ scope.setTag("tool", ctx.tool);
23482
+ if (ctx.code) scope.setTag("error_code", ctx.code);
23483
+ if (ctx.endpoint) scope.setTag("endpoint", ctx.endpoint);
23484
+ if (ctx.region) scope.setTag("region", ctx.region);
23485
+ if (ctx.http_status !== void 0) {
23486
+ scope.setTag("http_status", String(ctx.http_status));
23487
+ }
23488
+ if (ctx.source) scope.setTag("source", ctx.source);
23489
+ if (me?.organization?.id) {
23490
+ scope.setTag("organization", me.organization.id);
23491
+ }
23492
+ if (ctx.message) scope.setExtra("message", ctx.message);
23493
+ if (ctx.hint) scope.setExtra("hint", ctx.hint);
23494
+ if (ctx.triggered_by) scope.setExtra("triggered_by", ctx.triggered_by);
23495
+ if (ctx.latency_ms !== void 0 && ctx.latency_ms !== null) {
23496
+ scope.setExtra("latency_ms", ctx.latency_ms);
23497
+ }
23498
+ if (ctx.retry_after !== void 0 && ctx.retry_after !== null) {
23499
+ scope.setExtra("retry_after", ctx.retry_after);
23500
+ }
23501
+ if (ctx.code && ctx.source === "business") {
23502
+ scope.setFingerprint(["mcp", ctx.tool, ctx.code]);
23503
+ }
23504
+ Sentry.captureException(err);
23505
+ });
23506
+ } catch (e) {
23507
+ logger2?.warn?.(`sentry captureException failed: ${e?.message ?? e}`);
23508
+ }
23509
+ },
23510
+ async captureFeedback(message, opts2, identity) {
23511
+ if (!sentryReady) return false;
23512
+ const trimmed = (message ?? "").trim();
23513
+ if (!trimmed) return false;
23514
+ if (!identity && identityPromise) {
23515
+ let waitTimer;
23516
+ try {
23517
+ await Promise.race([
23518
+ identityPromise,
23519
+ new Promise((resolve) => {
23520
+ waitTimer = setTimeout(resolve, 2e3);
23521
+ })
23522
+ ]);
23523
+ } catch {
23524
+ } finally {
23525
+ if (waitTimer) clearTimeout(waitTimer);
23526
+ }
23527
+ }
23528
+ const fbName = identity?.name ?? me?.name;
23529
+ const fbEmail = identity?.email ?? me?.email;
23530
+ try {
23531
+ Sentry.captureFeedback({
23532
+ message: trimmed,
23533
+ ...fbName ? { name: fbName } : {},
23534
+ ...fbEmail ? { email: fbEmail } : {},
23535
+ ...opts2?.associatedEventId ? { associatedEventId: opts2.associatedEventId } : {}
23536
+ });
23537
+ const flushed = await Sentry.flush(4e3);
23538
+ if (!flushed) {
23539
+ logger2?.warn?.("sentry feedback flush timed out (event may be buffered)");
23540
+ }
23541
+ return flushed;
23542
+ } catch (e) {
23543
+ logger2?.warn?.(`sentry captureFeedback failed: ${e?.message ?? e}`);
23544
+ return false;
23545
+ }
23546
+ },
23547
+ async shutdown() {
23548
+ if (!me && identityPromise) {
23549
+ let waitTimer;
23550
+ try {
23551
+ await Promise.race([
23552
+ identityPromise,
23553
+ new Promise((resolve) => {
23554
+ waitTimer = setTimeout(resolve, 1500);
23555
+ })
23556
+ ]);
23557
+ } catch {
23558
+ } finally {
23559
+ if (waitTimer) clearTimeout(waitTimer);
23560
+ }
23561
+ }
23562
+ if (!me) {
23563
+ me = {
23564
+ id: "unknown",
23565
+ organization: { id: "unknown", name: "unknown" }
23566
+ };
23567
+ flushPending();
23568
+ }
23569
+ const tasks = [];
23570
+ if (posthog) tasks.push(posthog.shutdown(2e3).catch(() => void 0));
23571
+ if (sentryReady) tasks.push(Sentry.close(2e3).catch(() => void 0));
23572
+ await Promise.allSettled(tasks);
23573
+ }
23574
+ };
23575
+ }
23194
23576
 
23195
23577
  // src/update-check.ts
23196
23578
  var cachedInfo = null;
@@ -23838,7 +24220,7 @@ function buildServer(client, opts = {}) {
23838
24220
  });
23839
24221
  const DEBUG_RAW = process.env.LEADBAY_DEBUG ?? "";
23840
24222
  const DEBUG_ON = DEBUG_RAW === "1" || DEBUG_RAW.toLowerCase() === "true";
23841
- const telemetry = opts.telemetry ?? NOOP_TELEMETRY;
24223
+ const telemetry2 = opts.telemetry ?? NOOP_TELEMETRY;
23842
24224
  const promptedVersionsThisSession = /* @__PURE__ */ new Set();
23843
24225
  const serverVersion = opts.version ?? "0.0.0-dev";
23844
24226
  const UPDATE_CHECK_DISABLED = process.env.LEADBAY_UPDATE_CHECK_DISABLED === "1";
@@ -23848,7 +24230,7 @@ function buildServer(client, opts = {}) {
23848
24230
  void checkForUpdate({
23849
24231
  currentVersion: serverVersion,
23850
24232
  stateStore: opts.updateStateStore,
23851
- telemetry,
24233
+ telemetry: telemetry2,
23852
24234
  logger: opts.logger
23853
24235
  }).catch((err) => {
23854
24236
  opts.logger?.warn?.(
@@ -23898,7 +24280,7 @@ function buildServer(client, opts = {}) {
23898
24280
  }
23899
24281
  if (!alreadyPrompted) {
23900
24282
  promptedVersionsThisSession.add(info.latest_version);
23901
- telemetry.captureUpdatePrompted?.({
24283
+ telemetry2.captureUpdatePrompted?.({
23902
24284
  current_version: serverVersion,
23903
24285
  latest_version: info.latest_version
23904
24286
  });
@@ -23945,7 +24327,7 @@ function buildServer(client, opts = {}) {
23945
24327
  if (typeof fr.category !== "string" || typeof fr.user_quote !== "string") {
23946
24328
  return;
23947
24329
  }
23948
- telemetry.captureFrictionReported({
24330
+ telemetry2.captureFrictionReported({
23949
24331
  category: fr.category,
23950
24332
  user_quote: fr.user_quote,
23951
24333
  ...typeof fr.tool_called === "string" ? { tool_called: fr.tool_called } : {},
@@ -23957,7 +24339,7 @@ function buildServer(client, opts = {}) {
23957
24339
  if (!result || typeof result !== "object") return;
23958
24340
  const meta = result._meta ?? {};
23959
24341
  if (toolName === "leadbay_agent_memory_capture") {
23960
- telemetry.captureAgentMemoryCaptured({
24342
+ telemetry2.captureAgentMemoryCaptured({
23961
24343
  source: result.captured?.source ?? meta.source,
23962
24344
  scope: result.captured?.scope ?? meta.scope,
23963
24345
  key: result.captured?.key,
@@ -23965,13 +24347,13 @@ function buildServer(client, opts = {}) {
23965
24347
  account_id_hash: meta.account_id_hash
23966
24348
  });
23967
24349
  } else if (toolName === "leadbay_agent_memory_recall") {
23968
- telemetry.captureAgentMemoryRecalled({
24350
+ telemetry2.captureAgentMemoryRecalled({
23969
24351
  entries_returned: result.entries_returned,
23970
24352
  total_active: result.total_active,
23971
24353
  account_id_hash: meta.account_id_hash
23972
24354
  });
23973
24355
  } else if (toolName === "leadbay_agent_memory_review" && result.changed === true && (result.action === "retract" || result.action === "prune")) {
23974
- telemetry.captureAgentMemoryPruned({
24356
+ telemetry2.captureAgentMemoryPruned({
23975
24357
  action: result.action,
23976
24358
  account_id_hash: meta.account_id_hash
23977
24359
  });
@@ -24058,7 +24440,7 @@ ${url}
24058
24440
  };
24059
24441
  const pendingText = formatErrorForLLM(envelope);
24060
24442
  const pendingDur = Date.now() - callStart;
24061
- telemetry.captureToolCall({
24443
+ telemetry2.captureToolCall({
24062
24444
  tool: name,
24063
24445
  ok: false,
24064
24446
  duration_ms: pendingDur,
@@ -24087,7 +24469,7 @@ ${url}
24087
24469
  };
24088
24470
  const guardText = formatErrorForLLM(envelope);
24089
24471
  const guardDur = Date.now() - callStart;
24090
- telemetry.captureToolCall({
24472
+ telemetry2.captureToolCall({
24091
24473
  tool: name,
24092
24474
  ok: false,
24093
24475
  duration_ms: guardDur,
@@ -24096,7 +24478,7 @@ ${url}
24096
24478
  error_code: envelope.code,
24097
24479
  triggered_by
24098
24480
  });
24099
- telemetry.captureCompositeCall({
24481
+ telemetry2.captureCompositeCall({
24100
24482
  tool: name,
24101
24483
  last_prompt: triggered_by ?? "",
24102
24484
  ok: false,
@@ -24128,7 +24510,7 @@ ${url}
24128
24510
  // Route leadbay_send_feedback to Sentry's feedback inbox (same place
24129
24511
  // the web app's form lands). NOOP_TELEMETRY returns false, so the
24130
24512
  // tool reports honestly when telemetry is off.
24131
- sendFeedback: (message, fbOpts) => telemetry.captureFeedback(message, fbOpts)
24513
+ sendFeedback: (message, fbOpts) => telemetry2.captureFeedback(message, fbOpts)
24132
24514
  });
24133
24515
  await maybeAttachUpdate(name, result);
24134
24516
  maybeAttachNotifications(result);
@@ -24137,13 +24519,13 @@ ${url}
24137
24519
  const envDur = Date.now() - callStart;
24138
24520
  const envCode = result.code ?? "Error";
24139
24521
  if (envCode === "QUOTA_EXCEEDED") {
24140
- telemetry.captureQuotaHit({
24522
+ telemetry2.captureQuotaHit({
24141
24523
  tool: name,
24142
24524
  retry_after_s: result._meta?.retry_after,
24143
24525
  endpoint: result._meta?.endpoint
24144
24526
  });
24145
24527
  }
24146
- telemetry.captureToolCall({
24528
+ telemetry2.captureToolCall({
24147
24529
  tool: name,
24148
24530
  ok: false,
24149
24531
  duration_ms: envDur,
@@ -24153,7 +24535,7 @@ ${url}
24153
24535
  triggered_by
24154
24536
  });
24155
24537
  if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
24156
- telemetry.captureCompositeCall({
24538
+ telemetry2.captureCompositeCall({
24157
24539
  tool: name,
24158
24540
  last_prompt: triggered_by ?? "",
24159
24541
  ok: false,
@@ -24161,7 +24543,7 @@ ${url}
24161
24543
  error_code: envCode
24162
24544
  });
24163
24545
  }
24164
- telemetry.captureException(
24546
+ telemetry2.captureException(
24165
24547
  result,
24166
24548
  buildBusinessCtx(name, result, triggered_by)
24167
24549
  );
@@ -24189,7 +24571,7 @@ ${url}
24189
24571
  }
24190
24572
  const mdDur = Date.now() - callStart;
24191
24573
  const mdBytes = env.markdown.length;
24192
- telemetry.captureToolCall({
24574
+ telemetry2.captureToolCall({
24193
24575
  tool: name,
24194
24576
  ok: true,
24195
24577
  duration_ms: mdDur,
@@ -24198,7 +24580,7 @@ ${url}
24198
24580
  triggered_by
24199
24581
  });
24200
24582
  if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
24201
- telemetry.captureCompositeCall({
24583
+ telemetry2.captureCompositeCall({
24202
24584
  tool: name,
24203
24585
  last_prompt: triggered_by ?? "",
24204
24586
  ok: true,
@@ -24208,7 +24590,7 @@ ${url}
24208
24590
  captureAgentMemoryTelemetry(name, env.structured);
24209
24591
  captureFrictionTelemetry(name, env.structured);
24210
24592
  if (name === "leadbay_create_topup_link" && typeof env.structured?.url === "string") {
24211
- telemetry.captureTopupLink({ tool: name });
24593
+ telemetry2.captureTopupLink({ tool: name });
24212
24594
  }
24213
24595
  if (DEBUG_ON) {
24214
24596
  process.stderr.write(
@@ -24229,7 +24611,7 @@ ${url}
24229
24611
  const okText = response.content[0]?.text ?? "";
24230
24612
  const okBytes = typeof okText === "string" ? okText.length : 0;
24231
24613
  const okDur = Date.now() - callStart;
24232
- telemetry.captureToolCall({
24614
+ telemetry2.captureToolCall({
24233
24615
  tool: name,
24234
24616
  ok: true,
24235
24617
  duration_ms: okDur,
@@ -24238,7 +24620,7 @@ ${url}
24238
24620
  triggered_by
24239
24621
  });
24240
24622
  if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
24241
- telemetry.captureCompositeCall({
24623
+ telemetry2.captureCompositeCall({
24242
24624
  tool: name,
24243
24625
  last_prompt: triggered_by ?? "",
24244
24626
  ok: true,
@@ -24248,7 +24630,7 @@ ${url}
24248
24630
  captureAgentMemoryTelemetry(name, result);
24249
24631
  captureFrictionTelemetry(name, result);
24250
24632
  if (name === "leadbay_create_topup_link" && typeof result?.url === "string") {
24251
- telemetry.captureTopupLink({ tool: name });
24633
+ telemetry2.captureTopupLink({ tool: name });
24252
24634
  }
24253
24635
  if (DEBUG_ON) {
24254
24636
  process.stderr.write(
@@ -24263,14 +24645,14 @@ ${url}
24263
24645
  const code = err?.code ?? err?.name ?? "Error";
24264
24646
  if (isLeadbayBusinessError(err)) {
24265
24647
  if (err.code === "QUOTA_EXCEEDED") {
24266
- telemetry.captureQuotaHit({
24648
+ telemetry2.captureQuotaHit({
24267
24649
  tool: name,
24268
24650
  retry_after_s: err._meta?.retry_after,
24269
24651
  endpoint: err._meta?.endpoint
24270
24652
  });
24271
24653
  }
24272
24654
  const httpStatus2 = err._meta?.http_status;
24273
- telemetry.captureToolCall({
24655
+ telemetry2.captureToolCall({
24274
24656
  tool: name,
24275
24657
  ok: false,
24276
24658
  duration_ms: errDur,
@@ -24281,7 +24663,7 @@ ${url}
24281
24663
  triggered_by
24282
24664
  });
24283
24665
  if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
24284
- telemetry.captureCompositeCall({
24666
+ telemetry2.captureCompositeCall({
24285
24667
  tool: name,
24286
24668
  last_prompt: triggered_by ?? "",
24287
24669
  ok: false,
@@ -24290,15 +24672,15 @@ ${url}
24290
24672
  ...typeof httpStatus2 === "number" ? { http_status: httpStatus2 } : {}
24291
24673
  });
24292
24674
  }
24293
- telemetry.captureException(err, buildBusinessCtx(name, err, triggered_by));
24675
+ telemetry2.captureException(err, buildBusinessCtx(name, err, triggered_by));
24294
24676
  } else {
24295
- telemetry.captureException(err, {
24677
+ telemetry2.captureException(err, {
24296
24678
  tool: name,
24297
24679
  source: "unexpected",
24298
24680
  message: typeof err?.message === "string" ? err.message : void 0,
24299
24681
  triggered_by
24300
24682
  });
24301
- telemetry.captureToolCall({
24683
+ telemetry2.captureToolCall({
24302
24684
  tool: name,
24303
24685
  ok: false,
24304
24686
  duration_ms: errDur,
@@ -24308,7 +24690,7 @@ ${url}
24308
24690
  triggered_by
24309
24691
  });
24310
24692
  if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
24311
- telemetry.captureCompositeCall({
24693
+ telemetry2.captureCompositeCall({
24312
24694
  tool: name,
24313
24695
  last_prompt: triggered_by ?? "",
24314
24696
  ok: false,
@@ -24358,7 +24740,7 @@ function makeBrokenClient(stubError, region) {
24358
24740
 
24359
24741
  // src/auth-http.ts
24360
24742
  async function resolveClientFromToken(token, opts = {}) {
24361
- const { region, baseUrl, logger } = opts;
24743
+ const { region, baseUrl, logger: logger2 } = opts;
24362
24744
  if (!token || token.length === 0) {
24363
24745
  const fallbackRegion = region === "fr" ? "fr" : "us";
24364
24746
  return {
@@ -24380,10 +24762,10 @@ async function resolveClientFromToken(token, opts = {}) {
24380
24762
  if (region) config.region = region;
24381
24763
  return { client: createClient(config), authState: "ok" };
24382
24764
  }
24383
- logger?.info?.("hosted MCP: region unpinned, probing api-us + api-fr in parallel");
24765
+ logger2?.info?.("hosted MCP: region unpinned, probing api-us + api-fr in parallel");
24384
24766
  const probe = async (r) => {
24385
24767
  const c = createClient({ token, region: r });
24386
- await c.request("GET", "/users/me");
24768
+ await c.resolveMe();
24387
24769
  return c;
24388
24770
  };
24389
24771
  try {
@@ -24448,9 +24830,63 @@ function parseWriteEnv(env = process.env) {
24448
24830
  }
24449
24831
 
24450
24832
  // src/http-server.ts
24451
- var VERSION = true ? "0.24.0" : "0.0.0-dev";
24833
+ var VERSION = true ? "0.24.1" : "0.0.0-dev";
24452
24834
  var PORT = Number(process.env.PORT ?? 8080);
24453
24835
  var HOST = process.env.HOST ?? "0.0.0.0";
24836
+ var logger = {
24837
+ info: (m) => process.stderr.write(`[leadbay-mcp-http info] ${m}
24838
+ `),
24839
+ warn: (m) => process.stderr.write(`[leadbay-mcp-http warn] ${m}
24840
+ `),
24841
+ error: (m) => process.stderr.write(`[leadbay-mcp-http error] ${m}
24842
+ `)
24843
+ };
24844
+ var telemetry = initTelemetry({ version: VERSION, logger });
24845
+ var IDENTITY_RESOLVE_TIMEOUT_MS = 1500;
24846
+ async function resolveIdentity(client) {
24847
+ const region = client.region;
24848
+ try {
24849
+ const me = await Promise.race([
24850
+ client.resolveMe(),
24851
+ new Promise(
24852
+ (resolve) => setTimeout(() => resolve(null), IDENTITY_RESOLVE_TIMEOUT_MS)
24853
+ )
24854
+ ]);
24855
+ if (!me) return { distinctId: "mcp:unknown", region };
24856
+ const distinctId = me.email ?? (me.id ? `mcp:user-${me.id}` : "mcp:unknown");
24857
+ return {
24858
+ distinctId,
24859
+ groups: me.organization?.id ? { organization: me.organization.id } : void 0,
24860
+ region,
24861
+ // name/email so leadbay_send_feedback attributes correctly on HTTP — the
24862
+ // module-scoped `me` is never populated here (Codex P2).
24863
+ ...me.name ? { name: me.name } : {},
24864
+ ...me.email ? { email: me.email } : {}
24865
+ };
24866
+ } catch (err) {
24867
+ logger.warn?.(`telemetry identity resolve failed: ${err?.message ?? err}`);
24868
+ return { distinctId: "mcp:unknown", region };
24869
+ }
24870
+ }
24871
+ function bindTelemetryIdentity(base, identity) {
24872
+ return {
24873
+ ...base,
24874
+ captureToolCall: (p) => base.captureToolCall(p, identity),
24875
+ captureCompositeCall: (p) => base.captureCompositeCall(p, identity),
24876
+ captureQuotaHit: (p) => base.captureQuotaHit(p, identity),
24877
+ captureTopupLink: (p) => base.captureTopupLink(p, identity),
24878
+ captureStartup: (p) => base.captureStartup(p, identity),
24879
+ captureAgentMemoryCaptured: (p) => base.captureAgentMemoryCaptured(p, identity),
24880
+ captureAgentMemoryRecalled: (p) => base.captureAgentMemoryRecalled(p, identity),
24881
+ captureAgentMemoryPruned: (p) => base.captureAgentMemoryPruned(p, identity),
24882
+ captureFrictionReported: (p) => base.captureFrictionReported(p, identity),
24883
+ captureFeedback: (message, opts) => base.captureFeedback(message, opts, identity),
24884
+ identify: async () => {
24885
+ },
24886
+ shutdown: async () => {
24887
+ }
24888
+ };
24889
+ }
24454
24890
  var sseSessions = /* @__PURE__ */ new Map();
24455
24891
  var SSE_SESSION_TTL_MS = 30 * 60 * 1e3;
24456
24892
  setInterval(() => {
@@ -24470,10 +24906,16 @@ function extractBearer(authHeader) {
24470
24906
  const m = /^Bearer\s+(.+)$/i.exec(authHeader);
24471
24907
  return m ? m[1].trim() : void 0;
24472
24908
  }
24473
- function buildServerFromClient(client) {
24909
+ function buildServerFromClient(client, requestTelemetry) {
24474
24910
  const includeWrite = parseWriteEnv();
24475
24911
  const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
24476
- return buildServer(client, { version: VERSION, includeWrite, includeAdvanced });
24912
+ return buildServer(client, {
24913
+ version: VERSION,
24914
+ includeWrite,
24915
+ includeAdvanced,
24916
+ logger,
24917
+ telemetry: requestTelemetry
24918
+ });
24477
24919
  }
24478
24920
  var PRM_PREFIX = "/.well-known/oauth-protected-resource";
24479
24921
  var RESOURCE_PATHS = ["/mcp", "/fr/mcp", "/sse", "/fr/sse"];
@@ -24549,11 +24991,15 @@ async function handleStreamable(c, resourcePath) {
24549
24991
  const foreign = rejectForeignOrigin(c);
24550
24992
  if (foreign) return foreign;
24551
24993
  const token = extractBearer(c.req.header("authorization"));
24552
- const resolved = await resolveClientFromToken(token);
24994
+ const resolved = await resolveClientFromToken(token, { logger });
24553
24995
  if (resolved.authState === "missing" || resolved.authState === "expired") {
24554
24996
  return sendChallenge(c, resourcePath, resolved.authState);
24555
24997
  }
24556
- const server = buildServerFromClient(resolved.client);
24998
+ const identity = await resolveIdentity(resolved.client);
24999
+ const server = buildServerFromClient(
25000
+ resolved.client,
25001
+ bindTelemetryIdentity(telemetry, identity)
25002
+ );
24557
25003
  const transport = new StreamableHTTPServerTransport({
24558
25004
  sessionIdGenerator: void 0,
24559
25005
  // Return JSON responses instead of SSE so non-SSE clients (e.g. Codex) work.
@@ -24596,13 +25042,17 @@ async function handleSse(c, resourcePath) {
24596
25042
  const foreign = rejectForeignOrigin(c);
24597
25043
  if (foreign) return foreign;
24598
25044
  const token = extractBearer(c.req.header("authorization"));
24599
- const resolved = await resolveClientFromToken(token);
25045
+ const resolved = await resolveClientFromToken(token, { logger });
24600
25046
  if (resolved.authState === "missing" || resolved.authState === "expired") {
24601
25047
  return sendChallenge(c, resourcePath, resolved.authState);
24602
25048
  }
25049
+ const identity = await resolveIdentity(resolved.client);
24603
25050
  const env = c.env;
24604
25051
  const transport = new SSEServerTransport("/messages", env.outgoing);
24605
- const server = buildServerFromClient(resolved.client);
25052
+ const server = buildServerFromClient(
25053
+ resolved.client,
25054
+ bindTelemetryIdentity(telemetry, identity)
25055
+ );
24606
25056
  await server.connect(transport);
24607
25057
  const sessionId = transport.sessionId;
24608
25058
  sseSessions.set(sessionId, { transport, server, createdAt: Date.now() });
@@ -24649,8 +25099,23 @@ if (isEntrypoint) {
24649
25099
  `leadbay-mcp-http ${VERSION} listening on http://${info.address}:${info.port} (boot=${_boot})
24650
25100
  `
24651
25101
  );
25102
+ telemetry.captureStartup(
25103
+ { auth_state: "ok", region: "unknown" },
25104
+ { distinctId: "mcp:http-server", region: "unknown" }
25105
+ );
24652
25106
  });
25107
+ const gracefulShutdown = async () => {
25108
+ try {
25109
+ await telemetry.shutdown();
25110
+ } finally {
25111
+ process.exit(0);
25112
+ }
25113
+ };
25114
+ process.once("SIGTERM", () => void gracefulShutdown());
25115
+ process.once("SIGINT", () => void gracefulShutdown());
24653
25116
  }
24654
25117
  export {
24655
- app
25118
+ app,
25119
+ bindTelemetryIdentity,
25120
+ resolveIdentity
24656
25121
  };