@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.
@@ -6187,6 +6187,8 @@ var COMPOSITE_FILE_TOOL_NAMES = /* @__PURE__ */ new Set([
6187
6187
  "leadbay_import_leads",
6188
6188
  "leadbay_import_status",
6189
6189
  "leadbay_list_campaigns",
6190
+ "leadbay_my_lenses",
6191
+ "leadbay_new_lens",
6190
6192
  "leadbay_prepare_outreach",
6191
6193
  "leadbay_pull_followups",
6192
6194
  "leadbay_pull_leads",
@@ -6297,7 +6299,37 @@ WHEN NOT TO USE: to log an outreach action \u2014 use leadbay_report_outreach, w
6297
6299
 
6298
6300
  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\`.
6299
6301
  `;
6300
- var 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).
6302
+ var leadbay_adjust_audience = `## WHEN TO USE
6303
+
6304
+ 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>".
6305
+
6306
+ **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
6307
+
6308
+ 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\`.
6309
+
6310
+ 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.
6311
+
6312
+ Examples that SHOULD invoke this tool:
6313
+ - "Add fintech to my Joinery lens."
6314
+ - "Narrow my audience to manufacturing companies, 50\u2013500 people."
6315
+ - "Stop including retail in this lens."
6316
+
6317
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
6318
+ - "Create a lens called Joinery for fintech."
6319
+ - "Show me my lenses."
6320
+ - "Focus on hospitals that run their own IT."
6321
+
6322
+ ## RENDER (quick)
6323
+
6324
+ On \`applied\`: confirm the lens edited (name) + the sectors/sizes added as
6325
+ chips. On \`ambiguous_sectors\` / \`ambiguous_lens\` / \`lens_not_found\`: surface
6326
+ the candidates and ask the user to pick, then re-call with the id/exact name.
6327
+
6328
+ ---
6329
+
6330
+ 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).
6331
+
6332
+ **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.
6301
6333
 
6302
6334
  WHEN TO USE: when the user wants to see different kinds of leads (sector / size / etc.).
6303
6335
 
@@ -7511,6 +7543,258 @@ WHEN NOT TO USE: if a token is already preconfigured \u2014 you'll just overwrit
7511
7543
 
7512
7544
  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\`.
7513
7545
  `;
7546
+ var leadbay_my_lenses = `## WHEN TO USE
7547
+
7548
+ 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".
7549
+
7550
+ **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
7551
+
7552
+ 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\`.
7553
+
7554
+ 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
7555
+
7556
+ Examples that SHOULD invoke this tool:
7557
+ - "Show me my lenses."
7558
+ - "Rename my Auto lens to Automotive and add a description."
7559
+ - "Delete my old Auto lens."
7560
+
7561
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
7562
+ - "Narrow the audience to fintech only."
7563
+ - "I want more leads on this lens."
7564
+ - "Show me today's leads."
7565
+
7566
+ ## RENDER (quick)
7567
+
7568
+ Small markdown table, active lens first: col 1 = \u2B50 prefix when active +
7569
+ lens name; col 2 = description (or \`\u2014\`). After a switch lead with
7570
+ "Now showing **<name>**."; after a rename lead with the rename confirmation.
7571
+ Full algorithm below.
7572
+
7573
+ ---
7574
+
7575
+ 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\`).
7576
+
7577
+ **Three modes, one tool:**
7578
+
7579
+ - **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.
7580
+ - **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.
7581
+ - **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.
7582
+ - **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.
7583
+
7584
+ **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.
7585
+
7586
+ **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.
7587
+
7588
+ 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".
7589
+
7590
+ 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\`).
7591
+
7592
+ 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\`.
7593
+
7594
+
7595
+ ## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
7596
+
7597
+ 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.
7598
+
7599
+ **The Big Three** \u2014 when a tool result fits, route there:
7600
+
7601
+ | Host widget | Use when | Field map (from Leadbay payload) |
7602
+ |---|---|---|
7603
+ | \`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 |
7604
+ | \`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") |
7605
+ | \`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 |
7606
+
7607
+ 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.
7608
+
7609
+ **Rules:**
7610
+ - 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.
7611
+ - Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
7612
+ - 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.
7613
+ - One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
7614
+
7615
+
7616
+ ---
7617
+
7618
+ ## RENDERING \u2014 lenses table, active-first
7619
+
7620
+ Markdown table with TWO columns. Sort **active lens first**, then by \`name\`
7621
+ ascending. **No score bar** \u2014 the \`\u25B0\u2756\u25B1\` glyph identity belongs to lead
7622
+ discovery, not lenses.
7623
+
7624
+ **Column 1 \u2014 Lens**
7625
+ - Prefix \`\u2B50 \` when \`is_active\` is true; otherwise no prefix.
7626
+ - The lens name in **bold**. (Lenses have no public URL \u2014 do not fabricate a link.)
7627
+
7628
+ **Column 2 \u2014 Description**
7629
+ - \`description\` verbatim, clipped to \u2264 18 words.
7630
+ - When null/empty: render \`\u2014\`.
7631
+
7632
+ **After a \`switched: true\` response**, open with a single confirmation line
7633
+ ABOVE the table: \`Now showing **<name>**.\` For \`status: "not_found"\`, lead with
7634
+ the \`message\` (the bad id) and render the list so the user can pick a real one.
7635
+
7636
+ **Empty list** (\`lenses: []\`): render \`*You don't have any lenses yet.*\` \u2014 do not
7637
+ render an empty table.
7638
+
7639
+ **Legend:** \u2B50 active lens.
7640
+
7641
+
7642
+ ## NEXT STEPS \u2014 after \`leadbay_my_lenses\`
7643
+
7644
+ **RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
7645
+
7646
+ 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:
7647
+
7648
+ \`\`\`
7649
+ ask_user_input_v0({
7650
+ questions: [{
7651
+ question: "What next?",
7652
+ type: "single_select",
7653
+ options: [
7654
+ "<Suggest column from row 1>",
7655
+ "<Suggest column from row 2>",
7656
+ "<Suggest column from row 3>"
7657
+ ]
7658
+ }]
7659
+ })
7660
+ \`\`\`
7661
+
7662
+ 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.
7663
+
7664
+ **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.
7665
+
7666
+ ---
7667
+
7668
+
7669
+
7670
+ Pick the 2\u20133 rows that fit what the user is likely to want next. When the user
7671
+ named no target but wants to switch, offer the lenses themselves as the
7672
+ quick-select options (each option = a lens name \u2192 \`leadbay_my_lenses(switchToLensId=<id>)\`).
7673
+
7674
+ | Observation | Suggest | Calls |
7675
+ |--------------------------------------|------------------------------------------|------------------------------------------------------|
7676
+ | User wants a different lens | "Switch to <lens name>" | \`leadbay_my_lenses(switchToLensId=<id>)\` |
7677
+ | User wants to rename / describe a lens| "Rename or describe <lens>" | \`leadbay_my_lenses(editLensId=<id>, newName?=<X>, newDescription?=<Y>)\` |
7678
+ | User wants to delete a lens | "Delete <lens>" | \`leadbay_my_lenses(deleteLensId=<id>)\` \u2192 confirm \u2192 \`confirm=true\` |
7679
+ | \`delete_preview\` (not yet deleted) | "Yes, delete it" | \`leadbay_my_lenses(deleteLensId=<id>, confirm=true)\` |
7680
+ | User wants leads on the active lens | "Pull today's leads" | \`leadbay_pull_leads()\` |
7681
+ | User wants to change the audience | "Adjust this lens's audience" | \`leadbay_adjust_audience(...)\` |
7682
+ | User wants more of the same | "Get a bigger batch on this lens" | \`leadbay_extend_lens(...)\` |
7683
+
7684
+ If nothing fits, default to "pull today's leads on the active lens" \u2014 never
7685
+ invent a tool that doesn't exist.
7686
+ `;
7687
+ var leadbay_new_lens = `## WHEN TO USE
7688
+
7689
+ 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>".
7690
+
7691
+ **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
7692
+
7693
+ 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\`.
7694
+
7695
+ 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).
7696
+
7697
+ Examples that SHOULD invoke this tool:
7698
+ - "Create a lens called Joinery for the fintech sector."
7699
+ - "Make me a new audience for healthcare companies, 30\u2013300 people."
7700
+ - "Set up a new lens named Nordics SaaS."
7701
+
7702
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
7703
+ - "Add fintech to my Joinery lens."
7704
+ - "Show me my lenses."
7705
+ - "I want more leads on this lens."
7706
+
7707
+ ## RENDER (quick)
7708
+
7709
+ On \`preview\` (default \u2014 NOTHING created yet): show the lens that WILL be
7710
+ created (name + resolved sectors/sizes as chips) and ASK the user to confirm
7711
+ via ask_user_input_v0 ("Create this lens?" / "Change something"). Only on
7712
+ "yes" re-call with confirm:true. On \`created\`: confirm "Created **<name>**."
7713
+ On \`ambiguous_sectors\`: surface the candidate sectors to pick from.
7714
+
7715
+ ---
7716
+
7717
+ 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.
7718
+
7719
+ **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.)
7720
+
7721
+ **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\`.
7722
+
7723
+ **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.
7724
+
7725
+ 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>".
7726
+
7727
+ 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\`).
7728
+
7729
+ 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\`.
7730
+
7731
+
7732
+ ## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
7733
+
7734
+ 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.
7735
+
7736
+ **The Big Three** \u2014 when a tool result fits, route there:
7737
+
7738
+ | Host widget | Use when | Field map (from Leadbay payload) |
7739
+ |---|---|---|
7740
+ | \`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 |
7741
+ | \`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") |
7742
+ | \`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 |
7743
+
7744
+ 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.
7745
+
7746
+ **Rules:**
7747
+ - 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.
7748
+ - Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
7749
+ - 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.
7750
+ - One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
7751
+
7752
+
7753
+ ---
7754
+
7755
+ ## NEXT STEPS \u2014 after \`leadbay_new_lens\`
7756
+
7757
+ **RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
7758
+
7759
+ 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:
7760
+
7761
+ \`\`\`
7762
+ ask_user_input_v0({
7763
+ questions: [{
7764
+ question: "What next?",
7765
+ type: "single_select",
7766
+ options: [
7767
+ "<Suggest column from row 1>",
7768
+ "<Suggest column from row 2>",
7769
+ "<Suggest column from row 3>"
7770
+ ]
7771
+ }]
7772
+ })
7773
+ \`\`\`
7774
+
7775
+ 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.
7776
+
7777
+ **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.
7778
+
7779
+ ---
7780
+
7781
+
7782
+
7783
+ Pick the rows that fit. On \`created\`, the switch + pull rows are the natural
7784
+ follow-ups. On \`ambiguous_sectors\`, the only move is to pick a sector and re-call.
7785
+
7786
+ | Observation | Suggest | Calls |
7787
+ |-----------------------------------|------------------------------------------|--------------------------------------------------------|
7788
+ | \`preview\` (not yet created) | "Yes, create this lens" | \`leadbay_new_lens(...same args..., confirm=true)\` |
7789
+ | \`preview\` (not yet created) | "Change the sectors/size first" | (re-ask the user, then \`leadbay_new_lens\` with new args) |
7790
+ | Lens created | "Switch to it and pull leads" | \`leadbay_my_lenses(switchToLensId=<new id>)\` then \`leadbay_pull_leads()\` |
7791
+ | Lens created | "Refine the audience further" | \`leadbay_adjust_audience(lensName=<new name>, ...)\` |
7792
+ | Lens created | "Leave it; keep my current lens active" | (no call) |
7793
+ | \`ambiguous_sectors\` | "Pick the right sector and create" | \`leadbay_new_lens(name=..., sectors=[<chosen id>])\` |
7794
+
7795
+ If nothing fits, default to "switch to the new lens and pull leads" \u2014 never
7796
+ invent a tool that doesn't exist.
7797
+ `;
7514
7798
  var 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.
7515
7799
 
7516
7800
  Sibling of \`leadbay_create_topup_link\`. Use cases differ:
@@ -8143,7 +8427,33 @@ WHEN TO USE: before leadbay_enrich_titles, to plan which titles to order.
8143
8427
 
8144
8428
  WHEN NOT TO USE: when you already know the exact titles you want to enrich.
8145
8429
  `;
8146
- var 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).
8430
+ var leadbay_refine_prompt = `## WHEN TO USE
8431
+
8432
+ 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".
8433
+
8434
+ **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
8435
+
8436
+ 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\`.
8437
+
8438
+ 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.
8439
+
8440
+ Examples that SHOULD invoke this tool:
8441
+ - "Focus on hospitals that run their own IT in-house."
8442
+ - "Prioritize companies that have recently expanded headcount."
8443
+
8444
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
8445
+ - "Create a lens specialized in automobile."
8446
+ - "Add fintech to my Joinery lens."
8447
+ - "Show me my lenses."
8448
+
8449
+ ## RENDER (quick)
8450
+
8451
+ On success: confirm the refinement applied to the active lens. If a
8452
+ clarification was raised, surface its question (route via ask_user_input_v0).
8453
+
8454
+ ---
8455
+
8456
+ 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).
8147
8457
 
8148
8458
  WHEN TO USE: when audience filters (leadbay_adjust_audience) aren't enough.
8149
8459
 
@@ -12118,7 +12428,7 @@ var createLens = {
12118
12428
  },
12119
12429
  execute: async (client, params) => {
12120
12430
  const lens = await client.request("POST", "/lenses", {
12121
- base: params.base,
12431
+ base: String(params.base),
12122
12432
  name: params.name,
12123
12433
  description: params.description
12124
12434
  });
@@ -17373,6 +17683,8 @@ var bulkEnrichStatus = {
17373
17683
 
17374
17684
  // ../core/dist/composite/adjust-audience.js
17375
17685
  function tokens(s) {
17686
+ if (!s)
17687
+ return [];
17376
17688
  return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
17377
17689
  }
17378
17690
  function bestMatches(text, taxonomy) {
@@ -17386,11 +17698,11 @@ function bestMatches(text, taxonomy) {
17386
17698
  if (have.has(t))
17387
17699
  overlap += 1;
17388
17700
  const score = overlap / Math.max(want.size, 1);
17389
- return { id: s.id, name: s.name, score };
17701
+ return { id: s.id, name: s.name ?? "", score };
17390
17702
  }).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
17391
17703
  return ranked.slice(0, 5);
17392
17704
  }
17393
- async function resolveSectors(client, texts) {
17705
+ async function resolveSectors(client, texts, ctx) {
17394
17706
  const looksLikeId2 = (s) => /^\d+$/.test(s);
17395
17707
  const direct = texts.filter(looksLikeId2);
17396
17708
  const free = texts.filter((s) => !looksLikeId2(s));
@@ -17399,6 +17711,10 @@ async function resolveSectors(client, texts) {
17399
17711
  const me = await client.resolveMe().catch(() => null);
17400
17712
  const lang = me?.language ?? "en";
17401
17713
  const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
17714
+ const nullNames = taxonomy.filter((s) => !s.name).length;
17715
+ if (nullNames > 0) {
17716
+ ctx?.logger?.warn?.(`adjust_audience: /sectors/all returned ${nullNames}/${taxonomy.length} sector(s) with a null/missing name`);
17717
+ }
17402
17718
  const resolved = [...direct];
17403
17719
  const ambiguities = [];
17404
17720
  for (const text of free) {
@@ -17411,6 +17727,22 @@ async function resolveSectors(client, texts) {
17411
17727
  }
17412
17728
  return { resolved, ambiguities };
17413
17729
  }
17730
+ async function resolveLensByName(client, name) {
17731
+ const lenses = await client.request("GET", "/lenses");
17732
+ const all = lenses.map((l) => ({ id: l.id, name: l.name }));
17733
+ const needle = name.trim().toLowerCase();
17734
+ const exact = all.filter((l) => (l.name ?? "").trim().toLowerCase() === needle);
17735
+ if (exact.length === 1)
17736
+ return { ok: true, id: exact[0].id };
17737
+ if (exact.length > 1)
17738
+ return { ok: false, reason: "ambiguous", matches: exact };
17739
+ const partial = all.filter((l) => (l.name ?? "").toLowerCase().includes(needle));
17740
+ if (partial.length === 1)
17741
+ return { ok: true, id: partial[0].id };
17742
+ if (partial.length > 1)
17743
+ return { ok: false, reason: "ambiguous", matches: partial };
17744
+ return { ok: false, reason: "not_found", lenses: all };
17745
+ }
17414
17746
  function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
17415
17747
  const items = current?.lens_filter?.items ?? [];
17416
17748
  const item = items[0] ?? { criteria: [] };
@@ -17444,11 +17776,15 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
17444
17776
  }
17445
17777
  }
17446
17778
  if (sizes && sizes.length > 0) {
17779
+ const normalizedSizes = sizes.map((s) => ({
17780
+ min: s.min ?? 0,
17781
+ max: s.max ?? 1e6
17782
+ }));
17447
17783
  const idx = criteria.findIndex((c) => c.type === "size");
17448
17784
  if (idx >= 0) {
17449
- criteria[idx] = { type: "size", is_excluded: false, sizes };
17785
+ criteria[idx] = { type: "size", is_excluded: false, sizes: normalizedSizes };
17450
17786
  } else {
17451
- criteria.push({ type: "size", is_excluded: false, sizes });
17787
+ criteria.push({ type: "size", is_excluded: false, sizes: normalizedSizes });
17452
17788
  }
17453
17789
  }
17454
17790
  return {
@@ -17456,6 +17792,9 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
17456
17792
  locations: current.locations ?? { results: [], parents: [] }
17457
17793
  };
17458
17794
  }
17795
+ function filterWriteBody(filter) {
17796
+ return { items: filter.lens_filter.items };
17797
+ }
17459
17798
  var adjustAudience = {
17460
17799
  name: "leadbay_adjust_audience",
17461
17800
  annotations: {
@@ -17497,6 +17836,10 @@ var adjustAudience = {
17497
17836
  description: "Company size buckets, e.g. [{min:30,max:300}]"
17498
17837
  },
17499
17838
  lensId: { type: "number", description: "Lens id (escape hatch)" },
17839
+ lensName: {
17840
+ type: "string",
17841
+ 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."
17842
+ },
17500
17843
  save_for_org: {
17501
17844
  type: "boolean",
17502
17845
  description: "Admin only \u2014 propagate the change to the org-level lens for everyone (default false: per-user draft)"
@@ -17510,21 +17853,35 @@ var adjustAudience = {
17510
17853
  },
17511
17854
  outputSchema: {
17512
17855
  type: "object",
17513
- description: "Two return shapes: 'ambiguous_sectors' when free-text sectors matched multiple candidates (agent re-calls with sector_ids), 'applied' on success.",
17856
+ 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).",
17514
17857
  properties: {
17515
17858
  status: {
17516
17859
  type: "string",
17517
- description: "'ambiguous_sectors' or 'applied'."
17860
+ description: "'applied', 'ambiguous_sectors', 'lens_not_found', or 'ambiguous_lens'."
17518
17861
  },
17519
17862
  sector_ambiguities: {
17520
17863
  type: "array",
17521
17864
  description: "Per ambiguous text: {sector_text, matches:[{id, name, score}]}. Agent picks an id and re-calls.",
17522
17865
  items: { type: "object" }
17523
17866
  },
17867
+ lenses: {
17868
+ type: "array",
17869
+ description: "On 'lens_not_found': the user's lenses [{id, name}] to pick from.",
17870
+ items: { type: "object" }
17871
+ },
17872
+ matches: {
17873
+ type: "array",
17874
+ description: "On 'ambiguous_lens': the lenses [{id, name}] the name matched.",
17875
+ items: { type: "object" }
17876
+ },
17877
+ lens_query: {
17878
+ type: "string",
17879
+ description: "On 'lens_not_found' / 'ambiguous_lens': the lensName the user asked for."
17880
+ },
17524
17881
  message: { type: "string" },
17525
17882
  lens_used: {
17526
17883
  type: "object",
17527
- description: "Resolved lens metadata: {id, name, was_draft, was_new, save_for_org}."
17884
+ description: "Resolved lens metadata: {id, name, was_draft, was_new, active_lens_changed, save_for_org}."
17528
17885
  },
17529
17886
  filter_applied: {
17530
17887
  type: "object",
@@ -17537,28 +17894,63 @@ var adjustAudience = {
17537
17894
  execute: async (client, params, ctx) => {
17538
17895
  const me = await client.resolveMe();
17539
17896
  const isAdmin = me.admin === true;
17540
- const startingLensId = params.lensId ?? me.last_requested_lens ?? await client.resolveDefaultLens();
17897
+ let namedLensId;
17898
+ if (params.lensId == null && params.lensName != null && params.lensName.trim() !== "") {
17899
+ const res = await resolveLensByName(client, params.lensName);
17900
+ if (!res.ok && res.reason === "not_found") {
17901
+ return {
17902
+ status: "lens_not_found",
17903
+ lens_query: params.lensName,
17904
+ lenses: res.lenses,
17905
+ message: `No lens named "${params.lensName}". Pick one of the listed lenses (pass lensId or an exact lensName), or create it first.`
17906
+ };
17907
+ }
17908
+ if (!res.ok && res.reason === "ambiguous") {
17909
+ return {
17910
+ status: "ambiguous_lens",
17911
+ lens_query: params.lensName,
17912
+ matches: res.matches,
17913
+ message: `"${params.lensName}" matched multiple lenses. Re-call with the exact lensName or the lensId of the one you mean.`
17914
+ };
17915
+ }
17916
+ if (res.ok)
17917
+ namedLensId = res.id;
17918
+ }
17919
+ const startingLensId = params.lensId ?? namedLensId ?? me.last_requested_lens ?? await client.resolveDefaultLens();
17920
+ const isNamedEdit = namedLensId != null && params.lensId == null;
17541
17921
  const includeTexts = [
17542
17922
  ...params.sectors ?? [],
17543
17923
  ...params.sector_ids ?? []
17544
17924
  ];
17545
17925
  const excludeTexts = params.exclude_sectors ?? [];
17546
- const includeRes = await resolveSectors(client, includeTexts);
17547
- const excludeRes = await resolveSectors(client, excludeTexts);
17926
+ const includeRes = await resolveSectors(client, includeTexts, ctx);
17927
+ const excludeRes = await resolveSectors(client, excludeTexts, ctx);
17548
17928
  const ambiguities = [
17549
17929
  ...includeRes.ambiguities,
17550
17930
  ...excludeRes.ambiguities
17551
17931
  ];
17552
17932
  if (ambiguities.length > 0) {
17933
+ const noMatch = ambiguities.filter((a) => a.matches.length === 0);
17934
+ const multi = ambiguities.filter((a) => a.matches.length > 0);
17935
+ const parts = [];
17936
+ if (noMatch.length > 0) {
17937
+ const names = noMatch.map((a) => `"${a.sector_text}"`).join(", ");
17938
+ 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=...`);
17939
+ }
17940
+ if (multi.length > 0) {
17941
+ const names = multi.map((a) => `"${a.sector_text}"`).join(", ");
17942
+ parts.push(`${names} matched multiple sectors. Pick from the matches and re-call with sector_ids=...`);
17943
+ }
17553
17944
  return {
17554
17945
  status: "ambiguous_sectors",
17555
17946
  sector_ambiguities: ambiguities,
17556
- message: "One or more sector names matched multiple sectors. Pick from the matches and re-call with sector_ids=..."
17947
+ message: parts.join(" ")
17557
17948
  };
17558
17949
  }
17559
17950
  const lens = await client.request("GET", `/lenses/${startingLensId}`);
17560
17951
  const currentFilter = await client.request("GET", `/lenses/${startingLensId}/filter`);
17561
17952
  const merged = mergeFilter(currentFilter, includeRes.resolved, excludeRes.resolved, params.sizes);
17953
+ const mergedBody = filterWriteBody(merged);
17562
17954
  const isDefault = lens.is_default || lens.default;
17563
17955
  const isUserLevel = lens.user_id != null;
17564
17956
  const isOrgLevel = !isUserLevel && !isDefault;
@@ -17567,24 +17959,28 @@ var adjustAudience = {
17567
17959
  let wasNew = false;
17568
17960
  if (isDefault) {
17569
17961
  const name = params.newLensName ?? `Custom audience \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
17570
- const newLens = await client.request("POST", "/lenses", {
17571
- base: startingLensId,
17962
+ const newLens2 = await client.request("POST", "/lenses", {
17963
+ base: String(startingLensId),
17572
17964
  name
17573
17965
  });
17574
- targetLensId = newLens.id;
17966
+ targetLensId = newLens2.id;
17575
17967
  wasNew = true;
17576
- await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, merged);
17577
- await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
17968
+ await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, mergedBody);
17969
+ if (!isNamedEdit) {
17970
+ await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
17971
+ }
17578
17972
  } else if (isUserLevel) {
17579
17973
  try {
17580
- await client.requestVoid("POST", `/lenses/${startingLensId}/filter`, merged);
17974
+ await client.requestVoid("POST", `/lenses/${startingLensId}/filter`, mergedBody);
17581
17975
  } catch (err) {
17582
17976
  if (err?.code === "FORBIDDEN") {
17583
17977
  wasDraft = true;
17584
17978
  const draft = await client.request("POST", `/lenses/${startingLensId}/draft`);
17585
17979
  targetLensId = draft.id;
17586
- await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, merged);
17587
- await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
17980
+ await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, mergedBody);
17981
+ if (!isNamedEdit) {
17982
+ await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
17983
+ }
17588
17984
  } else {
17589
17985
  throw err;
17590
17986
  }
@@ -17596,7 +17992,7 @@ var adjustAudience = {
17596
17992
  const draft = await client.request("POST", `/lenses/${startingLensId}/draft`);
17597
17993
  targetLensId = draft.id;
17598
17994
  try {
17599
- await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, merged);
17995
+ await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, mergedBody);
17600
17996
  } catch (err) {
17601
17997
  ctx?.logger?.warn?.(`adjust_audience: filter on draft ${targetLensId} failed: ${err?.message}`);
17602
17998
  try {
@@ -17612,17 +18008,21 @@ var adjustAudience = {
17612
18008
  }
17613
18009
  throw err;
17614
18010
  }
17615
- await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
18011
+ if (!isNamedEdit) {
18012
+ await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
18013
+ }
17616
18014
  } else {
17617
18015
  try {
17618
- await client.requestVoid("POST", `/lenses/${startingLensId}/filter`, merged);
18016
+ await client.requestVoid("POST", `/lenses/${startingLensId}/filter`, mergedBody);
17619
18017
  } catch (err) {
17620
18018
  throw err;
17621
18019
  }
17622
18020
  }
17623
18021
  }
17624
- client.invalidateMe();
18022
+ if (!isNamedEdit)
18023
+ client.invalidateMe();
17625
18024
  client.invalidateDefaultLens();
18025
+ 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.` : "";
17626
18026
  return {
17627
18027
  status: "applied",
17628
18028
  lens_used: {
@@ -17630,10 +18030,11 @@ var adjustAudience = {
17630
18030
  name: lens.name,
17631
18031
  was_draft: wasDraft,
17632
18032
  was_new: wasNew,
18033
+ active_lens_changed: !isNamedEdit && (wasNew || wasDraft),
17633
18034
  save_for_org: params.save_for_org === true && isAdmin && isOrgLevel
17634
18035
  },
17635
18036
  filter_applied: merged,
17636
- 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.",
18037
+ 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,
17637
18038
  _meta: { region: client.region }
17638
18039
  };
17639
18040
  }
@@ -18005,6 +18406,384 @@ var extendLens = {
18005
18406
  }
18006
18407
  };
18007
18408
 
18409
+ // ../core/dist/composite/my-lenses.js
18410
+ var sid = (v) => v == null ? null : String(v);
18411
+ async function listWithActive(client) {
18412
+ const lenses = await client.request("GET", "/lenses");
18413
+ const me = await client.resolveMe().catch(() => null);
18414
+ const activeFromMe = sid(me?.last_requested_lens);
18415
+ const active_lens_id = activeFromMe ?? sid(lenses.find((l) => l.is_last_active)?.id) ?? null;
18416
+ return {
18417
+ active_lens_id,
18418
+ lenses: lenses.map((l) => ({
18419
+ id: sid(l.id),
18420
+ name: l.name,
18421
+ description: l.description ?? null,
18422
+ is_active: sid(l.id) === active_lens_id,
18423
+ is_default: l.is_default === true || l.default === true
18424
+ }))
18425
+ };
18426
+ }
18427
+ var myLenses = {
18428
+ name: "leadbay_my_lenses",
18429
+ annotations: {
18430
+ title: "List, switch, edit, or delete your lenses",
18431
+ // No args → pure read. The delete mode issues DELETE /lenses/:id (an
18432
+ // irreversible side effect), so the tool is destructive — clients must
18433
+ // treat it as approval-required, not auto-run. The delete path is itself
18434
+ // confirm-gated (preview unless confirm:true). switch/edit are not
18435
+ // idempotent across modes either, so don't claim idempotency.
18436
+ readOnlyHint: false,
18437
+ destructiveHint: true,
18438
+ idempotentHint: false,
18439
+ openWorldHint: true
18440
+ },
18441
+ description: leadbay_my_lenses,
18442
+ inputSchema: {
18443
+ type: "object",
18444
+ properties: {
18445
+ switchToLensId: {
18446
+ type: ["string", "number"],
18447
+ description: "When set, switch the active lens to this id (must be one of the user's lenses), then return the refreshed list."
18448
+ },
18449
+ editLensId: {
18450
+ type: ["string", "number"],
18451
+ description: "When set, edit this lens's metadata \u2014 provide newName and/or newDescription. Must be one of the user's lenses."
18452
+ },
18453
+ newName: {
18454
+ type: "string",
18455
+ description: "New lens name (used with editLensId)."
18456
+ },
18457
+ newDescription: {
18458
+ type: "string",
18459
+ description: "New lens description (used with editLensId). Pass an empty string to clear it."
18460
+ },
18461
+ deleteLensId: {
18462
+ type: ["string", "number"],
18463
+ description: "When set, delete this lens. DESTRUCTIVE \u2014 returns a delete_preview unless confirm:true. Cannot delete the default lens."
18464
+ },
18465
+ confirm: {
18466
+ type: "boolean",
18467
+ description: "Required (=true) to actually delete. Without it, deleteLensId returns a preview to confirm with the user first."
18468
+ }
18469
+ },
18470
+ additionalProperties: false
18471
+ },
18472
+ outputSchema: {
18473
+ type: "object",
18474
+ properties: {
18475
+ status: {
18476
+ type: "string",
18477
+ description: "'listed', 'switched', 'already_active', 'edited', 'deleted', 'delete_preview' (confirm to proceed), 'cannot_delete_default', or 'not_found'."
18478
+ },
18479
+ switched: { type: "boolean", description: "True when this call changed the active lens." },
18480
+ edited: { type: "boolean", description: "True when this call renamed/re-described a lens." },
18481
+ deleted: { type: "boolean", description: "True when this call deleted a lens." },
18482
+ will_delete: {
18483
+ type: "object",
18484
+ description: "On 'delete_preview': the lens that WILL be deleted {id, name}. Nothing removed yet."
18485
+ },
18486
+ active_lens_id: { type: ["string", "null"] },
18487
+ lenses: {
18488
+ type: "array",
18489
+ description: "The user's lenses. Each: {id, name, description, is_active}.",
18490
+ items: { type: "object" }
18491
+ },
18492
+ message: { type: "string" }
18493
+ },
18494
+ required: ["status", "lenses", "active_lens_id"]
18495
+ },
18496
+ execute: async (client, params) => {
18497
+ if (params.deleteLensId != null) {
18498
+ const targetId = sid(params.deleteLensId);
18499
+ const before = await listWithActive(client);
18500
+ const target = before.lenses.find((l) => l.id === targetId);
18501
+ if (!target) {
18502
+ return {
18503
+ status: "not_found",
18504
+ switched: false,
18505
+ edited: false,
18506
+ deleted: false,
18507
+ active_lens_id: before.active_lens_id,
18508
+ lenses: before.lenses,
18509
+ message: `No lens with id ${targetId}. Pick one from the list.`
18510
+ };
18511
+ }
18512
+ if (target.is_default) {
18513
+ return {
18514
+ status: "cannot_delete_default",
18515
+ switched: false,
18516
+ edited: false,
18517
+ deleted: false,
18518
+ active_lens_id: before.active_lens_id,
18519
+ lenses: before.lenses,
18520
+ message: `"${target.name}" is the default lens and can't be deleted.`
18521
+ };
18522
+ }
18523
+ if (params.confirm !== true) {
18524
+ return {
18525
+ status: "delete_preview",
18526
+ switched: false,
18527
+ edited: false,
18528
+ deleted: false,
18529
+ active_lens_id: before.active_lens_id,
18530
+ lenses: before.lenses,
18531
+ will_delete: { id: target.id, name: target.name },
18532
+ message: `About to delete "${target.name}". This can't be undone. Confirm with the user, then re-call with confirm:true.`
18533
+ };
18534
+ }
18535
+ await client.requestVoid("DELETE", `/lenses/${targetId}`);
18536
+ client.invalidateMe();
18537
+ client.invalidateDefaultLens();
18538
+ const after = await listWithActive(client);
18539
+ return {
18540
+ status: "deleted",
18541
+ switched: false,
18542
+ edited: false,
18543
+ deleted: true,
18544
+ active_lens_id: after.active_lens_id,
18545
+ lenses: after.lenses,
18546
+ message: `Deleted "${target.name}".`
18547
+ };
18548
+ }
18549
+ if (params.editLensId != null) {
18550
+ const targetId = sid(params.editLensId);
18551
+ const before = await listWithActive(client);
18552
+ const target = before.lenses.find((l) => l.id === targetId);
18553
+ if (!target) {
18554
+ return {
18555
+ status: "not_found",
18556
+ switched: false,
18557
+ edited: false,
18558
+ active_lens_id: before.active_lens_id,
18559
+ lenses: before.lenses,
18560
+ message: `No lens with id ${targetId}. Pick one from the list.`
18561
+ };
18562
+ }
18563
+ const body = {};
18564
+ const newName = params.newName?.trim();
18565
+ if (newName)
18566
+ body.name = newName;
18567
+ if (params.newDescription !== void 0)
18568
+ body.description = params.newDescription;
18569
+ if (Object.keys(body).length === 0) {
18570
+ return {
18571
+ status: "not_found",
18572
+ switched: false,
18573
+ edited: false,
18574
+ active_lens_id: before.active_lens_id,
18575
+ lenses: before.lenses,
18576
+ message: `Nothing to change on "${target.name}" \u2014 provide newName and/or newDescription.`
18577
+ };
18578
+ }
18579
+ await client.requestVoid("POST", `/lenses/${targetId}`, body);
18580
+ client.invalidateDefaultLens();
18581
+ const changed = [
18582
+ body.name != null ? `renamed to "${body.name}"` : null,
18583
+ body.description !== void 0 ? "description updated" : null
18584
+ ].filter(Boolean).join(", ");
18585
+ const after = await listWithActive(client);
18586
+ return {
18587
+ status: "edited",
18588
+ switched: false,
18589
+ edited: true,
18590
+ active_lens_id: after.active_lens_id,
18591
+ lenses: after.lenses,
18592
+ message: `"${target.name}" \u2014 ${changed}.`
18593
+ };
18594
+ }
18595
+ if (params.switchToLensId != null) {
18596
+ const targetId = sid(params.switchToLensId);
18597
+ const before = await listWithActive(client);
18598
+ const target = before.lenses.find((l) => l.id === targetId);
18599
+ if (!target) {
18600
+ return {
18601
+ status: "not_found",
18602
+ switched: false,
18603
+ edited: false,
18604
+ active_lens_id: before.active_lens_id,
18605
+ lenses: before.lenses,
18606
+ message: `No lens with id ${targetId}. Pick an id from the list.`
18607
+ };
18608
+ }
18609
+ if (target.is_active) {
18610
+ return {
18611
+ status: "already_active",
18612
+ switched: false,
18613
+ edited: false,
18614
+ active_lens_id: before.active_lens_id,
18615
+ lenses: before.lenses,
18616
+ message: `"${target.name}" is already your active lens.`
18617
+ };
18618
+ }
18619
+ await client.requestVoid("POST", `/lenses/${targetId}/update_last_requested`);
18620
+ client.invalidateMe();
18621
+ client.invalidateDefaultLens();
18622
+ const after = await listWithActive(client);
18623
+ return {
18624
+ status: "switched",
18625
+ switched: true,
18626
+ edited: false,
18627
+ active_lens_id: after.active_lens_id,
18628
+ lenses: after.lenses,
18629
+ message: `Now showing "${target.name}".`
18630
+ };
18631
+ }
18632
+ const { lenses, active_lens_id } = await listWithActive(client);
18633
+ return { status: "listed", switched: false, edited: false, active_lens_id, lenses };
18634
+ }
18635
+ };
18636
+
18637
+ // ../core/dist/composite/new-lens.js
18638
+ var EMPTY_FILTER = {
18639
+ lens_filter: { items: [{ criteria: [] }] },
18640
+ locations: { results: [], parents: [] }
18641
+ };
18642
+ var newLens = {
18643
+ name: "leadbay_new_lens",
18644
+ annotations: {
18645
+ title: "Create a new named lens",
18646
+ readOnlyHint: false,
18647
+ destructiveHint: false,
18648
+ idempotentHint: false,
18649
+ // each call creates a distinct lens
18650
+ openWorldHint: true
18651
+ },
18652
+ description: leadbay_new_lens,
18653
+ inputSchema: {
18654
+ type: "object",
18655
+ properties: {
18656
+ name: { type: "string", description: "Display name for the new lens (required)." },
18657
+ sectors: {
18658
+ type: "array",
18659
+ items: { type: "string" },
18660
+ description: "Sectors to include \u2014 free text (auto-resolved) or ids."
18661
+ },
18662
+ exclude_sectors: {
18663
+ type: "array",
18664
+ items: { type: "string" },
18665
+ description: "Sectors to exclude \u2014 free text or ids."
18666
+ },
18667
+ sizes: {
18668
+ type: "array",
18669
+ items: {
18670
+ type: "object",
18671
+ properties: { min: { type: "number" }, max: { type: "number" } }
18672
+ },
18673
+ description: "Company size buckets, e.g. [{min:30,max:300}]."
18674
+ },
18675
+ base: {
18676
+ type: "number",
18677
+ description: "Lens id to clone from. Defaults to the active/default lens."
18678
+ },
18679
+ description: { type: "string", description: "Optional lens description." },
18680
+ confirm: {
18681
+ type: "boolean",
18682
+ 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."
18683
+ }
18684
+ },
18685
+ required: ["name"],
18686
+ additionalProperties: false
18687
+ },
18688
+ outputSchema: {
18689
+ type: "object",
18690
+ 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).",
18691
+ properties: {
18692
+ status: { type: "string", description: "'preview', 'created', 'ambiguous_sectors', or 'orphan_created' (filter write failed + cleanup failed)." },
18693
+ will_create: {
18694
+ type: "object",
18695
+ description: "On 'preview': what WILL be created \u2014 {name, description, sectors, exclude_sectors, sizes}. Nothing has been written yet."
18696
+ },
18697
+ filter_preview: { type: "object", description: "On 'preview': the FilterPayload that would be applied." },
18698
+ lens: {
18699
+ type: "object",
18700
+ description: "On 'created': the created lens {id, name}."
18701
+ },
18702
+ sector_ambiguities: {
18703
+ type: "array",
18704
+ description: "On 'ambiguous_sectors': per text {sector_text, matches:[{id,name,score}]}.",
18705
+ items: { type: "object" }
18706
+ },
18707
+ filter_applied: { type: "object", description: "On 'created': the FilterPayload POSTed to the new lens." },
18708
+ message: { type: "string" },
18709
+ _meta: { type: "object" }
18710
+ },
18711
+ required: ["status"]
18712
+ },
18713
+ execute: async (client, params, ctx) => {
18714
+ const includeRes = await resolveSectors(client, params.sectors ?? [], ctx);
18715
+ const excludeRes = await resolveSectors(client, params.exclude_sectors ?? [], ctx);
18716
+ const ambiguities = [...includeRes.ambiguities, ...excludeRes.ambiguities];
18717
+ if (ambiguities.length > 0) {
18718
+ const noMatch = ambiguities.filter((a) => a.matches.length === 0);
18719
+ const multi = ambiguities.filter((a) => a.matches.length > 0);
18720
+ const parts = [];
18721
+ if (noMatch.length > 0) {
18722
+ 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).`);
18723
+ }
18724
+ if (multi.length > 0) {
18725
+ parts.push(`${multi.map((a) => `"${a.sector_text}"`).join(", ")} matched multiple sectors. Pick from the matches and re-call with the sector id.`);
18726
+ }
18727
+ return {
18728
+ status: "ambiguous_sectors",
18729
+ sector_ambiguities: ambiguities,
18730
+ message: parts.join(" ")
18731
+ };
18732
+ }
18733
+ const merged = mergeFilter(EMPTY_FILTER, includeRes.resolved, excludeRes.resolved, params.sizes);
18734
+ if (params.confirm !== true) {
18735
+ return {
18736
+ status: "preview",
18737
+ will_create: {
18738
+ name: params.name,
18739
+ description: params.description ?? null,
18740
+ sectors: includeRes.resolved,
18741
+ exclude_sectors: excludeRes.resolved,
18742
+ sizes: merged.lens_filter.items[0].criteria.find((c) => c.type === "size") ?? null
18743
+ },
18744
+ filter_preview: merged,
18745
+ message: `About to create "${params.name}". Confirm with the user, then re-call with confirm:true.`,
18746
+ _meta: { region: client.region }
18747
+ };
18748
+ }
18749
+ const base = params.base ?? await client.resolveDefaultLens();
18750
+ const created = await client.request("POST", "/lenses", {
18751
+ base: String(base),
18752
+ name: params.name,
18753
+ description: params.description
18754
+ });
18755
+ const hasCriteria = merged.lens_filter.items[0].criteria.length > 0;
18756
+ if (hasCriteria) {
18757
+ try {
18758
+ await client.requestVoid("POST", `/lenses/${created.id}/filter`, filterWriteBody(merged));
18759
+ } catch (err) {
18760
+ ctx?.logger?.warn?.(`new_lens: filter write on new lens ${created.id} failed: ${err?.message} \u2014 rolling back`);
18761
+ try {
18762
+ await client.requestVoid("DELETE", `/lenses/${created.id}`);
18763
+ } catch {
18764
+ client.invalidateDefaultLens();
18765
+ return {
18766
+ status: "orphan_created",
18767
+ lens: { id: created.id, name: created.name },
18768
+ 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.`,
18769
+ _meta: { region: client.region }
18770
+ };
18771
+ }
18772
+ client.invalidateDefaultLens();
18773
+ throw err;
18774
+ }
18775
+ }
18776
+ client.invalidateDefaultLens();
18777
+ return {
18778
+ status: "created",
18779
+ lens: { id: created.id, name: created.name },
18780
+ filter_applied: merged,
18781
+ message: `Created "${created.name}".`,
18782
+ _meta: { region: client.region }
18783
+ };
18784
+ }
18785
+ };
18786
+
18008
18787
  // ../core/dist/composite/answer-clarification.js
18009
18788
  var answerClarification = {
18010
18789
  name: "leadbay_answer_clarification",
@@ -18524,7 +19303,6 @@ var granularReadTools = [
18524
19303
  getQuota,
18525
19304
  getLensFilter,
18526
19305
  getLensScoring,
18527
- listSectors,
18528
19306
  listLocations,
18529
19307
  getUserPrompt,
18530
19308
  getClarification,
@@ -18595,6 +19373,11 @@ var compositeReadTools = [
18595
19373
  // it for discoverability; expose it always-on so agents can find custom fields
18596
19374
  // without needing LEADBAY_MCP_ADVANCED=1.
18597
19375
  listMappableFields,
19376
+ // listSectors is granular-shaped but ALWAYS exposed: it's the sector taxonomy
19377
+ // lookup the agent needs to STOP guessing sector names (and to feed
19378
+ // leadbay_new_lens / leadbay_adjust_audience). Without it the agent can only
19379
+ // probe sectors by trial-and-error or ask the user to read the web UI.
19380
+ listSectors,
18598
19381
  // Billing / top-up tools — granular-shaped but ALWAYS exposed because
18599
19382
  // they're the canonical recovery path from a QUOTA_EXCEEDED wall. If
18600
19383
  // they were gated behind LEADBAY_MCP_ADVANCED=1 the agent would
@@ -18637,7 +19420,16 @@ var compositeWriteTools = [
18637
19420
  removeLeadsFromCampaign,
18638
19421
  // Lens extend — agent-driven on-demand fill (additive). Gated behind
18639
19422
  // LEADBAY_MCP_WRITE=1. Subject to per-org daily LENS_EXTRA_REFILL quota.
18640
- extendLens
19423
+ extendLens,
19424
+ // Lens list/switch — read-first (no args = pure list); a switchToLensId
19425
+ // changes the active lens. In compositeWriteTools because the switch path
19426
+ // mutates last_requested_lens, but it stays on the default surface
19427
+ // (write is on by default since 0.3.0).
19428
+ myLenses,
19429
+ // Lens creation — make a brand-new named lens with sectors/sizes in one
19430
+ // call. Default-surface so "create a lens called X for Y" works without
19431
+ // the advanced gate.
19432
+ newLens
18641
19433
  ];
18642
19434
  var compositeTools = [
18643
19435
  ...compositeReadTools,
@@ -19837,7 +20629,7 @@ function parseWriteEnv(env = process.env) {
19837
20629
  }
19838
20630
 
19839
20631
  // src/http-server.ts
19840
- var VERSION = true ? "0.17.1" : "0.0.0-dev";
20632
+ var VERSION = true ? "0.17.3" : "0.0.0-dev";
19841
20633
  var PORT = Number(process.env.PORT ?? 8080);
19842
20634
  var HOST = process.env.HOST ?? "0.0.0.0";
19843
20635
  var sseSessions = /* @__PURE__ */ new Map();