@leadbay/mcp 0.17.1 → 0.17.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.js CHANGED
@@ -5102,6 +5102,8 @@ var init_composite_file_names = __esm({
5102
5102
  "leadbay_import_leads",
5103
5103
  "leadbay_import_status",
5104
5104
  "leadbay_list_campaigns",
5105
+ "leadbay_my_lenses",
5106
+ "leadbay_new_lens",
5105
5107
  "leadbay_prepare_outreach",
5106
5108
  "leadbay_pull_followups",
5107
5109
  "leadbay_pull_leads",
@@ -5121,7 +5123,7 @@ var init_composite_file_names = __esm({
5121
5123
  });
5122
5124
 
5123
5125
  // ../core/dist/tool-descriptions.generated.js
5124
- var leadbay_account_status, leadbay_add_leads_to_campaign, leadbay_add_note, leadbay_adjust_audience, leadbay_agent_memory_capture, leadbay_agent_memory_recall, leadbay_agent_memory_review, leadbay_answer_clarification, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_seed_candidates, leadbay_select_leads, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_user_prompt, leadbay_tour_plan, leadbay_update_lens, leadbay_update_lens_filter;
5126
+ var leadbay_account_status, leadbay_add_leads_to_campaign, leadbay_add_note, leadbay_adjust_audience, leadbay_agent_memory_capture, leadbay_agent_memory_recall, leadbay_agent_memory_review, leadbay_answer_clarification, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_seed_candidates, leadbay_select_leads, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_user_prompt, leadbay_tour_plan, leadbay_update_lens, leadbay_update_lens_filter;
5125
5127
  var init_tool_descriptions_generated = __esm({
5126
5128
  "../core/dist/tool-descriptions.generated.js"() {
5127
5129
  "use strict";
@@ -5218,7 +5220,37 @@ WHEN NOT TO USE: to log an outreach action \u2014 use leadbay_report_outreach, w
5218
5220
 
5219
5221
  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\`.
5220
5222
  `;
5221
- leadbay_adjust_audience = `Restrict (or expand) the lens audience by sector / size. Free-text sectors are auto-resolved against the sector taxonomy; ambiguous matches are surfaced to the agent rather than guessed silently. Permission routing is hidden: the default lens auto-clones to a new user lens; an org-level lens defaults to a per-user draft (admins can override with \`save_for_org:true\`). Filter MERGES with existing criteria (unrelated criteria are not dropped).
5223
+ leadbay_adjust_audience = `## WHEN TO USE
5224
+
5225
+ Trigger phrases: "narrow the audience to <sector>", "add <sector> to my <name> lens", "remove <sector> from this lens", "only show me companies of <size>", "stop including <sector>", "broaden this lens to also include <sector>".
5226
+
5227
+ **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
5228
+
5229
+ Do NOT use for: "create a new lens called X" \u2192 \`leadbay_new_lens\`; "make a new audience for Y" \u2192 \`leadbay_new_lens\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`; "focus on a kind of company beyond sector/size (e.g. 'hospitals running their own IT')" \u2192 \`leadbay_refine_prompt\`.
5230
+
5231
+ Prefer when: user wants to change an EXISTING lens's sectors/sizes. If the user NAMES a lens ('my Joinery lens'), you MUST pass lensName with that name \u2014 do NOT edit the active lens. To create a brand-new lens use leadbay_new_lens instead.
5232
+
5233
+ Examples that SHOULD invoke this tool:
5234
+ - "Add fintech to my Joinery lens."
5235
+ - "Narrow my audience to manufacturing companies, 50\u2013500 people."
5236
+ - "Stop including retail in this lens."
5237
+
5238
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
5239
+ - "Create a lens called Joinery for fintech."
5240
+ - "Show me my lenses."
5241
+ - "Focus on hospitals that run their own IT."
5242
+
5243
+ ## RENDER (quick)
5244
+
5245
+ On \`applied\`: confirm the lens edited (name) + the sectors/sizes added as
5246
+ chips. On \`ambiguous_sectors\` / \`ambiguous_lens\` / \`lens_not_found\`: surface
5247
+ the candidates and ask the user to pick, then re-call with the id/exact name.
5248
+
5249
+ ---
5250
+
5251
+ Restrict (or expand) the lens audience by sector / size. Free-text sectors are auto-resolved against the sector taxonomy; ambiguous matches are surfaced to the agent rather than guessed silently. Permission routing is hidden: the default lens auto-clones to a new user lens; an org-level lens defaults to a per-user draft (admins can override with \`save_for_org:true\`). Filter MERGES with existing criteria (unrelated criteria are not dropped).
5252
+
5253
+ **Targeting a lens \u2014 READ THIS.** By default this edits the user's ACTIVE lens. **If the user names a lens** ("add fintech to my **Joinery** lens", "in my Nordics lens, exclude retail"), you MUST pass \`lensName\` with that name (\`lensName:"Joinery"\`). Do NOT silently edit the active lens when a different one was named \u2014 that corrupts the wrong audience and is a top friction source. The name resolves against the user's lenses (case-insensitive, exact then unique-substring); it is edit-only and does NOT change which lens is active. An unmatched name returns \`status:"lens_not_found"\` with the lens list, and a name matching several returns \`status:"ambiguous_lens"\` with the candidates \u2014 surface them and re-call with the exact \`lensName\` or a \`lensId\`. Use \`leadbay_my_lenses\` if the user first wants to SEE or SWITCH lenses. To CREATE a brand-new lens, use \`leadbay_new_lens\` \u2014 not this tool.
5222
5254
 
5223
5255
  WHEN TO USE: when the user wants to see different kinds of leads (sector / size / etc.).
5224
5256
 
@@ -6431,6 +6463,258 @@ WHEN TO USE: at the start of a session if no token is preconfigured (cfg.token /
6431
6463
  WHEN NOT TO USE: if a token is already preconfigured \u2014 you'll just overwrite it. The user needs a Leadbay account first; they can register at https://wow.leadbay.ai/?register=true.
6432
6464
 
6433
6465
  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\`.
6466
+ `;
6467
+ leadbay_my_lenses = `## WHEN TO USE
6468
+
6469
+ Trigger phrases: "show me my lenses", "list my lenses", "which audiences do I have", "switch to my <name> lens", "change lens", "rename my <name> lens to <X>", "set the description of my <name> lens", "delete my <name> lens", "remove this lens".
6470
+
6471
+ **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
6472
+
6473
+ Do NOT use for: "narrow the audience" \u2192 \`leadbay_adjust_audience\`; "stop showing me <sector>" \u2192 \`leadbay_refine_prompt\`; "more leads on this lens" \u2192 \`leadbay_extend_lens\`; "show me today's leads" \u2192 \`leadbay_pull_leads\`.
6474
+
6475
+ Prefer when: user wants to SEE lenses, CHANGE which is active, RENAME one, or DELETE one \u2014 not edit a lens's sector/size criteria
6476
+
6477
+ Examples that SHOULD invoke this tool:
6478
+ - "Show me my lenses."
6479
+ - "Rename my Auto lens to Automotive and add a description."
6480
+ - "Delete my old Auto lens."
6481
+
6482
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
6483
+ - "Narrow the audience to fintech only."
6484
+ - "I want more leads on this lens."
6485
+ - "Show me today's leads."
6486
+
6487
+ ## RENDER (quick)
6488
+
6489
+ Small markdown table, active lens first: col 1 = \u2B50 prefix when active +
6490
+ lens name; col 2 = description (or \`\u2014\`). After a switch lead with
6491
+ "Now showing **<name>**."; after a rename lead with the rename confirmation.
6492
+ Full algorithm below.
6493
+
6494
+ ---
6495
+
6496
+ List the user's lenses (saved audiences) and, when asked, switch which one is active. A lens shapes the kind of leads delivered each day; this tool is how the user sees their audiences and moves between them \u2014 it does NOT edit a lens's criteria (that's \`leadbay_adjust_audience\`).
6497
+
6498
+ **Three modes, one tool:**
6499
+
6500
+ - **List (no args)** \u2014 pure read. Returns \`{status:"listed", lenses:[{id, name, description, is_active}], active_lens_id}\`. The active lens is resolved from the user's last-requested lens, so \`is_active\` is authoritative even if a row's flag is stale.
6501
+ - **Switch (\`switchToLensId\`)** \u2014 changes the active lens to that id and returns the REFRESHED list. The id MUST be one of the user's lenses; an unknown id returns \`{status:"not_found"}\` with the current list \u2014 surface it and ask the user to pick, do NOT invent an id. Switching to the already-active lens is a harmless no-op.
6502
+ - **Edit (\`editLensId\` + \`newName\` and/or \`newDescription\`)** \u2014 rename and/or set the description of a lens in one call, returns the REFRESHED list. Provide either or both; pass \`newDescription:""\` to clear a description. Same not_found handling. Use the \`id\` from the list for the lens the user named.
6503
+ - **Delete (\`deleteLensId\`)** \u2014 DESTRUCTIVE and confirm-gated. Without \`confirm:true\` it returns \`status:"delete_preview"\` with \`will_delete\` and removes NOTHING \u2014 show it, get the user's explicit yes, then re-call with \`confirm:true\`. The DEFAULT lens cannot be deleted (\`status:"cannot_delete_default"\`). Deleting the active lens leaves no active lens until the next switch/pull resolves one.
6504
+
6505
+ **Lens ids are strings** (e.g. \`"40005"\`) \u2014 pass the \`id\` value straight from the list when switching/renaming/deleting; it is fine to pass it as the string it came as.
6506
+
6507
+ **When the user is vague** ("switch lens" with no target), list first, then offer the lenses as a quick choice via \`ask_user_input_v0\` rather than guessing.
6508
+
6509
+ WHEN TO USE: when the user wants to see their lenses or switch the active one. Canonical phrasings: "show me my lenses", "which audiences do I have", "switch to my <name> lens".
6510
+
6511
+ WHEN NOT TO USE: to change a lens's audience criteria \u2014 that's \`leadbay_adjust_audience\`. Not for refining beyond firmographics (\`leadbay_refine_prompt\`), not for topping up the same lens (\`leadbay_extend_lens\`), not for the daily pull (\`leadbay_pull_leads\`).
6512
+
6513
+ 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\`.
6514
+
6515
+
6516
+ ## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
6517
+
6518
+ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can route into. These ALWAYS produce a better UX than markdown tables / inline prose for the data shapes they support \u2014 they're tappable on mobile, persistent across turns, and integrate with the host's quick-actions.
6519
+
6520
+ **The Big Three** \u2014 when a tool result fits, route there:
6521
+
6522
+ | Host widget | Use when | Field map (from Leadbay payload) |
6523
+ |---|---|---|
6524
+ | \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
6525
+ | \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
6526
+ | \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | \`{questions: [{question: "What next?", type: "single_select", options: [<2-4 short button labels>]}]}\`; max 3 questions per call |
6527
+
6528
+ ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
6529
+
6530
+ **Rules:**
6531
+ - The widget IS the visual. Do NOT emit a markdown table or prose list of the same data alongside \u2014 that produces two competing UIs.
6532
+ - Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
6533
+ - When the host doesn't expose the named widget, the agent falls back to the prose/table rendering the per-tool description already specifies. The directive is host-conditional; the fallback is automatic.
6534
+ - One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
6535
+
6536
+
6537
+ ---
6538
+
6539
+ ## RENDERING \u2014 lenses table, active-first
6540
+
6541
+ Markdown table with TWO columns. Sort **active lens first**, then by \`name\`
6542
+ ascending. **No score bar** \u2014 the \`\u25B0\u2756\u25B1\` glyph identity belongs to lead
6543
+ discovery, not lenses.
6544
+
6545
+ **Column 1 \u2014 Lens**
6546
+ - Prefix \`\u2B50 \` when \`is_active\` is true; otherwise no prefix.
6547
+ - The lens name in **bold**. (Lenses have no public URL \u2014 do not fabricate a link.)
6548
+
6549
+ **Column 2 \u2014 Description**
6550
+ - \`description\` verbatim, clipped to \u2264 18 words.
6551
+ - When null/empty: render \`\u2014\`.
6552
+
6553
+ **After a \`switched: true\` response**, open with a single confirmation line
6554
+ ABOVE the table: \`Now showing **<name>**.\` For \`status: "not_found"\`, lead with
6555
+ the \`message\` (the bad id) and render the list so the user can pick a real one.
6556
+
6557
+ **Empty list** (\`lenses: []\`): render \`*You don't have any lenses yet.*\` \u2014 do not
6558
+ render an empty table.
6559
+
6560
+ **Legend:** \u2B50 active lens.
6561
+
6562
+
6563
+ ## NEXT STEPS \u2014 after \`leadbay_my_lenses\`
6564
+
6565
+ **RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
6566
+
6567
+ The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
6568
+
6569
+ \`\`\`
6570
+ ask_user_input_v0({
6571
+ questions: [{
6572
+ question: "What next?",
6573
+ type: "single_select",
6574
+ options: [
6575
+ "<Suggest column from row 1>",
6576
+ "<Suggest column from row 2>",
6577
+ "<Suggest column from row 3>"
6578
+ ]
6579
+ }]
6580
+ })
6581
+ \`\`\`
6582
+
6583
+ When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
6584
+
6585
+ **Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
6586
+
6587
+ ---
6588
+
6589
+
6590
+
6591
+ Pick the 2\u20133 rows that fit what the user is likely to want next. When the user
6592
+ named no target but wants to switch, offer the lenses themselves as the
6593
+ quick-select options (each option = a lens name \u2192 \`leadbay_my_lenses(switchToLensId=<id>)\`).
6594
+
6595
+ | Observation | Suggest | Calls |
6596
+ |--------------------------------------|------------------------------------------|------------------------------------------------------|
6597
+ | User wants a different lens | "Switch to <lens name>" | \`leadbay_my_lenses(switchToLensId=<id>)\` |
6598
+ | User wants to rename / describe a lens| "Rename or describe <lens>" | \`leadbay_my_lenses(editLensId=<id>, newName?=<X>, newDescription?=<Y>)\` |
6599
+ | User wants to delete a lens | "Delete <lens>" | \`leadbay_my_lenses(deleteLensId=<id>)\` \u2192 confirm \u2192 \`confirm=true\` |
6600
+ | \`delete_preview\` (not yet deleted) | "Yes, delete it" | \`leadbay_my_lenses(deleteLensId=<id>, confirm=true)\` |
6601
+ | User wants leads on the active lens | "Pull today's leads" | \`leadbay_pull_leads()\` |
6602
+ | User wants to change the audience | "Adjust this lens's audience" | \`leadbay_adjust_audience(...)\` |
6603
+ | User wants more of the same | "Get a bigger batch on this lens" | \`leadbay_extend_lens(...)\` |
6604
+
6605
+ If nothing fits, default to "pull today's leads on the active lens" \u2014 never
6606
+ invent a tool that doesn't exist.
6607
+ `;
6608
+ leadbay_new_lens = `## WHEN TO USE
6609
+
6610
+ Trigger phrases: "create a lens", "create a new lens called <name>", "create a lens specialized in/into <X>", "make me a new audience for <X>", "set up a lens for <sector>", "new lens named <name>", "I want a lens just for <X>".
6611
+
6612
+ **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
6613
+
6614
+ Do NOT use for: "narrow the audience / add or remove a sector on an EXISTING lens" \u2192 \`leadbay_adjust_audience\`; "add <sector> to my <name> lens" \u2192 \`leadbay_adjust_audience\`; "focus on a qualitative trait beyond sector/size" \u2192 \`leadbay_refine_prompt\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`; "more leads on this lens" \u2192 \`leadbay_extend_lens\`.
6615
+
6616
+ Prefer when: user wants a brand-new lens (create/make/set up, often 'specialized in <X>'). Editing an existing lens \u2192 leadbay_adjust_audience (use lensName). Qualitative refinement \u2192 refine_prompt (admin-only).
6617
+
6618
+ Examples that SHOULD invoke this tool:
6619
+ - "Create a lens called Joinery for the fintech sector."
6620
+ - "Make me a new audience for healthcare companies, 30\u2013300 people."
6621
+ - "Set up a new lens named Nordics SaaS."
6622
+
6623
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
6624
+ - "Add fintech to my Joinery lens."
6625
+ - "Show me my lenses."
6626
+ - "I want more leads on this lens."
6627
+
6628
+ ## RENDER (quick)
6629
+
6630
+ On \`preview\` (default \u2014 NOTHING created yet): show the lens that WILL be
6631
+ created (name + resolved sectors/sizes as chips) and ASK the user to confirm
6632
+ via ask_user_input_v0 ("Create this lens?" / "Change something"). Only on
6633
+ "yes" re-call with confirm:true. On \`created\`: confirm "Created **<name>**."
6634
+ On \`ambiguous_sectors\`: surface the candidate sectors to pick from.
6635
+
6636
+ ---
6637
+
6638
+ Create a brand-new lens (saved audience) and apply its sector/size criteria. Clones a base lens (the user's active/default lens unless \`base\` is given), names it, and applies the filter.
6639
+
6640
+ **Confirm before creating \u2014 two-step by default.** A call WITHOUT \`confirm:true\` is a dry run: it resolves the sectors/sizes and returns \`status:"preview"\` with \`will_create\` (what it WOULD build) \u2014 **nothing is created**. Show that to the user, get an explicit yes (ask via \`ask_user_input_v0\`), then re-call the SAME args with \`confirm:true\` to actually create. Never pass \`confirm:true\` on the first call \u2014 the user must see the preview first. (Sector ambiguity is still surfaced in the preview step, so they pick before confirming.)
6641
+
6642
+ **Sectors resolve first.** Free-text \`sectors\`/\`exclude_sectors\` are auto-resolved against the taxonomy. If any don't resolve, the tool returns \`status:"ambiguous_sectors"\` with the candidates and **does NOT create the lens** \u2014 so re-calling after picking the right sector won't leave orphan half-built lenses. To discover valid sector labels up front, use \`leadbay_list_sectors\`.
6643
+
6644
+ **Does not switch the active lens.** The new lens is created but the user stays on their current one. Offer \`leadbay_my_lenses(switchToLensId=<new id>)\` as a next step if they want to start pulling from it.
6645
+
6646
+ WHEN TO USE: when the user wants a NEW lens. Canonical phrasings: "create a lens called X", "make a new audience for Y", "set up a lens for <sector>".
6647
+
6648
+ WHEN NOT TO USE: to EDIT an existing lens \u2014 use \`leadbay_adjust_audience\` (pass \`lensName\` to target one by name). Not for listing/switching (\`leadbay_my_lenses\`) or topping up (\`leadbay_extend_lens\`).
6649
+
6650
+ 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\`.
6651
+
6652
+
6653
+ ## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
6654
+
6655
+ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can route into. These ALWAYS produce a better UX than markdown tables / inline prose for the data shapes they support \u2014 they're tappable on mobile, persistent across turns, and integrate with the host's quick-actions.
6656
+
6657
+ **The Big Three** \u2014 when a tool result fits, route there:
6658
+
6659
+ | Host widget | Use when | Field map (from Leadbay payload) |
6660
+ |---|---|---|
6661
+ | \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
6662
+ | \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
6663
+ | \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | \`{questions: [{question: "What next?", type: "single_select", options: [<2-4 short button labels>]}]}\`; max 3 questions per call |
6664
+
6665
+ ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
6666
+
6667
+ **Rules:**
6668
+ - The widget IS the visual. Do NOT emit a markdown table or prose list of the same data alongside \u2014 that produces two competing UIs.
6669
+ - Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
6670
+ - When the host doesn't expose the named widget, the agent falls back to the prose/table rendering the per-tool description already specifies. The directive is host-conditional; the fallback is automatic.
6671
+ - One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
6672
+
6673
+
6674
+ ---
6675
+
6676
+ ## NEXT STEPS \u2014 after \`leadbay_new_lens\`
6677
+
6678
+ **RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
6679
+
6680
+ The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
6681
+
6682
+ \`\`\`
6683
+ ask_user_input_v0({
6684
+ questions: [{
6685
+ question: "What next?",
6686
+ type: "single_select",
6687
+ options: [
6688
+ "<Suggest column from row 1>",
6689
+ "<Suggest column from row 2>",
6690
+ "<Suggest column from row 3>"
6691
+ ]
6692
+ }]
6693
+ })
6694
+ \`\`\`
6695
+
6696
+ When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
6697
+
6698
+ **Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
6699
+
6700
+ ---
6701
+
6702
+
6703
+
6704
+ Pick the rows that fit. On \`created\`, the switch + pull rows are the natural
6705
+ follow-ups. On \`ambiguous_sectors\`, the only move is to pick a sector and re-call.
6706
+
6707
+ | Observation | Suggest | Calls |
6708
+ |-----------------------------------|------------------------------------------|--------------------------------------------------------|
6709
+ | \`preview\` (not yet created) | "Yes, create this lens" | \`leadbay_new_lens(...same args..., confirm=true)\` |
6710
+ | \`preview\` (not yet created) | "Change the sectors/size first" | (re-ask the user, then \`leadbay_new_lens\` with new args) |
6711
+ | Lens created | "Switch to it and pull leads" | \`leadbay_my_lenses(switchToLensId=<new id>)\` then \`leadbay_pull_leads()\` |
6712
+ | Lens created | "Refine the audience further" | \`leadbay_adjust_audience(lensName=<new name>, ...)\` |
6713
+ | Lens created | "Leave it; keep my current lens active" | (no call) |
6714
+ | \`ambiguous_sectors\` | "Pick the right sector and create" | \`leadbay_new_lens(name=..., sectors=[<chosen id>])\` |
6715
+
6716
+ If nothing fits, default to "switch to the new lens and pull leads" \u2014 never
6717
+ invent a tool that doesn't exist.
6434
6718
  `;
6435
6719
  leadbay_open_billing_portal = `Generate a one-shot Stripe customer-portal URL. Wraps \`GET /1.5/stripe/portal\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL the user can open to manage their existing Leadbay subscription: change plan tier, swap payment method, view invoices. The agent does NOT make subscription changes itself \u2014 it surfaces the URL and lets the user act.
6436
6720
 
@@ -7064,7 +7348,33 @@ WHEN TO USE: before leadbay_enrich_titles, to plan which titles to order.
7064
7348
 
7065
7349
  WHEN NOT TO USE: when you already know the exact titles you want to enrich.
7066
7350
  `;
7067
- leadbay_refine_prompt = `Refine the kind of leads Leadbay surfaces, beyond firmographics. Free-text instruction (e.g. "focus on hospitals running their own IT"). Sets the org's \`user_prompt\`; if the new prompt produces ambiguous criteria, Leadbay raises a clarification question, which this composite polls for and surfaces. Admin-only on the backend (will return 403 for non-admins).
7351
+ leadbay_refine_prompt = `## WHEN TO USE
7352
+
7353
+ Trigger phrases: "focus on companies that <qualitative trait>", "I prefer leads that <behavior/characteristic>", "prioritize companies running their own IT", "deprioritize companies that just raised".
7354
+
7355
+ **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
7356
+
7357
+ Do NOT use for: "create a new lens / a lens specialized into <X>" \u2192 \`leadbay_new_lens\`; "add/remove <sector> to/from my <name> lens" \u2192 \`leadbay_adjust_audience\`; "narrow the audience to <sector> / <size>" \u2192 \`leadbay_adjust_audience\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`.
7358
+
7359
+ Prefer when: ADMIN-ONLY. Qualitative refinement of the active lens that sector/size can't express. Creating/naming/listing/switching/sector-editing a lens routes elsewhere. Non-admin user \u2192 do NOT pick this.
7360
+
7361
+ Examples that SHOULD invoke this tool:
7362
+ - "Focus on hospitals that run their own IT in-house."
7363
+ - "Prioritize companies that have recently expanded headcount."
7364
+
7365
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
7366
+ - "Create a lens specialized in automobile."
7367
+ - "Add fintech to my Joinery lens."
7368
+ - "Show me my lenses."
7369
+
7370
+ ## RENDER (quick)
7371
+
7372
+ On success: confirm the refinement applied to the active lens. If a
7373
+ clarification was raised, surface its question (route via ask_user_input_v0).
7374
+
7375
+ ---
7376
+
7377
+ Refine the kind of leads Leadbay surfaces, beyond firmographics. Free-text instruction (e.g. "focus on hospitals running their own IT"). Sets the org's \`user_prompt\`; if the new prompt produces ambiguous criteria, Leadbay raises a clarification question, which this composite polls for and surfaces. Admin-only on the backend (will return 403 for non-admins).
7068
7378
 
7069
7379
  WHEN TO USE: when audience filters (leadbay_adjust_audience) aren't enough.
7070
7380
 
@@ -11296,7 +11606,7 @@ var init_create_lens = __esm({
11296
11606
  },
11297
11607
  execute: async (client, params) => {
11298
11608
  const lens = await client.request("POST", "/lenses", {
11299
- base: params.base,
11609
+ base: String(params.base),
11300
11610
  name: params.name,
11301
11611
  description: params.description
11302
11612
  });
@@ -17465,6 +17775,8 @@ var init_bulk_enrich_status = __esm({
17465
17775
 
17466
17776
  // ../core/dist/composite/adjust-audience.js
17467
17777
  function tokens(s) {
17778
+ if (!s)
17779
+ return [];
17468
17780
  return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
17469
17781
  }
17470
17782
  function bestMatches(text, taxonomy) {
@@ -17478,11 +17790,11 @@ function bestMatches(text, taxonomy) {
17478
17790
  if (have.has(t))
17479
17791
  overlap += 1;
17480
17792
  const score = overlap / Math.max(want.size, 1);
17481
- return { id: s.id, name: s.name, score };
17793
+ return { id: s.id, name: s.name ?? "", score };
17482
17794
  }).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
17483
17795
  return ranked.slice(0, 5);
17484
17796
  }
17485
- async function resolveSectors(client, texts) {
17797
+ async function resolveSectors(client, texts, ctx) {
17486
17798
  const looksLikeId2 = (s) => /^\d+$/.test(s);
17487
17799
  const direct = texts.filter(looksLikeId2);
17488
17800
  const free = texts.filter((s) => !looksLikeId2(s));
@@ -17491,6 +17803,10 @@ async function resolveSectors(client, texts) {
17491
17803
  const me = await client.resolveMe().catch(() => null);
17492
17804
  const lang = me?.language ?? "en";
17493
17805
  const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
17806
+ const nullNames = taxonomy.filter((s) => !s.name).length;
17807
+ if (nullNames > 0) {
17808
+ ctx?.logger?.warn?.(`adjust_audience: /sectors/all returned ${nullNames}/${taxonomy.length} sector(s) with a null/missing name`);
17809
+ }
17494
17810
  const resolved = [...direct];
17495
17811
  const ambiguities = [];
17496
17812
  for (const text of free) {
@@ -17503,6 +17819,22 @@ async function resolveSectors(client, texts) {
17503
17819
  }
17504
17820
  return { resolved, ambiguities };
17505
17821
  }
17822
+ async function resolveLensByName(client, name) {
17823
+ const lenses = await client.request("GET", "/lenses");
17824
+ const all = lenses.map((l) => ({ id: l.id, name: l.name }));
17825
+ const needle = name.trim().toLowerCase();
17826
+ const exact = all.filter((l) => (l.name ?? "").trim().toLowerCase() === needle);
17827
+ if (exact.length === 1)
17828
+ return { ok: true, id: exact[0].id };
17829
+ if (exact.length > 1)
17830
+ return { ok: false, reason: "ambiguous", matches: exact };
17831
+ const partial = all.filter((l) => (l.name ?? "").toLowerCase().includes(needle));
17832
+ if (partial.length === 1)
17833
+ return { ok: true, id: partial[0].id };
17834
+ if (partial.length > 1)
17835
+ return { ok: false, reason: "ambiguous", matches: partial };
17836
+ return { ok: false, reason: "not_found", lenses: all };
17837
+ }
17506
17838
  function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
17507
17839
  const items = current?.lens_filter?.items ?? [];
17508
17840
  const item = items[0] ?? { criteria: [] };
@@ -17536,11 +17868,15 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
17536
17868
  }
17537
17869
  }
17538
17870
  if (sizes && sizes.length > 0) {
17871
+ const normalizedSizes = sizes.map((s) => ({
17872
+ min: s.min ?? 0,
17873
+ max: s.max ?? 1e6
17874
+ }));
17539
17875
  const idx = criteria.findIndex((c) => c.type === "size");
17540
17876
  if (idx >= 0) {
17541
- criteria[idx] = { type: "size", is_excluded: false, sizes };
17877
+ criteria[idx] = { type: "size", is_excluded: false, sizes: normalizedSizes };
17542
17878
  } else {
17543
- criteria.push({ type: "size", is_excluded: false, sizes });
17879
+ criteria.push({ type: "size", is_excluded: false, sizes: normalizedSizes });
17544
17880
  }
17545
17881
  }
17546
17882
  return {
@@ -17548,6 +17884,9 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
17548
17884
  locations: current.locations ?? { results: [], parents: [] }
17549
17885
  };
17550
17886
  }
17887
+ function filterWriteBody(filter) {
17888
+ return { items: filter.lens_filter.items };
17889
+ }
17551
17890
  var adjustAudience;
17552
17891
  var init_adjust_audience = __esm({
17553
17892
  "../core/dist/composite/adjust-audience.js"() {
@@ -17594,6 +17933,10 @@ var init_adjust_audience = __esm({
17594
17933
  description: "Company size buckets, e.g. [{min:30,max:300}]"
17595
17934
  },
17596
17935
  lensId: { type: "number", description: "Lens id (escape hatch)" },
17936
+ lensName: {
17937
+ type: "string",
17938
+ description: "Target a lens BY NAME (e.g. 'Joinery') instead of the active one. Resolved against your lenses \u2014 edit-only, does NOT switch your active lens. Unknown/ambiguous names are surfaced to pick from. Takes effect only when lensId is not given."
17939
+ },
17597
17940
  save_for_org: {
17598
17941
  type: "boolean",
17599
17942
  description: "Admin only \u2014 propagate the change to the org-level lens for everyone (default false: per-user draft)"
@@ -17607,21 +17950,35 @@ var init_adjust_audience = __esm({
17607
17950
  },
17608
17951
  outputSchema: {
17609
17952
  type: "object",
17610
- description: "Two return shapes: 'ambiguous_sectors' when free-text sectors matched multiple candidates (agent re-calls with sector_ids), 'applied' on success.",
17953
+ description: "Return shapes: 'applied' on success; 'ambiguous_sectors' when free-text sectors matched multiple candidates (re-call with sector_ids); 'lens_not_found' / 'ambiguous_lens' when a lensName didn't resolve to exactly one lens (re-call with lensId or an exact lensName).",
17611
17954
  properties: {
17612
17955
  status: {
17613
17956
  type: "string",
17614
- description: "'ambiguous_sectors' or 'applied'."
17957
+ description: "'applied', 'ambiguous_sectors', 'lens_not_found', or 'ambiguous_lens'."
17615
17958
  },
17616
17959
  sector_ambiguities: {
17617
17960
  type: "array",
17618
17961
  description: "Per ambiguous text: {sector_text, matches:[{id, name, score}]}. Agent picks an id and re-calls.",
17619
17962
  items: { type: "object" }
17620
17963
  },
17964
+ lenses: {
17965
+ type: "array",
17966
+ description: "On 'lens_not_found': the user's lenses [{id, name}] to pick from.",
17967
+ items: { type: "object" }
17968
+ },
17969
+ matches: {
17970
+ type: "array",
17971
+ description: "On 'ambiguous_lens': the lenses [{id, name}] the name matched.",
17972
+ items: { type: "object" }
17973
+ },
17974
+ lens_query: {
17975
+ type: "string",
17976
+ description: "On 'lens_not_found' / 'ambiguous_lens': the lensName the user asked for."
17977
+ },
17621
17978
  message: { type: "string" },
17622
17979
  lens_used: {
17623
17980
  type: "object",
17624
- description: "Resolved lens metadata: {id, name, was_draft, was_new, save_for_org}."
17981
+ description: "Resolved lens metadata: {id, name, was_draft, was_new, active_lens_changed, save_for_org}."
17625
17982
  },
17626
17983
  filter_applied: {
17627
17984
  type: "object",
@@ -17634,28 +17991,63 @@ var init_adjust_audience = __esm({
17634
17991
  execute: async (client, params, ctx) => {
17635
17992
  const me = await client.resolveMe();
17636
17993
  const isAdmin = me.admin === true;
17637
- const startingLensId = params.lensId ?? me.last_requested_lens ?? await client.resolveDefaultLens();
17994
+ let namedLensId;
17995
+ if (params.lensId == null && params.lensName != null && params.lensName.trim() !== "") {
17996
+ const res = await resolveLensByName(client, params.lensName);
17997
+ if (!res.ok && res.reason === "not_found") {
17998
+ return {
17999
+ status: "lens_not_found",
18000
+ lens_query: params.lensName,
18001
+ lenses: res.lenses,
18002
+ message: `No lens named "${params.lensName}". Pick one of the listed lenses (pass lensId or an exact lensName), or create it first.`
18003
+ };
18004
+ }
18005
+ if (!res.ok && res.reason === "ambiguous") {
18006
+ return {
18007
+ status: "ambiguous_lens",
18008
+ lens_query: params.lensName,
18009
+ matches: res.matches,
18010
+ message: `"${params.lensName}" matched multiple lenses. Re-call with the exact lensName or the lensId of the one you mean.`
18011
+ };
18012
+ }
18013
+ if (res.ok)
18014
+ namedLensId = res.id;
18015
+ }
18016
+ const startingLensId = params.lensId ?? namedLensId ?? me.last_requested_lens ?? await client.resolveDefaultLens();
18017
+ const isNamedEdit = namedLensId != null && params.lensId == null;
17638
18018
  const includeTexts = [
17639
18019
  ...params.sectors ?? [],
17640
18020
  ...params.sector_ids ?? []
17641
18021
  ];
17642
18022
  const excludeTexts = params.exclude_sectors ?? [];
17643
- const includeRes = await resolveSectors(client, includeTexts);
17644
- const excludeRes = await resolveSectors(client, excludeTexts);
18023
+ const includeRes = await resolveSectors(client, includeTexts, ctx);
18024
+ const excludeRes = await resolveSectors(client, excludeTexts, ctx);
17645
18025
  const ambiguities = [
17646
18026
  ...includeRes.ambiguities,
17647
18027
  ...excludeRes.ambiguities
17648
18028
  ];
17649
18029
  if (ambiguities.length > 0) {
18030
+ const noMatch = ambiguities.filter((a) => a.matches.length === 0);
18031
+ const multi = ambiguities.filter((a) => a.matches.length > 0);
18032
+ const parts = [];
18033
+ if (noMatch.length > 0) {
18034
+ const names = noMatch.map((a) => `"${a.sector_text}"`).join(", ");
18035
+ parts.push(`Couldn't find a sector matching ${names}. Ask the user to rephrase or pick a known sector, then re-call with sector_ids=...`);
18036
+ }
18037
+ if (multi.length > 0) {
18038
+ const names = multi.map((a) => `"${a.sector_text}"`).join(", ");
18039
+ parts.push(`${names} matched multiple sectors. Pick from the matches and re-call with sector_ids=...`);
18040
+ }
17650
18041
  return {
17651
18042
  status: "ambiguous_sectors",
17652
18043
  sector_ambiguities: ambiguities,
17653
- message: "One or more sector names matched multiple sectors. Pick from the matches and re-call with sector_ids=..."
18044
+ message: parts.join(" ")
17654
18045
  };
17655
18046
  }
17656
18047
  const lens = await client.request("GET", `/lenses/${startingLensId}`);
17657
18048
  const currentFilter = await client.request("GET", `/lenses/${startingLensId}/filter`);
17658
18049
  const merged = mergeFilter(currentFilter, includeRes.resolved, excludeRes.resolved, params.sizes);
18050
+ const mergedBody = filterWriteBody(merged);
17659
18051
  const isDefault = lens.is_default || lens.default;
17660
18052
  const isUserLevel = lens.user_id != null;
17661
18053
  const isOrgLevel = !isUserLevel && !isDefault;
@@ -17664,24 +18056,28 @@ var init_adjust_audience = __esm({
17664
18056
  let wasNew = false;
17665
18057
  if (isDefault) {
17666
18058
  const name = params.newLensName ?? `Custom audience \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
17667
- const newLens = await client.request("POST", "/lenses", {
17668
- base: startingLensId,
18059
+ const newLens2 = await client.request("POST", "/lenses", {
18060
+ base: String(startingLensId),
17669
18061
  name
17670
18062
  });
17671
- targetLensId = newLens.id;
18063
+ targetLensId = newLens2.id;
17672
18064
  wasNew = true;
17673
- await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, merged);
17674
- await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
18065
+ await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, mergedBody);
18066
+ if (!isNamedEdit) {
18067
+ await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
18068
+ }
17675
18069
  } else if (isUserLevel) {
17676
18070
  try {
17677
- await client.requestVoid("POST", `/lenses/${startingLensId}/filter`, merged);
18071
+ await client.requestVoid("POST", `/lenses/${startingLensId}/filter`, mergedBody);
17678
18072
  } catch (err) {
17679
18073
  if (err?.code === "FORBIDDEN") {
17680
18074
  wasDraft = true;
17681
18075
  const draft = await client.request("POST", `/lenses/${startingLensId}/draft`);
17682
18076
  targetLensId = draft.id;
17683
- await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, merged);
17684
- await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
18077
+ await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, mergedBody);
18078
+ if (!isNamedEdit) {
18079
+ await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
18080
+ }
17685
18081
  } else {
17686
18082
  throw err;
17687
18083
  }
@@ -17693,7 +18089,7 @@ var init_adjust_audience = __esm({
17693
18089
  const draft = await client.request("POST", `/lenses/${startingLensId}/draft`);
17694
18090
  targetLensId = draft.id;
17695
18091
  try {
17696
- await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, merged);
18092
+ await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, mergedBody);
17697
18093
  } catch (err) {
17698
18094
  ctx?.logger?.warn?.(`adjust_audience: filter on draft ${targetLensId} failed: ${err?.message}`);
17699
18095
  try {
@@ -17709,17 +18105,21 @@ var init_adjust_audience = __esm({
17709
18105
  }
17710
18106
  throw err;
17711
18107
  }
17712
- await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
18108
+ if (!isNamedEdit) {
18109
+ await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
18110
+ }
17713
18111
  } else {
17714
18112
  try {
17715
- await client.requestVoid("POST", `/lenses/${startingLensId}/filter`, merged);
18113
+ await client.requestVoid("POST", `/lenses/${startingLensId}/filter`, mergedBody);
17716
18114
  } catch (err) {
17717
18115
  throw err;
17718
18116
  }
17719
18117
  }
17720
18118
  }
17721
- client.invalidateMe();
18119
+ if (!isNamedEdit)
18120
+ client.invalidateMe();
17722
18121
  client.invalidateDefaultLens();
18122
+ const namedEditForkedMessage = isNamedEdit && (wasNew || wasDraft) ? ` Note: "${lens.name}" can't be edited in place, so the change was applied to a ${wasDraft ? "personal draft" : "new copy"} (id ${targetLensId}); your active lens is unchanged.` : "";
17723
18123
  return {
17724
18124
  status: "applied",
17725
18125
  lens_used: {
@@ -17727,10 +18127,11 @@ var init_adjust_audience = __esm({
17727
18127
  name: lens.name,
17728
18128
  was_draft: wasDraft,
17729
18129
  was_new: wasNew,
18130
+ active_lens_changed: !isNamedEdit && (wasNew || wasDraft),
17730
18131
  save_for_org: params.save_for_org === true && isAdmin && isOrgLevel
17731
18132
  },
17732
18133
  filter_applied: merged,
17733
- message: wasDraft ? "Applied to your personal draft of the org lens (your view only)." : wasNew ? `Created a new user-level lens "${lens.name}" with the filter (you can rename via leadbay_update_lens).` : "Applied directly to the lens.",
18134
+ message: (wasDraft ? "Applied to your personal draft of the org lens (your view only)." : wasNew ? `Created a new user-level lens "${lens.name}" with the filter (you can rename via leadbay_update_lens).` : "Applied directly to the lens.") + namedEditForkedMessage,
17734
18135
  _meta: { region: client.region }
17735
18136
  };
17736
18137
  }
@@ -18125,6 +18526,399 @@ var init_extend_lens = __esm({
18125
18526
  }
18126
18527
  });
18127
18528
 
18529
+ // ../core/dist/composite/my-lenses.js
18530
+ async function listWithActive(client) {
18531
+ const lenses = await client.request("GET", "/lenses");
18532
+ const me = await client.resolveMe().catch(() => null);
18533
+ const activeFromMe = sid(me?.last_requested_lens);
18534
+ const active_lens_id = activeFromMe ?? sid(lenses.find((l) => l.is_last_active)?.id) ?? null;
18535
+ return {
18536
+ active_lens_id,
18537
+ lenses: lenses.map((l) => ({
18538
+ id: sid(l.id),
18539
+ name: l.name,
18540
+ description: l.description ?? null,
18541
+ is_active: sid(l.id) === active_lens_id,
18542
+ is_default: l.is_default === true || l.default === true
18543
+ }))
18544
+ };
18545
+ }
18546
+ var sid, myLenses;
18547
+ var init_my_lenses = __esm({
18548
+ "../core/dist/composite/my-lenses.js"() {
18549
+ "use strict";
18550
+ init_tool_descriptions_generated();
18551
+ sid = (v) => v == null ? null : String(v);
18552
+ myLenses = {
18553
+ name: "leadbay_my_lenses",
18554
+ annotations: {
18555
+ title: "List, switch, edit, or delete your lenses",
18556
+ // No args → pure read. The delete mode issues DELETE /lenses/:id (an
18557
+ // irreversible side effect), so the tool is destructive — clients must
18558
+ // treat it as approval-required, not auto-run. The delete path is itself
18559
+ // confirm-gated (preview unless confirm:true). switch/edit are not
18560
+ // idempotent across modes either, so don't claim idempotency.
18561
+ readOnlyHint: false,
18562
+ destructiveHint: true,
18563
+ idempotentHint: false,
18564
+ openWorldHint: true
18565
+ },
18566
+ description: leadbay_my_lenses,
18567
+ inputSchema: {
18568
+ type: "object",
18569
+ properties: {
18570
+ switchToLensId: {
18571
+ type: ["string", "number"],
18572
+ description: "When set, switch the active lens to this id (must be one of the user's lenses), then return the refreshed list."
18573
+ },
18574
+ editLensId: {
18575
+ type: ["string", "number"],
18576
+ description: "When set, edit this lens's metadata \u2014 provide newName and/or newDescription. Must be one of the user's lenses."
18577
+ },
18578
+ newName: {
18579
+ type: "string",
18580
+ description: "New lens name (used with editLensId)."
18581
+ },
18582
+ newDescription: {
18583
+ type: "string",
18584
+ description: "New lens description (used with editLensId). Pass an empty string to clear it."
18585
+ },
18586
+ deleteLensId: {
18587
+ type: ["string", "number"],
18588
+ description: "When set, delete this lens. DESTRUCTIVE \u2014 returns a delete_preview unless confirm:true. Cannot delete the default lens."
18589
+ },
18590
+ confirm: {
18591
+ type: "boolean",
18592
+ description: "Required (=true) to actually delete. Without it, deleteLensId returns a preview to confirm with the user first."
18593
+ }
18594
+ },
18595
+ additionalProperties: false
18596
+ },
18597
+ outputSchema: {
18598
+ type: "object",
18599
+ properties: {
18600
+ status: {
18601
+ type: "string",
18602
+ description: "'listed', 'switched', 'already_active', 'edited', 'deleted', 'delete_preview' (confirm to proceed), 'cannot_delete_default', or 'not_found'."
18603
+ },
18604
+ switched: { type: "boolean", description: "True when this call changed the active lens." },
18605
+ edited: { type: "boolean", description: "True when this call renamed/re-described a lens." },
18606
+ deleted: { type: "boolean", description: "True when this call deleted a lens." },
18607
+ will_delete: {
18608
+ type: "object",
18609
+ description: "On 'delete_preview': the lens that WILL be deleted {id, name}. Nothing removed yet."
18610
+ },
18611
+ active_lens_id: { type: ["string", "null"] },
18612
+ lenses: {
18613
+ type: "array",
18614
+ description: "The user's lenses. Each: {id, name, description, is_active}.",
18615
+ items: { type: "object" }
18616
+ },
18617
+ message: { type: "string" }
18618
+ },
18619
+ required: ["status", "lenses", "active_lens_id"]
18620
+ },
18621
+ execute: async (client, params) => {
18622
+ if (params.deleteLensId != null) {
18623
+ const targetId = sid(params.deleteLensId);
18624
+ const before = await listWithActive(client);
18625
+ const target = before.lenses.find((l) => l.id === targetId);
18626
+ if (!target) {
18627
+ return {
18628
+ status: "not_found",
18629
+ switched: false,
18630
+ edited: false,
18631
+ deleted: false,
18632
+ active_lens_id: before.active_lens_id,
18633
+ lenses: before.lenses,
18634
+ message: `No lens with id ${targetId}. Pick one from the list.`
18635
+ };
18636
+ }
18637
+ if (target.is_default) {
18638
+ return {
18639
+ status: "cannot_delete_default",
18640
+ switched: false,
18641
+ edited: false,
18642
+ deleted: false,
18643
+ active_lens_id: before.active_lens_id,
18644
+ lenses: before.lenses,
18645
+ message: `"${target.name}" is the default lens and can't be deleted.`
18646
+ };
18647
+ }
18648
+ if (params.confirm !== true) {
18649
+ return {
18650
+ status: "delete_preview",
18651
+ switched: false,
18652
+ edited: false,
18653
+ deleted: false,
18654
+ active_lens_id: before.active_lens_id,
18655
+ lenses: before.lenses,
18656
+ will_delete: { id: target.id, name: target.name },
18657
+ message: `About to delete "${target.name}". This can't be undone. Confirm with the user, then re-call with confirm:true.`
18658
+ };
18659
+ }
18660
+ await client.requestVoid("DELETE", `/lenses/${targetId}`);
18661
+ client.invalidateMe();
18662
+ client.invalidateDefaultLens();
18663
+ const after = await listWithActive(client);
18664
+ return {
18665
+ status: "deleted",
18666
+ switched: false,
18667
+ edited: false,
18668
+ deleted: true,
18669
+ active_lens_id: after.active_lens_id,
18670
+ lenses: after.lenses,
18671
+ message: `Deleted "${target.name}".`
18672
+ };
18673
+ }
18674
+ if (params.editLensId != null) {
18675
+ const targetId = sid(params.editLensId);
18676
+ const before = await listWithActive(client);
18677
+ const target = before.lenses.find((l) => l.id === targetId);
18678
+ if (!target) {
18679
+ return {
18680
+ status: "not_found",
18681
+ switched: false,
18682
+ edited: false,
18683
+ active_lens_id: before.active_lens_id,
18684
+ lenses: before.lenses,
18685
+ message: `No lens with id ${targetId}. Pick one from the list.`
18686
+ };
18687
+ }
18688
+ const body = {};
18689
+ const newName = params.newName?.trim();
18690
+ if (newName)
18691
+ body.name = newName;
18692
+ if (params.newDescription !== void 0)
18693
+ body.description = params.newDescription;
18694
+ if (Object.keys(body).length === 0) {
18695
+ return {
18696
+ status: "not_found",
18697
+ switched: false,
18698
+ edited: false,
18699
+ active_lens_id: before.active_lens_id,
18700
+ lenses: before.lenses,
18701
+ message: `Nothing to change on "${target.name}" \u2014 provide newName and/or newDescription.`
18702
+ };
18703
+ }
18704
+ await client.requestVoid("POST", `/lenses/${targetId}`, body);
18705
+ client.invalidateDefaultLens();
18706
+ const changed = [
18707
+ body.name != null ? `renamed to "${body.name}"` : null,
18708
+ body.description !== void 0 ? "description updated" : null
18709
+ ].filter(Boolean).join(", ");
18710
+ const after = await listWithActive(client);
18711
+ return {
18712
+ status: "edited",
18713
+ switched: false,
18714
+ edited: true,
18715
+ active_lens_id: after.active_lens_id,
18716
+ lenses: after.lenses,
18717
+ message: `"${target.name}" \u2014 ${changed}.`
18718
+ };
18719
+ }
18720
+ if (params.switchToLensId != null) {
18721
+ const targetId = sid(params.switchToLensId);
18722
+ const before = await listWithActive(client);
18723
+ const target = before.lenses.find((l) => l.id === targetId);
18724
+ if (!target) {
18725
+ return {
18726
+ status: "not_found",
18727
+ switched: false,
18728
+ edited: false,
18729
+ active_lens_id: before.active_lens_id,
18730
+ lenses: before.lenses,
18731
+ message: `No lens with id ${targetId}. Pick an id from the list.`
18732
+ };
18733
+ }
18734
+ if (target.is_active) {
18735
+ return {
18736
+ status: "already_active",
18737
+ switched: false,
18738
+ edited: false,
18739
+ active_lens_id: before.active_lens_id,
18740
+ lenses: before.lenses,
18741
+ message: `"${target.name}" is already your active lens.`
18742
+ };
18743
+ }
18744
+ await client.requestVoid("POST", `/lenses/${targetId}/update_last_requested`);
18745
+ client.invalidateMe();
18746
+ client.invalidateDefaultLens();
18747
+ const after = await listWithActive(client);
18748
+ return {
18749
+ status: "switched",
18750
+ switched: true,
18751
+ edited: false,
18752
+ active_lens_id: after.active_lens_id,
18753
+ lenses: after.lenses,
18754
+ message: `Now showing "${target.name}".`
18755
+ };
18756
+ }
18757
+ const { lenses, active_lens_id } = await listWithActive(client);
18758
+ return { status: "listed", switched: false, edited: false, active_lens_id, lenses };
18759
+ }
18760
+ };
18761
+ }
18762
+ });
18763
+
18764
+ // ../core/dist/composite/new-lens.js
18765
+ var EMPTY_FILTER, newLens;
18766
+ var init_new_lens = __esm({
18767
+ "../core/dist/composite/new-lens.js"() {
18768
+ "use strict";
18769
+ init_adjust_audience();
18770
+ init_tool_descriptions_generated();
18771
+ EMPTY_FILTER = {
18772
+ lens_filter: { items: [{ criteria: [] }] },
18773
+ locations: { results: [], parents: [] }
18774
+ };
18775
+ newLens = {
18776
+ name: "leadbay_new_lens",
18777
+ annotations: {
18778
+ title: "Create a new named lens",
18779
+ readOnlyHint: false,
18780
+ destructiveHint: false,
18781
+ idempotentHint: false,
18782
+ // each call creates a distinct lens
18783
+ openWorldHint: true
18784
+ },
18785
+ description: leadbay_new_lens,
18786
+ inputSchema: {
18787
+ type: "object",
18788
+ properties: {
18789
+ name: { type: "string", description: "Display name for the new lens (required)." },
18790
+ sectors: {
18791
+ type: "array",
18792
+ items: { type: "string" },
18793
+ description: "Sectors to include \u2014 free text (auto-resolved) or ids."
18794
+ },
18795
+ exclude_sectors: {
18796
+ type: "array",
18797
+ items: { type: "string" },
18798
+ description: "Sectors to exclude \u2014 free text or ids."
18799
+ },
18800
+ sizes: {
18801
+ type: "array",
18802
+ items: {
18803
+ type: "object",
18804
+ properties: { min: { type: "number" }, max: { type: "number" } }
18805
+ },
18806
+ description: "Company size buckets, e.g. [{min:30,max:300}]."
18807
+ },
18808
+ base: {
18809
+ type: "number",
18810
+ description: "Lens id to clone from. Defaults to the active/default lens."
18811
+ },
18812
+ description: { type: "string", description: "Optional lens description." },
18813
+ confirm: {
18814
+ type: "boolean",
18815
+ description: "Safety gate. Defaults to false \u2192 the tool returns a PREVIEW and creates nothing. Show the preview to the user, get their explicit go-ahead, then re-call the SAME args with confirm:true to actually create the lens."
18816
+ }
18817
+ },
18818
+ required: ["name"],
18819
+ additionalProperties: false
18820
+ },
18821
+ outputSchema: {
18822
+ type: "object",
18823
+ description: "'preview' (default, NOTHING created \u2014 confirm with the user then re-call with confirm:true); 'created' on success; 'ambiguous_sectors' when free-text sectors didn't resolve (re-call with sector ids \u2014 the lens was NOT created).",
18824
+ properties: {
18825
+ status: { type: "string", description: "'preview', 'created', 'ambiguous_sectors', or 'orphan_created' (filter write failed + cleanup failed)." },
18826
+ will_create: {
18827
+ type: "object",
18828
+ description: "On 'preview': what WILL be created \u2014 {name, description, sectors, exclude_sectors, sizes}. Nothing has been written yet."
18829
+ },
18830
+ filter_preview: { type: "object", description: "On 'preview': the FilterPayload that would be applied." },
18831
+ lens: {
18832
+ type: "object",
18833
+ description: "On 'created': the created lens {id, name}."
18834
+ },
18835
+ sector_ambiguities: {
18836
+ type: "array",
18837
+ description: "On 'ambiguous_sectors': per text {sector_text, matches:[{id,name,score}]}.",
18838
+ items: { type: "object" }
18839
+ },
18840
+ filter_applied: { type: "object", description: "On 'created': the FilterPayload POSTed to the new lens." },
18841
+ message: { type: "string" },
18842
+ _meta: { type: "object" }
18843
+ },
18844
+ required: ["status"]
18845
+ },
18846
+ execute: async (client, params, ctx) => {
18847
+ const includeRes = await resolveSectors(client, params.sectors ?? [], ctx);
18848
+ const excludeRes = await resolveSectors(client, params.exclude_sectors ?? [], ctx);
18849
+ const ambiguities = [...includeRes.ambiguities, ...excludeRes.ambiguities];
18850
+ if (ambiguities.length > 0) {
18851
+ const noMatch = ambiguities.filter((a) => a.matches.length === 0);
18852
+ const multi = ambiguities.filter((a) => a.matches.length > 0);
18853
+ const parts = [];
18854
+ if (noMatch.length > 0) {
18855
+ parts.push(`Couldn't find a sector matching ${noMatch.map((a) => `"${a.sector_text}"`).join(", ")}. Pick a known sector and re-call (lens not yet created).`);
18856
+ }
18857
+ if (multi.length > 0) {
18858
+ parts.push(`${multi.map((a) => `"${a.sector_text}"`).join(", ")} matched multiple sectors. Pick from the matches and re-call with the sector id.`);
18859
+ }
18860
+ return {
18861
+ status: "ambiguous_sectors",
18862
+ sector_ambiguities: ambiguities,
18863
+ message: parts.join(" ")
18864
+ };
18865
+ }
18866
+ const merged = mergeFilter(EMPTY_FILTER, includeRes.resolved, excludeRes.resolved, params.sizes);
18867
+ if (params.confirm !== true) {
18868
+ return {
18869
+ status: "preview",
18870
+ will_create: {
18871
+ name: params.name,
18872
+ description: params.description ?? null,
18873
+ sectors: includeRes.resolved,
18874
+ exclude_sectors: excludeRes.resolved,
18875
+ sizes: merged.lens_filter.items[0].criteria.find((c) => c.type === "size") ?? null
18876
+ },
18877
+ filter_preview: merged,
18878
+ message: `About to create "${params.name}". Confirm with the user, then re-call with confirm:true.`,
18879
+ _meta: { region: client.region }
18880
+ };
18881
+ }
18882
+ const base = params.base ?? await client.resolveDefaultLens();
18883
+ const created = await client.request("POST", "/lenses", {
18884
+ base: String(base),
18885
+ name: params.name,
18886
+ description: params.description
18887
+ });
18888
+ const hasCriteria = merged.lens_filter.items[0].criteria.length > 0;
18889
+ if (hasCriteria) {
18890
+ try {
18891
+ await client.requestVoid("POST", `/lenses/${created.id}/filter`, filterWriteBody(merged));
18892
+ } catch (err) {
18893
+ ctx?.logger?.warn?.(`new_lens: filter write on new lens ${created.id} failed: ${err?.message} \u2014 rolling back`);
18894
+ try {
18895
+ await client.requestVoid("DELETE", `/lenses/${created.id}`);
18896
+ } catch {
18897
+ client.invalidateDefaultLens();
18898
+ return {
18899
+ status: "orphan_created",
18900
+ lens: { id: created.id, name: created.name },
18901
+ message: `Created "${created.name}" but applying its filter failed, and cleanup also failed. The lens exists with no criteria \u2014 delete it via leadbay_my_lenses(deleteLensId:"${created.id}", confirm:true) or set its audience with leadbay_adjust_audience.`,
18902
+ _meta: { region: client.region }
18903
+ };
18904
+ }
18905
+ client.invalidateDefaultLens();
18906
+ throw err;
18907
+ }
18908
+ }
18909
+ client.invalidateDefaultLens();
18910
+ return {
18911
+ status: "created",
18912
+ lens: { id: created.id, name: created.name },
18913
+ filter_applied: merged,
18914
+ message: `Created "${created.name}".`,
18915
+ _meta: { region: client.region }
18916
+ };
18917
+ }
18918
+ };
18919
+ }
18920
+ });
18921
+
18128
18922
  // ../core/dist/composite/answer-clarification.js
18129
18923
  var answerClarification;
18130
18924
  var init_answer_clarification = __esm({
@@ -18867,6 +19661,8 @@ var init_dist = __esm({
18867
19661
  init_refine_prompt();
18868
19662
  init_seed_candidates();
18869
19663
  init_extend_lens();
19664
+ init_my_lenses();
19665
+ init_new_lens();
18870
19666
  init_answer_clarification();
18871
19667
  init_report_outreach();
18872
19668
  init_report_friction();
@@ -18886,7 +19682,6 @@ var init_dist = __esm({
18886
19682
  getQuota,
18887
19683
  getLensFilter,
18888
19684
  getLensScoring,
18889
- listSectors,
18890
19685
  listLocations,
18891
19686
  getUserPrompt,
18892
19687
  getClarification,
@@ -18957,6 +19752,11 @@ var init_dist = __esm({
18957
19752
  // it for discoverability; expose it always-on so agents can find custom fields
18958
19753
  // without needing LEADBAY_MCP_ADVANCED=1.
18959
19754
  listMappableFields,
19755
+ // listSectors is granular-shaped but ALWAYS exposed: it's the sector taxonomy
19756
+ // lookup the agent needs to STOP guessing sector names (and to feed
19757
+ // leadbay_new_lens / leadbay_adjust_audience). Without it the agent can only
19758
+ // probe sectors by trial-and-error or ask the user to read the web UI.
19759
+ listSectors,
18960
19760
  // Billing / top-up tools — granular-shaped but ALWAYS exposed because
18961
19761
  // they're the canonical recovery path from a QUOTA_EXCEEDED wall. If
18962
19762
  // they were gated behind LEADBAY_MCP_ADVANCED=1 the agent would
@@ -18999,7 +19799,16 @@ var init_dist = __esm({
18999
19799
  removeLeadsFromCampaign,
19000
19800
  // Lens extend — agent-driven on-demand fill (additive). Gated behind
19001
19801
  // LEADBAY_MCP_WRITE=1. Subject to per-org daily LENS_EXTRA_REFILL quota.
19002
- extendLens
19802
+ extendLens,
19803
+ // Lens list/switch — read-first (no args = pure list); a switchToLensId
19804
+ // changes the active lens. In compositeWriteTools because the switch path
19805
+ // mutates last_requested_lens, but it stays on the default surface
19806
+ // (write is on by default since 0.3.0).
19807
+ myLenses,
19808
+ // Lens creation — make a brand-new named lens with sectors/sizes in one
19809
+ // call. Default-surface so "create a lens called X for Y" works without
19810
+ // the advanced gate.
19811
+ newLens
19003
19812
  ];
19004
19813
  compositeTools = [
19005
19814
  ...compositeReadTools,
@@ -21795,7 +22604,7 @@ function buildClaudeCodeAddArgs(token, region, includeWrite, telemetryEnabled, l
21795
22604
  if (localBinPath) {
21796
22605
  args.push("--", "node", localBinPath);
21797
22606
  } else {
21798
- args.push("--", "npx", "-y", "@leadbay/mcp@latest");
22607
+ args.push("--", "npx", "-y", "-p", "@leadbay/mcp@latest", "leadbay-mcp");
21799
22608
  }
21800
22609
  return args;
21801
22610
  }
@@ -21875,7 +22684,7 @@ async function installInJsonConfig(configPath, token, region, includeWrite, tele
21875
22684
  LEADBAY_TELEMETRY_ENABLED: telemetryEnabled ? "true" : "false"
21876
22685
  };
21877
22686
  if (!includeWrite) env.LEADBAY_MCP_WRITE = "0";
21878
- parsed.mcpServers.leadbay = localBinPath ? { command: "node", args: [localBinPath], env } : { command: "npx", args: ["-y", "@leadbay/mcp@latest"], env };
22687
+ parsed.mcpServers.leadbay = localBinPath ? { command: "node", args: [localBinPath], env } : { command: "npx", args: ["-y", "-p", "@leadbay/mcp@latest", "leadbay-mcp"], env };
21879
22688
  const tmp = configPath + ".tmp";
21880
22689
  writeFileSync(tmp, JSON.stringify(parsed, null, 2) + "\n", "utf8");
21881
22690
  const { renameSync, chmodSync } = await import("fs");
@@ -22871,7 +23680,7 @@ var OAUTH_BASE_URLS = {
22871
23680
  fr: "https://staging.api.leadbay.app"
22872
23681
  }
22873
23682
  };
22874
- var VERSION = "0.17.1";
23683
+ var VERSION = "0.17.3";
22875
23684
  var HELP = `
22876
23685
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
22877
23686
 
@@ -22927,7 +23736,7 @@ EXAMPLE Claude Desktop config (~/Library/Application Support/Claude/claude_deskt
22927
23736
  "mcpServers": {
22928
23737
  "leadbay": {
22929
23738
  "command": "npx",
22930
- "args": ["-y", "@leadbay/mcp@latest"],
23739
+ "args": ["-y", "-p", "@leadbay/mcp@latest", "leadbay-mcp"],
22931
23740
  "env": {
22932
23741
  "LEADBAY_TOKEN": "lb_...",
22933
23742
  "LEADBAY_REGION": "us",
@@ -23413,7 +24222,7 @@ async function runLogin(args) {
23413
24222
  mcpServers: {
23414
24223
  leadbay: {
23415
24224
  command: "npx",
23416
- args: ["-y", "@leadbay/mcp@latest"],
24225
+ args: ["-y", "-p", "@leadbay/mcp@latest", "leadbay-mcp"],
23417
24226
  env: envBlock
23418
24227
  }
23419
24228
  }