@leadbay/mcp 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  granularReadTools,
9
9
  granularWriteTools,
10
10
  resolveRegion
11
- } from "./chunk-2EGNZRD7.js";
11
+ } from "./chunk-F3EWCHME.js";
12
12
 
13
13
  // src/bin.ts
14
14
  import { realpathSync } from "fs";
@@ -33,11 +33,11 @@ import {
33
33
 
34
34
  // src/prompts.generated.ts
35
35
  var leadbay_daily_check_in = `
36
- Run the Leadbay daily check-in for me. Treat this prompt the same way for any equivalent ask: "get me leadbay leads", "best leads to prospect today", "what should I work on", "show me my batch".
36
+ Run the Leadbay daily check-in for me. Treat this prompt the same way for any equivalent ask focused on NEW leads from the Discover wishlist: "get me leadbay leads", "best NEW leads to prospect today", "what's new today", "show me my batch", "let's prospect". For follow-up phrasings ("what should I follow up on", "leads I've already worked", "before my trip"), this is the wrong prompt \u2014 route to \`leadbay_followup_check_in\` instead. If the user's intent is ambiguous ("what should I work on?"), ASK once before picking an entry point.
37
37
 
38
38
  # Resilience rules for Leadbay long-running tools
39
39
 
40
- These four rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
40
+ These four rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
41
41
 
42
42
  ## Rule 1 \u2014 Pin the lens
43
43
 
@@ -47,9 +47,9 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
47
47
 
48
48
  \`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\`, which returns \`{status:'running', qualify_id}\` immediately. Then poll \`leadbay_qualify_status\` (or \`leadbay_import_status\`) every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
49
49
 
50
- ## Rule 3 \u2014 Serialize \`leadbay_research_lead\` fan-out
50
+ ## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
51
51
 
52
- \`leadbay_research_lead\` is composite and reads many sub-resources. Calling it on 10 leads in parallel can saturate the transport and produce \`"Tool permission stream closed"\` errors that look like permission failures but are really backpressure. **Call it sequentially**, or at most 3 in parallel. If one call fails with a stream/timeout error, retry that one call once before moving on; on a second failure, note the lead and continue \u2014 do not abandon the remaining leads.
52
+ \`leadbay_research_lead_by_id\` is composite and reads many sub-resources. Calling it on 10 leads in parallel can saturate the transport and produce \`"Tool permission stream closed"\` errors that look like permission failures but are really backpressure. **Call it sequentially**, or at most 3 in parallel. If one call fails with a stream/timeout error, retry that one call once before moving on; on a second failure, note the lead and continue \u2014 do not abandon the remaining leads.
53
53
 
54
54
  ## Rule 4 \u2014 Retry, don't replan
55
55
 
@@ -72,15 +72,99 @@ Call \`leadbay_account_status\` to see what quota I have left and which lens is
72
72
  # PHASE 2 \u2014 FRESH BATCH
73
73
  Call \`leadbay_pull_leads\` to get today's fresh batch. Capture \`response.lens.id\` (the response nests it under \`lens\`). **Use it as an explicit \`lensId\` argument on every subsequent Leadbay call this session** \u2014 including any re-pulls, bulk qualifies, or research calls that accept it. (See Rule 1 above \u2014 a mid-session lens shift discards your top-10 work.)
74
74
 
75
- # PHASE 3 \u2014 TRIAGE (top 10, motivational framing)
75
+ # PHASE 3 \u2014 TRIAGE (top 10, table + nudges)
76
76
 
77
- Pick the top **10** leads \u2014 prefer leads with a fresh \`ai_agent_lead_score\` (those have been newly AI-qualified); fall back to \`score\` only when \`ai_agent_lead_score\` is absent. For each, write ONE motivational sentence \u2014 framed as *why prospecting this lead today might be a good idea right now* (almost a coach's nudge, not a flat description). Lean on \`qualification_summary\` for the substance, but reframe \u2014 don't paste it verbatim.
77
+ Render the **top 10 leads** using the canonical \`leadbay_pull_leads\` layout:
78
+
79
+ GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
80
+
81
+ 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.
82
+
83
+
84
+ ## RENDERING \u2014 markdown table, three columns, score-bar driven
85
+
86
+ Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
87
+
88
+ ## Score-bar (10-segment, inline-code wrapped)
89
+
90
+ Wrap a 10-glyph bar in a SINGLE inline-code span (backticks). The inline-code styling is what gives the bar contrast in most chat renderers \u2014 HTML \`<span>\` is stripped inside table cells.
91
+
92
+ Glyphs (use these exact characters; do not substitute):
93
+
94
+ - \`\u25B0\` \u2014 firmographic-only fill
95
+ - \`\u2756\` \u2014 AI-booster cap (placed at the RIGHT END of the filled run, never the front)
96
+ - \`\u25B1\` \u2014 empty
97
+
98
+ Computation:
99
+
100
+ \`\`\`
101
+ total_filled = round(score / 10), clamped to 0..10
102
+ ai_segments = round(qualification_summary.avg_qualification_boost / 3.3),
103
+ clamped to [0, total_filled]
104
+ normal_filled = total_filled \u2212 ai_segments
105
+ bar = "\u25B0" \xD7 normal_filled
106
+ + "\u2756" \xD7 ai_segments
107
+ + "\u25B1" \xD7 (10 \u2212 total_filled)
108
+ \`\`\`
109
+
110
+ If \`qualification_summary.answered == 0\` or \`avg_qualification_boost\` is null, set \`ai_segments = 0\` (no \u2756). Always wrap the bar in backticks. Print the legend \`\` \`\u25B0\` firmographic \xB7 \`\u2756\` AI booster cap \xB7 \`\u25B1\` unfilled \`\` once below the table.
111
+
112
+
113
+ **Column 1 \u2014 Company**
114
+
115
+ - Line 1: the 10-segment score bar in inline-code backticks (see the score-bar snippet above for the algorithm).
116
+ - Insert \`<br>\` between lines.
117
+ - Line 2: linked company name + \` \xB7 \` + short location + \` \xB7 \` + compact size.
118
+ - Link target: \`website\` (prefix \`https://\` if it's a bare hostname). Don't synthesize an app deep-link.
119
+ - Location: shorten "City of New York" \u2192 "NYC"; otherwise "City ST"; state alone only when city missing.
120
+ - Size: \`"Xk+"\` when \`size.min >= 1000\`, \`"min\u2013max"\` otherwise.
121
+
122
+ **Column 2 \u2014 Why it fits**
123
+
124
+ - One sentence, \u2264 20 words.
125
+ - Synthesize from (in priority order, whichever is present) the lead's \`short_description\`, top 2 \`tags[].display_name\`, and the gist of \`qualification_summary.best_response_excerpt\`. The trim payload does NOT carry the longer \`description\` field \u2014 for that, agent must call \`leadbay_research_lead_by_id\` or \`leadbay_research_lead_by_name_fuzzy\`.
126
+ - Do NOT append \`(boost N)\` \u2014 the \u2756 cap in column 1 already carries that signal.
127
+ - No bullet lists, no line breaks inside the cell.
128
+
129
+ **Column 3 \u2014 Contact**
130
+
131
+ \`[Contact name](LINK) \xB7 short job title\`. See linking/contact-linkedin for LINK priority and the \xB0-flag fallback.
132
+
133
+ **Hide from the user (never include in any cell):** \`id\`, \`location.pos\`, \`location.country\` (unless city/state both missing), \`sector_id\`, \`is_hq\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`highlighted_fields\`, \`custom_fields\`, \`contacts_count\` when 0, \`notes_count\` / \`epilogue_actions_count\` / \`prospecting_actions_count\` when 0, \`stale_at\`, \`deal_insights\`, \`social_presence\` booleans (except as the \xB0-flag signal), \`need_attention\` flags, any field whose value is the string \`"null"\`.
134
+
135
+ ## Linking a contact's name
136
+
137
+ Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
138
+
139
+ When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
140
+
141
+ Otherwise fall back to a LinkedIn people-search URL:
142
+
143
+ \`\`\`
144
+ https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
145
+ \`\`\`
146
+
147
+ URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
148
+
149
+ Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
150
+
151
+ ## Linking the company
152
+
153
+ Use the lead's \`website\` as the company-name link target \u2014 prefix \`https://\` if the value is a bare hostname. (The MCP does NOT synthesize a Leadbay-app deep-link URL; the team has not standardized one. Linking to \`website\` is always real data.)
154
+
155
+ When the response carries \`social_urls\` (the post-fix multi-platform URL block on rich-lead responses), render every non-null platform as a pill chip in the company-info row. Iterate over \`social_urls\`'s keys \u2014 never hardcode a fixed list \u2014 and emit each as \`[<platform-label>](<url>)\`. Skip platforms whose URL is null.
156
+
157
+ \`social_presence\` carries booleans for the same 6 platforms (crunchbase, facebook, instagram, linkedin, tiktok, twitter) \u2014 useful when you only care that the company has a profile somewhere. Use it as the \xB0-flag signal in the contact people-search fallback (see linking/contact-linkedin).
158
+
159
+
160
+
161
+ ABOVE the table, add a 2\u20134 sentence "Today's nudges" paragraph for the 3 most-promising rows. The nudges speak to urgency / opportunity / freshness \u2014 what makes acting on these RIGHT NOW the right call. Do NOT repeat the "why it fits" column from the table; the nudges should add fresh framing the table doesn't carry (e.g., recent news from the \`qualification_summary\` excerpt, a window closing, a competitor activity the user mentioned earlier in the session). One sentence per nudge, salesperson voice, not coachspeak.
78
162
 
79
163
  If the batch returns fewer than 10 qualified leads, top it up: call \`leadbay_bulk_qualify_leads\` with \`lensId:<captured>\`, \`count:<1.5x deficit, capped at 25>\`, and **\`wait_for_completion:false\`**. Capture \`qualify_id\` from the response and poll \`leadbay_qualify_status\` every ~10s until \`status:'done'\`. Then re-pull with the same \`lensId\` to pick up the newly qualified leads. **Never re-pull without \`lensId\` \u2014 you will lose your batch to a lens shift.** (The \`leadbay_qualify_top_n\` slash-prompt wraps this same tool with a friendlier surface for users; agents should call the underlying tool directly here.)
80
164
 
81
165
  # PHASE 4 \u2014 DEEP DIVE (every promising lead)
82
166
 
83
- Call \`leadbay_research_lead\` on **every** lead from your top 10 that the user might realistically prospect today (filter out clearly weak fits if any). Don't pick just one. **Call it sequentially** \u2014 one at a time, or batches of at most 3 in parallel. Do not fire 10 in parallel \u2014 it triggers transport backpressure that surfaces as \`"Tool permission stream closed"\` errors (see Rule 3 above). If a call fails, retry that single lead once; if the retry also fails, note the lead id and continue. Report Phase 4 results even if 1\u20132 leads were unresearchable.
167
+ Call \`leadbay_research_lead_by_id\` on **every** lead from your top 10 that the user might realistically prospect today (filter out clearly weak fits if any). Don't pick just one. **Call it sequentially** \u2014 one at a time, or batches of at most 3 in parallel. Do not fire 10 in parallel \u2014 it triggers transport backpressure that surfaces as \`"Tool permission stream closed"\` errors (see Rule 3 above). If a call fails, retry that single lead once; if the retry also fails, note the lead id and continue. Report Phase 4 results even if 1\u20132 leads were unresearchable.
84
168
 
85
169
  For each researched lead surface:
86
170
  - what makes it promising (1\u20132 sentences citing signals from the research)
@@ -114,6 +198,11 @@ A job well done has TWO deliverables:
114
198
  IRON LAW \u2014 NO FABRICATION. Every lead id, contact email, custom field id, mapping decision, and tool argument must trace to a value you read from the file the user attached or to an output from a leadbay_* tool call in this session. Do not invent values. Do not "fill in" a missing leadId with a name match. Do not synthesize a CRM id from a guess. If a value is missing, leave the field blank and say so.
115
199
 
116
200
 
201
+ GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
202
+
203
+ 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.
204
+
205
+
117
206
  # PHASE 1 \u2014 SCAN
118
207
 
119
208
  Read the file yourself. Inspect every header, sample values from multiple rows, row count, duplicate/blank columns, and obvious dirty data. Build a column preservation plan before importing: for each meaningful column decide standard field, CONTACT_* field, Leadbay note, custom field, derived helper, or skip with a reason. Default to preserving client-provided business data; skip only blank placeholders, duplicate plumbing, raw unparsed blobs after extracting their useful values, or values that would actively harm data quality.
@@ -283,7 +372,7 @@ You are working with Leadbay through the \`leadbay_*\` MCP tools. This prompt or
283
372
 
284
373
  # Resilience rules for Leadbay long-running tools
285
374
 
286
- These four rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
375
+ These four rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
287
376
 
288
377
  ## Rule 1 \u2014 Pin the lens
289
378
 
@@ -293,9 +382,9 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into
293
382
 
294
383
  \`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\`, which returns \`{status:'running', qualify_id}\` immediately. Then poll \`leadbay_qualify_status\` (or \`leadbay_import_status\`) every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
295
384
 
296
- ## Rule 3 \u2014 Serialize \`leadbay_research_lead\` fan-out
385
+ ## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
297
386
 
298
- \`leadbay_research_lead\` is composite and reads many sub-resources. Calling it on 10 leads in parallel can saturate the transport and produce \`"Tool permission stream closed"\` errors that look like permission failures but are really backpressure. **Call it sequentially**, or at most 3 in parallel. If one call fails with a stream/timeout error, retry that one call once before moving on; on a second failure, note the lead and continue \u2014 do not abandon the remaining leads.
387
+ \`leadbay_research_lead_by_id\` is composite and reads many sub-resources. Calling it on 10 leads in parallel can saturate the transport and produce \`"Tool permission stream closed"\` errors that look like permission failures but are really backpressure. **Call it sequentially**, or at most 3 in parallel. If one call fails with a stream/timeout error, retry that one call once before moving on; on a second failure, note the lead and continue \u2014 do not abandon the remaining leads.
299
388
 
300
389
  ## Rule 4 \u2014 Retry, don't replan
301
390
 
@@ -315,13 +404,16 @@ Leadbay supports two parallel ways to find leads to act on. Detect which entry t
315
404
  \`\`\`
316
405
  Discovery entry Follow-up entry
317
406
  \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
318
- leadbay_pull_leads (re-engagement on the Monitor view \u2014
319
- (new from the Discover currently surfaced via the Leadbay app
320
- wishlist, lens-driven) UI; MCP-side composite forthcoming)
407
+ leadbay_pull_leads leadbay_pull_followups
408
+ (new from the Discover (re-engagement on the
409
+ wishlist, lens-driven) Monitor view of known leads)
410
+ \u2502 \u2502
411
+ wrapped by: wrapped by:
412
+ leadbay_daily_check_in leadbay_followup_check_in
321
413
  \u2502 \u2502
322
414
  \u2502 optional: \u2502 filters by user phrasing:
323
- \u2502 research_company / \u2502 geo, sector, recency,
324
- \u2502 research_lead \u2502 liked / pushback / outcome
415
+ \u2502 research_lead_by_name_fuzzy / \u2502 geo, sector, recency,
416
+ \u2502 research_lead_by_id \u2502 liked / pushback / outcome
325
417
  \u2502 (deepen profile) \u2502
326
418
  \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518
327
419
  \u25BC
@@ -343,6 +435,14 @@ leadbay_pull_leads (re-engagement on the Monitor view \u2014
343
435
 
344
436
  When in doubt, ask. The two paths return overlapping but differently-ranked data; presenting the wrong one wastes the user's time.
345
437
 
438
+ **Routing the user's first message to an entry point (orchestrator prompts):**
439
+
440
+ - "give me leads", "what's new", "let's prospect", "today's batch", "best NEW leads" \u2192 \`leadbay_daily_check_in\` (Discovery orchestrator \u2014 wraps \`leadbay_pull_leads\`)
441
+ - "follow up", "already known", "leads I should contact", "before my trip", "this week", "what's overdue", "re-engage" \u2192 \`leadbay_followup_check_in\` (Follow-up orchestrator \u2014 wraps \`leadbay_pull_followups\`)
442
+ - Ambiguous ("what should I work on?") \u2192 ASK once: "Looking for NEW leads from your wishlist, or follow-ups on leads you've already worked?"
443
+
444
+ Never call \`leadbay_pull_leads\` directly for a follow-up query. Never call \`leadbay_pull_followups\` for a discovery query. The two tools read from different backend tables; iterating pages of one to fake the other is a known failure mode (see the anti-confusion guardrail in \`pull_followups\`'s description).
445
+
346
446
  ## The outreach loop
347
447
 
348
448
  After \`leadbay_prepare_outreach\` returns a brief, the agent drafts. Adapt to the user's connected outreach tools \u2014 these change the draft idiom:
@@ -397,25 +497,128 @@ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` (the
397
497
  ## What to read once you've matched intent
398
498
 
399
499
  You don't need to memorize every tool here \u2014 each tool's own description carries a RENDERING block (how to present the response) and a NEXT STEPS block (observation \u2192 suggestion table). Read the relevant tool's description in full when the user picks an entry point. This overview just gets you to the right starting tool.
500
+
501
+ GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
502
+
503
+ 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.
400
504
  `;
401
505
  var leadbay_qualify_top_n = `
402
506
  Qualify the top {{arg:count_or_default}} un-qualified leads in the active Leadbay lens.
403
507
 
508
+ GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
509
+
510
+ 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.
511
+
512
+
404
513
  # PHASE 1 \u2014 LAUNCH
405
514
  Call \`leadbay_bulk_qualify_leads\` with \`count={{arg:count_or_default}}\`.
406
515
 
407
516
  # PHASE 2 \u2014 POLL
408
- While it polls, expect notifications / progress events showing per-lead transitions. Surface meaningful ones (e.g. "lead X just finished") to me as they arrive.
517
+ While it polls, expect notifications / progress events showing per-lead transitions. Surface meaningful ones (e.g. "lead X just finished") to me as they arrive \u2014 one inline status sentence per check, never expanded into a card:
518
+
519
+ ## Status / scalar \u2014 single-sentence shape
520
+
521
+ The response is a status confirmation or scalar \u2014 render exactly one sentence inline. Do NOT emit a card or a table. Do NOT enumerate the affected records (that's the next tool's job).
522
+
523
+ Template patterns to follow:
524
+
525
+ - Job kicked off \u2192 \`"\u2713 <Verb> N <noun(s)> \u2014 typically ~M minutes. I'll refresh when it's done."\`
526
+ - No work needed \u2192 \`"All N <noun(s)> already <state> \u2014 no work to do."\`
527
+ - Long-running \u2192 \`"\u23F3 <Verb> still running \u2014 N% complete; check back in ~M minutes."\`
528
+ - Failure \u2192 \`"\u26A0 <Verb> failed: <error>. <recovery hint>"\`
529
+
530
+ After the status line, propose the obvious refresh / progress-check / recovery action in the NEXT STEPS block. Never expand the status into a card.
531
+
409
532
 
410
533
  # PHASE 3 \u2014 SUMMARIZE
411
534
 
412
- When \`bulk_qualify_leads\` returns, summarize:
413
- - How many qualified (name the count)
414
- - How many are still running (name them, by lead_id + lead name if available, so I can poll again later)
415
- - The 3 highest-\`ai_agent_lead_score\` leads from THIS batch, with their \`qualification_summary\` (one sentence each)
535
+ When \`bulk_qualify_leads\` returns, surface results in two parts.
536
+
537
+ **Status line first** \u2014 one sentence using the status-inline shape above: how many qualified, how many are still running (name them by lead_id + lead name if available so the user can poll later).
538
+
539
+ **Then a refreshed table** \u2014 re-pull the newly-qualified leads via \`leadbay_pull_leads\` with the same \`lensId\` and render them using the canonical pull_leads layout:
540
+
541
+ ## RENDERING \u2014 markdown table, three columns, score-bar driven
542
+
543
+ Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
544
+
545
+ ## Score-bar (10-segment, inline-code wrapped)
546
+
547
+ Wrap a 10-glyph bar in a SINGLE inline-code span (backticks). The inline-code styling is what gives the bar contrast in most chat renderers \u2014 HTML \`<span>\` is stripped inside table cells.
548
+
549
+ Glyphs (use these exact characters; do not substitute):
550
+
551
+ - \`\u25B0\` \u2014 firmographic-only fill
552
+ - \`\u2756\` \u2014 AI-booster cap (placed at the RIGHT END of the filled run, never the front)
553
+ - \`\u25B1\` \u2014 empty
554
+
555
+ Computation:
556
+
557
+ \`\`\`
558
+ total_filled = round(score / 10), clamped to 0..10
559
+ ai_segments = round(qualification_summary.avg_qualification_boost / 3.3),
560
+ clamped to [0, total_filled]
561
+ normal_filled = total_filled \u2212 ai_segments
562
+ bar = "\u25B0" \xD7 normal_filled
563
+ + "\u2756" \xD7 ai_segments
564
+ + "\u25B1" \xD7 (10 \u2212 total_filled)
565
+ \`\`\`
566
+
567
+ If \`qualification_summary.answered == 0\` or \`avg_qualification_boost\` is null, set \`ai_segments = 0\` (no \u2756). Always wrap the bar in backticks. Print the legend \`\` \`\u25B0\` firmographic \xB7 \`\u2756\` AI booster cap \xB7 \`\u25B1\` unfilled \`\` once below the table.
568
+
569
+
570
+ **Column 1 \u2014 Company**
571
+
572
+ - Line 1: the 10-segment score bar in inline-code backticks (see the score-bar snippet above for the algorithm).
573
+ - Insert \`<br>\` between lines.
574
+ - Line 2: linked company name + \` \xB7 \` + short location + \` \xB7 \` + compact size.
575
+ - Link target: \`website\` (prefix \`https://\` if it's a bare hostname). Don't synthesize an app deep-link.
576
+ - Location: shorten "City of New York" \u2192 "NYC"; otherwise "City ST"; state alone only when city missing.
577
+ - Size: \`"Xk+"\` when \`size.min >= 1000\`, \`"min\u2013max"\` otherwise.
578
+
579
+ **Column 2 \u2014 Why it fits**
580
+
581
+ - One sentence, \u2264 20 words.
582
+ - Synthesize from (in priority order, whichever is present) the lead's \`short_description\`, top 2 \`tags[].display_name\`, and the gist of \`qualification_summary.best_response_excerpt\`. The trim payload does NOT carry the longer \`description\` field \u2014 for that, agent must call \`leadbay_research_lead_by_id\` or \`leadbay_research_lead_by_name_fuzzy\`.
583
+ - Do NOT append \`(boost N)\` \u2014 the \u2756 cap in column 1 already carries that signal.
584
+ - No bullet lists, no line breaks inside the cell.
585
+
586
+ **Column 3 \u2014 Contact**
587
+
588
+ \`[Contact name](LINK) \xB7 short job title\`. See linking/contact-linkedin for LINK priority and the \xB0-flag fallback.
589
+
590
+ **Hide from the user (never include in any cell):** \`id\`, \`location.pos\`, \`location.country\` (unless city/state both missing), \`sector_id\`, \`is_hq\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`highlighted_fields\`, \`custom_fields\`, \`contacts_count\` when 0, \`notes_count\` / \`epilogue_actions_count\` / \`prospecting_actions_count\` when 0, \`stale_at\`, \`deal_insights\`, \`social_presence\` booleans (except as the \xB0-flag signal), \`need_attention\` flags, any field whose value is the string \`"null"\`.
591
+
592
+ ## Linking a contact's name
593
+
594
+ Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
595
+
596
+ When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
597
+
598
+ Otherwise fall back to a LinkedIn people-search URL:
599
+
600
+ \`\`\`
601
+ https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
602
+ \`\`\`
603
+
604
+ URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
605
+
606
+ Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
607
+
608
+ ## Linking the company
609
+
610
+ Use the lead's \`website\` as the company-name link target \u2014 prefix \`https://\` if the value is a bare hostname. (The MCP does NOT synthesize a Leadbay-app deep-link URL; the team has not standardized one. Linking to \`website\` is always real data.)
611
+
612
+ When the response carries \`social_urls\` (the post-fix multi-platform URL block on rich-lead responses), render every non-null platform as a pill chip in the company-info row. Iterate over \`social_urls\`'s keys \u2014 never hardcode a fixed list \u2014 and emit each as \`[<platform-label>](<url>)\`. Skip platforms whose URL is null.
613
+
614
+ \`social_presence\` carries booleans for the same 6 platforms (crunchbase, facebook, instagram, linkedin, tiktok, twitter) \u2014 useful when you only care that the company has a profile somewhere. Use it as the \xB0-flag signal in the contact people-search fallback (see linking/contact-linkedin).
615
+
616
+
617
+
618
+ ABOVE the table, add a 1\u20132 sentence "Standouts from this batch" line that calls out the 3 highest-\`ai_agent_lead_score\` rows \u2014 this is supplementary commentary, not a replacement for the table.
416
619
 
417
620
  # PHASE 4 \u2014 RECOMMEND
418
- Recommend the single most-promising lead from this batch and offer to research it deeply with \`leadbay_research_lead\`. Do not actually call \`research_lead\` yet \u2014 wait for my go.
621
+ Recommend the single most-promising lead from this batch and offer to research it deeply with \`leadbay_research_lead_by_id\`. Do not actually call \`research_lead_by_id\` yet \u2014 wait for my go.
419
622
  `;
420
623
  var leadbay_refine_audience = `
421
624
  Refine the Leadbay audience prompt to: {{arg:instruction}}
@@ -434,46 +637,153 @@ var leadbay_research_a_domain = `
434
637
  IRON LAW \u2014 NO FABRICATION. Every lead id, contact email, custom field id, mapping decision, and tool argument must trace to a value you read from the file the user attached or to an output from a leadbay_* tool call in this session. Do not invent values. Do not "fill in" a missing leadId with a name match. Do not synthesize a CRM id from a guess. If a value is missing, leave the field blank and say so.
435
638
 
436
639
 
640
+ GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
641
+
642
+ 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.
643
+
644
+
437
645
  Research the company with domain '{{arg:domain}}' for me using Leadbay.
438
646
 
439
647
  # PHASE 1 \u2014 IMPORT + QUALIFY
440
- 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.
648
+ 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):
649
+
650
+ ## RENDERING \u2014 import result summary (single-record, terse)
651
+
652
+ The response carries either a completed result or an async handle. Render a brief summary; do NOT enumerate every imported lead.
653
+
654
+ **Header \u2014 single line, choose by status:**
655
+
656
+ - Completed: \`"\u2713 Import complete \u2014 N leads imported \xB7 M failed \xB7 P resolved-with-ambiguity"\`
657
+ - Running: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
658
+ - Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 qualify_id <id>"\`
659
+
660
+ **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*"\`.
661
+
662
+ **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.
663
+
664
+ Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_research_lead_by_id\` in NEXT STEPS.
665
+
441
666
 
442
667
  # PHASE 2 \u2014 DEEP DIVE
443
- When the import resolves, call \`leadbay_research_lead\` on the new leadId.
668
+ 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:
669
+
670
+ ## RENDERING \u2014 single-record research card, mode-adaptive
671
+
672
+ Present as a single-record card, not a table. This tool gets invoked in two distinct user contexts \u2014 detect which and adapt the body density accordingly.
673
+
674
+ **MODE A \u2014 Discovery.** The user is evaluating whether to pursue this company as a target. Signals: "tell me about", "what do they do", "is this a fit", "research [company]", arrival via a click-through from \`leadbay_pull_leads\`, no prior outreach context in the conversation. Next step is usually qualify, deep-dive via \`leadbay_research_lead_by_id\`, or decide whether to start outreach.
675
+
676
+ **MODE B \u2014 Contact preparation.** The user is about to call or email someone at this company and needs the talking points. Signals: "I'm calling them", "draft an email", "before my call", "outreach prep", "what should I say", or the conversation has already touched on a specific contact. Next step is usually \`leadbay_prepare_outreach\`.
677
+
678
+ Default to MODE A when uncertain. Always offer the cross-mode pivot at the end so the user can redirect if you guessed wrong.
679
+
680
+ ### Common structure (both modes)
681
+
682
+ - **Header** (H4 or H5): \`<10-segment score bar>\` \`[Company name](website)\`. Use the score-bar algorithm; the bar lives in a single inline-code span. Prefix \`https://\` to website if it's a bare hostname.
683
+ - **Pill row** (immediately below the header): short location \xB7 compact size \xB7 social pill chips iterated over \`social_urls\` (each non-null platform becomes \`[<platform-label>](<url>)\`) \xB7 \`[website-domain](website)\` \xB7 \`\u260E phone\` when \`phone_numbers[]\` is non-empty (use the first number). All \` \xB7 \`-separated.
684
+ - **Blurb**: render \`description\` (preferred) or \`short_description\` as a single blockquoted paragraph.
685
+ - **Staleness line**: italic, \`"Researched <relative time>"\` from \`web_insights_fetched_at\`. Use \`"today"\` / \`"yesterday"\` / \`"N days ago"\` up to 30 days, then absolute date. Prefix with \`\u26A0\` if older than 30 days.
686
+ - **Contacts table** (always at the bottom):
687
+ \`\`\`
688
+ | | Name | Title | LinkedIn |
689
+ \`\`\`
690
+ Markers in column 1:
691
+ - \`\u2605\` \u2014 \`recommended_contact\` match.
692
+ - \`\u{1F48E}\` \u2014 name fuzzy-matches a \`hot: true\` entry in \`web_insights\` key_people. (Use \`\u{1F48E}\`, not \`\u{1F525}\`, to avoid glyph collision with the follow-up status badge.)
693
+ Sort \`\u2605\` first, then \`\u{1F48E}\`-only rows, then API order. Link the name via \`linkedin_page\` first; fall back to LinkedIn people-search with \`<First>+<Last>+<Company>\`. Append \`\xB0\` only when the fallback is in use AND \`social_presence.linkedin == false\`. Cap to 6 rows; if \`contacts_count > shown\`, end with \`"+N more \u2014 ask to see the full list"\`.
694
+
695
+ ### MODE A body (Discovery, fuller, scannable)
696
+
697
+ Render each non-empty \`web_insights\` section as H5 with the emoji + label intact. Section order: \`\u{1F3E2} company profile\` \u2192 \`\u{1F4C8} business signals\` \u2192 \`\u{1F4A1} prospecting clues\` \u2192 \`\u{1F9E9} strategic positioning\` \u2192 \`\u{1F50E} technologies & innovation\`. Inside each, bullet 3\u20135 items. Sort \`hot: true\` items first. **Bold** the description text of hot items; leave cold items plain. Render \`source\` as \`[source](url)\` at the end; include \`date\` when present. Omit empty sections. Skip \`\u{1F517} social links\` (already in the pill row) and \`\u{1F464} key people\` (already in the contacts table).
698
+
699
+ ### MODE B body (Contact preparation, tighter)
700
+
701
+ Render exactly two H5 sections:
702
+
703
+ ##### \u{1F3AF} Conversation hooks
704
+
705
+ Distill the 3 most recent / most hot signals from \`\u{1F4C8} business signals\` and \`\u{1F4A1} prospecting clues\` into one-sentence talking points in salesperson voice. Strip the academic framing. Cite the source inline.
706
+
707
+ ##### \u{1F464} About the person *(only when recommended_contact is non-empty)*
708
+
709
+ 2-line summary: their title + any context from \`web_insights\` key_people. If they appear in a hot signal ("X appointed CEO"), surface that prominently.
710
+
711
+ Skip \u{1F3E2} profile, \u{1F9E9} strategic positioning, \u{1F50E} technologies in MODE B \u2014 context the user doesn't need for the next 30 seconds.
712
+
713
+ If \`qualification[]\` is non-empty, append one collapsed line: \`"Qualification: N questions answered, avg boost X"\` and offer to expand in NEXT STEPS.
714
+
715
+ **Hide:** \`id\`, \`lead.id\`, \`contact.id\`, \`lead.location.pos\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`recommended_contact_title\` (duplicates \`recommended_contact.job_title\`), empty arrays, fields whose value is the string \`"null"\`, \`contact.source\` (internal), insights whose \`source\` is empty.
716
+
717
+ **Legend (print once below the card):** \`\` \`\u25B0\` firmographic \xB7 \`\u2756\` AI booster \xB7 \`\u25B1\` unfilled \xB7 \u2605 recommended \xB7 \u{1F48E} hot in web_insights \xB7 \xB0 = no company LinkedIn (fallback link only) \`\`
718
+
719
+ ## Linking a contact's name
720
+
721
+ Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
722
+
723
+ When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
724
+
725
+ Otherwise fall back to a LinkedIn people-search URL:
726
+
727
+ \`\`\`
728
+ https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
729
+ \`\`\`
730
+
731
+ URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
732
+
733
+ Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
734
+
735
+ ## Linking the company
736
+
737
+ Use the lead's \`website\` as the company-name link target \u2014 prefix \`https://\` if the value is a bare hostname. (The MCP does NOT synthesize a Leadbay-app deep-link URL; the team has not standardized one. Linking to \`website\` is always real data.)
738
+
739
+ When the response carries \`social_urls\` (the post-fix multi-platform URL block on rich-lead responses), render every non-null platform as a pill chip in the company-info row. Iterate over \`social_urls\`'s keys \u2014 never hardcode a fixed list \u2014 and emit each as \`[<platform-label>](<url>)\`. Skip platforms whose URL is null.
740
+
741
+ \`social_presence\` carries booleans for the same 6 platforms (crunchbase, facebook, instagram, linkedin, tiktok, twitter) \u2014 useful when you only care that the company has a profile somewhere. Use it as the \xB0-flag signal in the contact people-search fallback (see linking/contact-linkedin).
742
+
743
+
444
744
 
445
745
  # PHASE 3 \u2014 SUMMARY
446
- Summarize:
746
+ Place a 2\u20133 sentence summary ABOVE the card with:
447
747
  - Who is this company (1 sentence)
448
748
  - Their fit (cite specific \`qualification_answers\` from the qualification response)
449
- - What signals stand out (cite specific research findings)
450
- - Which contact would I email first (name, role, source)
749
+ - Which contact would I email first (one short clause \u2014 the card's contacts table carries the rest)
451
750
 
452
- Be honest about uncertainty: if any field above is missing from tool responses, say "not surfaced by qualification" rather than guessing.
751
+ 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.
453
752
  `;
454
753
  var PROMPT_META = {
455
- leadbay_daily_check_in: { "name": "leadbay_daily_check_in", "short_description": `Run the canonical daily check-in: see account state, pull a fresh batch,
456
- triage the top 10, deep-dive on every promising one, and offer contact
457
- enrichment. The user's typical morning workflow. Trigger when the user
458
- asks for "leadbay leads", "best leads to prospect today", "what should
459
- I work on", or anything resembling "show me the day's batch".
460
- `, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_research_lead", "leadbay_bulk_qualify_leads", "leadbay_enrich_contacts"], "failure_modes": ["Calls leadbay_report_outreach without explicit user authorization", "Surfaces fewer than 10 leads when more are available, or fails to top up via leadbay_qualify_top_n when the batch is short", 'Writes flat one-line summaries instead of motivational "why prospect this today" framing', "Skips deep research on promising leads (Phase 4) \u2014 the agent must call leadbay_research_lead on each, not just one", "Triggers contact enrichment without asking the user first (it consumes quota)", "Skips the STOP byproduct and proposes next actions on its own", 'Fires 10 parallel leadbay_research_lead calls and treats "stream closed" errors as terminal \u2014 must serialize and retry singletons', "Re-pulls leadbay_pull_leads without passing the captured lensId, allowing a backend lens shift to discard the Phase 2 batch", 'Treats a "Request timed out" from leadbay_bulk_qualify_leads as terminal instead of retrying with wait_for_completion:false + qualify_status polling'] },
754
+ leadbay_daily_check_in: { "name": "leadbay_daily_check_in", "short_description": `Run the canonical daily check-in: account state, fresh batch, triage
755
+ top 10, deep-dive every promising one, offer contact enrichment. The
756
+ morning DISCOVERY workflow (new leads from the lens wishlist). Trigger
757
+ on "leadbay leads", "best NEW leads", "what's new today", "show me the
758
+ day's batch", "let's prospect". Do NOT trigger on follow-up phrasings
759
+ ("what should I follow up on", "before my trip") \u2014 those go to
760
+ \`leadbay_followup_check_in\`.
761
+ `, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_research_lead_by_id", "leadbay_bulk_qualify_leads", "leadbay_enrich_contacts"], "failure_modes": ["Calls leadbay_report_outreach without explicit user authorization", "Surfaces fewer than 10 leads when more are available, or fails to top up via leadbay_qualify_top_n when the batch is short", `Replaces the canonical pull_leads table layout with prose per row (the per-tool RENDERING block is the structural contract; "Today's nudges" goes above it, not in place of it)`, "Skips the nudge paragraph entirely \u2014 the table alone is fine but adding the nudge is the value-add", "Skips deep research on promising leads (Phase 4) \u2014 the agent must call leadbay_research_lead_by_id on each, not just one", "Triggers contact enrichment without asking the user first (it consumes quota)", "Skips the STOP byproduct and proposes next actions on its own", 'Fires 10 parallel leadbay_research_lead_by_id calls and treats "stream closed" errors as terminal \u2014 must serialize and retry singletons', "Re-pulls leadbay_pull_leads without passing the captured lensId, allowing a backend lens shift to discard the Phase 2 batch", 'Treats a "Request timed out" from leadbay_bulk_qualify_leads as terminal instead of retrying with wait_for_completion:false + qualify_status polling', 'Triggers on a follow-up query (e.g., "leads I should follow up with") that should have routed to `leadbay_followup_check_in` \u2014 the two entry points are different data sources (Discover wishlist vs Monitor view) per \xA71.6'] },
762
+ leadbay_followup_check_in: { "name": "leadbay_followup_check_in", "short_description": `Run the canonical follow-up check-in: surface KNOWN leads from the
763
+ Monitor view that need re-engagement today, ranked by AI urgency,
764
+ with the canonical pull_followups table layout. Trigger when the
765
+ user asks "follow up", "already known leads", "leads I haven't
766
+ contacted", "leads in [city]", "before my trip", "this week",
767
+ "this month", "what's overdue", "who should I re-engage", or
768
+ anything that implies pre-existing pipeline context.
769
+ `, "arguments": [], "expected_calls": ["leadbay_pull_followups", "leadbay_research_lead_by_id", "leadbay_prepare_outreach"], "failure_modes": ["Calls leadbay_pull_leads (the Discover entry point) instead of leadbay_pull_followups \u2014 these are different data sources; the Discover queue does NOT contain Monitor's known-but-cold pipeline", 'Iterates pages of leadbay_pull_leads filtering by engagement_count to "fake" a follow-up view (a real bug observed in 0.9.0 \u2014 the right move is to call pull_followups directly)', "Replaces the canonical pull_followups table layout with prose per row (the per-tool RENDERING block is the structural contract; commentary belongs above or below)", 'Skips the cross-mode pivot offer at the end ("Want to see NEW leads from your wishlist instead?" routes to leadbay_pull_leads)'] },
461
770
  leadbay_import_file: { "name": "leadbay_import_file", "short_description": "Import a user-supplied CSV/file into Leadbay through five phases with\nevidence gates \u2014 scan, derive, resolve identities, preserve & commit,\nthen optionally qualify and report. The job is to maximize how many\nrows the Leadbay system actually ingests and matches.\n", "arguments": [{ "name": "file", "description": "Path or user-visible name of the CSV/file to import. If omitted, use the file the user attached or referenced.", "required": false }, { "name": "instruction", "description": 'Additional user goal, e.g. "then qualify the leads", "preserve owner phone as a custom field", or "only import restaurants in Manhattan".', "required": false }], "expected_calls": ["leadbay_resolve_import_rows", "leadbay_list_mappable_fields", "leadbay_create_custom_field", "leadbay_import_leads", "leadbay_import_and_qualify", "leadbay_add_note", "leadbay_import_status"], "failure_modes": ["Picks LEADBAY_ID from score alone, name-only, fuzzy-name-only, root-domain-only, brand-only, postcode-only, or city-only evidence", "Drops meaningful business notes or CRM record links instead of preserving them as custom fields or lead notes", "Treats a consumer mailbox domain (gmail.com, hotmail.com, ...) as the company domain", "Skips deriving company_domain from a business email when no website column exists (this kills match rate)", "Skips the COLUMN PRESERVATION PLAN byproduct before importing", "Skips the DECISION LOG byproduct before writing LEADBAY_ID", "Returns the imported records WITHOUT writing LEADBAY_ID values back into the user's file (leaves the user no audit trail of what matched)", "Fabricates leadIds, contact emails, or mapping IDs not present in the file or a tool response"] },
462
- leadbay_log_outreach: { "name": "leadbay_log_outreach", "short_description": "Log outreach (an email I sent, a call I made, a meeting I had) on a\nspecific lead. Captures verification so the SDR pipeline trusts the entry.\n", "arguments": [{ "name": "lead_id", "description": "The lead UUID. Get it from leadbay_pull_leads or leadbay_research_lead.", "required": true }, { "name": "summary", "description": "1-2 sentences describing what I did (e.g. 'Sent intro email to CTO citing recent Hornsea contract').", "required": true }], "expected_calls": ["leadbay_report_outreach"], "failure_modes": ["Calls leadbay_report_outreach without first collecting a verification source", "Fabricates a gmail_message_id or calendar_event_id (the human team treats verification as canonical)", "Records outreach to a different lead_id than the one the user supplied", "Skips the dry_run step when the user is unsure what would be sent"] },
771
+ leadbay_log_outreach: { "name": "leadbay_log_outreach", "short_description": "Log outreach (an email I sent, a call I made, a meeting I had) on a\nspecific lead. Captures verification so the SDR pipeline trusts the entry.\n", "arguments": [{ "name": "lead_id", "description": "The lead UUID. Get it from leadbay_pull_leads or leadbay_research_lead_by_id.", "required": true }, { "name": "summary", "description": "1-2 sentences describing what I did (e.g. 'Sent intro email to CTO citing recent Hornsea contract').", "required": true }], "expected_calls": ["leadbay_report_outreach"], "failure_modes": ["Calls leadbay_report_outreach without first collecting a verification source", "Fabricates a gmail_message_id or calendar_event_id (the human team treats verification as canonical)", "Records outreach to a different lead_id than the one the user supplied", "Skips the dry_run step when the user is unsure what would be sent"] },
463
772
  leadbay_prospecting_overview: { "name": "leadbay_prospecting_overview", "short_description": `Orientation for working with Leadbay from any host \u2014 discovery vs.
464
773
  follow-up, the outreach loop, outcome recording, imports, pushback /
465
774
  snooze, and the connected-outreach-tool registry. Trigger when the
466
775
  conversation involves Leadbay leads, prospecting, pipeline, follow-up,
467
776
  outreach, or lens / ICP \u2014 anything from "show me my leads" to "what
468
777
  should I follow up on" to "I'll send via lemlist".
469
- `, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_pull_followups", "leadbay_research_company", "leadbay_research_lead", "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"] },
470
- 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_research_lead"], "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"] },
778
+ `, "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)"] },
779
+ 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"] },
471
780
  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"] },
472
- 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"], "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"] }
781
+ 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"] }
473
782
  };
474
783
  var PROMPT_CATALOG_HEADER = `This server exposes the following workflow prompts via \`prompts/list\` and \`prompts/get\`. Some MCP clients render them as slash commands; if your client does not, you (the agent) should invoke them directly via \`prompts/get\` when the user's request matches one of the triggers described below.`;
475
784
  var PROMPT_CATALOG_BULLETS = {
476
- leadbay_daily_check_in: `- \`leadbay_daily_check_in\`: Run the canonical daily check-in: see account state, pull a fresh batch, triage the top 10, deep-dive on every promising one, and offer contact enrichment. The user's typical morning workflow. Trigger when the user asks for "leadbay leads", "best leads to prospect today", "what should I work on", or anything resembling "show me the day's batch".`,
785
+ leadbay_daily_check_in: `- \`leadbay_daily_check_in\`: Run the canonical daily check-in: account state, fresh batch, triage top 10, deep-dive every promising one, offer contact enrichment. The morning DISCOVERY workflow (new leads from the lens wishlist). Trigger on "leadbay leads", "best NEW leads", "what's new today", "show me the day's batch", "let's prospect". Do NOT trigger on follow-up phrasings ("what should I follow up on", "before my trip") \u2014 those go to \`leadbay_followup_check_in\`.`,
786
+ leadbay_followup_check_in: `- \`leadbay_followup_check_in\`: Run the canonical follow-up check-in: surface KNOWN leads from the Monitor view that need re-engagement today, ranked by AI urgency, with the canonical pull_followups table layout. Trigger when the user asks "follow up", "already known leads", "leads I haven't contacted", "leads in [city]", "before my trip", "this week", "this month", "what's overdue", "who should I re-engage", or anything that implies pre-existing pipeline context.`,
477
787
  leadbay_import_file: `- \`leadbay_import_file\` (optional args: file, instruction): Import a user-supplied CSV/file into Leadbay through five phases with evidence gates \u2014 scan, derive, resolve identities, preserve & commit, then optionally qualify and report. The job is to maximize how many rows the Leadbay system actually ingests and matches.`,
478
788
  leadbay_log_outreach: `- \`leadbay_log_outreach\` (required args: lead_id, summary): Log outreach (an email I sent, a call I made, a meeting I had) on a specific lead. Captures verification so the SDR pipeline trusts the entry.`,
479
789
  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".`,
@@ -572,7 +882,7 @@ var CATALOG = [
572
882
  arguments: [
573
883
  {
574
884
  name: "lead_id",
575
- description: "The lead UUID. Get it from leadbay_pull_leads or leadbay_research_lead.",
885
+ description: "The lead UUID. Get it from leadbay_pull_leads or leadbay_research_lead_by_id.",
576
886
  required: true
577
887
  },
578
888
  {
@@ -655,7 +965,7 @@ function listResourceTemplates() {
655
965
  {
656
966
  uriTemplate: "lead://{uuid}/profile",
657
967
  name: "Lead profile",
658
- description: "Full profile for a single Leadbay lead by UUID. Read-only. Cached client-side; cheaper than calling leadbay_research_lead when you already have the id.",
968
+ description: "Full profile for a single Leadbay lead by UUID. Read-only. Cached client-side; cheaper than calling leadbay_research_lead_by_id when you already have the id.",
659
969
  mimeType: "application/json"
660
970
  },
661
971
  {
@@ -706,9 +1016,221 @@ async function readResource(uri, client) {
706
1016
  );
707
1017
  }
708
1018
 
1019
+ // src/host-widgets.ts
1020
+ var BUILTIN_WIDGETS_PARAGRAPH = 'Prefer host-native widgets over inline markdown when the data shape fits. Three to know: (1) `places_map_display_v0` \u2014 for \u22652 locations / map / travel intent. Pass `{name, address, latitude, longitude, notes}` per location; the host enriches via Google Places. (2) `message_compose_v1` \u2014 for any outreach draft (email / message / call opener). Pass 2\u20133 strategic variants with goal-oriented labels ("Push for alignment", "Reference M&A signal") \u2014 NOT tone labels. (3) `ask_user_input_v0` \u2014 for the NEXT STEPS questions every Leadbay tool emits. Pass `single_select` with 2\u20134 mutually-exclusive options from the tool\'s NEXT STEPS table. When the host doesn\'t expose the named widget, fall back to the per-tool markdown RENDERING block. The directive is host-conditional; the fallback is automatic.';
1021
+
1022
+ // src/telemetry.ts
1023
+ import { PostHog } from "posthog-node";
1024
+ import * as Sentry from "@sentry/node";
1025
+
1026
+ // src/telemetry-constants.ts
1027
+ var EMBEDDED_POSTHOG_KEY = "phc_N9SnA7OULuAlXReQJZ0Y3rPI4eC0mJLpMRbzgqamhHR";
1028
+ var EMBEDDED_POSTHOG_HOST = "https://eu.i.posthog.com";
1029
+ var EMBEDDED_SENTRY_DSN = "https://301f1c433433b76132956ed5415bea19@o4505874436849664.ingest.us.sentry.io/4511419984248832";
1030
+
1031
+ // src/telemetry-events.ts
1032
+ var EV_TOOL_CALL = "mcp tool called";
1033
+ var EV_QUOTA_HIT = "mcp quota hit";
1034
+ var EV_TOPUP_LINK = "mcp topup link created";
1035
+
1036
+ // src/telemetry.ts
1037
+ var NOOP_TELEMETRY = {
1038
+ identify: async () => {
1039
+ },
1040
+ captureToolCall: () => {
1041
+ },
1042
+ captureQuotaHit: () => {
1043
+ },
1044
+ captureTopupLink: () => {
1045
+ },
1046
+ captureException: () => {
1047
+ },
1048
+ shutdown: async () => {
1049
+ }
1050
+ };
1051
+ function parseTelemetryEnv(raw) {
1052
+ if (raw === void 0 || raw === "") return true;
1053
+ const v = raw.trim().toLowerCase();
1054
+ if (v === "false" || v === "0" || v === "no" || v === "off") return false;
1055
+ return true;
1056
+ }
1057
+ function initTelemetry(opts) {
1058
+ if (!parseTelemetryEnv(process.env.LEADBAY_TELEMETRY_ENABLED)) return NOOP_TELEMETRY;
1059
+ if (process.env.NODE_ENV === "test") return NOOP_TELEMETRY;
1060
+ const posthogKey = process.env.LEADBAY_POSTHOG_KEY ?? EMBEDDED_POSTHOG_KEY;
1061
+ const sentryDsn = process.env.LEADBAY_SENTRY_DSN ?? EMBEDDED_SENTRY_DSN;
1062
+ if (!posthogKey && !sentryDsn) return NOOP_TELEMETRY;
1063
+ const { version, logger } = opts;
1064
+ const environment = process.env.LEADBAY_ENV ?? (version.includes("-dev.") ? "dev" : "production");
1065
+ let posthog = null;
1066
+ let sentryReady = false;
1067
+ let initError = null;
1068
+ try {
1069
+ if (posthogKey) {
1070
+ posthog = new PostHog(posthogKey, {
1071
+ host: process.env.LEADBAY_POSTHOG_HOST ?? EMBEDDED_POSTHOG_HOST,
1072
+ flushAt: 20,
1073
+ flushInterval: 1e4,
1074
+ disableGeoip: false
1075
+ });
1076
+ }
1077
+ } catch (err) {
1078
+ initError = err;
1079
+ posthog = null;
1080
+ }
1081
+ try {
1082
+ if (sentryDsn) {
1083
+ Sentry.init({
1084
+ dsn: sentryDsn,
1085
+ release: `@leadbay/mcp@${version}`,
1086
+ environment,
1087
+ tracesSampleRate: 0,
1088
+ profilesSampleRate: 0,
1089
+ defaultIntegrations: false,
1090
+ integrations: [Sentry.httpIntegration()],
1091
+ sendDefaultPii: false
1092
+ });
1093
+ sentryReady = true;
1094
+ }
1095
+ } catch (err) {
1096
+ initError = initError ?? err;
1097
+ sentryReady = false;
1098
+ }
1099
+ if (initError) {
1100
+ logger?.warn?.(`telemetry init failed: ${initError.message ?? initError}`);
1101
+ }
1102
+ if (!posthog && !sentryReady) return NOOP_TELEMETRY;
1103
+ let me = null;
1104
+ let identityPromise = null;
1105
+ const pendingEvents = [];
1106
+ let region = "unknown";
1107
+ const baseProps = () => ({
1108
+ mcp_version: version,
1109
+ node_version: process.versions.node,
1110
+ platform: process.platform,
1111
+ region
1112
+ });
1113
+ const distinctIdFor = () => {
1114
+ if (me?.email) return me.email;
1115
+ if (me?.id) return `mcp:user-${me.id}`;
1116
+ return "mcp:unknown";
1117
+ };
1118
+ const groupsFor = () => {
1119
+ return me?.organization?.id ? { organization: me.organization.id } : void 0;
1120
+ };
1121
+ const doCapture = (event, properties) => {
1122
+ if (!posthog) return;
1123
+ try {
1124
+ posthog.capture({
1125
+ distinctId: distinctIdFor(),
1126
+ event,
1127
+ properties: { ...baseProps(), ...properties },
1128
+ groups: groupsFor()
1129
+ });
1130
+ } catch (err) {
1131
+ logger?.warn?.(`posthog capture failed: ${err?.message ?? err}`);
1132
+ }
1133
+ };
1134
+ const emit = (event, properties) => {
1135
+ if (!posthog) return;
1136
+ if (!me) {
1137
+ pendingEvents.push({ event, properties });
1138
+ return;
1139
+ }
1140
+ doCapture(event, properties);
1141
+ };
1142
+ const flushPending = () => {
1143
+ if (!posthog || pendingEvents.length === 0) return;
1144
+ const buf = pendingEvents.splice(0, pendingEvents.length);
1145
+ for (const { event, properties } of buf) {
1146
+ doCapture(event, properties);
1147
+ }
1148
+ };
1149
+ return {
1150
+ identify(client) {
1151
+ if (identityPromise) return identityPromise;
1152
+ region = client.region;
1153
+ identityPromise = (async () => {
1154
+ try {
1155
+ const resolved = await client.resolveMe();
1156
+ me = resolved;
1157
+ if (posthog && resolved.email) {
1158
+ try {
1159
+ posthog.identify({
1160
+ distinctId: resolved.email,
1161
+ properties: {
1162
+ email: resolved.email,
1163
+ leadbay_id: resolved.id,
1164
+ leadbay_name: resolved.name,
1165
+ leadbay_organization: resolved.organization?.name,
1166
+ leadbay_organization_id: resolved.organization?.id
1167
+ }
1168
+ });
1169
+ } catch (err) {
1170
+ logger?.warn?.(`posthog identify failed: ${err?.message ?? err}`);
1171
+ }
1172
+ }
1173
+ if (sentryReady) {
1174
+ try {
1175
+ Sentry.setUser({
1176
+ id: resolved.id,
1177
+ email: resolved.email,
1178
+ username: resolved.name
1179
+ });
1180
+ } catch (err) {
1181
+ logger?.warn?.(`sentry setUser failed: ${err?.message ?? err}`);
1182
+ }
1183
+ }
1184
+ flushPending();
1185
+ } catch (err) {
1186
+ logger?.warn?.(
1187
+ `telemetry identify failed (${err?.message ?? err}); flushing events anonymously`
1188
+ );
1189
+ me = {
1190
+ id: "unknown",
1191
+ organization: { id: "unknown", name: "unknown" }
1192
+ };
1193
+ flushPending();
1194
+ }
1195
+ })();
1196
+ return identityPromise;
1197
+ },
1198
+ captureToolCall(props) {
1199
+ emit(EV_TOOL_CALL, { ...props });
1200
+ },
1201
+ captureQuotaHit(props) {
1202
+ emit(EV_QUOTA_HIT, { ...props });
1203
+ },
1204
+ captureTopupLink(props) {
1205
+ emit(EV_TOPUP_LINK, { ...props });
1206
+ },
1207
+ captureException(err, ctx) {
1208
+ if (!sentryReady) return;
1209
+ try {
1210
+ Sentry.withScope((scope) => {
1211
+ scope.setTag("tool", ctx.tool);
1212
+ if (me?.organization?.id) {
1213
+ scope.setTag("organization", me.organization.id);
1214
+ }
1215
+ Sentry.captureException(err);
1216
+ });
1217
+ } catch (e) {
1218
+ logger?.warn?.(`sentry captureException failed: ${e?.message ?? e}`);
1219
+ }
1220
+ },
1221
+ async shutdown() {
1222
+ const tasks = [];
1223
+ if (posthog) tasks.push(posthog.shutdown(2e3).catch(() => void 0));
1224
+ if (sentryReady) tasks.push(Sentry.close(2e3).catch(() => void 0));
1225
+ await Promise.allSettled(tasks);
1226
+ }
1227
+ };
1228
+ }
1229
+
709
1230
  // src/server.ts
710
1231
  var VERIFICATION_MANDATE = "After every email, call, message, or meeting with a lead's contact, you MUST call leadbay_report_outreach with verification={source, ref} (gmail_message_id from the Gmail send, calendar_event_id from a booking, or user_confirmed='<the user's literal confirmation>'). Skipping or fabricating verification poisons the human team's pipeline.";
711
1232
  var MENTAL_MODEL_PARAGRAPH = "How Leadbay works (mental model): Leadbay is a sales inbox, not a queryable database. Each day the user logs back in, a fresh batch of leads is delivered. Batch size is paced by how many leads the user has actually acted on recently \u2014 some workflows produce a big stream of smaller prospects, others a narrow stream of bigger ones. Pulling more won't produce more; the user acting on leads (outreach, skips, saves) does.";
1233
+ var QUOTA_AND_TOPUP_PARAGRAPH = "Quota & top-ups: when a tool returns QUOTA_EXCEEDED / 429, the user has TWO options \u2014 wait for the window reset (daily / weekly / monthly resets shown in leadbay_account_status), OR top up AI credits (top-ups clear the throttle IMMEDIATELY \u2014 they are not subject to the same window). Always offer BOTH options; default-recommending 'wait until tomorrow' is wrong when a 30-second top-up unblocks the same call. If the host exposes leadbay_create_topup_link, OFFER it on every quota wall: 'Want me to generate a top-up link?' \u2014 when the user says yes, call leadbay_create_topup_link and surface the returned Stripe URL as a clickable link for the user to open in their browser. (Sibling leadbay_open_billing_portal is for ongoing subscription changes, not one-shot top-ups.) AFTER the user has topped up: do NOT keep refusing operations. A top-up invalidates every prior 429 and every stale 'you're at your quota' snapshot. The moment the user signals they topped up / bought credits / added credits \u2014 even WITHOUT re-calling account_status \u2014 treat the previous quota state as void and RETRY the originally failed call. (Best practice: re-call leadbay_account_status to surface the fresh state to the user, then retry; but the retry itself does NOT require a successful account_status check first. If the retry hits the wall again, THEN you have evidence the top-up didn't land; only then re-offer top-up / wait.) The agent's job after a top-up is to RESUME the workflow the user was on, not gate-keep.";
712
1234
  function buildScoringParagraph(has) {
713
1235
  const base = "Two scoring layers: every lead has a basic `score` (firmographic \u2014 already decent, usually correlates with AI). Roughly the top 10 of each batch are also AI-qualified (targeted web research + qualification questions \u2192 `ai_agent_lead_score`, surfaced as `qualification_summary` on leadbay_pull_leads). Leads past the top ~10 are not worse \u2014 the system is saving resources.";
714
1236
  const deepenTools = [];
@@ -720,7 +1242,7 @@ function buildScoringParagraph(has) {
720
1242
  return base;
721
1243
  }
722
1244
  function buildStartHereParagraph(has) {
723
- const base = "Start with leadbay_account_status to see the user's state, then leadbay_pull_leads to surface fresh leads. Use leadbay_research_lead to dig into one lead deeply (qualification answers, signals, contacts).";
1245
+ const base = "Start with leadbay_account_status to see the user's state, then leadbay_pull_leads to surface fresh leads. Use leadbay_research_lead_by_id to dig into one lead deeply (qualification answers, signals, contacts).";
724
1246
  const compositeNames = ["bulk_qualify_leads", "adjust_audience", "refine_prompt", "enrich_titles"].filter((n) => has(`leadbay_${n}`));
725
1247
  if (compositeNames.length > 0) {
726
1248
  return base + ` When the user wants more leads, narrower audience, refined criteria, or contact enrichment, use the matching composite tool (${compositeNames.join(" / ")}) \u2014 they hide lens permissions, region routing, polling, and selection state from you.`;
@@ -734,6 +1256,7 @@ function buildRhythmParagraph(has) {
734
1256
  return "Suggested rhythm: a healthy agent pattern is a daily check-in \u2014 pull fresh leads, skim the auto-qualified top, deepen 1-3 promising ones, propose outreach to the user. If your host supports scheduling, offer to set up a daily run.";
735
1257
  }
736
1258
  var TOOL_REFERENCE_PATTERN = /\bleadbay_[a-z][a-z0-9_]*\b/g;
1259
+ var PROMPT_NAMES = new Set(Object.keys(PROMPT_CATALOG_BULLETS));
737
1260
  function buildPromptsCatalogParagraph(has) {
738
1261
  const safeBullets = [];
739
1262
  for (const [promptName, bullet] of Object.entries(PROMPT_CATALOG_BULLETS)) {
@@ -741,6 +1264,7 @@ function buildPromptsCatalogParagraph(has) {
741
1264
  for (const match of bullet.matchAll(TOOL_REFERENCE_PATTERN)) {
742
1265
  const name = match[0];
743
1266
  if (name === promptName) continue;
1267
+ if (PROMPT_NAMES.has(name)) continue;
744
1268
  referencedTools.add(name);
745
1269
  }
746
1270
  const allExposed = [...referencedTools].every((n) => has(n));
@@ -801,6 +1325,7 @@ function buildServerInstructions(exposed) {
801
1325
  parts.push(VERIFICATION_MANDATE);
802
1326
  }
803
1327
  parts.push(MENTAL_MODEL_PARAGRAPH);
1328
+ parts.push(QUOTA_AND_TOPUP_PARAGRAPH);
804
1329
  parts.push(buildScoringParagraph(has));
805
1330
  parts.push(buildStartHereParagraph(has));
806
1331
  parts.push(buildRhythmParagraph(has));
@@ -808,6 +1333,7 @@ function buildServerInstructions(exposed) {
808
1333
  if (promptsCatalog) parts.push(promptsCatalog);
809
1334
  parts.push(RESOURCES_PARAGRAPH);
810
1335
  parts.push(buildProtocolPrimitivesParagraph(has));
1336
+ parts.push(BUILTIN_WIDGETS_PARAGRAPH);
811
1337
  return parts.join("\n\n");
812
1338
  }
813
1339
  function formatErrorForLLM(err) {
@@ -938,8 +1464,10 @@ function buildServer(client, opts = {}) {
938
1464
  });
939
1465
  const DEBUG_RAW = process.env.LEADBAY_DEBUG ?? "";
940
1466
  const DEBUG_ON = DEBUG_RAW === "1" || DEBUG_RAW.toLowerCase() === "true";
1467
+ const telemetry = opts.telemetry ?? NOOP_TELEMETRY;
1468
+ const isLeadbayBusinessError = (err) => err != null && typeof err === "object" && err.error === true && typeof err.code === "string";
941
1469
  server.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
942
- const debugStart = DEBUG_ON ? Date.now() : 0;
1470
+ const callStart = Date.now();
943
1471
  const name = req.params.name;
944
1472
  const tool = toolByName.get(name);
945
1473
  if (!tool) {
@@ -995,9 +1523,33 @@ function buildServer(client, opts = {}) {
995
1523
  elicit
996
1524
  });
997
1525
  if (result && typeof result === "object" && result.error === true) {
1526
+ const envText = formatErrorForLLM(result);
1527
+ const envDur = Date.now() - callStart;
1528
+ const envCode = result.code ?? "Error";
1529
+ if (envCode === "QUOTA_EXCEEDED") {
1530
+ telemetry.captureQuotaHit({
1531
+ tool: name,
1532
+ retry_after_s: result._meta?.retry_after,
1533
+ endpoint: result._meta?.endpoint
1534
+ });
1535
+ }
1536
+ telemetry.captureToolCall({
1537
+ tool: name,
1538
+ ok: false,
1539
+ duration_ms: envDur,
1540
+ format: "error-envelope",
1541
+ bytes: envText.length,
1542
+ error_code: envCode
1543
+ });
1544
+ if (DEBUG_ON) {
1545
+ process.stderr.write(
1546
+ `[leadbay-mcp debug] tool=${name} dur=${envDur}ms ok=false code=${envCode}
1547
+ `
1548
+ );
1549
+ }
998
1550
  return {
999
1551
  content: [
1000
- { type: "text", text: formatErrorForLLM(result) }
1552
+ { type: "text", text: envText }
1001
1553
  ],
1002
1554
  isError: true
1003
1555
  };
@@ -1011,11 +1563,21 @@ function buildServer(client, opts = {}) {
1011
1563
  if (tool.outputSchema && env.structured !== null && typeof env.structured === "object" && !Array.isArray(env.structured)) {
1012
1564
  out.structuredContent = env.structured;
1013
1565
  }
1566
+ const mdDur = Date.now() - callStart;
1567
+ const mdBytes = env.markdown.length;
1568
+ telemetry.captureToolCall({
1569
+ tool: name,
1570
+ ok: true,
1571
+ duration_ms: mdDur,
1572
+ format: "markdown",
1573
+ bytes: mdBytes
1574
+ });
1575
+ if (name === "leadbay_create_topup_link" && typeof env.structured?.url === "string") {
1576
+ telemetry.captureTopupLink({ tool: name });
1577
+ }
1014
1578
  if (DEBUG_ON) {
1015
- const dur = Date.now() - debugStart;
1016
- const bytes = env.markdown.length;
1017
1579
  process.stderr.write(
1018
- `[leadbay-mcp debug] tool=${name} dur=${dur}ms ok=true bytes=${bytes} format=markdown
1580
+ `[leadbay-mcp debug] tool=${name} dur=${mdDur}ms ok=true bytes=${mdBytes} format=markdown
1019
1581
  `
1020
1582
  );
1021
1583
  }
@@ -1029,28 +1591,66 @@ function buildServer(client, opts = {}) {
1029
1591
  if (tool.outputSchema && result !== null && typeof result === "object" && !Array.isArray(result)) {
1030
1592
  response.structuredContent = result;
1031
1593
  }
1594
+ const okText = response.content[0]?.text ?? "";
1595
+ const okBytes = typeof okText === "string" ? okText.length : 0;
1596
+ const okDur = Date.now() - callStart;
1597
+ telemetry.captureToolCall({
1598
+ tool: name,
1599
+ ok: true,
1600
+ duration_ms: okDur,
1601
+ format: "json",
1602
+ bytes: okBytes
1603
+ });
1604
+ if (name === "leadbay_create_topup_link" && typeof result?.url === "string") {
1605
+ telemetry.captureTopupLink({ tool: name });
1606
+ }
1032
1607
  if (DEBUG_ON) {
1033
- const dur = Date.now() - debugStart;
1034
- const text = response.content[0]?.text ?? "";
1035
- const bytes = typeof text === "string" ? text.length : 0;
1036
1608
  process.stderr.write(
1037
- `[leadbay-mcp debug] tool=${name} dur=${dur}ms ok=true bytes=${bytes}
1609
+ `[leadbay-mcp debug] tool=${name} dur=${okDur}ms ok=true bytes=${okBytes}
1038
1610
  `
1039
1611
  );
1040
1612
  }
1041
1613
  return response;
1042
1614
  } catch (err) {
1615
+ const errDur = Date.now() - callStart;
1616
+ const errText = formatErrorForLLM(err);
1617
+ const code = err?.code ?? err?.name ?? "Error";
1618
+ if (isLeadbayBusinessError(err)) {
1619
+ if (err.code === "QUOTA_EXCEEDED") {
1620
+ telemetry.captureQuotaHit({
1621
+ tool: name,
1622
+ retry_after_s: err._meta?.retry_after,
1623
+ endpoint: err._meta?.endpoint
1624
+ });
1625
+ }
1626
+ telemetry.captureToolCall({
1627
+ tool: name,
1628
+ ok: false,
1629
+ duration_ms: errDur,
1630
+ format: "error-envelope",
1631
+ bytes: errText.length,
1632
+ error_code: code
1633
+ });
1634
+ } else {
1635
+ telemetry.captureException(err, { tool: name });
1636
+ telemetry.captureToolCall({
1637
+ tool: name,
1638
+ ok: false,
1639
+ duration_ms: errDur,
1640
+ format: "error-envelope",
1641
+ bytes: errText.length,
1642
+ error_code: code
1643
+ });
1644
+ }
1043
1645
  if (DEBUG_ON) {
1044
- const dur = Date.now() - debugStart;
1045
- const code = err?.code ?? err?.name ?? "Error";
1046
1646
  process.stderr.write(
1047
- `[leadbay-mcp debug] tool=${name} dur=${dur}ms ok=false code=${code}
1647
+ `[leadbay-mcp debug] tool=${name} dur=${errDur}ms ok=false code=${code}
1048
1648
  `
1049
1649
  );
1050
1650
  }
1051
1651
  return {
1052
1652
  content: [
1053
- { type: "text", text: formatErrorForLLM(err) }
1653
+ { type: "text", text: errText }
1054
1654
  ],
1055
1655
  isError: true
1056
1656
  };
@@ -1061,7 +1661,7 @@ function buildServer(client, opts = {}) {
1061
1661
 
1062
1662
  // src/bin.ts
1063
1663
  import { createRequire } from "module";
1064
- var VERSION = "0.9.0";
1664
+ var VERSION = "0.10.0";
1065
1665
  var HELP = `
1066
1666
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
1067
1667
 
@@ -1097,6 +1697,13 @@ ENV VARS
1097
1697
  journaled in-process and return {mocked: true, would_call: {...}}.
1098
1698
  LEADBAY_MOCK_DIR (optional) Fixture directory. Default: ./.context/leadbay-live-shapes/
1099
1699
  LEADBAY_LOG_LEVEL (optional) "debug" | "info" | "error" (default "error"). Logs to stderr.
1700
+ LEADBAY_TELEMETRY_ENABLED (optional) Default "true". Sends product usage events
1701
+ (tool name, duration, ok flag, error code) to PostHog and
1702
+ unexpected errors to Sentry, helping Leadbay improve the MCP.
1703
+ Events are tied to your Leadbay account email (so MCP usage
1704
+ consolidates with web-app usage in our analytics). Tool
1705
+ arguments, response bodies, and lead PII are NEVER captured.
1706
+ Set to "false" to opt out. See README "Privacy & telemetry".
1100
1707
 
1101
1708
  EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_desktop_config.json)
1102
1709
  {
@@ -1106,7 +1713,8 @@ EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
1106
1713
  "args": ["-y", "@leadbay/mcp@0.3"],
1107
1714
  "env": {
1108
1715
  "LEADBAY_TOKEN": "lb_...",
1109
- "LEADBAY_REGION": "us"
1716
+ "LEADBAY_REGION": "us",
1717
+ "LEADBAY_TELEMETRY_ENABLED": "true"
1110
1718
  }
1111
1719
  }
1112
1720
  }
@@ -1350,7 +1958,7 @@ async function runLogin(args) {
1350
1958
  let result;
1351
1959
  try {
1352
1960
  if (pinnedRegion && !allowFallback) {
1353
- const { REGIONS } = await import("./dist-IIPWWDS5.js");
1961
+ const { REGIONS } = await import("./dist-BHLIJAIH.js");
1354
1962
  const baseUrl = REGIONS[pinnedRegion];
1355
1963
  const c = createClient({ region: pinnedRegion });
1356
1964
  const token = await loginAt(baseUrl, email, password);
@@ -1677,7 +2285,7 @@ async function readChoice(prompt, def = true) {
1677
2285
  process.stdin.on("data", onData);
1678
2286
  });
1679
2287
  }
1680
- function buildClaudeCodeAddArgs(token, region, includeWrite) {
2288
+ function buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled) {
1681
2289
  const args = [
1682
2290
  "mcp",
1683
2291
  "add",
@@ -1687,15 +2295,20 @@ function buildClaudeCodeAddArgs(token, region, includeWrite) {
1687
2295
  "--env",
1688
2296
  `LEADBAY_TOKEN=${token}`,
1689
2297
  "--env",
1690
- `LEADBAY_REGION=${region}`
2298
+ `LEADBAY_REGION=${region}`,
2299
+ // Always written explicitly (not just when opting out) so MCP-client
2300
+ // config UIs can render it as a toggle the user can flip without
2301
+ // editing the file by hand.
2302
+ "--env",
2303
+ `LEADBAY_TELEMETRY_ENABLED=${telemetryEnabled ? "true" : "false"}`
1691
2304
  ];
1692
2305
  if (!includeWrite) args.push("--env", `LEADBAY_MCP_WRITE=0`);
1693
2306
  args.push("--", "npx", "-y", "@leadbay/mcp@0.3");
1694
2307
  return args;
1695
2308
  }
1696
- async function installInClaudeCode(token, region, includeWrite) {
2309
+ async function installInClaudeCode(token, region, includeWrite, telemetryEnabled) {
1697
2310
  const cp = await import("child_process");
1698
- const args = buildClaudeCodeAddArgs(token, region, includeWrite);
2311
+ const args = buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled);
1699
2312
  return await new Promise((resolve) => {
1700
2313
  const child = cp.spawn("claude", args, { stdio: ["ignore", "pipe", "pipe"] });
1701
2314
  let stderr = "";
@@ -1713,7 +2326,7 @@ async function installInClaudeCode(token, region, includeWrite) {
1713
2326
  );
1714
2327
  });
1715
2328
  }
1716
- async function installInJsonConfig(configPath, token, region, includeWrite) {
2329
+ async function installInJsonConfig(configPath, token, region, includeWrite, telemetryEnabled) {
1717
2330
  try {
1718
2331
  const { readFileSync, writeFileSync, existsSync, mkdirSync, statSync } = await import("fs");
1719
2332
  const { dirname } = await import("path");
@@ -1733,7 +2346,9 @@ async function installInJsonConfig(configPath, token, region, includeWrite) {
1733
2346
  parsed.mcpServers = parsed.mcpServers ?? {};
1734
2347
  const env = {
1735
2348
  LEADBAY_TOKEN: token,
1736
- LEADBAY_REGION: region
2349
+ LEADBAY_REGION: region,
2350
+ // Always written so MCP-client config UIs can render it as a toggle.
2351
+ LEADBAY_TELEMETRY_ENABLED: telemetryEnabled ? "true" : "false"
1737
2352
  };
1738
2353
  if (!includeWrite) env.LEADBAY_MCP_WRITE = "0";
1739
2354
  parsed.mcpServers.leadbay = {
@@ -1761,7 +2376,7 @@ async function runInstall(args) {
1761
2376
  const email = parseFlag(args, "email");
1762
2377
  if (!email) {
1763
2378
  process.stderr.write(
1764
- "Usage: leadbay-mcp install --email you@example.com [--region us|fr]\n [--allow-region-fallback] [--no-write]\n [--target claude-code,claude-desktop,cursor]\n [--yes] [--force-legacy]\n Mints a token AND registers the MCP server with your installed clients (at user scope).\n --target Comma-separated subset; default = all detected.\n --no-write Disable composite write tools (refine_prompt, report_outreach,\n adjust_audience, etc.). They are ON by default since 0.3.0;\n pass --no-write for read-only agents.\n --include-write (deprecated since 0.3.0; now a no-op \u2014 writes are on by default).\n --yes Don't ask before installing into each detected client.\n --force-legacy Write to claude_desktop_config.json even when Claude Desktop 2026\n DXT is detected. Not recommended \u2014 the app overwrites that file.\n Use the .dxt bundle instead: https://github.com/leadbay/leadclaw/releases\n"
2379
+ "Usage: leadbay-mcp install --email you@example.com [--region us|fr]\n [--allow-region-fallback] [--no-write] [--no-telemetry]\n [--target claude-code,claude-desktop,cursor]\n [--yes] [--force-legacy]\n Mints a token AND registers the MCP server with your installed clients (at user scope).\n --target Comma-separated subset; default = all detected.\n --no-write Disable composite write tools (refine_prompt, report_outreach,\n adjust_audience, etc.). They are ON by default since 0.3.0;\n pass --no-write for read-only agents.\n --no-telemetry Opt out of product usage events (PostHog + Sentry).\n Defaults to ON: helps Leadbay improve the MCP. Events are\n tied to your Leadbay email; tool args, response bodies,\n and lead PII are NEVER captured.\n --include-write (deprecated since 0.3.0; now a no-op \u2014 writes are on by default).\n --yes Don't ask before installing into each detected client.\n --force-legacy Write to claude_desktop_config.json even when Claude Desktop 2026\n DXT is detected. Not recommended \u2014 the app overwrites that file.\n Use the .dxt bundle instead: https://github.com/leadbay/leadclaw/releases\n"
1765
2380
  );
1766
2381
  return 2;
1767
2382
  }
@@ -1842,7 +2457,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
1842
2457
  let region;
1843
2458
  try {
1844
2459
  if (pinnedRegion && !allowFallback) {
1845
- const { REGIONS } = await import("./dist-IIPWWDS5.js");
2460
+ const { REGIONS } = await import("./dist-BHLIJAIH.js");
1846
2461
  const baseUrl = REGIONS[pinnedRegion];
1847
2462
  token = await loginAt(baseUrl, email, password);
1848
2463
  region = pinnedRegion;
@@ -1869,6 +2484,16 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
1869
2484
  "Composite write tools DISABLED (read-only agent). Re-run without --no-write to enable.\n\n"
1870
2485
  );
1871
2486
  }
2487
+ const telemetryEnabled = !hasFlag(args, "no-telemetry");
2488
+ if (telemetryEnabled) {
2489
+ process.stderr.write(
2490
+ "Product usage events ENABLED \u2014 helps Leadbay improve the MCP. We capture\n per-tool-call metrics (name, duration, ok/error code) and unexpected exceptions.\n Events are tied to your Leadbay email (so MCP usage consolidates with web-app\n usage in our analytics) \u2014 they are NOT anonymous. Tool arguments, response\n bodies, and lead PII are NEVER sent. Flip the toggle\n LEADBAY_TELEMETRY_ENABLED=false in your client's env block to opt out anytime.\n\n"
2491
+ );
2492
+ } else {
2493
+ process.stderr.write(
2494
+ "Product usage events DISABLED. Re-run without --no-telemetry to enable.\n\n"
2495
+ );
2496
+ }
1872
2497
  const skipPrompts = hasFlag(args, "yes");
1873
2498
  const results = [];
1874
2499
  for (const c of chosen) {
@@ -1888,10 +2513,10 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
1888
2513
  }
1889
2514
  let res;
1890
2515
  if (c.id === "claude-code") {
1891
- res = await installInClaudeCode(token, region, includeWrite);
2516
+ res = await installInClaudeCode(token, region, includeWrite, telemetryEnabled);
1892
2517
  } else {
1893
2518
  const path = c.detail.split(" ")[0];
1894
- res = await installInJsonConfig(path, token, region, includeWrite);
2519
+ res = await installInJsonConfig(path, token, region, includeWrite, telemetryEnabled);
1895
2520
  }
1896
2521
  results.push({ id: c.id, label: c.label, ...res });
1897
2522
  }
@@ -1978,7 +2603,9 @@ async function main() {
1978
2603
  process.exit(await runDoctor());
1979
2604
  }
1980
2605
  const logger = makeStderrLogger(parseLogLevel(process.env.LEADBAY_LOG_LEVEL));
2606
+ const telemetry = initTelemetry({ version: VERSION, logger });
1981
2607
  const client = await resolveClientFromEnv(logger);
2608
+ telemetry.identify(client);
1982
2609
  const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
1983
2610
  const includeWrite = parseWriteEnv();
1984
2611
  const bulkTracker = await createDefaultBulkStore({ logger });
@@ -1987,13 +2614,24 @@ async function main() {
1987
2614
  includeWrite,
1988
2615
  logger,
1989
2616
  bulkTracker,
1990
- version: VERSION
2617
+ version: VERSION,
2618
+ telemetry
1991
2619
  });
1992
2620
  const transport = new StdioServerTransport();
1993
2621
  logger.info?.(
1994
2622
  `Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability})`
1995
2623
  );
1996
2624
  await server.connect(transport);
2625
+ const shutdown = async (code) => {
2626
+ try {
2627
+ await telemetry.shutdown();
2628
+ } finally {
2629
+ process.exit(code);
2630
+ }
2631
+ };
2632
+ process.once("SIGINT", () => void shutdown(130));
2633
+ process.once("SIGTERM", () => void shutdown(143));
2634
+ process.stdin.once("end", () => void shutdown(0));
1997
2635
  }
1998
2636
  var isEntrypoint = (() => {
1999
2637
  try {
@@ -2006,9 +2644,15 @@ var isEntrypoint = (() => {
2006
2644
  }
2007
2645
  })();
2008
2646
  if (isEntrypoint) {
2009
- main().catch((err) => {
2647
+ main().catch(async (err) => {
2010
2648
  process.stderr.write(`leadbay-mcp: ${err?.message ?? err}
2011
2649
  `);
2650
+ try {
2651
+ const bootTelemetry = initTelemetry({ version: VERSION });
2652
+ bootTelemetry.captureException(err, { tool: "__bootstrap__" });
2653
+ await bootTelemetry.shutdown();
2654
+ } catch {
2655
+ }
2012
2656
  process.exit(1);
2013
2657
  });
2014
2658
  }