@leadbay/mcp 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -8,7 +8,7 @@ import {
8
8
  granularReadTools,
9
9
  granularWriteTools,
10
10
  resolveRegion
11
- } from "./chunk-NVGZ432E.js";
11
+ } from "./chunk-ZRTAIXEJ.js";
12
12
 
13
13
  // src/bin.ts
14
14
  import { realpathSync } from "fs";
@@ -33,7 +33,7 @@ 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
 
@@ -41,7 +41,7 @@ These four rules apply to every Leadbay workflow that calls \`leadbay_pull_leads
41
41
 
42
42
  ## Rule 1 \u2014 Pin the lens
43
43
 
44
- After your first \`leadbay_pull_leads\` call, capture \`response.lensId\` into your working memory and **pass it explicitly as \`lensId\` to every subsequent call** in this session \u2014 including any re-pulls, bulk qualifies, or research calls that accept it. The active lens can shift between calls (5-minute client cache + backend \`last_requested_lens\` can change if the user touches the web UI). A lens shift mid-workflow throws away your top-10 work.
44
+ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into your working memory and **pass it explicitly as the \`lensId\` argument to every subsequent call** in this session \u2014 including any re-pulls, bulk qualifies, or research calls that accept it. (Field-name caveat: the response nests it as \`lens.id\`; the parameter on subsequent calls is \`lensId\`.) The active lens can shift between calls (5-minute client cache + backend \`last_requested_lens\` can change if the user touches the web UI). A lens shift mid-workflow throws away your top-10 work.
45
45
 
46
46
  ## Rule 2 \u2014 Prefer async for bulk operations
47
47
 
@@ -70,11 +70,95 @@ If you're resuming an interrupted session (you see a previous Phase already comp
70
70
  Call \`leadbay_account_status\` to see what quota I have left and which lens is active. Note the remaining \`ai_rescore_remaining\` and \`web_fetch_remaining\` budgets \u2014 Phase 4 enrichment depends on them.
71
71
 
72
72
  # PHASE 2 \u2014 FRESH BATCH
73
- Call \`leadbay_pull_leads\` to get today's fresh batch. Capture \`lensId\` from the response. **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.)
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\` or \`leadbay_research_company\`.
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
 
@@ -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.
@@ -276,21 +365,257 @@ After I answer, call \`leadbay_report_outreach({lead_id: '{{arg:lead_id}}', note
276
365
  # PHASE 3 \u2014 CONFIRM
277
366
  Tell me the outreach was logged, name the verification.source used, and surface the response's \`outreach_id\` if present so I can refer back to it.
278
367
  `;
368
+ var leadbay_prospecting_overview = `
369
+ # Leadbay Prospecting \u2014 Orientation
370
+
371
+ You are working with Leadbay through the \`leadbay_*\` MCP tools. This prompt orients you to the user's mental model so you don't re-discover the workflow each session.
372
+
373
+ # Resilience rules for Leadbay long-running tools
374
+
375
+ 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.**
376
+
377
+ ## Rule 1 \u2014 Pin the lens
378
+
379
+ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into your working memory and **pass it explicitly as the \`lensId\` argument to every subsequent call** in this session \u2014 including any re-pulls, bulk qualifies, or research calls that accept it. (Field-name caveat: the response nests it as \`lens.id\`; the parameter on subsequent calls is \`lensId\`.) The active lens can shift between calls (5-minute client cache + backend \`last_requested_lens\` can change if the user touches the web UI). A lens shift mid-workflow throws away your top-10 work.
380
+
381
+ ## Rule 2 \u2014 Prefer async for bulk operations
382
+
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.
384
+
385
+ ## Rule 3 \u2014 Serialize \`leadbay_research_lead\` fan-out
386
+
387
+ \`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.
388
+
389
+ ## Rule 4 \u2014 Retry, don't replan
390
+
391
+ If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any other transport-level error (distinct from a Leadbay-issued error payload), the work may still be running server-side. Do this in order:
392
+
393
+ 1. For bulk tools \u2014 retry with \`wait_for_completion:false\` and poll the status tool with the returned id. Don't re-pull leads; that can shift the lens.
394
+ 2. For single-lead tools \u2014 retry the same call once. If it still fails, record the lead id and continue with the rest of the workflow.
395
+ 3. **Do not** switch strategies (e.g. "the endpoint is broken, let me re-pull from scratch"). The earlier work is still valid; the timeout was the wire.
396
+
397
+ If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
398
+
399
+
400
+ ## The two entry points
401
+
402
+ Leadbay supports two parallel ways to find leads to act on. Detect which entry the user wants from their natural language, then route accordingly.
403
+
404
+ \`\`\`
405
+ Discovery entry Follow-up entry
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
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
413
+ \u2502 \u2502
414
+ \u2502 optional: \u2502 filters by user phrasing:
415
+ \u2502 research_company / \u2502 geo, sector, recency,
416
+ \u2502 research_lead \u2502 liked / pushback / outcome
417
+ \u2502 (deepen profile) \u2502
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
419
+ \u25BC
420
+ leadbay_prepare_outreach
421
+ (single-contact brief)
422
+ \u2502
423
+ \u25BC
424
+ user does outreach
425
+ (agent drafts email / call / DM)
426
+ \u2502
427
+ \u25BC
428
+ leadbay_report_outreach
429
+ (close the loop \u2014 verification + outcome)
430
+ \`\`\`
431
+
432
+ **Discovery signals**: "show me my leads", "what's new today", "any new prospects", "let's prospect", no mention of prior context. Route to \`leadbay_pull_leads\`.
433
+
434
+ **Follow-up signals**: "what should I follow up on", "leads I haven't contacted", "leads in [city]", "before my trip", "this week", "this month", "what's overdue", explicit mention of recent or pending actions. Route to \`leadbay_pull_followups\` \u2014 the Monitor view of known leads. Apply \`set_filter\` for geo / sector / recency / action-type refinement; the filter is server-persisted across sessions.
435
+
436
+ When in doubt, ask. The two paths return overlapping but differently-ranked data; presenting the wrong one wastes the user's time.
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
+
446
+ ## The outreach loop
447
+
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:
449
+
450
+ | Tool | Channel strength | Draft idiom |
451
+ |---------------------|-------------------------------------------|---------------------------------------------------------|
452
+ | **Lemlist** | Email + LinkedIn + WhatsApp + cold call | Sequence step (subject + body + step-N timing) |
453
+ | **Outreach.io** | Email + call cadence | Sequence step; surface intent signals for forecasting |
454
+ | **Salesloft** | Email + call + LinkedIn cadence | Cadence step; pair with deal context if available |
455
+ | **Apollo** | Email-first | Clean cold email; include prospect signal references |
456
+ | **HubSpot Sales Hub** | Email + tasks | HubSpot sequence email; recommend a task type |
457
+ | **Instantly** | Email at scale | Deliverability-conscious email (<80 words, no link spam)|
458
+ | **Attio** | Email + LinkedIn from CRM | Outreach record on the Attio person; reference the deal |
459
+ | **Amplemarket** | 7 channels (email/LI/call/SMS/WA/voice/video) | Per-channel variants; suggest the strongest channel |
460
+ | **Generic / Gmail / Outlook** | Email | Clean copy-paste email; no tool-specific syntax |
461
+
462
+ **Detect the active tool** in this priority order:
463
+ 1. The host's installed-connector / installed-MCP inventory, when available (Claude Desktop, Cowork).
464
+ 2. The conversation \u2014 what tools has the user mentioned or used recently? ("I'll send via lemlist" \u2192 assume lemlist.)
465
+ 3. Ask the user when uncertain.
466
+
467
+ ## The outcome / closing-the-loop habit
468
+
469
+ IRON LAW \u2014 OUTCOME AFTER OUTREACH. The moment the user reports outreach happened ("I sent it", "she didn't pick up", "left a voicemail", "they replied", a forwarded email thread, a calendar invite), you MUST (1) call leadbay_report_outreach with verification (gmail_message_id, calendar_event_id, or the user's literal one-sentence confirmation as user_confirmed.ref) AND (2) ask the user about the outcome and set epilogue_status to one of the 4 canonical values: EPILOGUE_INTEREST_VALIDATED_OR_MEETING_PLANED ("Meeting booked"), EPILOGUE_COULD_NOT_REACH_STILL_TRYING ("Trying to reach"), EPILOGUE_NOT_INTERESTED_LOST ("Not interested"), EPILOGUE_STILL_CHASING ("In progress"). Use the user-facing labels in dialogue ("What's the outcome \u2014 meeting booked, trying to reach, not interested, or in progress?"); never say "epilogue" out loud. Skipping this step silently de-ranks every future follow-up suggestion because pull_followups depends on honest, current outcomes.
470
+
471
+
472
+ User-facing dialogue:
473
+
474
+ - **Always say "outcome", never "epilogue"** \u2014 the backend field is \`epilogue_status\` of type \`EpilogueStatusType\` but that's wire-format jargon. The 4 user-facing labels:
475
+ - \`EPILOGUE_INTEREST_VALIDATED_OR_MEETING_PLANED\` \u2192 **Meeting booked** \u{1F3AF}
476
+ - \`EPILOGUE_COULD_NOT_REACH_STILL_TRYING\` \u2192 **Trying to reach** \u26A1
477
+ - \`EPILOGUE_NOT_INTERESTED_LOST\` \u2192 **Not interested** \u2744
478
+ - \`EPILOGUE_STILL_CHASING\` \u2192 **In progress** \u{1F7E2}
479
+ - **Always say "follow-ups", never "Monitor"** \u2014 "Monitor" is internal app jargon; salespeople say "follow-ups".
480
+
481
+ ## "Not now" / snooze / pushback
482
+
483
+ When the user says "not now", "next quarter", "follow up in 3 / 6 / 12 months", "next year", etc., this is a **pushback** action (not a note). Call \`leadbay_set_pushback({lead_ids, status})\` where \`status\` is \`3\`, \`6\`, or \`12\` (months). The lead drops out of \`leadbay_pull_followups\` until the window expires. Use \`leadbay_remove_pushback\` to revive a lead ahead of expiry. User-facing dialogue: say "snooze for N months", not "pushback".
484
+
485
+ ## Imports
486
+
487
+ When the user mentions a CSV / list / their CRM, use the **\`leadbay_import_file\`** prompt \u2014 it walks through scan \u2192 resolve \u2192 preserve \u2192 commit. The single-shot tool \`leadbay_import_leads\` is for clean, mechanical imports; the prompt handles messy ones.
488
+
489
+ ## AI scoring on the daily batch
490
+
491
+ Roughly the **top 10** of every \`leadbay_pull_leads\` response carry full AI qualification (\`qualification_summary.answered > 0\`, \`ai_agent_lead_score\`, \u2756 caps in the rendered bar). Leads below the top ~10 are NOT worse \u2014 the system is saving resources. A healthy daily rhythm: bulk-qualify the rows WITHOUT \u2756 caps so tomorrow's top-10 is richer. Use \`leadbay_bulk_qualify_leads([leadIds])\` for this; default to \`wait_for_completion:false\` for any count > 5.
492
+
493
+ ## Lens pinning
494
+
495
+ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` (the response nests it under \`lens\`) and pass it explicitly as the \`lensId\` argument to every subsequent Leadbay call this session. Lens shifts mid-workflow throw away your prior batch \u2014 see Rule 1 in the long-running-tools heuristics above.
496
+
497
+ ## What to read once you've matched intent
498
+
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.
504
+ `;
279
505
  var leadbay_qualify_top_n = `
280
506
  Qualify the top {{arg:count_or_default}} un-qualified leads in the active Leadbay lens.
281
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
+
282
513
  # PHASE 1 \u2014 LAUNCH
283
514
  Call \`leadbay_bulk_qualify_leads\` with \`count={{arg:count_or_default}}\`.
284
515
 
285
516
  # PHASE 2 \u2014 POLL
286
- 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
+
287
532
 
288
533
  # PHASE 3 \u2014 SUMMARIZE
289
534
 
290
- When \`bulk_qualify_leads\` returns, summarize:
291
- - How many qualified (name the count)
292
- - How many are still running (name them, by lead_id + lead name if available, so I can poll again later)
293
- - 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\` or \`leadbay_research_company\`.
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.
294
619
 
295
620
  # PHASE 4 \u2014 RECOMMEND
296
621
  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.
@@ -312,41 +637,156 @@ var leadbay_research_a_domain = `
312
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.
313
638
 
314
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
+
315
645
  Research the company with domain '{{arg:domain}}' for me using Leadbay.
316
646
 
317
647
  # PHASE 1 \u2014 IMPORT + QUALIFY
318
- 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\` in NEXT STEPS.
665
+
319
666
 
320
667
  # PHASE 2 \u2014 DEEP DIVE
321
- When the import resolves, call \`leadbay_research_lead\` on the new leadId.
668
+ When the import resolves, call \`leadbay_research_lead\` 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\`, 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
+
322
744
 
323
745
  # PHASE 3 \u2014 SUMMARY
324
- Summarize:
746
+ Place a 2\u20133 sentence summary ABOVE the card with:
325
747
  - Who is this company (1 sentence)
326
748
  - Their fit (cite specific \`qualification_answers\` from the qualification response)
327
- - What signals stand out (cite specific research findings)
328
- - 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)
329
750
 
330
- 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.
331
752
  `;
332
753
  var PROMPT_META = {
333
- leadbay_daily_check_in: { "name": "leadbay_daily_check_in", "short_description": `Run the canonical daily check-in: see account state, pull a fresh batch,
334
- triage the top 10, deep-dive on every promising one, and offer contact
335
- enrichment. The user's typical morning workflow. Trigger when the user
336
- asks for "leadbay leads", "best leads to prospect today", "what should
337
- I work on", or anything resembling "show me the day's batch".
338
- `, "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", "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 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', '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", "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)'] },
339
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"] },
340
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.", "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"] },
341
- 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"] },
772
+ leadbay_prospecting_overview: { "name": "leadbay_prospecting_overview", "short_description": `Orientation for working with Leadbay from any host \u2014 discovery vs.
773
+ follow-up, the outreach loop, outcome recording, imports, pushback /
774
+ snooze, and the connected-outreach-tool registry. Trigger when the
775
+ conversation involves Leadbay leads, prospecting, pipeline, follow-up,
776
+ outreach, or lens / ICP \u2014 anything from "show me my leads" to "what
777
+ should I follow up on" to "I'll send via lemlist".
778
+ `, "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", "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"], "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"] },
342
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"] },
343
- 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"], "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 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"] }
344
782
  };
345
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.`;
346
784
  var PROMPT_CATALOG_BULLETS = {
347
- 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.`,
348
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.`,
349
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.`,
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".`,
350
790
  leadbay_qualify_top_n: `- \`leadbay_qualify_top_n\` (optional args: count): Bulk-qualify the top N un-qualified leads in the active lens. Uses leadbay_bulk_qualify_leads with a sensible default budget.`,
351
791
  leadbay_refine_audience: `- \`leadbay_refine_audience\` (required args: instruction): Refine the kind of leads Leadbay surfaces beyond firmographics, with a free-text instruction. Handles the clarification round-trip if the new prompt is ambiguous.`,
352
792
  leadbay_research_a_domain: `- \`leadbay_research_a_domain\` (required args: domain): Import a company by domain and run deep qualification + research in one pass. Use when a colleague mentions a name and you want everything Leadbay knows about it.`
@@ -370,6 +810,12 @@ var CATALOG = [
370
810
  arguments: [],
371
811
  render: () => [userMessage(leadbay_daily_check_in)]
372
812
  },
813
+ {
814
+ name: "leadbay_prospecting_overview",
815
+ description: PROMPT_META.leadbay_prospecting_overview.short_description,
816
+ arguments: [],
817
+ render: () => [userMessage(leadbay_prospecting_overview)]
818
+ },
373
819
  {
374
820
  name: "leadbay_research_a_domain",
375
821
  description: PROMPT_META.leadbay_research_a_domain.short_description,
@@ -598,6 +1044,7 @@ function buildRhythmParagraph(has) {
598
1044
  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.";
599
1045
  }
600
1046
  var TOOL_REFERENCE_PATTERN = /\bleadbay_[a-z][a-z0-9_]*\b/g;
1047
+ var PROMPT_NAMES = new Set(Object.keys(PROMPT_CATALOG_BULLETS));
601
1048
  function buildPromptsCatalogParagraph(has) {
602
1049
  const safeBullets = [];
603
1050
  for (const [promptName, bullet] of Object.entries(PROMPT_CATALOG_BULLETS)) {
@@ -605,6 +1052,7 @@ function buildPromptsCatalogParagraph(has) {
605
1052
  for (const match of bullet.matchAll(TOOL_REFERENCE_PATTERN)) {
606
1053
  const name = match[0];
607
1054
  if (name === promptName) continue;
1055
+ if (PROMPT_NAMES.has(name)) continue;
608
1056
  referencedTools.add(name);
609
1057
  }
610
1058
  const allExposed = [...referencedTools].every((n) => has(n));
@@ -725,7 +1173,7 @@ function buildServer(client, opts = {}) {
725
1173
  }
726
1174
  const exposedNames = new Set(toolByName.keys());
727
1175
  const server = new Server(
728
- { name: "leadbay", version: "0.6.2" },
1176
+ { name: "leadbay", version: opts.version ?? "0.0.0-dev" },
729
1177
  {
730
1178
  capabilities: {
731
1179
  tools: {},
@@ -925,7 +1373,7 @@ function buildServer(client, opts = {}) {
925
1373
 
926
1374
  // src/bin.ts
927
1375
  import { createRequire } from "module";
928
- var VERSION = "0.8.0";
1376
+ var VERSION = "0.9.1";
929
1377
  var HELP = `
930
1378
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
931
1379
 
@@ -1214,7 +1662,7 @@ async function runLogin(args) {
1214
1662
  let result;
1215
1663
  try {
1216
1664
  if (pinnedRegion && !allowFallback) {
1217
- const { REGIONS } = await import("./dist-HS5N4SIS.js");
1665
+ const { REGIONS } = await import("./dist-X5AOPZ2V.js");
1218
1666
  const baseUrl = REGIONS[pinnedRegion];
1219
1667
  const c = createClient({ region: pinnedRegion });
1220
1668
  const token = await loginAt(baseUrl, email, password);
@@ -1706,7 +2154,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
1706
2154
  let region;
1707
2155
  try {
1708
2156
  if (pinnedRegion && !allowFallback) {
1709
- const { REGIONS } = await import("./dist-HS5N4SIS.js");
2157
+ const { REGIONS } = await import("./dist-X5AOPZ2V.js");
1710
2158
  const baseUrl = REGIONS[pinnedRegion];
1711
2159
  token = await loginAt(baseUrl, email, password);
1712
2160
  region = pinnedRegion;
@@ -1850,7 +2298,8 @@ async function main() {
1850
2298
  includeAdvanced,
1851
2299
  includeWrite,
1852
2300
  logger,
1853
- bulkTracker
2301
+ bulkTracker,
2302
+ version: VERSION
1854
2303
  });
1855
2304
  const transport = new StdioServerTransport();
1856
2305
  logger.info?.(