@leadbay/mcp 0.24.2 → 0.27.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/CHANGELOG.md +12 -0
- package/README.md +4 -2
- package/dist/bin.js +724 -255
- package/dist/http-server.js +874 -276
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/dist/http-server.js
CHANGED
|
@@ -39,7 +39,9 @@ var leadbay_build_campaign = `
|
|
|
39
39
|
Before responding, glance at any \`_meta.agent_memory.summary\` returned by tool calls earlier in this session and reflect its top signals in your reasoning ("Filtering by your stated preference for healthcare"). After any material new signal from the user this conversation (sector, region, deal size, communication style, qualification rule, explicit retraction, or recurrence / scheduling preference such as "I do this every day" or "remind me every morning"), call \`leadbay_agent_memory_capture\` to persist it: \`source:"user_stated"\` if literal, \`source:"inferred"\` with confidence <=6 if inferred.
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
Build me a Leadbay campaign from scratch{{arg:campaign_name_paren}}. {{arg:audience_block}}
|
|
42
|
+
Build me a Leadbay campaign from scratch{{arg:campaign_name_paren}} \u2014 a cohort of **{{arg:count_or_default}}** fully-actionable leads: each in-ICP, high \`ai_agent_lead_score\`, AND with a reachable buyer contact. {{arg:audience_block}} {{arg:job_titles_block}}
|
|
43
|
+
|
|
44
|
+
**Run this end-to-end, autonomously, without pausing.** Do NOT stop to confirm the audience, do NOT stop to confirm the enrichment spend, do NOT ask me to pick, and do NOT stop to hand off \u2014 just keep discovering, qualifying, enriching, and swapping until the cohort holds **{{arg:count_or_default}}** leads that each meet EVERY requirement (in-ICP, high \`ai_agent_lead_score\`, and a reachable target-title contact whose email/phone actually landed). The ONLY reasons to stop short: the lens genuinely can't supply that many buyer-ready in-ICP leads, or enrichment quota is exhausted (a backend 429). In those cases, finish with whatever you locked and tell me plainly how many you got and why it stopped. Enrichment consumes quota, not credits \u2014 never pre-refuse on a credit balance.
|
|
43
45
|
|
|
44
46
|
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.
|
|
45
47
|
|
|
@@ -77,14 +79,14 @@ If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retr
|
|
|
77
79
|
|
|
78
80
|
Call \`leadbay_account_status\` to see my remaining **quota** and my **active lens**. Enrichment (Phase 3) consumes quota \u2014 email + phone reveals draw on the per-window allowance. Reason in quota, NOT in "credits": there is no separate credit wall to clear, and a freemium/fresh account with quota left can enrich even if a credit counter reads 0. Never pre-refuse enrichment on a credit balance. If \`organization.unlimited_credits\` is true, this is an internal/unlimited account: proceed freely and say nothing about quota or credits.
|
|
79
81
|
|
|
80
|
-
Resolve the audience:
|
|
82
|
+
Resolve the audience (do NOT stop to ask):
|
|
81
83
|
|
|
82
84
|
- **Default \u2014 use my active lens.** If I didn't name a fresh audience, the active lens IS the audience. Do NOT create a new lens.
|
|
83
|
-
- **Fresh-audience fork.**
|
|
85
|
+
- **Fresh-audience fork.** If I described a NEW audience the active lens doesn't already cover, set it up first \u2014 \`leadbay_adjust_audience\` for sector/size tweaks, or \`leadbay_new_lens\` to create a brand-new named lens \u2014 then continue on that lens. Naming the audience IS my authorization; switch without asking. Just state in one line which lens you're building on.
|
|
84
86
|
|
|
85
87
|
# PHASE 1 \u2014 DISCOVER
|
|
86
88
|
|
|
87
|
-
Call \`leadbay_pull_leads\` on the resolved lens. **Capture \`response.lens.id\` and pass it as an explicit \`lensId\` on every later call this session** \u2014 a mid-session lens shift would discard the cohort I'm
|
|
89
|
+
Call \`leadbay_pull_leads\` on the resolved lens. **Capture \`response.lens.id\` and pass it as an explicit \`lensId\` on every later call this session** \u2014 a mid-session lens shift would discard the cohort I'm building. Render each batch you show with the canonical layout:
|
|
88
90
|
|
|
89
91
|
## RENDERING \u2014 markdown table, three columns, score-bar driven
|
|
90
92
|
|
|
@@ -158,47 +160,45 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
158
160
|
|
|
159
161
|
|
|
160
162
|
|
|
161
|
-
|
|
163
|
+
The target is **{{arg:count_or_default}}** buyer-ready in-ICP leads, so keep the pipeline deep. Whenever the workable in-ICP pool is thinner than ~1.5\xD7 the target, top it up: call \`leadbay_bulk_qualify_leads({lensId:<captured>, count:<deficit, max 25 per call>, wait_for_completion:false})\`, poll \`leadbay_qualify_status\` until done, then re-pull with the same \`lensId\`. Repeat this qualify\u2192re-pull loop as many times as needed to feed Phases 2\u20133. Never re-pull without \`lensId\`.
|
|
162
164
|
|
|
163
165
|
# PHASE 2 \u2014 PICK AN ICP CANDIDATE POOL
|
|
164
166
|
|
|
165
|
-
A campaign is only as good as the leads in it \u2014 AND only as good as whether each lead has a reachable BUYER (see Phase 3). So
|
|
167
|
+
A campaign is only as good as the leads in it \u2014 AND only as good as whether each lead has a reachable BUYER (see Phase 3). So build a **generous candidate pool**, not the final cohort: aim for ~1.5\xD7 the target ({{arg:count_or_default}}) of in-ICP leads (highest \`ai_agent_lead_score\`), so Phase 3 can drop any lead that turns out to have no buyer contact and still reach the target. If the pool is short, top up via \`leadbay_bulk_qualify_leads\` / \`leadbay_extend_lens\` and loop back \u2014 keep going until the pool is deep enough to yield the target after coverage filtering.
|
|
166
168
|
|
|
167
|
-
If I named specific leads,
|
|
169
|
+
If I named specific leads, seed with those (still apply the Phase 3 buyer-coverage check). Otherwise pick the top-scoring in-ICP leads yourself \u2014 do NOT ask me to choose. Capture the candidate \`leadIds\`. Do NOT create the campaign yet \u2014 the final cohort is locked after Phase 3's coverage check.
|
|
168
170
|
|
|
169
171
|
# PHASE 3 \u2014 ENRICH THE RIGHT CONTACTS (load-bearing)
|
|
170
172
|
|
|
171
|
-
This is the phase that decides whether the campaign is worth a salesperson's time. Contacts aren't attached by default and enrichment is paid \u2014 so spend it ONLY on the people who would actually **buy what I sell**, not on whoever is most senior.
|
|
173
|
+
This is the phase that decides whether the campaign is worth a salesperson's time. Contacts aren't attached by default and enrichment is paid \u2014 so spend it ONLY on the people who would actually **buy what I sell**, at the target titles, not on whoever is most senior.
|
|
172
174
|
|
|
173
|
-
**Step A \u2014
|
|
174
|
-
Figure out what *I* sell and therefore who, inside the target company, owns the decision to buy it:
|
|
175
|
+
**Step A \u2014 settle the target titles / buyer persona.**
|
|
175
176
|
|
|
176
|
-
-
|
|
177
|
-
-
|
|
177
|
+
- **If I named target titles at the top of this request:** those ARE the persona \u2014 enrich exactly those titles. Do NOT re-derive and do NOT substitute "more senior" titles. If a given title looks off for what I sell, you may note it in one line, but honor my titles.
|
|
178
|
+
- **If I did NOT name titles:** derive my buyer persona yourself (do NOT ask me). Infer my product / value-prop from my org + account (\`leadbay_account_status\`) and especially my lens's \`qualification_summary\` \u2014 it tells you *why* these companies are targets, which implies what I'm offering. Then map value-prop \u2192 the **buying department/persona**, NOT seniority:
|
|
178
179
|
- A sales / prospecting / lead-gen / outbound / marketing / GTM / revenue tool \u2192 the **revenue org**: VP / Head / Director of Sales, Business Development, Account/Carrier Sales, CRO, CMO / VP Marketing, Head of Growth / Demand Gen, RevOps. (This is Leadbay's own persona.)
|
|
179
180
|
- An operations / logistics tool \u2192 operations leaders. A finance tool \u2192 finance. A dev tool \u2192 engineering. Etc.
|
|
180
|
-
- **Company size caveat:** Founder / CEO / Owner is a real buyer at small companies (\u2264~50), but at larger ones
|
|
181
|
-
-
|
|
181
|
+
- **Company size caveat:** Founder / CEO / Owner is a real buyer at small companies (\u2264~50), but at larger ones the functional leader (e.g. VP Sales) is the buyer.
|
|
182
|
+
- State the persona in one line \u2014 for the record, NOT to wait for my approval.
|
|
182
183
|
|
|
183
|
-
**ANTI-PATTERN \u2014 do NOT do this:** picking the most senior or most "decision-maker-sounding" title regardless of department. A Director of Operations, COO, Mgr of Logistics, CFO, or CTO will **never** buy a sales tool \u2014 enriching them wastes
|
|
184
|
+
**ANTI-PATTERN \u2014 do NOT do this:** picking the most senior or most "decision-maker-sounding" title regardless of department. A Director of Operations, COO, Mgr of Logistics, CFO, or CTO will **never** buy a sales tool \u2014 enriching them wastes quota and hands me a useless list. Seniority is not the same as being my buyer.
|
|
184
185
|
|
|
185
186
|
**Step B \u2014 find the persona-matching, enrichable contacts.**
|
|
186
|
-
Call \`leadbay_recall_ordered_titles({leadIds, lensId})\` and \`leadbay_enrich_titles({leadIds, lensId})\` in **discovery mode** (no \`titles\`). These return previously-enriched titles, \`title_suggestions\`, \`auto_included_titles\`, \`available_in_selection\`, \`enrichable_contacts\`, and \`credits_remaining\`. Treat them as a **menu to filter against my
|
|
187
|
+
Call \`leadbay_recall_ordered_titles({leadIds, lensId})\` and \`leadbay_enrich_titles({leadIds, lensId})\` in **discovery mode** (no \`titles\`). These return previously-enriched titles, \`title_suggestions\`, \`auto_included_titles\`, \`available_in_selection\`, \`enrichable_contacts\`, and \`credits_remaining\`. Treat them as a **menu to filter against my target titles \u2014 not the answer.** If past-enriched titles or suggestions are off-persona (e.g. operations roles for a sales tool), do NOT repeat them. Select the titles that match my target persona AND are actually enrichable.
|
|
187
188
|
|
|
188
|
-
**Step B.5 \u2014 coverage guarantee (lock the
|
|
189
|
+
**Step B.5 \u2014 coverage guarantee + run-to-goal (lock the cohort here).** A campaign where leads have no buyer is a failed campaign, and I asked for **{{arg:count_or_default}}** actionable leads \u2014 so this step LOOPS until you have that many. For each candidate, determine whether it has an **enrichable target-title contact** \u2014 use the discovery data plus, where it's ambiguous, a quick \`leadbay_research_lead_by_id\` to see that lead's available contact titles. Then:
|
|
189
190
|
|
|
190
|
-
- **KEEP** candidates that have \u22651 enrichable
|
|
191
|
-
- **SWAP OUT** candidates whose only contacts are off-persona (e.g. ops/dispatch/finance only) or who have no enrichable contact at all. Replace each with the **highest-\`ai_agent_lead_score\` in-ICP candidate** from the pool that DOES have a buyer
|
|
191
|
+
- **KEEP** candidates that have \u22651 enrichable target-title contact.
|
|
192
|
+
- **SWAP OUT** candidates whose only contacts are off-persona (e.g. ops/dispatch/finance only) or who have no enrichable contact at all. Replace each with the **highest-\`ai_agent_lead_score\` in-ICP candidate** from the pool that DOES have a buyer.
|
|
193
|
+
- **Keep pulling more.** If keeps + available swaps still fall short of {{arg:count_or_default}}, go back to Phase 1/2 (\`leadbay_bulk_qualify_leads\` / \`leadbay_extend_lens\`, re-pull, re-check coverage) and keep going until you have {{arg:count_or_default}} buyer-covered in-ICP leads \u2014 or the lens is genuinely exhausted.
|
|
192
194
|
- **Do NOT trade ICP fit for coverage.** A lead with a buyer but weak ICP fit (low \`ai_agent_lead_score\`, a vertical that doesn't match what I sell) is still the wrong lead \u2014 coverage is a filter applied AFTER ICP, never a reason to admit an off-ICP company. The final cohort must be both high-ICP AND buyer-covered.
|
|
193
|
-
- If the lens genuinely can't supply
|
|
194
|
-
|
|
195
|
-
Tell me what you swapped in one line ("dropped Corbett + RBS \u2014 ops-only; swapped in Acme + Globex which have Sales VPs"). The goal is a final cohort where EVERY lead has a real buyer to call.
|
|
195
|
+
- If the lens genuinely can't supply {{arg:count_or_default}} buyer-ready in-ICP leads, lock what you have, and after the call sheet tell me how many you reached and offer to widen/extend \u2014 do NOT pad with no-buyer or off-ICP leads.
|
|
196
196
|
|
|
197
|
-
|
|
197
|
+
Tell me what you swapped in one line ("dropped Corbett + RBS \u2014 ops-only; swapped in Acme + Globex which have Sales VPs").
|
|
198
198
|
|
|
199
|
-
**Step
|
|
199
|
+
**Step C \u2014 enrich (NO confirm gate \u2014 just spend).** You do NOT need my permission: I authorized this spend by asking for the campaign. Do NOT call \`ask_user_input_v0\`, do NOT ask "enrich these N now?", do NOT wait. State the persona + titles + "enriching {enrichable_contacts} contacts (email + phone, consumes quota)" in one line for the record, then immediately launch: \`leadbay_enrich_titles({leadIds, lensId, titles:[...chosen], email:true, phone:true})\`. Enrich up to {{arg:count_or_default}} best target-title contacts. Do NOT quote a "credits" figure or refuse on a credit balance \u2014 the only real limit is quota (a backend 429). If a 429 stops you mid-run, keep the leads already enriched, note how many landed, and continue to Phase 4 with those.
|
|
200
200
|
|
|
201
|
-
|
|
201
|
+
**Step D \u2014 poll + count only landed.** Poll \`leadbay_bulk_enrich_status\` until done (enrichment can take several minutes \u2014 keep polling, don't render an empty sheet prematurely). Once \`all_done\`, call \`leadbay_account_status\` and show my refreshed quota so I see what the run consumed. A lead only counts toward the {{arg:count_or_default}} once its target-title contact actually landed (email/phone present); if some came back empty, swap + enrich replacements (loop back to Step B.5) until the cohort is genuinely {{arg:count_or_default}} deep or the lens is exhausted.
|
|
202
202
|
|
|
203
203
|
# PHASE 4 \u2014 CREATE THE CAMPAIGN
|
|
204
204
|
|
|
@@ -206,30 +206,29 @@ Derive a name (\`<lens or audience> \u2013 <today's date>\`) or use the one I ga
|
|
|
206
206
|
|
|
207
207
|
# PHASE 5 \u2014 THE VIEW (call / email ready)
|
|
208
208
|
|
|
209
|
-
|
|
209
|
+
**Poll \`leadbay_bulk_enrich_status\` until it's actually done before rendering** \u2014 do not render a "still enriching" sheet with empty contact cells; the whole point is the landed phones/emails. Enrichment can take several minutes; keep polling.
|
|
210
210
|
|
|
211
211
|
Then call \`leadbay_campaign_call_sheet({campaign_id})\` and render it per its RENDERING block \u2014 one card per lead, contacts with \`[phone](tel:)\` + \`[email](mailto:)\` one-tap links, the readiness chip at the top, map optional. This is the view I work from: scan \u2192 tap to call \u2192 tap to email.
|
|
212
212
|
|
|
213
213
|
**Flag suspect contacts** so I don't email the wrong person blind: mark with \u26A0 any enriched contact whose email domain doesn't match the company's website, or who shows up on more than one lead in this campaign (a sign of a mis-attributed enrichment). Keep the phone (it's usually still right) but tell me the email looks off.
|
|
214
214
|
|
|
215
|
-
# PHASE 6 \u2014
|
|
216
|
-
|
|
217
|
-
The campaign exists and is call/email ready. End by offering, via \`ask_user_input_v0\`:
|
|
215
|
+
# PHASE 6 \u2014 DONE (no handoff prompt)
|
|
218
216
|
|
|
219
|
-
|
|
220
|
-
- "See the pulse" \u2192 \`leadbay_campaign_progression\` for per-lead status.
|
|
217
|
+
The campaign exists and is call/email ready. State in one line how many actionable leads landed vs. the {{arg:count_or_default}} target, and \u2014 as plain text, NOT an \`ask_user_input_v0\` question \u2014 mention I can work it later with \`leadbay_work_campaign\` (the calling/email + outcome-logging loop) or check its pulse with \`leadbay_campaign_progression\`. Then STOP.
|
|
221
218
|
|
|
222
|
-
|
|
219
|
+
Building a campaign is NOT outreaching \u2014 do not send anything and do not call \`leadbay_report_outreach\`. Do not run \`leadbay_work_campaign\` yourself; that's a separate session I start when I'm ready to call.
|
|
223
220
|
|
|
224
221
|
# Iron laws
|
|
225
222
|
|
|
226
|
-
-
|
|
227
|
-
-
|
|
228
|
-
-
|
|
229
|
-
-
|
|
223
|
+
- **Run to the goal, autonomously.** Keep discovering \u2192 qualifying \u2192 enriching \u2192 swapping until the cohort holds {{arg:count_or_default}} leads that are ALL in-ICP, high-score, and buyer-covered \u2014 or the lens is genuinely exhausted. Do NOT stop early, do NOT ask me to pick, do NOT hand off mid-flow.
|
|
224
|
+
- **No confirm gates. No pauses.** Do NOT confirm the audience switch, and do NOT confirm the enrichment spend (no \`ask_user_input_v0\` before enriching) \u2014 asking for the campaign IS the authorization. The only acceptable stops are lens exhaustion or a backend 429.
|
|
225
|
+
- Enrichment targets MY buyer titles \u2014 the people who would actually buy what *I* sell (my given titles, or the persona derived from my product/ICP) \u2014 NOT generic seniority. For a sales/prospecting tool that means the revenue org; a Director of Operations, COO, or logistics manager is useless no matter how senior.
|
|
226
|
+
- Selection is DATA-DRIVEN (\`leadbay_recall_ordered_titles\` + \`leadbay_enrich_titles\` discovery) but FILTERED to the target titles \u2014 never blindly repeat past-enriched or suggested titles that don't match who buys my product.
|
|
227
|
+
- The FINAL cohort must be all buyer-ready: a lead counts only once its target-title contact actually landed. Drop/swap + re-enrich any lead with no reachable buyer rather than shipping it empty.
|
|
228
|
+
- Enrichment consumes quota \u2014 never show a "credits" figure or refuse on a credit balance; the gate is quota (or a backend 429), not credits.
|
|
230
229
|
- Qualify / pick BEFORE \`leadbay_create_campaign\` \u2014 never seed a campaign with unvetted leads.
|
|
231
230
|
- Carry the captured \`lensId\` on every call. A lens shift loses the cohort.
|
|
232
|
-
- End at the rendered call sheet
|
|
231
|
+
- End at the rendered call sheet. Do NOT re-implement the calling / follow-up loop here, do NOT run \`leadbay_work_campaign\` yourself, and do NOT call \`leadbay_report_outreach\`.
|
|
233
232
|
`;
|
|
234
233
|
var leadbay_daily_check_in = `
|
|
235
234
|
## MEMORY
|
|
@@ -568,7 +567,7 @@ Build the final mappings yourself. Start from \`leadbay_resolve_import_rows.mapp
|
|
|
568
567
|
|
|
569
568
|
# PHASE 5 \u2014 QUALIFY (optional) + REPORT
|
|
570
569
|
|
|
571
|
-
Prefer \`leadbay_import_and_qualify\` when the user asks to qualify/research after import; otherwise use \`leadbay_import_leads\`. For large files or short client timeouts, pass \`wait_for_completion=false\` and poll \`leadbay_import_status\`. After import, qualify only lead IDs returned by the import;
|
|
570
|
+
Prefer \`leadbay_import_and_qualify\` when the user asks to qualify/research after import; otherwise use \`leadbay_import_leads\`. For large files or short client timeouts, pass \`wait_for_completion=false\` and poll \`leadbay_import_status\`. After import, qualify only lead IDs returned by the import. Rows that came back \`uncrawled\` are pending a background crawl (not failures); the leads Leadbay adds for them populate in the user's Leadbay account as the crawl completes \u2014 tell the user that, not that a tool call will fetch them (\`import_status\` refreshes status/progress only; \`pull_leads\` reads the active lens, so an imported lead outside it may not appear; re-running the import later re-reconciles those companies).
|
|
572
571
|
|
|
573
572
|
**Deliver the augmented file back to the user**: the original file plus a new \`LEADBAY_ID\` column populated from the resolution step. This is the second deliverable of a job well done.
|
|
574
573
|
|
|
@@ -1291,7 +1290,7 @@ Optional: offer to review the \`leadbay_campaign_progression\` for the same camp
|
|
|
1291
1290
|
- If the user dictates an outcome that doesn't cleanly map to one of the four epilogue values, ASK ONCE before guessing.
|
|
1292
1291
|
`;
|
|
1293
1292
|
var PROMPT_META = {
|
|
1294
|
-
leadbay_build_campaign: { "name": "leadbay_build_campaign", "short_description": 'Build a sales campaign from scratch
|
|
1293
|
+
leadbay_build_campaign: { "name": "leadbay_build_campaign", "short_description": 'Build a sales campaign from scratch, autonomously, to a target size:\ndiscover on the lens, qualify, and enrich the buyer titles until `count`\nleads each have a reachable target-title contact \u2014 no pauses, no confirm\ngates. Saves via `leadbay_create_campaign` and renders a one-tap\ncall/email view via `leadbay_campaign_call_sheet`. Trigger on "build me a\ncampaign", "build N leads", "create a campaign from scratch". Work an\nexisting one with `leadbay_work_campaign`.\n', "arguments": [{ "name": "audience", "description": "Optional: a fresh audience to target (e.g. 'dental clinics in Texas'). Omit to build from your ACTIVE lens \u2014 the default.", "required": false }, { "name": "campaign_name", "description": "Optional: a name for the campaign. Omit and one is derived from the lens/audience + date (or the backend AI-names it).", "required": false }, { "name": "count", "description": "Optional: how many fully-actionable leads to build (default 20). The loop keeps discovering, qualifying and enriching until this many in-ICP leads each have a reachable target-title contact \u2014 or the lens is exhausted. Higher counts take longer and consume more quota.", "required": false }, { "name": "job_titles", "description": "Optional: the exact buyer job titles to enrich, comma-separated (e.g. 'VP Sales, Head of Growth, Director of Business Development'). Omit and the buyer persona is derived from what you sell. A lead only counts toward the target when it has a reachable contact matching one of these titles.", "required": false }], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_bulk_qualify_leads", "leadbay_qualify_status", "leadbay_recall_ordered_titles", "leadbay_enrich_titles", "leadbay_bulk_enrich_status", "leadbay_create_campaign", "leadbay_add_leads_to_campaign", "leadbay_campaign_call_sheet", "leadbay_campaign_progression", "leadbay_new_lens", "leadbay_adjust_audience"], "failure_modes": ["Pauses to confirm before enriching (or asks 'enrich these N now?' via ask_user_input_v0) \u2014 this prompt runs to goal with NO confirm gate; asking for the campaign IS the authorization. Never stop for a spend confirmation.", "Stops to confirm a lens switch when the user named a fresh audience \u2014 naming the audience IS the authorization; switch, state which lens in one line, and don't ask.", "Pauses at any point to ask the user to choose, confirm, or hand off \u2014 the only acceptable stops are lens exhaustion (can't supply the count) or a backend 429 (quota out). Anything else is purpose drift.", "Stops at fewer than the target `count` of actionable leads without looping back to pull / qualify / enrich more \u2014 must run to the target, or honestly report the lens is exhausted and offer to widen.", "Counts a lead toward `count` before its target-title contact actually landed (email/phone present) \u2014 an empty enrichment doesn't count; swap and re-enrich until the cohort is genuinely `count` deep.", "Enriches by seniority instead of by buyer persona \u2014 picks COO / Director of Operations / Mgr of Logistics / CFO / CTO because they sound senior, when the user sells a SALES tool whose buyer is the revenue org (VP/Head/Director of Sales, BD, growth, marketing). Operations people never buy a sales tool; this hands the salesperson a useless list.", "When no titles are given, fails to derive the user's buyer persona from their product/ICP before choosing titles \u2014 jumps straight to generic exec titles instead of working out who buys what THIS user sells. (When titles ARE given, use them verbatim \u2014 don't substitute 'more senior' ones.)", "Blindly repeats leadbay_recall_ordered_titles / discovery suggestions even when they are off-persona (e.g. operations roles a prior session wrongly enriched) \u2014 recall is a filtered input, not the answer.", "Poor coverage \u2014 leaves picked leads with no target-title contact (or 0 enrichments on some leads) and ships them anyway, so the salesperson opens the campaign to half-empty rows. Swap them out and refill to the count instead.", "Creates the campaign before qualifying / picking \u2014 seeds a campaign with unvetted leads. Qualify and lock the buyer-covered cohort FIRST, then leadbay_create_campaign.", "Ends at 'campaign created' without rendering the leadbay_campaign_call_sheet view \u2014 the ready-to-work view IS the deliverable; stopping short is purpose drift.", "Runs the calling / outcome / follow-up loop, or calls leadbay_work_campaign itself, instead of stopping at the call sheet \u2014 this prompt BUILDS and stops; work_campaign is a separate session the user starts later.", "Auto-sends outreach or calls leadbay_report_outreach \u2014 building a campaign is not outreaching. No send, no log.", "Re-pulls leadbay_pull_leads without the captured lensId \u2014 a mid-session lens shift discards the cohort being built.", "Renders the picked leads or the call sheet as prose instead of the canonical per-tool RENDERING layout."] },
|
|
1295
1294
|
leadbay_daily_check_in: { "name": "leadbay_daily_check_in", "short_description": 'Morning DISCOVERY workflow \u2014 new leads from the lens wishlist. Trigger\non "show me leads", "what\'s new today", "let\'s prospect", "run my check-in",\n"my morning check-in", "I do this every day", "every morning". Recurrence\nlanguage always means this prompt. Do NOT trigger on follow-up phrasings\n("follow up", "before my trip") \u2014 those go to `leadbay_followup_check_in`.\n', "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 when the user's intent is to research specific leads; Phase 4 is intentionally skipped for batch-view requests ("show me today's leads", "run my morning check-in") per the Phase 4 skip gate`, "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'] },
|
|
1296
1295
|
leadbay_extend_my_lens: { "name": "leadbay_extend_my_lens", "short_description": "Add more leads to the current lens on demand \u2014 for users whose appetite\nexceeds the standard daily fill. The agent picks seeds silently from\nwhat's already on the lens, fires the extra refill, and surfaces the\nqueue confirmation. The user never reviews the seed list.\n", "arguments": [{ "name": "extra_count", "description": "How many extra leads to add. Optional. Omit to use the backend default.", "required": false }], "expected_calls": ["leadbay_account_status", "leadbay_seed_candidates", "leadbay_extend_lens", "leadbay_pull_leads"], "failure_modes": ["Surfaces the seed candidate list to the user instead of picking silently \u2014 the user asked for MORE LEADS, not a candidate review meeting", "Skips the seeded path and calls `leadbay_extend_lens` with no `seed_lead_ids`, losing the bias signal the recommender needs", "On 429, silently retries instead of surfacing the three options (smaller / wait / upgrade) via your host's choice widget (`ask_user_input_v0` or `AskUserQuestion`)", "Forgets to pre-check `LENS_EXTRA_REFILL` quota in `leadbay_account_status` and burns a wasted API call", "Skips the post-queue pull-leads suggestion, so the user doesn't see what just got added"] },
|
|
1297
1296
|
leadbay_followup_check_in: { "name": "leadbay_followup_check_in", "short_description": 'Follow-up check-in: surface KNOWN leads from the Monitor view needing\nre-engagement. Trigger on "follow up", "already known leads", "what\'s\noverdue", "before my trip", "who should I re-engage". Do NOT trigger on\n"show me today\'s leads", "my morning check-in", "run my check-in",\n"I do this every day", "every morning" \u2014 those go to\n`leadbay_daily_check_in`.\n', "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)'] },
|
|
@@ -1313,7 +1312,7 @@ should I follow up on" to "I'll send via lemlist".
|
|
|
1313
1312
|
};
|
|
1314
1313
|
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.`;
|
|
1315
1314
|
var PROMPT_CATALOG_BULLETS = {
|
|
1316
|
-
leadbay_build_campaign: `- \`leadbay_build_campaign\` (optional args: audience, campaign_name): Build a sales campaign from scratch
|
|
1315
|
+
leadbay_build_campaign: `- \`leadbay_build_campaign\` (optional args: audience, campaign_name, count, job_titles): Build a sales campaign from scratch, autonomously, to a target size: discover on the lens, qualify, and enrich the buyer titles until \`count\` leads each have a reachable target-title contact \u2014 no pauses, no confirm gates. Saves via \`leadbay_create_campaign\` and renders a one-tap call/email view via \`leadbay_campaign_call_sheet\`. Trigger on "build me a campaign", "build N leads", "create a campaign from scratch". Work an existing one with \`leadbay_work_campaign\`.`,
|
|
1317
1316
|
leadbay_daily_check_in: `- \`leadbay_daily_check_in\`: Morning DISCOVERY workflow \u2014 new leads from the lens wishlist. Trigger on "show me leads", "what's new today", "let's prospect", "run my check-in", "my morning check-in", "I do this every day", "every morning". Recurrence language always means this prompt. Do NOT trigger on follow-up phrasings ("follow up", "before my trip") \u2014 those go to \`leadbay_followup_check_in\`.`,
|
|
1318
1317
|
leadbay_extend_my_lens: `- \`leadbay_extend_my_lens\` (optional args: extra_count): Add more leads to the current lens on demand \u2014 for users whose appetite exceeds the standard daily fill. The agent picks seeds silently from what's already on the lens, fires the extra refill, and surfaces the queue confirmation. The user never reviews the seed list.`,
|
|
1319
1318
|
leadbay_followup_check_in: `- \`leadbay_followup_check_in\`: Follow-up check-in: surface KNOWN leads from the Monitor view needing re-engagement. Trigger on "follow up", "already known leads", "what's overdue", "before my trip", "who should I re-engage". Do NOT trigger on "show me today's leads", "my morning check-in", "run my check-in", "I do this every day", "every morning" \u2014 those go to \`leadbay_daily_check_in\`.`,
|
|
@@ -1474,16 +1473,31 @@ var CATALOG = [
|
|
|
1474
1473
|
name: "campaign_name",
|
|
1475
1474
|
description: "Optional: a name for the campaign. Omit and one is derived from the lens/audience + date (or the backend AI-names it).",
|
|
1476
1475
|
required: false
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
name: "count",
|
|
1479
|
+
description: "Optional: how many fully-actionable leads to build (default 20). The loop keeps discovering, qualifying and enriching until this many in-ICP leads each have a reachable target-title contact \u2014 or the lens is exhausted. Higher counts take longer and consume more quota.",
|
|
1480
|
+
required: false
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
name: "job_titles",
|
|
1484
|
+
description: "Optional: the exact buyer job titles to enrich, comma-separated (e.g. 'VP Sales, Head of Growth, Director of Business Development'). Omit and the buyer persona is derived from what you sell. A lead only counts toward the target when it has a reachable contact matching one of these titles.",
|
|
1485
|
+
required: false
|
|
1477
1486
|
}
|
|
1478
1487
|
],
|
|
1479
|
-
render: (args) =>
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1488
|
+
render: (args) => {
|
|
1489
|
+
const n = args.count ?? "20";
|
|
1490
|
+
return [
|
|
1491
|
+
userMessage(
|
|
1492
|
+
substitutePlaceholders(leadbay_build_campaign, {
|
|
1493
|
+
audience_block: args.audience ? `Target audience: **${args.audience}** \u2014 if my active lens doesn't already cover it, set it up first and continue on it (no need to ask me).` : "Use my active Leadbay lens as the audience.",
|
|
1494
|
+
campaign_name_paren: args.campaign_name ? ` named **${args.campaign_name}**` : "",
|
|
1495
|
+
count_or_default: n,
|
|
1496
|
+
job_titles_block: args.job_titles ? `Enrich exactly these buyer titles: **${args.job_titles}**. A lead only counts toward the ${n} when it has a reachable contact matching one of these titles.` : `No titles given \u2014 derive my buyer persona from what I sell (Phase 3 Step A) and enrich those titles.`
|
|
1497
|
+
})
|
|
1498
|
+
)
|
|
1499
|
+
];
|
|
1500
|
+
}
|
|
1487
1501
|
},
|
|
1488
1502
|
{
|
|
1489
1503
|
name: "leadbay_setup_team_prospecting",
|
|
@@ -1731,6 +1745,34 @@ var LeadbayClient = class {
|
|
|
1731
1745
|
defaultLensCachedAt = null;
|
|
1732
1746
|
mePayload = null;
|
|
1733
1747
|
mePayloadCachedAt = null;
|
|
1748
|
+
// Monotonic sequence bumped whenever the telemetry preference is decided by a
|
|
1749
|
+
// fresher signal — an explicit stamp (setCachedTelemetryEnabled) or the START
|
|
1750
|
+
// of a telemetry read (resolveMe / fetchTelemetryEnabled). A read snapshots it
|
|
1751
|
+
// and only writes telemetryEnabledCache if the sequence is UNCHANGED when it
|
|
1752
|
+
// completes, so (a) a stamp landing mid-read wins over the stale read and (b)
|
|
1753
|
+
// an older overlapping read that resolves last can't clobber a newer read's
|
|
1754
|
+
// value (product#3879, Codex P1).
|
|
1755
|
+
telemetryStateSeq = 0;
|
|
1756
|
+
// The telemetry preference lives in its OWN field, separate from mePayload,
|
|
1757
|
+
// so it survives invalidateMe() (Codex P1). Otherwise a leadbay_set_telemetry
|
|
1758
|
+
// disable would be forgotten the moment the very next same-session tool
|
|
1759
|
+
// invalidates the /me cache (refine_prompt, my_lenses, set_active_lens, …),
|
|
1760
|
+
// dropping cachedTelemetryEnabled() back to undefined and letting the hosted
|
|
1761
|
+
// suppression predicate fall through to a stale "enabled". undefined = never
|
|
1762
|
+
// observed; the last read/stamp always wins and persists across /me churn.
|
|
1763
|
+
telemetryEnabledCache = void 0;
|
|
1764
|
+
// True when telemetryEnabledCache came from an EXPLICIT user stamp
|
|
1765
|
+
// (leadbay_set_telemetry via setCachedTelemetryEnabled), as opposed to a
|
|
1766
|
+
// /users/me read. A stamp is the user's direct choice for THIS request and is
|
|
1767
|
+
// the single most authoritative signal — it outranks even a fail-closed
|
|
1768
|
+
// verdict from a timed-out/errored read, so a same-request opt-IN takes effect
|
|
1769
|
+
// even when a background refresh just failed closed (Codex P2). Reset to false
|
|
1770
|
+
// whenever a read writes the cache or the tenant switches.
|
|
1771
|
+
telemetryEnabledFromStamp = false;
|
|
1772
|
+
// Counts explicit user stamps only. Unlike telemetryStateSeq, read-starts do
|
|
1773
|
+
// not move it, so callers can distinguish "a same-message stamp happened" from
|
|
1774
|
+
// "a background refresh merely started" when demoting stale opt-in stamps.
|
|
1775
|
+
telemetryStampStateSeq = 0;
|
|
1734
1776
|
tasteProfile = null;
|
|
1735
1777
|
tasteProfileCachedAt = null;
|
|
1736
1778
|
// Simple semaphore for concurrency limiting.
|
|
@@ -1766,20 +1808,28 @@ var LeadbayClient = class {
|
|
|
1766
1808
|
get lastMeta() {
|
|
1767
1809
|
return this._lastMeta;
|
|
1768
1810
|
}
|
|
1769
|
-
|
|
1770
|
-
// one the client was constructed with.
|
|
1771
|
-
setBaseUrl(baseUrl, region) {
|
|
1772
|
-
this._baseUrl = baseUrl.replace(/\/+$/, "");
|
|
1773
|
-
this._region = region ?? (baseUrl === REGIONS.us ? "us" : baseUrl === REGIONS.fr ? "fr" : "custom");
|
|
1811
|
+
clearTenantScopedCaches() {
|
|
1774
1812
|
this.defaultLensId = null;
|
|
1775
1813
|
this.defaultLensCachedAt = null;
|
|
1776
1814
|
this.mePayload = null;
|
|
1777
1815
|
this.mePayloadCachedAt = null;
|
|
1778
1816
|
this.tasteProfile = null;
|
|
1779
1817
|
this.tasteProfileCachedAt = null;
|
|
1818
|
+
this.telemetryEnabledCache = void 0;
|
|
1819
|
+
this.telemetryEnabledFromStamp = false;
|
|
1820
|
+
this.telemetryStateSeq++;
|
|
1821
|
+
this.telemetryStampStateSeq++;
|
|
1822
|
+
}
|
|
1823
|
+
// Used by login when region auto-detect picks a different backend than the
|
|
1824
|
+
// one the client was constructed with.
|
|
1825
|
+
setBaseUrl(baseUrl, region) {
|
|
1826
|
+
this._baseUrl = baseUrl.replace(/\/+$/, "");
|
|
1827
|
+
this._region = region ?? (baseUrl === REGIONS.us ? "us" : baseUrl === REGIONS.fr ? "fr" : "custom");
|
|
1828
|
+
this.clearTenantScopedCaches();
|
|
1780
1829
|
}
|
|
1781
1830
|
setToken(token) {
|
|
1782
1831
|
this.token = token;
|
|
1832
|
+
this.clearTenantScopedCaches();
|
|
1783
1833
|
}
|
|
1784
1834
|
get isAuthenticated() {
|
|
1785
1835
|
return this.token !== null;
|
|
@@ -2060,17 +2110,139 @@ var LeadbayClient = class {
|
|
|
2060
2110
|
if (!force && this.mePayload !== null && this.mePayloadCachedAt !== null && now - this.mePayloadCachedAt < ME_CACHE_TTL_MS) {
|
|
2061
2111
|
return this.mePayload;
|
|
2062
2112
|
}
|
|
2113
|
+
const seqAtStart = ++this.telemetryStateSeq;
|
|
2063
2114
|
const me = await this.request("GET", "/users/me");
|
|
2064
2115
|
this.mePayload = me;
|
|
2065
2116
|
this.mePayloadCachedAt = now;
|
|
2117
|
+
if (this.telemetryStateSeq === seqAtStart && me.telemetry_enabled !== void 0) {
|
|
2118
|
+
this.telemetryEnabledCache = me.telemetry_enabled;
|
|
2119
|
+
this.telemetryEnabledFromStamp = false;
|
|
2120
|
+
}
|
|
2066
2121
|
return me;
|
|
2067
2122
|
}
|
|
2123
|
+
// Lightweight cross-session telemetry-preference read for the hosted SSE
|
|
2124
|
+
// per-message refresh (product#3879, Codex P2). UNLIKE resolveMe() this does
|
|
2125
|
+
// NOT touch mePayload / the general /me cache — so a slow background refresh
|
|
2126
|
+
// can never repopulate a stale last_requested_lens over a tool's mutation, and
|
|
2127
|
+
// it never serves the 60s /me cache (always a fresh read). It reads the SAME
|
|
2128
|
+
// /users/me endpoint (telemetry_enabled lives there) but only reconciles the
|
|
2129
|
+
// dedicated telemetry field, under the same sequence guard as resolveMe.
|
|
2130
|
+
//
|
|
2131
|
+
// It deliberately bypasses request() and therefore never writes _lastMeta
|
|
2132
|
+
// (Codex P2): the refresh shares the tool's client, and request() rewrites
|
|
2133
|
+
// _lastMeta on every call. Without isolation, a refresh completing between a
|
|
2134
|
+
// tool's real backend call and that tool copying client.lastMeta into its
|
|
2135
|
+
// result (e.g. pull-leads' _meta.latency_ms) could make the metadata describe
|
|
2136
|
+
// GET /users/me instead of the tool call.
|
|
2137
|
+
//
|
|
2138
|
+
// Returns the observed preference: true/false, or undefined when the backend
|
|
2139
|
+
// omitted the field (older backend → caller treats as enabled default).
|
|
2140
|
+
async fetchTelemetryEnabled() {
|
|
2141
|
+
const seqAtStart = ++this.telemetryStateSeq;
|
|
2142
|
+
if (process.env.LEADBAY_MOCK === "1") {
|
|
2143
|
+
const metaBefore = this._lastMeta;
|
|
2144
|
+
try {
|
|
2145
|
+
const me = this.mockRequest("GET", "/users/me");
|
|
2146
|
+
const observed = me.telemetry_enabled;
|
|
2147
|
+
if (this.telemetryStateSeq === seqAtStart && observed !== void 0) {
|
|
2148
|
+
this.telemetryEnabledCache = observed;
|
|
2149
|
+
this.telemetryEnabledFromStamp = false;
|
|
2150
|
+
}
|
|
2151
|
+
return observed;
|
|
2152
|
+
} finally {
|
|
2153
|
+
this._lastMeta = metaBefore;
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
if (!this.token) {
|
|
2157
|
+
throw this.makeError("NOT_AUTHENTICATED", "Not logged in to Leadbay", "Set LEADBAY_TOKEN in your MCP client config, or run: npx -y -p @leadbay/mcp@latest installer", "/users/me");
|
|
2158
|
+
}
|
|
2159
|
+
await this.acquireSemaphore();
|
|
2160
|
+
try {
|
|
2161
|
+
const res = await this.httpsRequestWithRetry("GET", `${this._baseUrl}${API_PREFIX}/users/me`, { Authorization: `Bearer ${this.token}` }, void 0);
|
|
2162
|
+
if (res.status < 200 || res.status >= 300) {
|
|
2163
|
+
throw this.mapErrorResponse(res.status, res.body, "/users/me", res.headers);
|
|
2164
|
+
}
|
|
2165
|
+
const me = JSON.parse(res.body);
|
|
2166
|
+
const observed = me.telemetry_enabled;
|
|
2167
|
+
if (this.telemetryStateSeq === seqAtStart && observed !== void 0) {
|
|
2168
|
+
this.telemetryEnabledCache = observed;
|
|
2169
|
+
this.telemetryEnabledFromStamp = false;
|
|
2170
|
+
}
|
|
2171
|
+
return observed;
|
|
2172
|
+
} finally {
|
|
2173
|
+
this.releaseSemaphore();
|
|
2174
|
+
}
|
|
2175
|
+
}
|
|
2068
2176
|
// Force re-fetch on next resolveMe(). Call from any tool that mutates a
|
|
2069
|
-
// /me-cached field (last_requested_lens, billing, etc.).
|
|
2177
|
+
// /me-cached field (last_requested_lens, billing, etc.). Deliberately does
|
|
2178
|
+
// NOT clear telemetryEnabledCache — the opt-out preference is orthogonal to
|
|
2179
|
+
// /me staleness and must survive invalidation (Codex P1).
|
|
2070
2180
|
invalidateMe() {
|
|
2071
2181
|
this.mePayload = null;
|
|
2072
2182
|
this.mePayloadCachedAt = null;
|
|
2073
2183
|
}
|
|
2184
|
+
// Synchronous read of the last-cached telemetry preference, without a fetch.
|
|
2185
|
+
// Returns undefined when /users/me hasn't been resolved (or was invalidated).
|
|
2186
|
+
// The hosted telemetry suppression predicate reads this AT CAPTURE TIME so a
|
|
2187
|
+
// leadbay_set_telemetry disable within the same request suppresses that very
|
|
2188
|
+
// request's tracking — the opt-out action isn't itself the last tracked event
|
|
2189
|
+
// (product#3879). resolveMe() keeps mePayload populated after a write, so this
|
|
2190
|
+
// reflects the post-write state.
|
|
2191
|
+
cachedTelemetryEnabled() {
|
|
2192
|
+
return this.telemetryEnabledCache;
|
|
2193
|
+
}
|
|
2194
|
+
// True when the cached preference came from an explicit user stamp (a
|
|
2195
|
+
// leadbay_set_telemetry toggle), not a read. The hosted suppression predicate
|
|
2196
|
+
// treats a stamp as the single most-authoritative signal — it outranks a
|
|
2197
|
+
// fail-closed verdict from a failed background read, so a same-request opt-IN
|
|
2198
|
+
// takes effect even when a refresh just timed out (product#3879, Codex P2).
|
|
2199
|
+
cachedTelemetryStamped() {
|
|
2200
|
+
return this.telemetryEnabledFromStamp && this.telemetryEnabledCache !== void 0;
|
|
2201
|
+
}
|
|
2202
|
+
// Monotonic sequence exposed so callers can tell whether a telemetry stamp
|
|
2203
|
+
// happened AFTER a reference point (e.g. an SSE message start). Bumped by every
|
|
2204
|
+
// stamp and every telemetry read-start; see telemetryStateSeq.
|
|
2205
|
+
telemetrySeq() {
|
|
2206
|
+
return this.telemetryStateSeq;
|
|
2207
|
+
}
|
|
2208
|
+
// Monotonic sequence moved only by explicit user stamps. Used by the SSE
|
|
2209
|
+
// refresh failure path to demote stale opt-in stamps without mistaking a
|
|
2210
|
+
// read-start sequence bump for a same-message opt-in.
|
|
2211
|
+
telemetryStampSeq() {
|
|
2212
|
+
return this.telemetryStampStateSeq;
|
|
2213
|
+
}
|
|
2214
|
+
// Demote the cached preference from "explicit stamp" to ordinary read-level
|
|
2215
|
+
// authority WITHOUT changing its value. A stamp is scoped to the request that
|
|
2216
|
+
// made it (Codex P2): once a LATER SSE message's refresh produces a
|
|
2217
|
+
// fail-closed verdict (timeout/error), that earlier stamp must no longer
|
|
2218
|
+
// outrank it, or a session that once enabled would keep emitting through every
|
|
2219
|
+
// subsequent unreadable refresh.
|
|
2220
|
+
//
|
|
2221
|
+
// `onlyIfStampSeqAtMost` guards against demoting a stamp made by the CURRENT
|
|
2222
|
+
// message (Codex P2): pass the STAMP sequence captured at message start; if a
|
|
2223
|
+
// stamp has bumped it beyond the snapshot, that stamp is same-message (a fresh
|
|
2224
|
+
// opt-in) and must be preserved. Read-starts do not affect this guard.
|
|
2225
|
+
clearTelemetryStampOrigin(onlyIfStampSeqAtMost) {
|
|
2226
|
+
if (onlyIfStampSeqAtMost !== void 0 && this.telemetryStampStateSeq > onlyIfStampSeqAtMost) {
|
|
2227
|
+
return;
|
|
2228
|
+
}
|
|
2229
|
+
this.telemetryEnabledFromStamp = false;
|
|
2230
|
+
}
|
|
2231
|
+
// Deterministically stamp the cached telemetry preference to a known value,
|
|
2232
|
+
// WITHOUT a fetch. leadbay_set_telemetry calls this right after a successful
|
|
2233
|
+
// POST /users/telemetry so the suppression predicate reflects the new state
|
|
2234
|
+
// even if the follow-up refresh fails (product#3879) — a disable must never
|
|
2235
|
+
// fail open and let the opt-out request emit error telemetry. Creates a
|
|
2236
|
+
// minimal cache entry if /users/me was never resolved.
|
|
2237
|
+
setCachedTelemetryEnabled(enabled) {
|
|
2238
|
+
this.telemetryStateSeq++;
|
|
2239
|
+
this.telemetryStampStateSeq++;
|
|
2240
|
+
this.telemetryEnabledCache = enabled;
|
|
2241
|
+
this.telemetryEnabledFromStamp = true;
|
|
2242
|
+
if (this.mePayload) {
|
|
2243
|
+
this.mePayload = { ...this.mePayload, telemetry_enabled: enabled };
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2074
2246
|
async resolveDefaultLens() {
|
|
2075
2247
|
const now = Date.now();
|
|
2076
2248
|
if (this.defaultLensId !== null && this.defaultLensCachedAt !== null && now - this.defaultLensCachedAt < LENS_CACHE_TTL_MS) {
|
|
@@ -8181,6 +8353,8 @@ WHEN NOT TO USE: discovery (use leadbay_pull_leads); single-lead deep dive (use
|
|
|
8181
8353
|
|
|
8182
8354
|
Budgets: \`total_budget_ms\` caps wall-clock; \`per_lead_budget_ms\` caps each lead's poll. For short transport timeouts, pass \`wait_for_completion:false\` and poll \`leadbay_import_status\`. Outputs \`qualified[]\`, \`still_running[]\`, \`not_imported[]\`, \`qualify_id\` (resumable handle). Idempotent within a 5-min window. \`dry_run:'preview'\` returns mapping hints + custom-field candidates without importing.
|
|
8183
8355
|
|
|
8356
|
+
\`not_imported\` rows with \`reason:"uncrawled"\` are **pending a background crawl**, NOT failures: Leadbay just hasn't matched/crawled that domain yet and will add the lead asynchronously (the label doesn't verify the URL resolves \u2014 don't call the site bad, but don't certify it valid either). Surface them as pending; the leads populate in the user's Leadbay account as the crawl completes (no tool here fetches them on demand \u2014 \`leadbay_import_status\` returns status/progress only, and \`leadbay_pull_leads\` reads the active lens's wishlist so an imported lead outside that lens may not appear). To pull those specific companies back through the MCP, re-run the import later. A large \`uncrawled\` share on a fresh list is normal.
|
|
8357
|
+
|
|
8184
8358
|
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
8185
8359
|
|
|
8186
8360
|
|
|
@@ -8192,18 +8366,42 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
8192
8366
|
|
|
8193
8367
|
The response carries either a completed result or an async handle. Render a brief summary; do NOT enumerate every imported lead.
|
|
8194
8368
|
|
|
8369
|
+
**Dry run first:** if the result has \`dry_run:true\` (or ANY \`not_imported\` row has \`reason: "dry_run"\`), this was a VALIDATION pass \u2014 nothing was committed. Render \`"\u{1F50E} Dry run \u2014 V rows validated OK, nothing imported yet. Re-run without dry_run to commit."\` where V = the count of \`dry_run\` rows. If malformed rows are ALSO present (\`reason: "malformed"\`), list those separately as \`"\u26A0 M rows can't be imported as-is: <row \xB7 malformed>"\` so the validation count is never swallowed. Do NOT use the pending-crawl/need-attention bucket header below for a dry run (those buckets are for a real committed import).
|
|
8370
|
+
|
|
8371
|
+
Otherwise, partition \`not_imported\` by \`reason\` into these buckets before you write the header:
|
|
8372
|
+
|
|
8373
|
+
- **Pending crawl** \u2014 \`reason: "uncrawled"\` **AND the row has a \`domain\`**: Leadbay just hasn't crawled that domain yet and will add the lead asynchronously. These are NOT failures. (The label doesn't verify the URL resolves \u2014 don't claim the site is bad, but don't certify it's valid either. See the note below.)
|
|
8374
|
+
- **Need attention** \u2014 everything else that didn't import:
|
|
8375
|
+
- \`reason: "uncrawled"\` but the row has **no \`domain\`** (name/CRM-id-only row): there is nothing for Leadbay to crawl, so it will NOT self-resolve \u2014 count these under need-attention, not pending crawl, and tell the user to supply a company website/identity and re-import.
|
|
8376
|
+
- \`reason\` \u2208 \`malformed\` / \`internal_error\` / \`no_match\` / \`ambiguous\`: genuinely un-actionable or needs a follow-up call.
|
|
8377
|
+
|
|
8195
8378
|
**Header \u2014 single line, choose by status:**
|
|
8196
8379
|
|
|
8197
|
-
- Completed: \`"\u2713 Import complete \u2014 N
|
|
8380
|
+
- Completed: \`"\u2713 Import complete \u2014 N imported \xB7 P pending crawl \xB7 Q need attention"\` (drop any segment whose count is 0)
|
|
8198
8381
|
- Running: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
|
|
8199
8382
|
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 qualify_id <id>"\`
|
|
8200
8383
|
|
|
8201
|
-
|
|
8384
|
+
Count \`uncrawled\` rows as **pending**, never as failures \u2014 never say "M failed" when the M is mostly/entirely uncrawled rows.
|
|
8385
|
+
|
|
8386
|
+
**When the "need attention" or pending-crawl rows are non-empty**, follow the header with a small bulleted list (\u2264 5 items): \`<row identifier or domain> \xB7 <reason>\`. Label each row by its real reason \u2014 "pending crawl" for \`uncrawled\`, and the specific reason otherwise. Frame pending rows reassuringly (Leadbay is crawling them; the leads it adds will populate in the user's Leadbay account as the crawl completes \u2014 see the semantics note below for where they show up), not as errors. The full \`not_imported\` breakdown is already in THIS response \u2014 list from it directly; then \`"*+N more (see the full not_imported list in the response)*"\`.
|
|
8202
8387
|
|
|
8203
8388
|
**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.
|
|
8204
8389
|
|
|
8205
8390
|
Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_research_lead_by_id\` in NEXT STEPS.
|
|
8206
8391
|
|
|
8392
|
+
**\`uncrawled\` is NOT a failed import \u2014 it means "pending a crawl".** A row lands \`uncrawled\` when Leadbay hasn't matched or crawled that domain **yet** \u2014 the row simply didn't match an existing lead at import time and isn't a public-mailbox domain. It does NOT mean the import failed, and it is NOT a verdict that the website is broken (the tool doesn't check whether the URL resolves \u2014 so don't claim the site is bad, but don't guarantee it's valid either).
|
|
8393
|
+
|
|
8394
|
+
**One caveat \u2014 \`uncrawled\` only means "pending" when the row actually had a website.** A row imported by name / CRM id / registry number only (no \`LEAD_WEBSITE\` mapped) that finds no existing match ALSO lands \`uncrawled\`, but there's no domain for Leadbay to crawl \u2014 so it will NOT self-resolve via a late crawl. For those name-only rows, don't give the "Leadbay is crawling it" reassurance; tell the user to supply a company website (or another resolvable identity) and re-import. So: \`uncrawled\` + a website \u2192 genuinely pending a background crawl; \`uncrawled\` + no website \u2192 the user needs to add an identity, it won't crawl on its own. The import itself completed successfully; Leadbay then crawls the domain in the background and adds the lead asynchronously (a *late import*), so most of these rows resolve on their own within minutes to hours. Where do those late-added leads show up? **In the user's Leadbay account as the crawl completes.** \`leadbay_import_status\` does NOT return them \u2014 it only refreshes status/progress. There's no bulk "list the leads this import just added" call: \`leadbay_pull_leads\` reads the active lens's wishlist, so an imported lead not admitted to that lens won't appear there. For **one specific company by name**, \`leadbay_research_lead_by_name_fuzzy\` searches across the visible Leadbay corpus (not lens-scoped) and can surface it once crawled \u2014 a reasonable check for a named company. Otherwise tell the user the leads will populate in Leadbay over the next minutes\u2013hours; to pull those specific companies back through the MCP in bulk, **re-run the same import later** (the now-crawled domains match). Do NOT promise \`leadbay_pull_leads\` or \`import_status\` will list the late additions.
|
|
8395
|
+
|
|
8396
|
+
So when reporting an import: count \`uncrawled\` rows as **pending**, never as failures. Do NOT tell the user these rows "failed", were "rejected", had "bad/unreachable websites", or point to a backend problem \u2014 that is wrong and needlessly erodes trust in the whole lead set. A high \`uncrawled\` share on a fresh list is normal and expected, not a red flag.
|
|
8397
|
+
|
|
8398
|
+
How the OTHER reasons map to the "Need attention" bucket (see the render block above) \u2014 none of these should be lumped in with \`uncrawled\`/pending, but each is still surfaced to the user, not suppressed:
|
|
8399
|
+
|
|
8400
|
+
- \`malformed\` (row couldn't be parsed) and \`internal_error\` (a real backend error) are genuine failures \u2014 flag them plainly.
|
|
8401
|
+
- \`no_match\` on a public-mailbox domain (gmail.com, outlook.com, \u2026) means no company domain was resolvable from that row \u2014 surface it so the user can supply a real company domain. Not a crawler failure.
|
|
8402
|
+
- \`ambiguous\` rows matched several candidates \u2014 surface them as needing disambiguation via \`leadbay_resolve_import_rows\`. Not a failure, but the user still needs to act.
|
|
8403
|
+
|
|
8404
|
+
|
|
8207
8405
|
|
|
8208
8406
|
---
|
|
8209
8407
|
|
|
@@ -8231,8 +8429,9 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
8231
8429
|
|------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
8232
8430
|
| Status: running | "Check progress" | leadbay_import_status(handle_id) |
|
|
8233
8431
|
| Status: complete, imports succeeded | "Run AI qualification on the imported leads" | leadbay_bulk_qualify_leads([leadIds]) \u2014 or use leadbay_import_and_qualify next time |
|
|
8432
|
+
| Pending-crawl (\`uncrawled\`) rows present | "Re-run the import for those domains later, once Leadbay has crawled them" | leadbay_import_leads (re-run with just the uncrawled domains, later \u2014 they re-reconcile once crawled). NOTE: not a live-fetch of the added leads; those populate in the user's Leadbay account as the crawl completes |
|
|
8234
8433
|
| Ambiguous / unresolved rows present | "Resolve the ambiguous rows" | leadbay_resolve_import_rows(records, identity_mappings)|
|
|
8235
|
-
|
|
|
8434
|
+
| \`malformed\` / bad-mapping rows present | "Check the org's mappable fields and remap the bad rows" | leadbay_list_mappable_fields |
|
|
8236
8435
|
| User wants to see the imported leads | "See the imported leads in your view" | leadbay_pull_leads |
|
|
8237
8436
|
| User had follow-up intent for the imports | "Prep outreach for [a specific imported lead]" | leadbay_prepare_outreach(leadId) |
|
|
8238
8437
|
`;
|
|
@@ -8240,6 +8439,8 @@ var leadbay_import_leads = `Import leads into Leadbay's CRM via the file-import
|
|
|
8240
8439
|
|
|
8241
8440
|
TWO MODES: (A) Domain-list shortcut \u2014 pass \`domains: [{domain, name?}]\`. The tool builds a 2-column CSV (LEAD_NAME, LEAD_WEBSITE) and imports with the default mapping. (B) Custom records + mapping \u2014 pass \`records: [{Col1, Col2, ...}]\` plus \`mappings.fields: {Col1: 'LEAD_NAME', ...}\`. \`mappings.fields\` must include LEADBAY_ID, CRM_ID, SIREN, LEAD_NAME, or LEAD_WEBSITE (resolver needs at least one identity key). Pass exactly one of \`domains\` / \`records\`. Reserved column \`MCP_ROW_ID\` cannot appear in records/mappings \u2014 the tool injects it for stable reconciliation.
|
|
8242
8441
|
|
|
8442
|
+
\`not_imported\` rows with \`reason:"uncrawled"\` are **pending a background crawl**, NOT failures: Leadbay just hasn't matched/crawled that domain yet and will add the lead asynchronously (the label doesn't verify the URL resolves \u2014 don't call the site bad, but don't certify it valid either). Surface them as pending; the leads populate in the user's Leadbay account as the crawl completes (no tool here fetches them on demand \u2014 \`leadbay_import_status\` returns status/progress only, and \`leadbay_pull_leads\` reads the active lens's wishlist so an imported lead outside that lens may not appear). To pull those specific companies back through the MCP, re-run the import later. A large \`uncrawled\` share on a fresh list is normal.
|
|
8443
|
+
|
|
8243
8444
|
MUTATES USER STATE: each call creates a row in the user's CRM-imports list (visible in the web UI) and touches onboarding state. Suitable for occasional automation, NOT for high-cadence (>5 calls/day). Imported leads are NOT auto-promoted to the user's Monitor view; lens-scoring threshold decides. For messy files call leadbay_resolve_import_rows first, then pass \`records_for_import\`/\`mappings_for_import\` here. Agents should inspect every column, build a preservation plan, and pass an explicit final mapping. For each meaningful column decide standard field, CONTACT_* field, Leadbay note, custom field, derived helper, or skip with a reason. For contact-only exports, derive a company-domain column from CONTACT_EMAIL only when it's a real business domain. Multiple rows can share the same LEADBAY_ID and import as separate contacts on that lead. Custom fields use \`CUSTOM.<id>\` in \`mappings.fields\` or the \`mappings.custom_fields\` shorthand. For source-system deep links create a custom field via leadbay_create_custom_field first (prefer EXTERNAL_ID + url_template). Preserve meaningful per-lead notes by calling leadbay_add_note after import returns lead IDs.
|
|
8244
8445
|
|
|
8245
8446
|
WHEN TO USE: you have a list of company domains from another system (CRM, analytics, email correspondents) and need stable Leadbay leadIds; or CRM-shaped rows with custom columns and want to drive the wizard with explicit field mappings.
|
|
@@ -8257,18 +8458,42 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role o
|
|
|
8257
8458
|
|
|
8258
8459
|
The response carries either a completed result or an async handle. Render a brief summary; do NOT enumerate every imported lead.
|
|
8259
8460
|
|
|
8461
|
+
**Dry run first:** if the result has \`dry_run:true\` (or ANY \`not_imported\` row has \`reason: "dry_run"\`), this was a VALIDATION pass \u2014 nothing was committed. Render \`"\u{1F50E} Dry run \u2014 V rows validated OK, nothing imported yet. Re-run without dry_run to commit."\` where V = the count of \`dry_run\` rows. If malformed rows are ALSO present (\`reason: "malformed"\`), list those separately as \`"\u26A0 M rows can't be imported as-is: <row \xB7 malformed>"\` so the validation count is never swallowed. Do NOT use the pending-crawl/need-attention bucket header below for a dry run (those buckets are for a real committed import).
|
|
8462
|
+
|
|
8463
|
+
Otherwise, partition \`not_imported\` by \`reason\` into these buckets before you write the header:
|
|
8464
|
+
|
|
8465
|
+
- **Pending crawl** \u2014 \`reason: "uncrawled"\` **AND the row has a \`domain\`**: Leadbay just hasn't crawled that domain yet and will add the lead asynchronously. These are NOT failures. (The label doesn't verify the URL resolves \u2014 don't claim the site is bad, but don't certify it's valid either. See the note below.)
|
|
8466
|
+
- **Need attention** \u2014 everything else that didn't import:
|
|
8467
|
+
- \`reason: "uncrawled"\` but the row has **no \`domain\`** (name/CRM-id-only row): there is nothing for Leadbay to crawl, so it will NOT self-resolve \u2014 count these under need-attention, not pending crawl, and tell the user to supply a company website/identity and re-import.
|
|
8468
|
+
- \`reason\` \u2208 \`malformed\` / \`internal_error\` / \`no_match\` / \`ambiguous\`: genuinely un-actionable or needs a follow-up call.
|
|
8469
|
+
|
|
8260
8470
|
**Header \u2014 single line, choose by status:**
|
|
8261
8471
|
|
|
8262
|
-
- Completed: \`"\u2713 Import complete \u2014 N
|
|
8472
|
+
- Completed: \`"\u2713 Import complete \u2014 N imported \xB7 P pending crawl \xB7 Q need attention"\` (drop any segment whose count is 0)
|
|
8263
8473
|
- Running: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
|
|
8264
8474
|
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 qualify_id <id>"\`
|
|
8265
8475
|
|
|
8266
|
-
|
|
8476
|
+
Count \`uncrawled\` rows as **pending**, never as failures \u2014 never say "M failed" when the M is mostly/entirely uncrawled rows.
|
|
8477
|
+
|
|
8478
|
+
**When the "need attention" or pending-crawl rows are non-empty**, follow the header with a small bulleted list (\u2264 5 items): \`<row identifier or domain> \xB7 <reason>\`. Label each row by its real reason \u2014 "pending crawl" for \`uncrawled\`, and the specific reason otherwise. Frame pending rows reassuringly (Leadbay is crawling them; the leads it adds will populate in the user's Leadbay account as the crawl completes \u2014 see the semantics note below for where they show up), not as errors. The full \`not_imported\` breakdown is already in THIS response \u2014 list from it directly; then \`"*+N more (see the full not_imported list in the response)*"\`.
|
|
8267
8479
|
|
|
8268
8480
|
**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.
|
|
8269
8481
|
|
|
8270
8482
|
Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_research_lead_by_id\` in NEXT STEPS.
|
|
8271
8483
|
|
|
8484
|
+
**\`uncrawled\` is NOT a failed import \u2014 it means "pending a crawl".** A row lands \`uncrawled\` when Leadbay hasn't matched or crawled that domain **yet** \u2014 the row simply didn't match an existing lead at import time and isn't a public-mailbox domain. It does NOT mean the import failed, and it is NOT a verdict that the website is broken (the tool doesn't check whether the URL resolves \u2014 so don't claim the site is bad, but don't guarantee it's valid either).
|
|
8485
|
+
|
|
8486
|
+
**One caveat \u2014 \`uncrawled\` only means "pending" when the row actually had a website.** A row imported by name / CRM id / registry number only (no \`LEAD_WEBSITE\` mapped) that finds no existing match ALSO lands \`uncrawled\`, but there's no domain for Leadbay to crawl \u2014 so it will NOT self-resolve via a late crawl. For those name-only rows, don't give the "Leadbay is crawling it" reassurance; tell the user to supply a company website (or another resolvable identity) and re-import. So: \`uncrawled\` + a website \u2192 genuinely pending a background crawl; \`uncrawled\` + no website \u2192 the user needs to add an identity, it won't crawl on its own. The import itself completed successfully; Leadbay then crawls the domain in the background and adds the lead asynchronously (a *late import*), so most of these rows resolve on their own within minutes to hours. Where do those late-added leads show up? **In the user's Leadbay account as the crawl completes.** \`leadbay_import_status\` does NOT return them \u2014 it only refreshes status/progress. There's no bulk "list the leads this import just added" call: \`leadbay_pull_leads\` reads the active lens's wishlist, so an imported lead not admitted to that lens won't appear there. For **one specific company by name**, \`leadbay_research_lead_by_name_fuzzy\` searches across the visible Leadbay corpus (not lens-scoped) and can surface it once crawled \u2014 a reasonable check for a named company. Otherwise tell the user the leads will populate in Leadbay over the next minutes\u2013hours; to pull those specific companies back through the MCP in bulk, **re-run the same import later** (the now-crawled domains match). Do NOT promise \`leadbay_pull_leads\` or \`import_status\` will list the late additions.
|
|
8487
|
+
|
|
8488
|
+
So when reporting an import: count \`uncrawled\` rows as **pending**, never as failures. Do NOT tell the user these rows "failed", were "rejected", had "bad/unreachable websites", or point to a backend problem \u2014 that is wrong and needlessly erodes trust in the whole lead set. A high \`uncrawled\` share on a fresh list is normal and expected, not a red flag.
|
|
8489
|
+
|
|
8490
|
+
How the OTHER reasons map to the "Need attention" bucket (see the render block above) \u2014 none of these should be lumped in with \`uncrawled\`/pending, but each is still surfaced to the user, not suppressed:
|
|
8491
|
+
|
|
8492
|
+
- \`malformed\` (row couldn't be parsed) and \`internal_error\` (a real backend error) are genuine failures \u2014 flag them plainly.
|
|
8493
|
+
- \`no_match\` on a public-mailbox domain (gmail.com, outlook.com, \u2026) means no company domain was resolvable from that row \u2014 surface it so the user can supply a real company domain. Not a crawler failure.
|
|
8494
|
+
- \`ambiguous\` rows matched several candidates \u2014 surface them as needing disambiguation via \`leadbay_resolve_import_rows\`. Not a failure, but the user still needs to act.
|
|
8495
|
+
|
|
8496
|
+
|
|
8272
8497
|
|
|
8273
8498
|
---
|
|
8274
8499
|
|
|
@@ -8296,14 +8521,15 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
8296
8521
|
|------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
8297
8522
|
| Status: running | "Check progress" | leadbay_import_status(handle_id) |
|
|
8298
8523
|
| Status: complete, imports succeeded | "Run AI qualification on the imported leads" | leadbay_bulk_qualify_leads([leadIds]) \u2014 or use leadbay_import_and_qualify next time |
|
|
8524
|
+
| Pending-crawl (\`uncrawled\`) rows present | "Re-run the import for those domains later, once Leadbay has crawled them" | leadbay_import_leads (re-run with just the uncrawled domains, later \u2014 they re-reconcile once crawled). NOTE: not a live-fetch of the added leads; those populate in the user's Leadbay account as the crawl completes |
|
|
8299
8525
|
| Ambiguous / unresolved rows present | "Resolve the ambiguous rows" | leadbay_resolve_import_rows(records, identity_mappings)|
|
|
8300
|
-
|
|
|
8526
|
+
| \`malformed\` / bad-mapping rows present | "Check the org's mappable fields and remap the bad rows" | leadbay_list_mappable_fields |
|
|
8301
8527
|
| User wants to see the imported leads | "See the imported leads in your view" | leadbay_pull_leads |
|
|
8302
8528
|
| User had follow-up intent for the imports | "Prep outreach for [a specific imported lead]" | leadbay_prepare_outreach(leadId) |
|
|
8303
8529
|
`;
|
|
8304
|
-
var leadbay_import_status = `Retrieve the current
|
|
8530
|
+
var leadbay_import_status = `Retrieve the current **status/progress** of a lead import. Pass \`handle_id\` \u2014 returned by either \`leadbay_import_leads\` OR \`leadbay_import_and_qualify\` when called with \`wait_for_completion:false\` \u2014 to resolve the stored result (leads + not_imported) once that async run has completed in this MCP instance. **If you were given a \`handle_id\`, poll with it, not with \`importIds[]\`** \u2014 only the \`handle_id\` path returns the stored result/not_imported breakdown. Pass \`importIds[]\` (a completed import returns \`importIds\`; \`leadbay_import_and_qualify\` returns \`import_ids\`) only when you don't have a handle, to refresh the backend wizard rows' phase + record counts. Note: the \`importIds[]\` path returns status/progress only \u2014 it does NOT re-reconcile records or return refreshed leads/not_imported. This status call performs a single refresh pass and never polls in a loop.
|
|
8305
8531
|
|
|
8306
|
-
WHEN TO USE: after leadbay_import_leads
|
|
8532
|
+
WHEN TO USE: after an async import (\`leadbay_import_leads\` OR \`leadbay_import_and_qualify\` with \`wait_for_completion:false\`) returns \`{status:'running', handle_id}\`, poll with that \`handle_id\`; OR to check whether a finished import is still processing. This tool does NOT surface the leads Leadbay adds later for pending-crawl (\`uncrawled\`) rows \u2014 those populate in the user's Leadbay account as the crawl completes; no tool here fetches them on demand (re-run the import to pull them back through the MCP).
|
|
8307
8533
|
|
|
8308
8534
|
WHEN NOT TO USE: for qualification handles returned as \`qualify_id\` \u2014 use leadbay_qualify_status for those; or when you still want the legacy blocking behavior from leadbay_import_leads with \`wait_for_completion=true\`.
|
|
8309
8535
|
|
|
@@ -8325,19 +8551,39 @@ After the status line, propose the obvious refresh / progress-check / recovery a
|
|
|
8325
8551
|
|
|
8326
8552
|
Specifically for import status:
|
|
8327
8553
|
|
|
8328
|
-
|
|
8329
|
-
|
|
8330
|
-
|
|
8554
|
+
This tool returns \`status\`, \`importIds\`, and \`progress\` ({phase, records_processed, records_total}). It carries a \`result\` object (with \`leads\` + \`not_imported\`) ONLY when resolving an async \`handle_id\` whose run completed in this MCP instance \u2014 the \`importIds[]\` status-check path does NOT return \`result\`. **Render only from the fields actually present; never invent counts.**
|
|
8555
|
+
|
|
8556
|
+
Caveat on \`progress\`: \`records_processed\` counts only the rows that MATCHED an existing lead (backend \`imported_records\`), not every row that finished processing \u2014 so for a complete import whose rows are mostly/all \`uncrawled\` (pending crawl), \`records_processed\` is legitimately low or 0. Never read a low \`records_processed\` on a \`complete\` import as "stuck" or "failed": once \`status:"complete"\`, processing is done; the pending-crawl rows just matched no existing lead yet.
|
|
8557
|
+
|
|
8558
|
+
- Running \u2192 \`"\u23F3 Import still running \u2014 phase <phase>; check back in ~M minutes."\` (use the phase; don't turn the matched-count into an "X/Y processed" progress bar).
|
|
8559
|
+
- Complete, **no \`result\`** (the usual \`importIds\` status check) \u2192 \`"\u2713 Import complete."\` Do NOT append a \`records_processed/records_total\` fraction (it undercounts pending-crawl rows and looks stuck) and do NOT report pending-crawl / need-attention bucket counts \u2014 the row-level \`not_imported\` breakdown isn't in this response.
|
|
8560
|
+
- Complete, **\`result\` present AND it was a dry run** (\`result.dry_run:true\`, or every \`result.not_imported\` row has \`reason:"dry_run"\`) \u2192 this resolved handle was a VALIDATION pass, nothing committed. Render \`"\u{1F50E} Dry run complete \u2014 V rows validated, nothing imported. Re-run without dry_run to commit."\` \u2014 do NOT render it as a real import completion or use the pending/attention buckets.
|
|
8561
|
+
- Complete, **\`result\` present** (async handle resolved, real import) \u2192 then, and only then, partition \`result.not_imported\` as in the shared import-result render block below \u2014 \`"\u2713 Import complete \u2014 N imported \xB7 P pending crawl \xB7 Q need attention"\` where **pending crawl** is \`uncrawled\` rows that HAVE a \`domain\` (not failures) and no-\`domain\` \`uncrawled\` rows fall under need-attention. Drop any zero segment.
|
|
8562
|
+
- Error / failed \u2192 \`"\u26A0 Import failed: <error>. See leadbay_resolve_import_rows for diagnosis."\` \u2014 reserve this ONLY for a true transport/backend error on the import itself, never for \`uncrawled\` rows.
|
|
8563
|
+
|
|
8564
|
+
**\`uncrawled\` is NOT a failed import \u2014 it means "pending a crawl".** A row lands \`uncrawled\` when Leadbay hasn't matched or crawled that domain **yet** \u2014 the row simply didn't match an existing lead at import time and isn't a public-mailbox domain. It does NOT mean the import failed, and it is NOT a verdict that the website is broken (the tool doesn't check whether the URL resolves \u2014 so don't claim the site is bad, but don't guarantee it's valid either).
|
|
8565
|
+
|
|
8566
|
+
**One caveat \u2014 \`uncrawled\` only means "pending" when the row actually had a website.** A row imported by name / CRM id / registry number only (no \`LEAD_WEBSITE\` mapped) that finds no existing match ALSO lands \`uncrawled\`, but there's no domain for Leadbay to crawl \u2014 so it will NOT self-resolve via a late crawl. For those name-only rows, don't give the "Leadbay is crawling it" reassurance; tell the user to supply a company website (or another resolvable identity) and re-import. So: \`uncrawled\` + a website \u2192 genuinely pending a background crawl; \`uncrawled\` + no website \u2192 the user needs to add an identity, it won't crawl on its own. The import itself completed successfully; Leadbay then crawls the domain in the background and adds the lead asynchronously (a *late import*), so most of these rows resolve on their own within minutes to hours. Where do those late-added leads show up? **In the user's Leadbay account as the crawl completes.** \`leadbay_import_status\` does NOT return them \u2014 it only refreshes status/progress. There's no bulk "list the leads this import just added" call: \`leadbay_pull_leads\` reads the active lens's wishlist, so an imported lead not admitted to that lens won't appear there. For **one specific company by name**, \`leadbay_research_lead_by_name_fuzzy\` searches across the visible Leadbay corpus (not lens-scoped) and can surface it once crawled \u2014 a reasonable check for a named company. Otherwise tell the user the leads will populate in Leadbay over the next minutes\u2013hours; to pull those specific companies back through the MCP in bulk, **re-run the same import later** (the now-crawled domains match). Do NOT promise \`leadbay_pull_leads\` or \`import_status\` will list the late additions.
|
|
8567
|
+
|
|
8568
|
+
So when reporting an import: count \`uncrawled\` rows as **pending**, never as failures. Do NOT tell the user these rows "failed", were "rejected", had "bad/unreachable websites", or point to a backend problem \u2014 that is wrong and needlessly erodes trust in the whole lead set. A high \`uncrawled\` share on a fresh list is normal and expected, not a red flag.
|
|
8569
|
+
|
|
8570
|
+
How the OTHER reasons map to the "Need attention" bucket (see the render block above) \u2014 none of these should be lumped in with \`uncrawled\`/pending, but each is still surfaced to the user, not suppressed:
|
|
8571
|
+
|
|
8572
|
+
- \`malformed\` (row couldn't be parsed) and \`internal_error\` (a real backend error) are genuine failures \u2014 flag them plainly.
|
|
8573
|
+
- \`no_match\` on a public-mailbox domain (gmail.com, outlook.com, \u2026) means no company domain was resolvable from that row \u2014 surface it so the user can supply a real company domain. Not a crawler failure.
|
|
8574
|
+
- \`ambiguous\` rows matched several candidates \u2014 surface them as needing disambiguation via \`leadbay_resolve_import_rows\`. Not a failure, but the user still needs to act.
|
|
8575
|
+
|
|
8331
8576
|
|
|
8332
8577
|
---
|
|
8333
8578
|
|
|
8334
8579
|
## NEXT STEPS
|
|
8335
8580
|
|
|
8336
|
-
| Observation
|
|
8337
|
-
|
|
8338
|
-
| Status: complete
|
|
8339
|
-
|
|
|
8340
|
-
| Status:
|
|
8581
|
+
| Observation | Suggest | Calls |
|
|
8582
|
+
|--------------------------------------|------------------------------------------------------|--------------------------------|
|
|
8583
|
+
| Status: complete | "See the imported (matched) leads" | leadbay_pull_leads |
|
|
8584
|
+
| Pending-crawl (\`uncrawled\`) rows | "Re-run the import for those domains later, once Leadbay has crawled them" | leadbay_import_leads (re-run with just the uncrawled domains, later \u2014 they re-reconcile once crawled). The added leads otherwise populate in the user's Leadbay account as the crawl completes; no live-fetch here |
|
|
8585
|
+
| Status: running | "Check again in N minutes" | leadbay_import_status \u2014 re-call|
|
|
8586
|
+
| Status: error / failed (true error) | "Diagnose the failure" | leadbay_resolve_import_rows |
|
|
8341
8587
|
`;
|
|
8342
8588
|
var leadbay_launch_bulk_enrichment = `Launch a bulk-enrichment job against the current selection. The backend requires \`email=true\` OR \`phone=true\` (both can be true). Returns 204 with no body \u2014 there is no bulk_id and no per-job status endpoint. Track results by polling individual leads via leadbay_get_contacts after ~60s; a contact is done for this run only when the REQUESTED channel landed (requested \`email\` and/or \`phone_number\` present), not \`contact.enrichment.done\` alone (that flag is already true for a contact enriched on the other channel earlier). \`dry_run:true\` returns the call shape without contacting the backend.
|
|
8343
8589
|
|
|
@@ -9496,55 +9742,94 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
|
|
|
9496
9742
|
`;
|
|
9497
9743
|
var leadbay_report_friction = `## WHEN TO USE
|
|
9498
9744
|
|
|
9499
|
-
Trigger phrases: "
|
|
9745
|
+
Trigger phrases: "report this problem", "tell the Leadbay team this didn't work", "this is broken, let them know", "file a report about this", "flag this to Leadbay".
|
|
9500
9746
|
|
|
9501
9747
|
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
9502
9748
|
|
|
9503
|
-
Do NOT use for: "log outreach" \u2192 \`leadbay_report_outreach\`; "thumbs up / down" \u2192 \`leadbay_like_lead\`; "snooze / pushback" \u2192 \`leadbay_set_pushback\`.
|
|
9749
|
+
Do NOT use for: "user vents about follow-ups but has not asked to report anything \u2014 keep solving the ask they actually made" \u2192 \`leadbay_pull_followups\`; "user vents about a company or result but has not asked to report anything \u2014 answer the underlying question" \u2192 \`leadbay_research_lead_by_name_fuzzy\`; "general feedback, praise, or a feature request the user wants sent" \u2192 \`leadbay_send_feedback\`; "log outreach" \u2192 \`leadbay_report_outreach\`; "thumbs up / down" \u2192 \`leadbay_like_lead\`; "snooze / pushback" \u2192 \`leadbay_set_pushback\`.
|
|
9504
9750
|
|
|
9505
|
-
Prefer when: user
|
|
9751
|
+
Prefer when: the user has asked for a specific Leadbay problem to be reported, or has said yes to your offer to report one. Frustration on its own is NOT a trigger \u2014 offer first, and only call this if they agree.
|
|
9506
9752
|
|
|
9507
9753
|
Examples that SHOULD invoke this tool:
|
|
9508
|
-
- "
|
|
9509
|
-
- "
|
|
9510
|
-
- "
|
|
9754
|
+
- "Report this to the Leadbay team \u2014 searching Wisconsin returns nothing."
|
|
9755
|
+
- "Yes, please let them know the enrichment came back empty."
|
|
9756
|
+
- "Can you flag to Leadbay that the region filter is wrong?"
|
|
9511
9757
|
|
|
9512
9758
|
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
9759
|
+
- "Ugh, this never finds what I'm looking for."
|
|
9513
9760
|
- "I sent the intro email to Acme \u2014 log it."
|
|
9514
9761
|
- "Thumbs down on this lead, wrong industry."
|
|
9515
|
-
- "Snooze this lead for 3 months."
|
|
9516
9762
|
|
|
9517
9763
|
## RENDER (quick)
|
|
9518
9764
|
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9765
|
+
Ask the user before calling \u2014 never fire this on your own. Show the
|
|
9766
|
+
one-line confirmation from the result's \`message\` (e.g. "\u2713 Shared with
|
|
9767
|
+
the Leadbay team"). If \`reported\` is false the report was NOT delivered
|
|
9768
|
+
\u2014 tell the user that plainly, never imply it was sent. If the user
|
|
9769
|
+
declines, don't call the tool at all.
|
|
9523
9770
|
|
|
9524
9771
|
---
|
|
9525
9772
|
|
|
9526
|
-
|
|
9773
|
+
Report a concrete Leadbay problem to the team \u2014 a tool that returned nothing when
|
|
9774
|
+
the user expected hits, a result that answered the wrong question, a capability
|
|
9775
|
+
that doesn't exist yet. The backend only sees explicit errors (4xx, 5xx, business-error
|
|
9776
|
+
envelopes); it never sees "that search came back empty again". This tool closes
|
|
9777
|
+
that gap, **with the user's agreement**.
|
|
9778
|
+
|
|
9779
|
+
## CONSENT \u2014 ask first, always visible
|
|
9527
9780
|
|
|
9528
|
-
**
|
|
9781
|
+
**Never call this tool unprompted.** One of two things must happen first:
|
|
9529
9782
|
|
|
9530
|
-
|
|
9783
|
+
1. The user asks you to report something ("tell the team", "report this"), or
|
|
9784
|
+
2. You notice a problem worth reporting and **offer once** \u2014 *"Want me to report
|
|
9785
|
+
this to the Leadbay team?"* \u2014 and they say yes.
|
|
9531
9786
|
|
|
9532
|
-
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9787
|
+
The report is the **user's** message, not yours \u2014 never paraphrase their
|
|
9788
|
+
complaint into a report they never saw, and never quote them without agreement.
|
|
9789
|
+
|
|
9790
|
+
**Don't ask twice.** If the user already stated the problem in the same breath
|
|
9791
|
+
as the request ("Wisconsin returns nothing \u2014 report this"), their words ARE the
|
|
9792
|
+
message: send it, and show them exactly what you sent. Only go back to them when
|
|
9793
|
+
you genuinely lack a message to send \u2014 you'd otherwise have to invent the
|
|
9794
|
+
wording \u2014 or when they asked you to report something you'd have to guess at.
|
|
9795
|
+
Optional fields (\`tool_called\`, \`severity\`) are never worth a round-trip: omit
|
|
9796
|
+
what you don't know. If they decline, or don't answer, don't call the tool.
|
|
9797
|
+
|
|
9798
|
+
After a successful call, show the one-line confirmation. The user should always
|
|
9799
|
+
know a report was sent and what it said. Never send silently.
|
|
9800
|
+
|
|
9801
|
+
## Result
|
|
9538
9802
|
|
|
9539
|
-
|
|
9803
|
+
- \`reported: true\` \u2192 it reached the Leadbay team. Show the confirmation from \`message\`.
|
|
9804
|
+
- \`reported: false\` \u2192 delivery wasn't possible on this client (problem reporting
|
|
9805
|
+
is unavailable \u2014 e.g. the user turned telemetry off). Tell the user it was NOT
|
|
9806
|
+
sent. Do not claim success, and do not retry in a loop.
|
|
9540
9807
|
|
|
9541
|
-
|
|
9808
|
+
## Categories
|
|
9542
9809
|
|
|
9543
|
-
|
|
9810
|
+
Pick the closest fit; \`other\` is fine when nothing matches:
|
|
9544
9811
|
|
|
9545
|
-
|
|
9812
|
+
- \`silent_failure\` \u2014 a tool returned ok but produced no useful output. Empty lead list when the user expected hits. Research returned a stub.
|
|
9813
|
+
- \`repeated_request\` \u2014 the user had to ask for the same thing 2+ times because earlier turns didn't deliver.
|
|
9814
|
+
- \`wrong_result\` \u2014 the tool answered a different question than the user asked. User wanted Wisconsin, got Wyoming.
|
|
9815
|
+
- \`dissatisfaction\` \u2014 the user is unhappy with a result and wants the team to know.
|
|
9816
|
+
- \`missing_capability\` \u2014 the user wants something the MCP cannot do today. "Why can't I export to HubSpot?"
|
|
9817
|
+
- \`other\` \u2014 none of the above.
|
|
9546
9818
|
|
|
9547
|
-
|
|
9819
|
+
## Parameters
|
|
9820
|
+
|
|
9821
|
+
- \`category\` (required) \u2014 one of the buckets above.
|
|
9822
|
+
- \`message\` (required) \u2014 what the user wants to report, in their own words,
|
|
9823
|
+
confirmed with them before calling. Cap 500 chars.
|
|
9824
|
+
- \`tool_called\` (optional) \u2014 the tool that disappointed, e.g. \`leadbay_pull_leads\`.
|
|
9825
|
+
- \`severity\` (optional) \u2014 \`low\` | \`medium\` | \`high\`.
|
|
9826
|
+
|
|
9827
|
+
WHEN TO USE: the user asks you to report a Leadbay problem, or accepts your offer to report one you noticed. The user has seen and approved the message being sent.
|
|
9828
|
+
|
|
9829
|
+
WHEN NOT TO USE: unprompted, and not for normal acknowledgement flows. **Bare frustration with no request to report \u2192 keep solving the ask they actually made. Do NOT reach for a delivery tool at all** \u2014 not this one and not \`leadbay_send_feedback\`, which also sends to the team. Route to whatever their real request was: follow-ups \u2192 \`leadbay_pull_followups\`, a named company \u2192 research, today's batch \u2192 \`leadbay_pull_leads\`. Venting is not consent; you may offer to report, but send nothing unless they say yes. General feedback, praise, or feature requests the user wants delivered \u2192 \`leadbay_send_feedback\`. Thumbs-up/down on a lead \u2192 \`leadbay_like_lead\` / \`leadbay_dislike_lead\`. Logged outreach \u2192 \`leadbay_report_outreach\`. Snooze a lead \u2192 \`leadbay_set_pushback\`.
|
|
9830
|
+
|
|
9831
|
+
After reporting, continue the user's original task \u2014 a report is a step on the way
|
|
9832
|
+
to actually trying again or pivoting, not the end of the conversation.
|
|
9548
9833
|
`;
|
|
9549
9834
|
var leadbay_report_outreach = `Log an outreach action (email, call, message, meeting) on a lead so the human team using Leadbay sees the progress in their UI. Writes a NOTE on the lead and (optionally) sets an EPILOGUE status (still chasing, meeting booked, etc.). Bulk variant: pass \`lead_ids=[uuid,...]\` instead of \`lead_id\` (epilogue is bulk-native; notes fan out per-lead).
|
|
9550
9835
|
|
|
@@ -9938,7 +10223,7 @@ When \`_meta.match_candidates\` is non-empty, prepend one extra NEXT STEPS row:
|
|
|
9938
10223
|
`;
|
|
9939
10224
|
var leadbay_resolve_import_rows = `Resolve messy CSV-shaped lead rows against Leadbay before file import. The tool sends each row's available identity signals to \`POST /leads/resolve\`, returns matched lead IDs or ambiguous candidate IDs, and produces \`records_for_import\` plus a SAFE identity-only \`mappings_for_import\` starting point for leadbay_import_leads / leadbay_import_and_qualify. This tool deliberately does not try to understand every CSV dialect; the agent should inspect the file, derive clean helper columns when useful, pass explicit \`identity_mappings\`, and build the final CRM mapping from \`mapping_guidance\`.
|
|
9940
10225
|
|
|
9941
|
-
WHEN TO USE: before importing user-supplied files when domains, names, CRM IDs, registry numbers, or Leadbay IDs may be inconsistently formatted; when the agent needs to pre-resolve messy rows, inspect ambiguous candidates, or prepare LEADBAY_ID values for the import composites. For contact-only files, first derive company website/domain from business contact emails where possible, while ignoring consumer mailbox domains. Deterministic matches get a LEADBAY_ID column inserted so the standard import commits immediately. Ambiguous rows are deliberately left without LEADBAY_ID; inspect candidates and choose one only when the evidence is good. Rows with websites but no match can still be imported; Leadbay may crawl and match them later, and
|
|
10226
|
+
WHEN TO USE: before importing user-supplied files when domains, names, CRM IDs, registry numbers, or Leadbay IDs may be inconsistently formatted; when the agent needs to pre-resolve messy rows, inspect ambiguous candidates, or prepare LEADBAY_ID values for the import composites. For contact-only files, first derive company website/domain from business contact emails where possible, while ignoring consumer mailbox domains. Deterministic matches get a LEADBAY_ID column inserted so the standard import commits immediately. Ambiguous rows are deliberately left without LEADBAY_ID; inspect candidates and choose one only when the evidence is good. Rows with websites but no match can still be imported; Leadbay may crawl and match them later (a late import), and those leads then populate in the user's Leadbay account as the crawl completes (no tool here fetches them on demand \u2014 re-run the import later to pull them back through the MCP).
|
|
9942
10227
|
|
|
9943
10228
|
WHEN NOT TO USE: for prospect discovery from scratch (use leadbay_pull_leads); for one known company profile (use leadbay_research_lead_by_name_fuzzy / leadbay_research_lead_by_id); or when the file already has clean, final LEADBAY_ID/CRM_ID/SIREN mappings and no row-level identity disambiguation is needed.
|
|
9944
10229
|
|
|
@@ -9971,7 +10256,7 @@ Below the table, a one-liner: \`"Ready: K rows \xB7 Ambiguous: A rows \xB7 Unmat
|
|
|
9971
10256
|
|----------------------------------------|-------------------------------------------------------------|--------------------------------------------------------|
|
|
9972
10257
|
| All rows resolved cleanly | "Import these rows now" | leadbay_import_leads(records_for_import, mappings_for_import) |
|
|
9973
10258
|
| Ambiguous rows present | "Inspect candidates for each ambiguous row" | (re-call with include_candidate_profiles=true) |
|
|
9974
|
-
| Unmatched rows but websites present | "Import anyway \u2014 Leadbay
|
|
10259
|
+
| Unmatched rows but websites present | "Import anyway \u2014 Leadbay crawls & adds them to your account later" | leadbay_import_leads (the late-added leads populate in Leadbay; re-run the import to pull them back through the MCP) |
|
|
9975
10260
|
| User wants to skip rows they can't ID | "Drop unmatched rows and import the rest" | leadbay_import_leads (with filtered records) |
|
|
9976
10261
|
`;
|
|
9977
10262
|
var leadbay_scan_portfolio_signals = `## WHEN TO USE
|
|
@@ -10210,19 +10495,19 @@ Trigger phrases: "send feedback", "I want to report a bug", "tell the Leadbay te
|
|
|
10210
10495
|
|
|
10211
10496
|
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
10212
10497
|
|
|
10213
|
-
Do NOT use for: "
|
|
10498
|
+
Do NOT use for: "report this specific empty/wrong result to the team" \u2192 \`leadbay_report_friction\`; "log the email I sent" \u2192 \`leadbay_report_outreach\`.
|
|
10214
10499
|
|
|
10215
|
-
Prefer when: the user explicitly wants the Leadbay TEAM to receive a message they authored \u2014 or accepts your offer to report an error.
|
|
10500
|
+
Prefer when: the user explicitly wants the Leadbay TEAM to receive a message they authored \u2014 or accepts your offer to report an error. When the report is about one specific tool result that disappointed them, use leadbay_report_friction instead.
|
|
10216
10501
|
|
|
10217
10502
|
Examples that SHOULD invoke this tool:
|
|
10218
10503
|
- "Send feedback to the team: the lead scores feel off this week."
|
|
10219
|
-
- "Can you
|
|
10504
|
+
- "Can you tell Leadbay the onboarding was confusing?"
|
|
10220
10505
|
- "Tell Leadbay I'd love a way to schedule my morning check-in."
|
|
10221
10506
|
|
|
10222
10507
|
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
10223
|
-
- "
|
|
10508
|
+
- "Pulling leads in Lyon returns nothing \u2014 report that."
|
|
10509
|
+
- "Ugh, this never finds what I'm looking for. Show me today's leads."
|
|
10224
10510
|
- "I emailed Acme \u2014 log that outreach."
|
|
10225
|
-
- "Thumbs down on this lead."
|
|
10226
10511
|
|
|
10227
10512
|
## RENDER (quick)
|
|
10228
10513
|
|
|
@@ -10237,6 +10522,10 @@ Deliver a user-authored message to the Leadbay team's feedback inbox \u2014 the
|
|
|
10237
10522
|
destination as the web app's feedback form. **You do not write the feedback;
|
|
10238
10523
|
the user does.** Capture their words, confirm the phrasing, then send.
|
|
10239
10524
|
|
|
10525
|
+
**Venting is not consent.** If the user is simply frustrated and has not asked
|
|
10526
|
+
for anything to be sent, do NOT call this tool \u2014 keep solving their actual
|
|
10527
|
+
request. You may offer once; send only if they say yes.
|
|
10528
|
+
|
|
10240
10529
|
## Parameters
|
|
10241
10530
|
- \`message\` (required) \u2014 the user's feedback, in their own words. Confirm it
|
|
10242
10531
|
with the user before sending. Cap 4000 chars.
|
|
@@ -10256,9 +10545,9 @@ you may OFFER: *"Want me to send feedback about this to the Leadbay team?"*
|
|
|
10256
10545
|
- \`sent: false\` \u2192 delivery wasn't possible (feedback not available on this
|
|
10257
10546
|
client). Tell the user it was NOT sent. Do not claim success.
|
|
10258
10547
|
|
|
10259
|
-
This is the
|
|
10260
|
-
Leadbay data.
|
|
10261
|
-
\`leadbay_report_friction\` instead.
|
|
10548
|
+
This is the general "talk to the Leadbay team" tool. It does not mutate any
|
|
10549
|
+
Leadbay data. To report one specific tool result that disappointed the user \u2014
|
|
10550
|
+
with their agreement \u2014 use \`leadbay_report_friction\` instead.
|
|
10262
10551
|
|
|
10263
10552
|
## NEXT STEPS \u2014 after sending feedback
|
|
10264
10553
|
|
|
@@ -10336,6 +10625,67 @@ WHEN NOT TO USE: to READ the questions (use leadbay_get_qualification_questions)
|
|
|
10336
10625
|
|
|
10337
10626
|
After a change, confirm in one line \u2014 e.g. **"Added 1 question \u2014 you now score leads against 4 questions."** or **"Removed 'the flooring question' \u2014 3 questions remain."** Then list the resulting questions as a numbered list. When the result is a non-changing preview (a removal awaiting confirmation), surface the \`hint\` (what would be removed) and ask the user to confirm \u2014 do NOT auto-confirm.
|
|
10338
10627
|
`;
|
|
10628
|
+
var leadbay_set_telemetry = `## WHEN TO USE
|
|
10629
|
+
|
|
10630
|
+
Trigger phrases: "disable telemetry", "turn off telemetry", "opt out of analytics", "stop sending usage data", "enable telemetry", "turn analytics back on", "is telemetry on", "is my usage being tracked", "what's my telemetry setting".
|
|
10631
|
+
|
|
10632
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
10633
|
+
|
|
10634
|
+
Prefer when: user wants to change or read the telemetry/analytics on-off preference for their account
|
|
10635
|
+
|
|
10636
|
+
Examples that SHOULD invoke this tool:
|
|
10637
|
+
- "Turn off telemetry, I don't want my usage tracked."
|
|
10638
|
+
- "Re-enable analytics for my account."
|
|
10639
|
+
- "Is telemetry currently on for me?"
|
|
10640
|
+
|
|
10641
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
10642
|
+
- "I want to report a bug in the pull-leads tool."
|
|
10643
|
+
- "Send feedback to the Leadbay team."
|
|
10644
|
+
- "Why isn't my event showing up in PostHog?"
|
|
10645
|
+
|
|
10646
|
+
## RENDER (quick)
|
|
10647
|
+
|
|
10648
|
+
One short confirmation line reflecting the result: state whether telemetry is
|
|
10649
|
+
now ON or OFF (or, for \`status\`, what it currently is) and \u2014 from \`hint\` \u2014
|
|
10650
|
+
the one-line way to flip it. No table; a single sentence is enough.
|
|
10651
|
+
|
|
10652
|
+
---
|
|
10653
|
+
|
|
10654
|
+
Enable, disable, or check **product-usage telemetry** for the current user.
|
|
10655
|
+
|
|
10656
|
+
Telemetry (PostHog analytics \u2014 which tools fire, durations, error rates) is
|
|
10657
|
+
**ON by default** (opt-out model). It does not capture tool argument bodies,
|
|
10658
|
+
response bodies, or lead PII. This is a granular endpoint tool, so
|
|
10659
|
+
\`_triggered_by\` is optional like other granular tools; when it is present on an
|
|
10660
|
+
opt-out attempt, the MCP server suppresses/sanitizes the privacy-control
|
|
10661
|
+
telemetry paths so the opt-out prompt is not recorded. This tool is the
|
|
10662
|
+
in-product control so a user can change or check the setting without editing
|
|
10663
|
+
config. The preference is stored on the user's Leadbay
|
|
10664
|
+
account. The **hosted/web connector** reads it per-request and stops sending a
|
|
10665
|
+
disabled user's events. A **local (self-hosted / stdio) install** decides
|
|
10666
|
+
telemetry at process start from the \`LEADBAY_TELEMETRY_ENABLED\` env var and does
|
|
10667
|
+
NOT consult this account flag \u2014 so a local user who wants to opt out should also
|
|
10668
|
+
set \`LEADBAY_TELEMETRY_ENABLED=false\`. Do NOT tell a local user that disabling
|
|
10669
|
+
here alone stops their events.
|
|
10670
|
+
|
|
10671
|
+
Parameter:
|
|
10672
|
+
|
|
10673
|
+
- **\`action\`** \u2014 \`"enable"\` | \`"disable"\` | \`"status"\`. Defaults to \`"status"\`
|
|
10674
|
+
(a bare call safely reports the setting without changing it).
|
|
10675
|
+
|
|
10676
|
+
Returns:
|
|
10677
|
+
|
|
10678
|
+
- **\`telemetry_enabled\`** \u2014 the setting AFTER this call.
|
|
10679
|
+
- **\`changed\`** \u2014 whether this call actually flipped it (\`false\` for \`status\`
|
|
10680
|
+
and for a no-op set, e.g. disabling when already off).
|
|
10681
|
+
- **\`action\`**, **\`region\`**, and a one-line **\`hint\`** describing how to flip it.
|
|
10682
|
+
|
|
10683
|
+
Setting the preference takes effect going forward on the **hosted connector**,
|
|
10684
|
+
which reads the flag per-request and stops emitting analytics for an opted-out
|
|
10685
|
+
user. On a local install the account flag is not consulted (see above).
|
|
10686
|
+
\`enable\`/\`disable\` are idempotent \u2014 setting the value it already has is a no-op
|
|
10687
|
+
that reports \`changed: false\`.
|
|
10688
|
+
`;
|
|
10339
10689
|
var leadbay_set_user_prompt = `Set the org's intelligence-refinement prompt \u2014 free-text instruction that steers Leadbay's lead recommendations beyond firmographics. Admin-only. Setting this clears any pending clarification and triggers a full intelligence regeneration (web search + high-reasoning). \`dry_run:true\` returns the call shape without contacting the backend.
|
|
10340
10690
|
|
|
10341
10691
|
WHEN TO USE: low-level.
|
|
@@ -14891,6 +15241,91 @@ var dislikeLead = {
|
|
|
14891
15241
|
}
|
|
14892
15242
|
};
|
|
14893
15243
|
|
|
15244
|
+
// ../core/dist/tools/set-telemetry.js
|
|
15245
|
+
function isEnabled(telemetry_enabled) {
|
|
15246
|
+
return telemetry_enabled !== false;
|
|
15247
|
+
}
|
|
15248
|
+
var LOCAL_OFF_CAVEAT = " On a local (self-hosted / stdio) install, also set LEADBAY_TELEMETRY_ENABLED=false to stop events there \u2014 the account flag alone does not.";
|
|
15249
|
+
var LOCAL_ON_CAVEAT = " This is your account setting; a local (self-hosted / stdio) install follows LEADBAY_TELEMETRY_ENABLED at startup instead, so it may not be sending events regardless.";
|
|
15250
|
+
var VALID_ACTIONS = ["enable", "disable", "status"];
|
|
15251
|
+
var setTelemetry = {
|
|
15252
|
+
name: "leadbay_set_telemetry",
|
|
15253
|
+
annotations: {
|
|
15254
|
+
title: "Enable, disable, or check product-usage telemetry",
|
|
15255
|
+
readOnlyHint: false,
|
|
15256
|
+
destructiveHint: false,
|
|
15257
|
+
idempotentHint: true,
|
|
15258
|
+
openWorldHint: true
|
|
15259
|
+
},
|
|
15260
|
+
description: leadbay_set_telemetry,
|
|
15261
|
+
optional: true,
|
|
15262
|
+
write: true,
|
|
15263
|
+
inputSchema: {
|
|
15264
|
+
type: "object",
|
|
15265
|
+
properties: {
|
|
15266
|
+
action: {
|
|
15267
|
+
type: "string",
|
|
15268
|
+
enum: ["enable", "disable", "status"],
|
|
15269
|
+
description: "enable / disable flip telemetry for the user; status just reports the current setting. Defaults to status."
|
|
15270
|
+
}
|
|
15271
|
+
},
|
|
15272
|
+
additionalProperties: false
|
|
15273
|
+
},
|
|
15274
|
+
// No outputSchema: the result is a small self-describing object (telemetry_enabled,
|
|
15275
|
+
// changed, action, region, hint — all documented in the description). Declaring
|
|
15276
|
+
// an outputSchema would opt this tool into the structuredContent conformance
|
|
15277
|
+
// suite for no benefit here.
|
|
15278
|
+
execute: async (client, params) => {
|
|
15279
|
+
const action = params.action ?? "status";
|
|
15280
|
+
if (!VALID_ACTIONS.includes(action)) {
|
|
15281
|
+
return {
|
|
15282
|
+
error: true,
|
|
15283
|
+
code: "BAD_ACTION",
|
|
15284
|
+
message: `Unknown action "${action}".`,
|
|
15285
|
+
hint: `Use one of: ${VALID_ACTIONS.join(", ")}. Defaults to "status".`
|
|
15286
|
+
};
|
|
15287
|
+
}
|
|
15288
|
+
const meBefore = await client.resolveMe(true);
|
|
15289
|
+
const currentlyEnabled = isEnabled(meBefore.telemetry_enabled);
|
|
15290
|
+
if (action === "status") {
|
|
15291
|
+
return {
|
|
15292
|
+
telemetry_enabled: currentlyEnabled,
|
|
15293
|
+
changed: false,
|
|
15294
|
+
action,
|
|
15295
|
+
region: client.region,
|
|
15296
|
+
hint: currentlyEnabled ? "Telemetry is ON for your account. Call with action:'disable' to opt out." + LOCAL_ON_CAVEAT : "Telemetry is OFF for your account. Call with action:'enable' to opt back in." + LOCAL_OFF_CAVEAT
|
|
15297
|
+
};
|
|
15298
|
+
}
|
|
15299
|
+
const target = action === "enable";
|
|
15300
|
+
if (target === currentlyEnabled) {
|
|
15301
|
+
if (target) {
|
|
15302
|
+
client.setCachedTelemetryEnabled(true);
|
|
15303
|
+
}
|
|
15304
|
+
return {
|
|
15305
|
+
telemetry_enabled: currentlyEnabled,
|
|
15306
|
+
changed: false,
|
|
15307
|
+
action,
|
|
15308
|
+
region: client.region,
|
|
15309
|
+
hint: target ? "Telemetry was already ON for your account; nothing to change. Call leadbay_set_telemetry with action:'disable' to opt out." + LOCAL_ON_CAVEAT : "Telemetry was already OFF for your account; nothing to change. Call leadbay_set_telemetry with action:'enable' to opt back in." + LOCAL_OFF_CAVEAT
|
|
15310
|
+
};
|
|
15311
|
+
}
|
|
15312
|
+
await client.requestVoid("POST", "/users/telemetry", {
|
|
15313
|
+
telemetry_enabled: target
|
|
15314
|
+
});
|
|
15315
|
+
client.setCachedTelemetryEnabled(target);
|
|
15316
|
+
return {
|
|
15317
|
+
telemetry_enabled: target,
|
|
15318
|
+
changed: true,
|
|
15319
|
+
action,
|
|
15320
|
+
region: client.region,
|
|
15321
|
+
// Honest about WHERE the account flag is enforced: the hosted connector
|
|
15322
|
+
// reads it per-request; a local/stdio install needs the env var (see
|
|
15323
|
+
// LOCAL_OFF_CAVEAT). Never imply the account flag alone stops local events.
|
|
15324
|
+
hint: target ? "Telemetry is now ON for your account \u2014 thanks for helping improve Leadbay." + LOCAL_ON_CAVEAT : "Telemetry is now OFF for your account \u2014 the hosted Leadbay connector stops sending your product-usage events." + LOCAL_OFF_CAVEAT
|
|
15325
|
+
};
|
|
15326
|
+
}
|
|
15327
|
+
};
|
|
15328
|
+
|
|
14894
15329
|
// ../core/dist/tools/add-contact.js
|
|
14895
15330
|
var addContact = {
|
|
14896
15331
|
name: "leadbay_add_contact",
|
|
@@ -22578,12 +23013,12 @@ var VALID_CATEGORIES = /* @__PURE__ */ new Set([
|
|
|
22578
23013
|
"other"
|
|
22579
23014
|
]);
|
|
22580
23015
|
var VALID_SEVERITIES = /* @__PURE__ */ new Set(["low", "medium", "high"]);
|
|
22581
|
-
var
|
|
22582
|
-
var
|
|
23016
|
+
var KNOWN_TOOL_NAMES = COMPOSITE_FILE_TOOL_NAMES;
|
|
23017
|
+
var MESSAGE_MAX = 500;
|
|
22583
23018
|
var reportFriction = {
|
|
22584
23019
|
name: "leadbay_report_friction",
|
|
22585
23020
|
annotations: {
|
|
22586
|
-
title: "Report
|
|
23021
|
+
title: "Report a problem to the Leadbay team",
|
|
22587
23022
|
readOnlyHint: false,
|
|
22588
23023
|
destructiveHint: false,
|
|
22589
23024
|
idempotentHint: false,
|
|
@@ -22593,8 +23028,8 @@ var reportFriction = {
|
|
|
22593
23028
|
optional: true,
|
|
22594
23029
|
// Not write:true — friction reporting does NOT mutate Leadbay state and
|
|
22595
23030
|
// must remain callable even when LEADBAY_MCP_WRITE=0. Registered in
|
|
22596
|
-
// compositeReadTools (always-on) so a read-only deployment can
|
|
22597
|
-
//
|
|
23031
|
+
// compositeReadTools (always-on) so a user on a read-only deployment can
|
|
23032
|
+
// still ask for a problem to be reported.
|
|
22598
23033
|
write: false,
|
|
22599
23034
|
inputSchema: {
|
|
22600
23035
|
type: "object",
|
|
@@ -22609,32 +23044,29 @@ var reportFriction = {
|
|
|
22609
23044
|
"missing_capability",
|
|
22610
23045
|
"other"
|
|
22611
23046
|
],
|
|
22612
|
-
description: "Bucket: silent_failure (tool returned ok but produced no useful output \u2014 empty list, wrong region, etc.), repeated_request (user
|
|
23047
|
+
description: "Bucket: silent_failure (tool returned ok but produced no useful output \u2014 empty list, wrong region, etc.), repeated_request (user had to ask for the same thing 2+ times because earlier turns didn't deliver), wrong_result (tool returned data but it answered a different question than the user asked), dissatisfaction (user is unhappy with a result and wants the team to know), missing_capability (user wants something the MCP can't do \u2014 'why can't I\u2026', 'I wish you could\u2026'), other."
|
|
22613
23048
|
},
|
|
22614
|
-
|
|
23049
|
+
message: {
|
|
22615
23050
|
type: "string",
|
|
22616
|
-
description: "
|
|
23051
|
+
description: "What the user wants to report, in their own words (cap 500 chars). Required. If the user already stated the problem when asking you to report it, those words ARE the message \u2014 send them in the same turn; do NOT ask them to re-confirm wording they just gave you. Only go back to them when you would otherwise have to invent the wording. Never call this tool unprompted."
|
|
22617
23052
|
},
|
|
22618
23053
|
tool_called: {
|
|
22619
23054
|
type: "string",
|
|
22620
|
-
|
|
23055
|
+
pattern: "^leadbay_[a-z0-9_]{1,60}$",
|
|
23056
|
+
description: "Optional: the bare name of the registered Leadbay tool that disappointed, e.g. 'leadbay_pull_leads'. This is NOT a free-text field \u2014 any value that is not an actual registered tool name is dropped, so never encode context, detail, or user data here. Put context in the user-approved `message` instead."
|
|
22621
23057
|
},
|
|
22622
23058
|
severity: {
|
|
22623
23059
|
type: "string",
|
|
22624
23060
|
enum: ["low", "medium", "high"],
|
|
22625
23061
|
description: "Optional: low (minor papercut, user moved on), medium (user noticeably frustrated or had to repeat), high (user gave up / explicitly said this is broken)."
|
|
22626
|
-
},
|
|
22627
|
-
details: {
|
|
22628
|
-
type: "string",
|
|
22629
|
-
description: "Optional: 1-3 sentences with extra context \u2014 what the user asked, what happened, what they expected. Cap 2000 chars."
|
|
22630
23062
|
}
|
|
22631
23063
|
},
|
|
22632
|
-
required: ["category", "
|
|
23064
|
+
required: ["category", "message"],
|
|
22633
23065
|
additionalProperties: false
|
|
22634
23066
|
},
|
|
22635
23067
|
outputSchema: {
|
|
22636
23068
|
type: "object",
|
|
22637
|
-
description: "Confirmation the
|
|
23069
|
+
description: "Confirmation the report was sent. `reported: true` + a user-facing `message` the agent should show back to the user. The `_friction` block carries the analytics payload \u2014 the MCP server detects it and emits a `mcp friction reported` PostHog event containing only the fields the user approved.",
|
|
22638
23070
|
properties: {
|
|
22639
23071
|
reported: { type: "boolean" },
|
|
22640
23072
|
message: { type: "string" },
|
|
@@ -22642,10 +23074,9 @@ var reportFriction = {
|
|
|
22642
23074
|
type: "object",
|
|
22643
23075
|
properties: {
|
|
22644
23076
|
category: { type: "string" },
|
|
22645
|
-
|
|
23077
|
+
message: { type: "string" },
|
|
22646
23078
|
tool_called: { type: "string" },
|
|
22647
|
-
severity: { type: "string" }
|
|
22648
|
-
details: { type: "string" }
|
|
23079
|
+
severity: { type: "string" }
|
|
22649
23080
|
}
|
|
22650
23081
|
},
|
|
22651
23082
|
_meta: {
|
|
@@ -22654,7 +23085,7 @@ var reportFriction = {
|
|
|
22654
23085
|
}
|
|
22655
23086
|
}
|
|
22656
23087
|
},
|
|
22657
|
-
execute: async (client, params,
|
|
23088
|
+
execute: async (client, params, ctx) => {
|
|
22658
23089
|
if (!params.category || !VALID_CATEGORIES.has(params.category)) {
|
|
22659
23090
|
return {
|
|
22660
23091
|
error: true,
|
|
@@ -22663,12 +23094,12 @@ var reportFriction = {
|
|
|
22663
23094
|
hint: "Set `category` to one of: silent_failure (tool returned ok but produced no useful output), repeated_request (user asked 2+ times), wrong_result (tool answered a different question), dissatisfaction (user expressed unhappiness), missing_capability (MCP can't do it), other."
|
|
22664
23095
|
};
|
|
22665
23096
|
}
|
|
22666
|
-
if (typeof params.
|
|
23097
|
+
if (typeof params.message !== "string" || params.message.trim().length === 0) {
|
|
22667
23098
|
return {
|
|
22668
23099
|
error: true,
|
|
22669
23100
|
code: "BAD_INPUT",
|
|
22670
|
-
message: "
|
|
22671
|
-
hint: "
|
|
23101
|
+
message: "message is required \u2014 pass what the user wants to report, in their own words.",
|
|
23102
|
+
hint: "Ask the user what they want reported and confirm the wording, then pass it as `message`. Do not call this tool unprompted."
|
|
22672
23103
|
};
|
|
22673
23104
|
}
|
|
22674
23105
|
if (params.severity && !VALID_SEVERITIES.has(params.severity)) {
|
|
@@ -22679,22 +23110,30 @@ var reportFriction = {
|
|
|
22679
23110
|
hint: "Set `severity` to low | medium | high, or drop the field entirely."
|
|
22680
23111
|
};
|
|
22681
23112
|
}
|
|
22682
|
-
const
|
|
22683
|
-
const
|
|
23113
|
+
const message = params.message.length > MESSAGE_MAX ? `${params.message.slice(0, MESSAGE_MAX)}\u2026` : params.message;
|
|
23114
|
+
const toolCalled = typeof params.tool_called === "string" && KNOWN_TOOL_NAMES.has(params.tool_called) ? params.tool_called : void 0;
|
|
23115
|
+
const report = {
|
|
23116
|
+
category: params.category,
|
|
23117
|
+
message,
|
|
23118
|
+
...toolCalled ? { tool_called: toolCalled } : {},
|
|
23119
|
+
...params.severity ? { severity: params.severity } : {}
|
|
23120
|
+
};
|
|
23121
|
+
const delivered = ctx?.reportFriction ? ctx.reportFriction(report) : false;
|
|
23122
|
+
if (!delivered) {
|
|
23123
|
+
return {
|
|
23124
|
+
reported: false,
|
|
23125
|
+
message: "This report could NOT be sent from this client (problem reporting isn't available here \u2014 telemetry is off or unavailable). Tell the user it was not delivered; do not claim it was shared.",
|
|
23126
|
+
_friction: report,
|
|
23127
|
+
_meta: { region: client.region }
|
|
23128
|
+
};
|
|
23129
|
+
}
|
|
22684
23130
|
return {
|
|
22685
23131
|
reported: true,
|
|
22686
|
-
//
|
|
22687
|
-
//
|
|
22688
|
-
//
|
|
22689
|
-
|
|
22690
|
-
|
|
22691
|
-
_friction: {
|
|
22692
|
-
category: params.category,
|
|
22693
|
-
user_quote: quote,
|
|
22694
|
-
...params.tool_called ? { tool_called: params.tool_called } : {},
|
|
22695
|
-
...params.severity ? { severity: params.severity } : {},
|
|
22696
|
-
...details ? { details } : {}
|
|
22697
|
-
},
|
|
23132
|
+
// User-facing confirmation. This tool is consent-gated and visible: the
|
|
23133
|
+
// agent shows this line back so the user always knows the report was
|
|
23134
|
+
// sent and is never surprised by it.
|
|
23135
|
+
message: "Shared with the Leadbay team \u2014 thanks for flagging it.",
|
|
23136
|
+
_friction: report,
|
|
22698
23137
|
_meta: { region: client.region }
|
|
22699
23138
|
};
|
|
22700
23139
|
}
|
|
@@ -22772,7 +23211,7 @@ var teamActivity = {
|
|
|
22772
23211
|
};
|
|
22773
23212
|
|
|
22774
23213
|
// ../core/dist/tools/send-feedback.js
|
|
22775
|
-
var
|
|
23214
|
+
var MESSAGE_MAX2 = 4e3;
|
|
22776
23215
|
var sendFeedback = {
|
|
22777
23216
|
name: "leadbay_send_feedback",
|
|
22778
23217
|
annotations: {
|
|
@@ -22823,7 +23262,7 @@ var sendFeedback = {
|
|
|
22823
23262
|
hint: "Ask the user what they'd like to tell the Leadbay team, then call again with their words in `message`."
|
|
22824
23263
|
};
|
|
22825
23264
|
}
|
|
22826
|
-
const message = text.length >
|
|
23265
|
+
const message = text.length > MESSAGE_MAX2 ? `${text.slice(0, MESSAGE_MAX2 - 1)}\u2026` : text;
|
|
22827
23266
|
if (!ctx?.sendFeedback) {
|
|
22828
23267
|
return {
|
|
22829
23268
|
sent: false,
|
|
@@ -22935,6 +23374,7 @@ var granularWriteTools = [
|
|
|
22935
23374
|
removePushback,
|
|
22936
23375
|
previewBulkEnrichment,
|
|
22937
23376
|
launchBulkEnrichment,
|
|
23377
|
+
setTelemetry,
|
|
22938
23378
|
createCustomField
|
|
22939
23379
|
];
|
|
22940
23380
|
var granularTools = [
|
|
@@ -23008,11 +23448,12 @@ var compositeReadTools = [
|
|
|
23008
23448
|
createTopupLink,
|
|
23009
23449
|
openBillingPortal,
|
|
23010
23450
|
prepareOutreach,
|
|
23011
|
-
//
|
|
23012
|
-
//
|
|
23013
|
-
//
|
|
23014
|
-
//
|
|
23015
|
-
//
|
|
23451
|
+
// Problem reporting — ALWAYS exposed so a user on a read-only deployment
|
|
23452
|
+
// can still ask for a problem to be reported. Consent-gated and visible:
|
|
23453
|
+
// the agent calls it only when the user asks or accepts an offer, and shows
|
|
23454
|
+
// the confirmation back (product#3943). Does not mutate Leadbay state;
|
|
23455
|
+
// emits a PostHog event carrying only what the user approved. Companion to
|
|
23456
|
+
// leadbay_report_outreach (which DOES write and stays behind LEADBAY_MCP_WRITE).
|
|
23016
23457
|
reportFriction,
|
|
23017
23458
|
// Notification ack — ALWAYS exposed even though it POSTs to /seen.
|
|
23018
23459
|
// _meta.notifications surfaces terminal bulk-progress notifications on
|
|
@@ -23241,8 +23682,8 @@ var NOOP_TELEMETRY = {
|
|
|
23241
23682
|
},
|
|
23242
23683
|
captureAgentMemoryPruned: () => {
|
|
23243
23684
|
},
|
|
23244
|
-
|
|
23245
|
-
|
|
23685
|
+
// NOOP delivers nothing — say so, so the tool never claims a false send.
|
|
23686
|
+
captureFrictionReported: () => false,
|
|
23246
23687
|
captureException: () => {
|
|
23247
23688
|
},
|
|
23248
23689
|
captureFeedback: async () => false,
|
|
@@ -23457,7 +23898,9 @@ function initTelemetry(opts) {
|
|
|
23457
23898
|
emit(EV_AGENT_MEMORY_PRUNED, { ...props }, identity);
|
|
23458
23899
|
},
|
|
23459
23900
|
captureFrictionReported(props, identity) {
|
|
23901
|
+
if (!posthog) return false;
|
|
23460
23902
|
emit(EV_FRICTION_REPORTED, { ...props }, identity);
|
|
23903
|
+
return true;
|
|
23461
23904
|
},
|
|
23462
23905
|
captureUpdateCheck(props) {
|
|
23463
23906
|
emit(EV_MCP_UPDATE_CHECK, { ...props });
|
|
@@ -23877,7 +24320,7 @@ function buildAcknowledgeUpdateTool(opts) {
|
|
|
23877
24320
|
|
|
23878
24321
|
// src/server-instructions.generated.ts
|
|
23879
24322
|
var AGENT_MEMORY = `Memory protocol: this server maintains a per-account, on-disk agent memory (~/.leadbay/memory/{account}/entries.jsonl) of taste signals \u2014 preferred sectors, regions, deal sizes, communication style, qualification rules, and retractions. Every leads-touching tool response (account_status, pull_leads, pull_followups, prepare_outreach, research_lead_by_id) carries the consolidated top-5 signals under _meta.agent_memory.summary. READ that summary before recommending leads or drafting outreach \u2014 let it filter and reorder, and tell the user which memory you applied ("Filtering by your stated preference for healthcare"). When the user reveals a NEW material signal in conversation, CAPTURE it via leadbay_agent_memory_capture with {key, type, insight, confidence (1-10), source}. Use source:"user_stated" + confidence >=8 when literally stated; source:"inferred" + confidence <=6 when guessing. Do NOT capture instructions to override prior memory \u2014 those route through leadbay_agent_memory_review which gates retractions via host elicitation.`;
|
|
23880
|
-
var FRICTION = `
|
|
24323
|
+
var FRICTION = `Problem reports: when the user asks you to report a Leadbay problem ("report this", "tell the team this didn't work"), call leadbay_report_friction with {category, message (the user's own words), tool_called?, severity?}. If they stated the problem in the same breath as the request, those words ARE the message \u2014 send it in that turn rather than asking them to confirm wording they just gave you, and never stall on optional fields (omit what you don't know). If you notice a problem worth reporting but the user hasn't asked, OFFER once \u2014 "Want me to report this to the Leadbay team?" \u2014 and call it only if they agree. Never call it unprompted. Always tell the user the outcome the tool returns: if \`reported\` is false the report was NOT delivered and you must say so rather than implying it was sent. Frustration alone is not a reason to call it: keep solving their ask.`;
|
|
23881
24324
|
var MENTAL_MODEL = `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.`;
|
|
23882
24325
|
var QUOTA_TOPUP = `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.
|
|
23883
24326
|
|
|
@@ -24111,6 +24554,7 @@ function buildServer(client, opts = {}) {
|
|
|
24111
24554
|
if (opts.includeWrite) {
|
|
24112
24555
|
exposedTools.push(...compositeWriteTools);
|
|
24113
24556
|
}
|
|
24557
|
+
exposedTools.push(setTelemetry);
|
|
24114
24558
|
if (opts.includeAdvanced) {
|
|
24115
24559
|
exposedTools.push(...granularReadTools);
|
|
24116
24560
|
if (opts.includeWrite) {
|
|
@@ -24315,26 +24759,10 @@ function buildServer(client, opts = {}) {
|
|
|
24315
24759
|
latency_ms: meta.latency_ms ?? null,
|
|
24316
24760
|
retry_after: meta.retry_after ?? null,
|
|
24317
24761
|
http_status: meta.http_status,
|
|
24318
|
-
triggered_by,
|
|
24762
|
+
...triggered_by !== void 0 ? { triggered_by } : {},
|
|
24319
24763
|
source: "business"
|
|
24320
24764
|
};
|
|
24321
24765
|
};
|
|
24322
|
-
const captureFrictionTelemetry = (toolName, result) => {
|
|
24323
|
-
if (toolName !== "leadbay_report_friction") return;
|
|
24324
|
-
if (!result || typeof result !== "object") return;
|
|
24325
|
-
const fr = result._friction;
|
|
24326
|
-
if (!fr || typeof fr !== "object") return;
|
|
24327
|
-
if (typeof fr.category !== "string" || typeof fr.user_quote !== "string") {
|
|
24328
|
-
return;
|
|
24329
|
-
}
|
|
24330
|
-
telemetry2.captureFrictionReported({
|
|
24331
|
-
category: fr.category,
|
|
24332
|
-
user_quote: fr.user_quote,
|
|
24333
|
-
...typeof fr.tool_called === "string" ? { tool_called: fr.tool_called } : {},
|
|
24334
|
-
...typeof fr.severity === "string" ? { severity: fr.severity } : {},
|
|
24335
|
-
...typeof fr.details === "string" ? { details: fr.details } : {}
|
|
24336
|
-
});
|
|
24337
|
-
};
|
|
24338
24766
|
const captureAgentMemoryTelemetry = (toolName, result) => {
|
|
24339
24767
|
if (!result || typeof result !== "object") return;
|
|
24340
24768
|
const meta = result._meta ?? {};
|
|
@@ -24376,7 +24804,8 @@ function buildServer(client, opts = {}) {
|
|
|
24376
24804
|
};
|
|
24377
24805
|
}
|
|
24378
24806
|
const rawArgs = req.params.arguments ?? {};
|
|
24379
|
-
const { triggered_by, cleaned: args } = extractTriggeredBy(rawArgs);
|
|
24807
|
+
const { triggered_by: rawTriggeredBy, cleaned: args } = extractTriggeredBy(rawArgs);
|
|
24808
|
+
const triggered_by = name === "leadbay_report_friction" ? void 0 : rawTriggeredBy;
|
|
24380
24809
|
const progressToken = req.params?._meta?.progressToken;
|
|
24381
24810
|
const progress = progressToken !== void 0 ? (params) => {
|
|
24382
24811
|
extra.sendNotification({
|
|
@@ -24440,15 +24869,17 @@ ${url}
|
|
|
24440
24869
|
};
|
|
24441
24870
|
const pendingText = formatErrorForLLM(envelope);
|
|
24442
24871
|
const pendingDur = Date.now() - callStart;
|
|
24443
|
-
|
|
24444
|
-
|
|
24445
|
-
|
|
24446
|
-
|
|
24447
|
-
|
|
24448
|
-
|
|
24449
|
-
|
|
24450
|
-
|
|
24451
|
-
|
|
24872
|
+
if (name !== "leadbay_set_telemetry") {
|
|
24873
|
+
telemetry2.captureToolCall({
|
|
24874
|
+
tool: name,
|
|
24875
|
+
ok: false,
|
|
24876
|
+
duration_ms: pendingDur,
|
|
24877
|
+
format: "error-envelope",
|
|
24878
|
+
bytes: pendingText.length,
|
|
24879
|
+
error_code: envelope.code,
|
|
24880
|
+
triggered_by
|
|
24881
|
+
});
|
|
24882
|
+
}
|
|
24452
24883
|
if (DEBUG_ON) {
|
|
24453
24884
|
process.stderr.write(
|
|
24454
24885
|
`[leadbay-mcp debug] tool=${name} dur=${pendingDur}ms ok=false code=${envelope.code} (auth-bootstrap, no-sentry)
|
|
@@ -24460,11 +24891,11 @@ ${url}
|
|
|
24460
24891
|
isError: true
|
|
24461
24892
|
};
|
|
24462
24893
|
}
|
|
24463
|
-
if (COMPOSITE_FILE_TOOL_NAMES.has(name) && !
|
|
24894
|
+
if (COMPOSITE_FILE_TOOL_NAMES.has(name) && !rawTriggeredBy) {
|
|
24464
24895
|
const envelope = {
|
|
24465
24896
|
error: true,
|
|
24466
24897
|
code: "LAST_PROMPT_REQUIRED",
|
|
24467
|
-
message: "Every call to this
|
|
24898
|
+
message: "Every call to this tool must carry `_triggered_by` \u2014 the verbatim part of the user's most recent message this call is acting upon (secrets stripped).",
|
|
24468
24899
|
hint: "Re-call with `_triggered_by` set to the literal user-message slice this invocation is fulfilling."
|
|
24469
24900
|
};
|
|
24470
24901
|
const guardText = formatErrorForLLM(envelope);
|
|
@@ -24505,12 +24936,30 @@ ${url}
|
|
|
24505
24936
|
elicit,
|
|
24506
24937
|
// Verbatim user-message slice (stripped from args above). Lets a
|
|
24507
24938
|
// composite gate optional output on what the user asked — account_status
|
|
24508
|
-
// uses it to surface the lens only when asked (product#3761).
|
|
24509
|
-
|
|
24939
|
+
// uses it to surface the lens only when asked (product#3761). Uses the
|
|
24940
|
+
// RAW value: the friction redaction above is an ANALYTICS control, and
|
|
24941
|
+
// must not change in-process tool behaviour.
|
|
24942
|
+
triggered_by: rawTriggeredBy,
|
|
24510
24943
|
// Route leadbay_send_feedback to Sentry's feedback inbox (same place
|
|
24511
24944
|
// the web app's form lands). NOOP_TELEMETRY returns false, so the
|
|
24512
24945
|
// tool reports honestly when telemetry is off.
|
|
24513
|
-
sendFeedback: (message, fbOpts) => telemetry2.captureFeedback(message, fbOpts)
|
|
24946
|
+
sendFeedback: (message, fbOpts) => telemetry2.captureFeedback(message, fbOpts),
|
|
24947
|
+
// Consent-gated problem report (product#3943). Threaded as a transport
|
|
24948
|
+
// — rather than captured post-hoc from the result — so the tool knows
|
|
24949
|
+
// whether delivery actually happened and can confirm honestly to the
|
|
24950
|
+
// user instead of always claiming success. Returns false under NOOP
|
|
24951
|
+
// telemetry (opted out / no keys / tests), mirroring sendFeedback.
|
|
24952
|
+
// Delivery is REPORTED by the handle, not inferred from its identity:
|
|
24953
|
+
// a non-NOOP handle can still have no PostHog sink (Sentry-only config,
|
|
24954
|
+
// failed init), and the hosted wrapper is a fresh object that never
|
|
24955
|
+
// equals NOOP_TELEMETRY. Both cases previously produced a false
|
|
24956
|
+
// "shared with the team" confirmation (product#3943).
|
|
24957
|
+
reportFriction: (report) => telemetry2.captureFrictionReported({
|
|
24958
|
+
category: report.category,
|
|
24959
|
+
message: report.message,
|
|
24960
|
+
...report.tool_called ? { tool_called: report.tool_called } : {},
|
|
24961
|
+
...report.severity ? { severity: report.severity } : {}
|
|
24962
|
+
}) === true
|
|
24514
24963
|
});
|
|
24515
24964
|
await maybeAttachUpdate(name, result);
|
|
24516
24965
|
maybeAttachNotifications(result);
|
|
@@ -24518,35 +24967,38 @@ ${url}
|
|
|
24518
24967
|
const envText = formatErrorForLLM(result);
|
|
24519
24968
|
const envDur = Date.now() - callStart;
|
|
24520
24969
|
const envCode = result.code ?? "Error";
|
|
24521
|
-
|
|
24522
|
-
|
|
24523
|
-
|
|
24524
|
-
|
|
24525
|
-
|
|
24526
|
-
|
|
24527
|
-
|
|
24528
|
-
|
|
24529
|
-
|
|
24530
|
-
|
|
24531
|
-
duration_ms: envDur,
|
|
24532
|
-
format: "error-envelope",
|
|
24533
|
-
bytes: envText.length,
|
|
24534
|
-
error_code: envCode,
|
|
24535
|
-
triggered_by
|
|
24536
|
-
});
|
|
24537
|
-
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
24538
|
-
telemetry2.captureCompositeCall({
|
|
24970
|
+
const isPrivacyControl = name === "leadbay_set_telemetry";
|
|
24971
|
+
if (!isPrivacyControl) {
|
|
24972
|
+
if (envCode === "QUOTA_EXCEEDED") {
|
|
24973
|
+
telemetry2.captureQuotaHit({
|
|
24974
|
+
tool: name,
|
|
24975
|
+
retry_after_s: result._meta?.retry_after,
|
|
24976
|
+
endpoint: result._meta?.endpoint
|
|
24977
|
+
});
|
|
24978
|
+
}
|
|
24979
|
+
telemetry2.captureToolCall({
|
|
24539
24980
|
tool: name,
|
|
24540
|
-
last_prompt: triggered_by ?? "",
|
|
24541
24981
|
ok: false,
|
|
24542
24982
|
duration_ms: envDur,
|
|
24543
|
-
|
|
24983
|
+
format: "error-envelope",
|
|
24984
|
+
bytes: envText.length,
|
|
24985
|
+
error_code: envCode,
|
|
24986
|
+
triggered_by
|
|
24544
24987
|
});
|
|
24988
|
+
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
24989
|
+
telemetry2.captureCompositeCall({
|
|
24990
|
+
tool: name,
|
|
24991
|
+
last_prompt: triggered_by ?? "",
|
|
24992
|
+
ok: false,
|
|
24993
|
+
duration_ms: envDur,
|
|
24994
|
+
error_code: envCode
|
|
24995
|
+
});
|
|
24996
|
+
}
|
|
24997
|
+
telemetry2.captureException(
|
|
24998
|
+
result,
|
|
24999
|
+
buildBusinessCtx(name, result, triggered_by)
|
|
25000
|
+
);
|
|
24545
25001
|
}
|
|
24546
|
-
telemetry2.captureException(
|
|
24547
|
-
result,
|
|
24548
|
-
buildBusinessCtx(name, result, triggered_by)
|
|
24549
|
-
);
|
|
24550
25002
|
if (DEBUG_ON) {
|
|
24551
25003
|
process.stderr.write(
|
|
24552
25004
|
`[leadbay-mcp debug] tool=${name} dur=${envDur}ms ok=false code=${envCode}
|
|
@@ -24588,7 +25040,6 @@ ${url}
|
|
|
24588
25040
|
});
|
|
24589
25041
|
}
|
|
24590
25042
|
captureAgentMemoryTelemetry(name, env.structured);
|
|
24591
|
-
captureFrictionTelemetry(name, env.structured);
|
|
24592
25043
|
if (name === "leadbay_create_topup_link" && typeof env.structured?.url === "string") {
|
|
24593
25044
|
telemetry2.captureTopupLink({ tool: name });
|
|
24594
25045
|
}
|
|
@@ -24611,24 +25062,26 @@ ${url}
|
|
|
24611
25062
|
const okText = response.content[0]?.text ?? "";
|
|
24612
25063
|
const okBytes = typeof okText === "string" ? okText.length : 0;
|
|
24613
25064
|
const okDur = Date.now() - callStart;
|
|
24614
|
-
|
|
24615
|
-
|
|
24616
|
-
|
|
24617
|
-
duration_ms: okDur,
|
|
24618
|
-
format: "json",
|
|
24619
|
-
bytes: okBytes,
|
|
24620
|
-
triggered_by
|
|
24621
|
-
});
|
|
24622
|
-
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
24623
|
-
telemetry2.captureCompositeCall({
|
|
25065
|
+
const suppressSuccessfulTelemetryDisable = name === "leadbay_set_telemetry" && result !== null && typeof result === "object" && !Array.isArray(result) && result.action === "disable" && result.telemetry_enabled === false;
|
|
25066
|
+
if (!suppressSuccessfulTelemetryDisable) {
|
|
25067
|
+
telemetry2.captureToolCall({
|
|
24624
25068
|
tool: name,
|
|
24625
|
-
last_prompt: triggered_by ?? "",
|
|
24626
25069
|
ok: true,
|
|
24627
|
-
duration_ms: okDur
|
|
25070
|
+
duration_ms: okDur,
|
|
25071
|
+
format: "json",
|
|
25072
|
+
bytes: okBytes,
|
|
25073
|
+
triggered_by
|
|
24628
25074
|
});
|
|
25075
|
+
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
25076
|
+
telemetry2.captureCompositeCall({
|
|
25077
|
+
tool: name,
|
|
25078
|
+
last_prompt: triggered_by ?? "",
|
|
25079
|
+
ok: true,
|
|
25080
|
+
duration_ms: okDur
|
|
25081
|
+
});
|
|
25082
|
+
}
|
|
24629
25083
|
}
|
|
24630
25084
|
captureAgentMemoryTelemetry(name, result);
|
|
24631
|
-
captureFrictionTelemetry(name, result);
|
|
24632
25085
|
if (name === "leadbay_create_topup_link" && typeof result?.url === "string") {
|
|
24633
25086
|
telemetry2.captureTopupLink({ tool: name });
|
|
24634
25087
|
}
|
|
@@ -24643,8 +25096,10 @@ ${url}
|
|
|
24643
25096
|
const errDur = Date.now() - callStart;
|
|
24644
25097
|
const errText = formatErrorForLLM(err);
|
|
24645
25098
|
const code = err?.code ?? err?.name ?? "Error";
|
|
25099
|
+
const skipAnalytics = name === "leadbay_set_telemetry";
|
|
25100
|
+
const sentryTriggeredBy = skipAnalytics ? void 0 : triggered_by;
|
|
24646
25101
|
if (isLeadbayBusinessError(err)) {
|
|
24647
|
-
if (err.code === "QUOTA_EXCEEDED") {
|
|
25102
|
+
if (!skipAnalytics && err.code === "QUOTA_EXCEEDED") {
|
|
24648
25103
|
telemetry2.captureQuotaHit({
|
|
24649
25104
|
tool: name,
|
|
24650
25105
|
retry_after_s: err._meta?.retry_after,
|
|
@@ -24652,51 +25107,55 @@ ${url}
|
|
|
24652
25107
|
});
|
|
24653
25108
|
}
|
|
24654
25109
|
const httpStatus2 = err._meta?.http_status;
|
|
24655
|
-
|
|
24656
|
-
|
|
24657
|
-
ok: false,
|
|
24658
|
-
duration_ms: errDur,
|
|
24659
|
-
format: "error-envelope",
|
|
24660
|
-
bytes: errText.length,
|
|
24661
|
-
error_code: code,
|
|
24662
|
-
...typeof httpStatus2 === "number" ? { http_status: httpStatus2 } : {},
|
|
24663
|
-
triggered_by
|
|
24664
|
-
});
|
|
24665
|
-
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
24666
|
-
telemetry2.captureCompositeCall({
|
|
25110
|
+
if (!skipAnalytics) {
|
|
25111
|
+
telemetry2.captureToolCall({
|
|
24667
25112
|
tool: name,
|
|
24668
|
-
last_prompt: triggered_by ?? "",
|
|
24669
25113
|
ok: false,
|
|
24670
25114
|
duration_ms: errDur,
|
|
25115
|
+
format: "error-envelope",
|
|
25116
|
+
bytes: errText.length,
|
|
24671
25117
|
error_code: code,
|
|
24672
|
-
...typeof httpStatus2 === "number" ? { http_status: httpStatus2 } : {}
|
|
25118
|
+
...typeof httpStatus2 === "number" ? { http_status: httpStatus2 } : {},
|
|
25119
|
+
triggered_by
|
|
24673
25120
|
});
|
|
25121
|
+
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
25122
|
+
telemetry2.captureCompositeCall({
|
|
25123
|
+
tool: name,
|
|
25124
|
+
last_prompt: triggered_by ?? "",
|
|
25125
|
+
ok: false,
|
|
25126
|
+
duration_ms: errDur,
|
|
25127
|
+
error_code: code,
|
|
25128
|
+
...typeof httpStatus2 === "number" ? { http_status: httpStatus2 } : {}
|
|
25129
|
+
});
|
|
25130
|
+
}
|
|
24674
25131
|
}
|
|
24675
|
-
telemetry2.captureException(err, buildBusinessCtx(name, err,
|
|
25132
|
+
telemetry2.captureException(err, buildBusinessCtx(name, err, sentryTriggeredBy));
|
|
24676
25133
|
} else {
|
|
24677
25134
|
telemetry2.captureException(err, {
|
|
24678
25135
|
tool: name,
|
|
24679
25136
|
source: "unexpected",
|
|
24680
25137
|
message: typeof err?.message === "string" ? err.message : void 0,
|
|
24681
|
-
triggered_by
|
|
24682
|
-
});
|
|
24683
|
-
telemetry2.captureToolCall({
|
|
24684
|
-
tool: name,
|
|
24685
|
-
ok: false,
|
|
24686
|
-
duration_ms: errDur,
|
|
24687
|
-
format: "error-envelope",
|
|
24688
|
-
bytes: errText.length,
|
|
24689
|
-
error_code: code,
|
|
24690
|
-
triggered_by
|
|
25138
|
+
...sentryTriggeredBy !== void 0 ? { triggered_by: sentryTriggeredBy } : {}
|
|
24691
25139
|
});
|
|
24692
|
-
if (
|
|
24693
|
-
telemetry2.
|
|
25140
|
+
if (!skipAnalytics) {
|
|
25141
|
+
telemetry2.captureToolCall({
|
|
24694
25142
|
tool: name,
|
|
24695
|
-
last_prompt: triggered_by ?? "",
|
|
24696
25143
|
ok: false,
|
|
24697
25144
|
duration_ms: errDur,
|
|
24698
|
-
|
|
25145
|
+
format: "error-envelope",
|
|
25146
|
+
bytes: errText.length,
|
|
25147
|
+
error_code: code,
|
|
25148
|
+
triggered_by
|
|
24699
25149
|
});
|
|
25150
|
+
if (COMPOSITE_FILE_TOOL_NAMES.has(name)) {
|
|
25151
|
+
telemetry2.captureCompositeCall({
|
|
25152
|
+
tool: name,
|
|
25153
|
+
last_prompt: triggered_by ?? "",
|
|
25154
|
+
ok: false,
|
|
25155
|
+
duration_ms: errDur,
|
|
25156
|
+
error_code: code
|
|
25157
|
+
});
|
|
25158
|
+
}
|
|
24700
25159
|
}
|
|
24701
25160
|
}
|
|
24702
25161
|
if (DEBUG_ON) {
|
|
@@ -24830,7 +25289,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
24830
25289
|
}
|
|
24831
25290
|
|
|
24832
25291
|
// src/http-server.ts
|
|
24833
|
-
var VERSION = true ? "0.
|
|
25292
|
+
var VERSION = true ? "0.27.0" : "0.0.0-dev";
|
|
24834
25293
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
24835
25294
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
24836
25295
|
var logger = {
|
|
@@ -24844,6 +25303,9 @@ var logger = {
|
|
|
24844
25303
|
var telemetry = initTelemetry({ version: VERSION, logger });
|
|
24845
25304
|
var IDENTITY_RESOLVE_TIMEOUT_MS = 1500;
|
|
24846
25305
|
async function resolveIdentity(client) {
|
|
25306
|
+
return (await resolveTelemetryContext(client)).identity;
|
|
25307
|
+
}
|
|
25308
|
+
async function resolveTelemetryContext(client) {
|
|
24847
25309
|
const region = client.region;
|
|
24848
25310
|
try {
|
|
24849
25311
|
const me = await Promise.race([
|
|
@@ -24852,34 +25314,78 @@ async function resolveIdentity(client) {
|
|
|
24852
25314
|
(resolve) => setTimeout(() => resolve(null), IDENTITY_RESOLVE_TIMEOUT_MS)
|
|
24853
25315
|
)
|
|
24854
25316
|
]);
|
|
24855
|
-
if (!me) return { distinctId: "mcp:unknown", region };
|
|
25317
|
+
if (!me) return { identity: { distinctId: "mcp:unknown", region }, enabled: false, forceClosed: true };
|
|
24856
25318
|
const distinctId = me.email ?? (me.id ? `mcp:user-${me.id}` : "mcp:unknown");
|
|
24857
25319
|
return {
|
|
24858
|
-
|
|
24859
|
-
|
|
24860
|
-
|
|
24861
|
-
|
|
24862
|
-
|
|
24863
|
-
|
|
24864
|
-
|
|
25320
|
+
identity: {
|
|
25321
|
+
distinctId,
|
|
25322
|
+
groups: me.organization?.id ? { organization: me.organization.id } : void 0,
|
|
25323
|
+
region,
|
|
25324
|
+
// name/email so leadbay_send_feedback attributes correctly on HTTP — the
|
|
25325
|
+
// module-scoped `me` is never populated here (Codex P2).
|
|
25326
|
+
...me.name ? { name: me.name } : {},
|
|
25327
|
+
...me.email ? { email: me.email } : {}
|
|
25328
|
+
},
|
|
25329
|
+
// Known preference: honor the per-user opt-out. Absent field → enabled
|
|
25330
|
+
// (older backend / opt-out default). A clean read is NOT force-closed.
|
|
25331
|
+
enabled: me.telemetry_enabled !== false,
|
|
25332
|
+
forceClosed: false
|
|
24865
25333
|
};
|
|
24866
25334
|
} catch (err) {
|
|
24867
25335
|
logger.warn?.(`telemetry identity resolve failed: ${err?.message ?? err}`);
|
|
24868
|
-
return { distinctId: "mcp:unknown", region };
|
|
25336
|
+
return { identity: { distinctId: "mcp:unknown", region }, enabled: false, forceClosed: true };
|
|
24869
25337
|
}
|
|
24870
25338
|
}
|
|
24871
|
-
function
|
|
25339
|
+
function suppressTelemetry(opts) {
|
|
25340
|
+
const { stamped, cached, forceClosed, sessionOptedOut, fallbackEnabled } = opts;
|
|
25341
|
+
if (stamped) return cached === false;
|
|
25342
|
+
if (forceClosed || cached === false || sessionOptedOut) return true;
|
|
25343
|
+
if (cached === true) return false;
|
|
25344
|
+
return !fallbackEnabled;
|
|
25345
|
+
}
|
|
25346
|
+
async function telemetryHandleForRequest(client) {
|
|
25347
|
+
const { identity, enabled, forceClosed } = await resolveTelemetryContext(client);
|
|
25348
|
+
const isSuppressed = () => suppressTelemetry({
|
|
25349
|
+
stamped: client.cachedTelemetryStamped(),
|
|
25350
|
+
cached: client.cachedTelemetryEnabled(),
|
|
25351
|
+
forceClosed,
|
|
25352
|
+
sessionOptedOut: false,
|
|
25353
|
+
// no session on the streamable path
|
|
25354
|
+
fallbackEnabled: enabled
|
|
25355
|
+
});
|
|
25356
|
+
return bindTelemetryIdentity(telemetry, identity, isSuppressed);
|
|
25357
|
+
}
|
|
25358
|
+
function bindTelemetryIdentity(base, identity, isSuppressed) {
|
|
25359
|
+
const on = (fn) => (...a) => {
|
|
25360
|
+
if (isSuppressed?.()) return;
|
|
25361
|
+
fn(...a);
|
|
25362
|
+
};
|
|
24872
25363
|
return {
|
|
24873
25364
|
...base,
|
|
24874
|
-
captureToolCall: (p) => base.captureToolCall(p, identity),
|
|
24875
|
-
captureCompositeCall: (p) => base.captureCompositeCall(p, identity),
|
|
24876
|
-
captureQuotaHit: (p) => base.captureQuotaHit(p, identity),
|
|
24877
|
-
captureTopupLink: (p) => base.captureTopupLink(p, identity),
|
|
24878
|
-
captureStartup: (p) => base.captureStartup(p, identity),
|
|
24879
|
-
captureAgentMemoryCaptured: (p) => base.captureAgentMemoryCaptured(p, identity),
|
|
24880
|
-
captureAgentMemoryRecalled: (p) => base.captureAgentMemoryRecalled(p, identity),
|
|
24881
|
-
captureAgentMemoryPruned: (p) => base.captureAgentMemoryPruned(p, identity),
|
|
25365
|
+
captureToolCall: on((p) => base.captureToolCall(p, identity)),
|
|
25366
|
+
captureCompositeCall: on((p) => base.captureCompositeCall(p, identity)),
|
|
25367
|
+
captureQuotaHit: on((p) => base.captureQuotaHit(p, identity)),
|
|
25368
|
+
captureTopupLink: on((p) => base.captureTopupLink(p, identity)),
|
|
25369
|
+
captureStartup: on((p) => base.captureStartup(p, identity)),
|
|
25370
|
+
captureAgentMemoryCaptured: on((p) => base.captureAgentMemoryCaptured(p, identity)),
|
|
25371
|
+
captureAgentMemoryRecalled: on((p) => base.captureAgentMemoryRecalled(p, identity)),
|
|
25372
|
+
captureAgentMemoryPruned: on((p) => base.captureAgentMemoryPruned(p, identity)),
|
|
25373
|
+
// captureFrictionReported is NOT gated by isSuppressed, for the same reason
|
|
25374
|
+
// as captureFeedback below: since product#3943 `leadbay_report_friction` is
|
|
25375
|
+
// a consent-gated, user-initiated "deliver my problem report to the team"
|
|
25376
|
+
// action the user explicitly approved and sees confirmed — not passive
|
|
25377
|
+
// analytics. Suppressing it here would silently drop the user's own report
|
|
25378
|
+
// while the agent tells them it was shared. (The tool returns reported:false
|
|
25379
|
+
// when delivery genuinely isn't possible, so the confirmation stays honest.)
|
|
24882
25380
|
captureFrictionReported: (p) => base.captureFrictionReported(p, identity),
|
|
25381
|
+
// ^ returns the base handle's real delivery result, so a Sentry-only or
|
|
25382
|
+
// PostHog-less process reports reported:false rather than a false confirm.
|
|
25383
|
+
captureException: on((err, ctx) => base.captureException(err, ctx)),
|
|
25384
|
+
// captureFeedback is NOT gated by isSuppressed (Codex P2): leadbay_send_feedback
|
|
25385
|
+
// is an explicit user-initiated "deliver my message to the team" action, not
|
|
25386
|
+
// passive telemetry. Opting out of analytics must not silently drop the user's
|
|
25387
|
+
// own feedback (it would return sent:false). Identity still rides along so the
|
|
25388
|
+
// Sentry feedback is attributed.
|
|
24883
25389
|
captureFeedback: (message, opts) => base.captureFeedback(message, opts, identity),
|
|
24884
25390
|
identify: async () => {
|
|
24885
25391
|
},
|
|
@@ -24995,10 +25501,9 @@ async function handleStreamable(c, resourcePath) {
|
|
|
24995
25501
|
if (resolved.authState === "missing" || resolved.authState === "expired") {
|
|
24996
25502
|
return sendChallenge(c, resourcePath, resolved.authState);
|
|
24997
25503
|
}
|
|
24998
|
-
const identity = await resolveIdentity(resolved.client);
|
|
24999
25504
|
const server = buildServerFromClient(
|
|
25000
25505
|
resolved.client,
|
|
25001
|
-
|
|
25506
|
+
await telemetryHandleForRequest(resolved.client)
|
|
25002
25507
|
);
|
|
25003
25508
|
const transport = new StreamableHTTPServerTransport({
|
|
25004
25509
|
sessionIdGenerator: void 0,
|
|
@@ -25046,16 +25551,51 @@ async function handleSse(c, resourcePath) {
|
|
|
25046
25551
|
if (resolved.authState === "missing" || resolved.authState === "expired") {
|
|
25047
25552
|
return sendChallenge(c, resourcePath, resolved.authState);
|
|
25048
25553
|
}
|
|
25049
|
-
const identity = await
|
|
25554
|
+
const { identity, enabled, forceClosed } = await resolveTelemetryContext(resolved.client);
|
|
25555
|
+
const session = {
|
|
25556
|
+
transport: void 0,
|
|
25557
|
+
// set below
|
|
25558
|
+
server: void 0,
|
|
25559
|
+
// set below
|
|
25560
|
+
createdAt: Date.now(),
|
|
25561
|
+
client: resolved.client,
|
|
25562
|
+
suppressed: !enabled,
|
|
25563
|
+
// Carry the resolve verdict's hard fail-closed (timeout/error at open). It
|
|
25564
|
+
// overrides even a cached `true` a late/orphaned resolveMe might populate,
|
|
25565
|
+
// and is cleared on the next SUCCESSFUL /messages refresh.
|
|
25566
|
+
forceClosed,
|
|
25567
|
+
refreshPending: false,
|
|
25568
|
+
refreshEpoch: 0
|
|
25569
|
+
};
|
|
25050
25570
|
const env = c.env;
|
|
25051
25571
|
const transport = new SSEServerTransport("/messages", env.outgoing);
|
|
25052
25572
|
const server = buildServerFromClient(
|
|
25053
25573
|
resolved.client,
|
|
25054
|
-
|
|
25574
|
+
// Suppression precedence via the shared suppressTelemetry() helper (Codex
|
|
25575
|
+
// P1/P2). An explicit same-session stamp (leadbay_set_telemetry) wins over
|
|
25576
|
+
// everything — so a mid-session opt-IN takes effect even if a background
|
|
25577
|
+
// refresh just failed closed. Otherwise any opt-out signal suppresses:
|
|
25578
|
+
// session.forceClosed (unreadable refresh), a cache read of `false`, OR
|
|
25579
|
+
// session.suppressed (a refresh that OBSERVED `false` this cycle, even if a
|
|
25580
|
+
// concurrent tool read left the shared cache at a stale `true`). Privacy
|
|
25581
|
+
// fails safe; only an explicit stamp can force emit.
|
|
25582
|
+
bindTelemetryIdentity(
|
|
25583
|
+
telemetry,
|
|
25584
|
+
identity,
|
|
25585
|
+
() => suppressTelemetry({
|
|
25586
|
+
stamped: resolved.client.cachedTelemetryStamped(),
|
|
25587
|
+
cached: resolved.client.cachedTelemetryEnabled(),
|
|
25588
|
+
forceClosed: session.forceClosed || session.refreshPending,
|
|
25589
|
+
sessionOptedOut: session.suppressed,
|
|
25590
|
+
fallbackEnabled: !session.suppressed
|
|
25591
|
+
})
|
|
25592
|
+
)
|
|
25055
25593
|
);
|
|
25056
25594
|
await server.connect(transport);
|
|
25057
25595
|
const sessionId = transport.sessionId;
|
|
25058
|
-
|
|
25596
|
+
session.transport = transport;
|
|
25597
|
+
session.server = server;
|
|
25598
|
+
sseSessions.set(sessionId, session);
|
|
25059
25599
|
transport.onclose = () => {
|
|
25060
25600
|
sseSessions.delete(sessionId);
|
|
25061
25601
|
server.close().catch(() => {
|
|
@@ -25065,6 +25605,58 @@ async function handleSse(c, resourcePath) {
|
|
|
25065
25605
|
}
|
|
25066
25606
|
app.get("/sse", (c) => handleSse(c, "/sse"));
|
|
25067
25607
|
app.get("/fr/sse", (c) => handleSse(c, "/fr/sse"));
|
|
25608
|
+
function scheduleSseTelemetryRefresh(session, stampSeqAtMessageStart, timeoutMs = IDENTITY_RESOLVE_TIMEOUT_MS) {
|
|
25609
|
+
if (session.refreshing) return;
|
|
25610
|
+
session.refreshing = true;
|
|
25611
|
+
session.refreshPending = true;
|
|
25612
|
+
const epoch = ++session.refreshEpoch;
|
|
25613
|
+
let readSettled = false;
|
|
25614
|
+
let timedOut = false;
|
|
25615
|
+
const applyIfCurrent = (apply, releaseGuard) => {
|
|
25616
|
+
if (session.refreshEpoch !== epoch) return false;
|
|
25617
|
+
session.refreshEpoch++;
|
|
25618
|
+
apply();
|
|
25619
|
+
session.refreshPending = false;
|
|
25620
|
+
if (releaseGuard) session.refreshing = false;
|
|
25621
|
+
return true;
|
|
25622
|
+
};
|
|
25623
|
+
const failClosed = () => {
|
|
25624
|
+
session.suppressed = true;
|
|
25625
|
+
session.forceClosed = true;
|
|
25626
|
+
session.client.clearTelemetryStampOrigin(stampSeqAtMessageStart);
|
|
25627
|
+
};
|
|
25628
|
+
const timeout = setTimeout(() => {
|
|
25629
|
+
if (readSettled) return;
|
|
25630
|
+
timedOut = true;
|
|
25631
|
+
applyIfCurrent(failClosed, false);
|
|
25632
|
+
}, timeoutMs);
|
|
25633
|
+
void session.client.fetchTelemetryEnabled().then(
|
|
25634
|
+
(enabled) => {
|
|
25635
|
+
readSettled = true;
|
|
25636
|
+
clearTimeout(timeout);
|
|
25637
|
+
if (timedOut) {
|
|
25638
|
+
session.refreshing = false;
|
|
25639
|
+
return;
|
|
25640
|
+
}
|
|
25641
|
+
applyIfCurrent(() => {
|
|
25642
|
+
if (enabled === false) {
|
|
25643
|
+
session.client.clearTelemetryStampOrigin(stampSeqAtMessageStart);
|
|
25644
|
+
}
|
|
25645
|
+
session.suppressed = enabled === false;
|
|
25646
|
+
session.forceClosed = false;
|
|
25647
|
+
}, true);
|
|
25648
|
+
},
|
|
25649
|
+
() => {
|
|
25650
|
+
readSettled = true;
|
|
25651
|
+
clearTimeout(timeout);
|
|
25652
|
+
if (timedOut) {
|
|
25653
|
+
session.refreshing = false;
|
|
25654
|
+
return;
|
|
25655
|
+
}
|
|
25656
|
+
applyIfCurrent(failClosed, true);
|
|
25657
|
+
}
|
|
25658
|
+
);
|
|
25659
|
+
}
|
|
25068
25660
|
app.post("/messages", async (c) => {
|
|
25069
25661
|
const foreign = rejectForeignOrigin(c);
|
|
25070
25662
|
if (foreign) return foreign;
|
|
@@ -25078,6 +25670,9 @@ app.post("/messages", async (c) => {
|
|
|
25078
25670
|
}
|
|
25079
25671
|
const env = c.env;
|
|
25080
25672
|
const body = await c.req.json().catch(() => void 0);
|
|
25673
|
+
const stampSeqAtMessageStart = session.client.telemetryStampSeq();
|
|
25674
|
+
session.client.clearTelemetryStampOrigin(stampSeqAtMessageStart);
|
|
25675
|
+
scheduleSseTelemetryRefresh(session, stampSeqAtMessageStart);
|
|
25081
25676
|
await session.transport.handlePostMessage(env.incoming, env.outgoing, body);
|
|
25082
25677
|
return new Response(null, { headers: { "x-hono-already-sent": "1" } });
|
|
25083
25678
|
});
|
|
@@ -25117,5 +25712,8 @@ if (isEntrypoint) {
|
|
|
25117
25712
|
export {
|
|
25118
25713
|
app,
|
|
25119
25714
|
bindTelemetryIdentity,
|
|
25120
|
-
resolveIdentity
|
|
25715
|
+
resolveIdentity,
|
|
25716
|
+
scheduleSseTelemetryRefresh,
|
|
25717
|
+
suppressTelemetry,
|
|
25718
|
+
telemetryHandleForRequest
|
|
25121
25719
|
};
|