@leadbay/mcp 0.23.5 → 0.23.7

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.
@@ -625,44 +625,80 @@ GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ship
625
625
  If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.
626
626
 
627
627
 
628
- # PHASE 1 \u2014 BUILD THE ITINERARY
628
+ # PHASE 1 \u2014 CLARIFY SCOPE FIRST (ask before building)
629
629
 
630
- Call \`leadbay_tour_plan({city: "{{arg:city}}"})\` with the default counts (6 follow-ups + 6 discover). If the response is \`status: "ambiguous_locations"\`, surface the candidates and ask me to pick one, then re-call with \`city_id\`.
630
+ Before calling \`leadbay_tour_plan\`, ask me what the tour should cover \u2014 a field tour is high-intent and the scope changes what lands on the map. Use your host's interactive question widget (\`ask_user_input_v0\` in Claude chat \u2014 \`{questions:[{question, type:"single_select", options:[\u2026]}]}\`; or \`AskUserQuestion\` in cowork / Claude Code \u2014 \`{questions:[{question, header, multiSelect, options:[{label, description}]}]}\`). Ask at most 2 short tap-to-answer questions:
631
631
 
632
- Split the returned \`monitor_leads\` into two buckets client-side using \`last_monitor_action\`:
632
+ 1. **Who to include?** \u2014 options: "Mix \u2014 known accounts + fresh prospects" (default), "Only my known accounts (follow-ups)", "Only fresh new prospects".
633
+ 2. **Enrich contacts that have no phone/email?** \u2014 options: "Yes, enrich the top stops", "No, just plan the tour".
633
634
 
634
- - **Customers** \u2014 leads with any \`last_monitor_action\` history (CONTACTED, MEETING_BOOKED, etc.). Treat as known accounts with prior engagement.
635
- - **Qualified prospects** \u2014 leads with high \`ai_agent_lead_score\` (or \`score\`) but no recent action.
635
+ Skip a question only if I already answered it in my request (e.g. "plan a tour of just my new leads in NYC" \u2192 who-to-include is already "only new"; don't re-ask). If I gave the full scope up front, skip PHASE 1 entirely and go to PHASE 2.
636
+
637
+ Map my answers to the \`leadbay_tour_plan\` call:
638
+ - **Mix** \u2192 defaults (\`followups_count: 6, discover_count: 6\`).
639
+ - **Only known accounts** \u2192 \`discover_count: 0\`.
640
+ - **Only new prospects** \u2192 \`followups_count: 0\`.
641
+ - **Enrich = yes** \u2192 after the tour is planned (PHASE 2), call \`leadbay_enrich_titles\` / \`leadbay_prepare_outreach(enrich:true)\` on the top stops that have no \`recommended_contact\` channel, before drafting outreach.
642
+
643
+ # PHASE 2 \u2014 BUILD THE ITINERARY
644
+
645
+ Call \`leadbay_tour_plan({city: "{{arg:city}}", \u2026scope from PHASE 1})\`. If the response is \`status: "ambiguous_locations"\`, surface the candidates and ask me to pick one, then re-call with \`city_id\`.
646
+
647
+ Split the returned \`monitor_leads\` into two buckets client-side using their engagement-history fields:
648
+
649
+ - **Customers** \u2014 leads with prior engagement history: any of \`epilogue_status\`, \`last_prospecting_action_at\`, or \`last_monitor_action_at\` is set. Treat as known accounts with prior interaction.
650
+ - **Qualified prospects** \u2014 leads with a high \`ai_agent_lead_score\` (or \`score\`) but none of those history fields set (scored, not yet worked).
636
651
 
637
652
  \`discover_leads\` are the **New** bucket.
638
653
 
639
654
  Aim for a 3+3+3 split if possible. If the customers bucket has fewer than 3, fill from qualified. If discover_filter_note indicates a low match ratio for the city, mention it: "Only N/30 fresh leads matched your city" \u2014 better honest than padded.
640
655
 
641
- # PHASE 2 \u2014 RENDER THE MAP
656
+ # PHASE 3 \u2014 PRESENT THE ITINERARY + OFFER THE MAP
657
+
658
+ Show the planned tour as a concise per-lead list grouped by mode (Customers \u2192 Qualified \u2192 New), each line carrying its \`\u2605\`/\`\u2726\` badge, the company, city, and best contact. Keep it tight \u2014 this is the summary, not the map.
659
+
660
+ Then **ALWAYS offer the map as the immediate next step** \u2014 a tour is inherently geographic, so the map is the natural payoff. End the turn with a single clear proposal:
661
+
662
+ > **Want me to put these <N> stops on a map?** I'll plot them so you can see the route for your <city> day.
642
663
 
643
- Route the union of \`monitor_leads + discover_leads\` into \`places_map_display_v0\` (when the host exposes it). Per-lead \`notes\` string:
664
+ Make it a genuine yes/no offer (route it through \`ask_user_input_v0\` / \`AskUserQuestion\` when that's in your tool set, so it's a tappable choice; otherwise the one-line question above). Do NOT render the map yet, and do NOT bury the offer \u2014 proposing the map is mandatory on every tour, for BOTH "plan a prospecting tour in <city>" and "who's worth meeting in <city>" phrasings. If the user already said "show me on a map" / "give me the map" in their request, skip the offer and go straight to PHASE 4 (render).
644
665
 
645
- - \`\u2605 Customer \u2014 <one-sentence sector + why-now>. Reach <name>, <role>: <bare phone>, <bare email>.\`
646
- - \`\u2605 Qualified \u2014 <one-sentence>. Reach <name>...\`
647
- - \`\u2726 New \u2014 <one-sentence>. Reach <name>...\`
666
+ # PHASE 4 \u2014 RENDER THE MAP (when the user accepts, or asked for it up front)
648
667
 
649
- Skip leads with \`location.pos === null\` (no coordinates \u2192 no pin) \u2014 list them as "+ N leads without coordinates" below the widget.
668
+ When the user says yes (or asked for the map in their original message), render it now. Two ways \u2014 you MUST do one, never a flat prose paragraph:
650
669
 
651
- Below the widget, emit a chat-prose summary grouped by mode (Customers / Qualified / New), with LinkedIn-linked contact name + bare phone/email pills per lead. Use the canonical \`linking/contact-linkedin\` rules.
670
+ **Path A \u2014 \`places_search\` + \`places_map_display_v0\` are in your tool set.** Use the **two-step** host flow (this is what draws a real street map, not a schematic scatter):
671
+
672
+ 1. **\`places_search\` first, once per lead** \u2014 query = each \`map_locations\` entry's \`name\` + full street \`address\` (e.g. "Brooklyn Brewery, 79 N 11th St, Brooklyn, NY"). Resolve all stops up front to get real \`place_id\`s + verified coords/ratings.
673
+ 2. **\`places_map_display_v0\` second**, fed those resolved places, in **Itinerary mode** (a tour is a multi-stop route \u2014 show order, optionally arrival times / stop durations). Carry each lead's \u2605/\u2726 badge + one-line pitch from its \`notes\` into the place note.
674
+
675
+ Do NOT push raw \`latitude\`/\`longitude\` straight into the display widget without \`places_search\` \u2014 that degrades to the neighborhood-dot scatter. \`map_locations\` already carries \`name\`, full \`address\`, coords, and badge-tagged \`notes\`; don't shorten the address to the city.
676
+
677
+ **Path B \u2014 the widget is NOT in your tool set (e.g. Claude Desktop).** Emit one **place-card block per lead** in EXACTLY this shape so the chat host auto-detects the address and renders its own Google-Place-card map carousel \u2014 the \`### Company \xB7 City, State\` heading + address is what triggers it:
678
+
679
+ \`\`\`
680
+ ### <Company Name> \xB7 <City>, <State>
681
+
682
+ <\u2605 Customer | \u2605 Qualified | \u2726 New> \u2014 <one-sentence why-it-fits>. Reach **[<Contact name>](<LinkedIn URL>)**, <role>. \u260E <bare phone>.
683
+ \`\`\`
652
684
 
653
- # PHASE 3 \u2014 DRAFT IN-AREA OUTREACH (optional, ask first)
685
+ One block per lead in \`map_locations\`, grouped by mode. Pull company, city/state, badge, and contact straight from the tool response. A flat paragraph like "Brooklyn Brewery \u2014 Broadway, 10018 (Midtown). Best contact: \u2026" does NOT auto-detect and is WRONG \u2014 the per-lead heading with the city is mandatory.
686
+
687
+ Coordinate-less leads are already omitted from \`map_locations\`; footnote them with \`map_summary.leads_without_coords\` ("+ N leads without coordinates").
688
+
689
+ # PHASE 5 \u2014 DRAFT IN-AREA OUTREACH (optional, ask first)
654
690
 
655
691
  After the map, ask me ONCE: "Want me to draft 'I'll be in {{arg:city}}{{arg:date_paren}}' outreach for the top accounts?" If I say yes, for each of the top 3 leads (1 Customer / 1 Qualified / 1 New), call \`leadbay_prepare_outreach(leadId)\` and route the draft through \`message_compose_v1\` with a single variant labeled "In-area visit" \u2014 body opens with the visit context, references the AI-summary angle, ends with a clear ask (15-min coffee / on-site stopover).
656
692
 
657
693
  Serialize the prepare_outreach calls (max 3 in parallel \u2014 see the long-running-tools rule).
658
694
 
659
- # PHASE 4 \u2014 PERSIST AS A CAMPAIGN (optional, ask first)
695
+ # PHASE 6 \u2014 PERSIST AS A CAMPAIGN (optional, ask first)
660
696
 
661
697
  After drafts, ask me ONCE: "Save these 9 accounts as a campaign called '**{{arg:city}} Tour{{arg:date_dash}}**'?" If I say yes, call \`leadbay_create_campaign({lead_ids: [...all_nine_lead_ids], name: "{{arg:city}} Tour{{arg:date_dash}}"})\`. Surface the returned \`id\` + \`name\` as a confirmation line, and offer the NEXT STEPS chip "View progression" (which routes to \`leadbay_campaign_progression\`).
662
698
 
663
699
  If I declined the campaign step, end the turn \u2014 the map + drafts are enough for an ad-hoc trip.
664
700
 
665
- # PHASE 5 \u2014 STOP
701
+ # PHASE 7 \u2014 STOP
666
702
 
667
703
  Done. The map is the surface; the drafts are the action; the campaign is the persistence layer for managerial follow-up after the trip.
668
704
  `;
@@ -1261,7 +1297,7 @@ var PROMPT_META = {
1261
1297
  leadbay_followup_check_in: { "name": "leadbay_followup_check_in", "short_description": 'Follow-up check-in: surface KNOWN leads from the Monitor view needing\nre-engagement. Trigger on "follow up", "already known leads", "what\'s\noverdue", "before my trip", "who should I re-engage". Do NOT trigger on\n"show me today\'s leads", "my morning check-in", "run my check-in",\n"I do this every day", "every morning" \u2014 those go to\n`leadbay_daily_check_in`.\n', "arguments": [], "expected_calls": ["leadbay_pull_followups", "leadbay_research_lead_by_id", "leadbay_prepare_outreach"], "failure_modes": ["Calls leadbay_pull_leads (the Discover entry point) instead of leadbay_pull_followups \u2014 these are different data sources; the Discover queue does NOT contain Monitor's known-but-cold pipeline", 'Iterates pages of leadbay_pull_leads filtering by engagement_count to "fake" a follow-up view (a real bug observed in 0.9.0 \u2014 the right move is to call pull_followups directly)', "Replaces the canonical pull_followups table layout with prose per row (the per-tool RENDERING block is the structural contract; commentary belongs above or below)", 'Skips the cross-mode pivot offer at the end ("Want to see NEW leads from your wishlist instead?" routes to leadbay_pull_leads)'] },
1262
1298
  leadbay_import_file: { "name": "leadbay_import_file", "short_description": "Import a user-supplied CSV/file into Leadbay through five phases with\nevidence gates \u2014 scan, derive, resolve identities, preserve & commit,\nthen optionally qualify and report. The job is to maximize how many\nrows the Leadbay system actually ingests and matches.\n", "arguments": [{ "name": "file", "description": "Path or user-visible name of the CSV/file to import. If omitted, use the file the user attached or referenced.", "required": false }, { "name": "instruction", "description": 'Additional user goal, e.g. "then qualify the leads", "preserve owner phone as a custom field", or "only import restaurants in Manhattan".', "required": false }], "expected_calls": ["leadbay_resolve_import_rows", "leadbay_list_mappable_fields", "leadbay_create_custom_field", "leadbay_import_leads", "leadbay_import_and_qualify", "leadbay_add_note", "leadbay_import_status"], "failure_modes": ["Picks LEADBAY_ID from score alone, name-only, fuzzy-name-only, root-domain-only, brand-only, postcode-only, or city-only evidence", "Drops meaningful business notes or CRM record links instead of preserving them as custom fields or lead notes", "Treats a consumer mailbox domain (gmail.com, hotmail.com, ...) as the company domain", "Skips deriving company_domain from a business email when no website column exists (this kills match rate)", "Skips the COLUMN PRESERVATION PLAN byproduct before importing", "Skips the DECISION LOG byproduct before writing LEADBAY_ID", "Returns the imported records WITHOUT writing LEADBAY_ID values back into the user's file (leaves the user no audit trail of what matched)", "Fabricates leadIds, contact emails, or mapping IDs not present in the file or a tool response"] },
1263
1299
  leadbay_log_outreach: { "name": "leadbay_log_outreach", "short_description": "Log outreach (an email I sent, a call I made, a meeting I had) on a\nspecific lead. Captures verification so the SDR pipeline trusts the entry.\n", "arguments": [{ "name": "lead_id", "description": "The lead UUID. Get it from leadbay_pull_leads or leadbay_research_lead_by_id.", "required": true }, { "name": "summary", "description": "1-2 sentences describing what I did (e.g. 'Sent intro email to CTO citing recent Hornsea contract').", "required": true }], "expected_calls": ["leadbay_report_outreach"], "failure_modes": ["Calls leadbay_report_outreach without first collecting a verification source", "Fabricates a gmail_message_id or calendar_event_id (the human team treats verification as canonical)", "Records outreach to a different lead_id than the one the user supplied", "Skips the dry_run step when the user is unsure what would be sent"] },
1264
- leadbay_plan_tour_in_city: { "name": "leadbay_plan_tour_in_city", "short_description": "Plan a field sales tour: in one flow, surface follow-ups + fresh\nDiscover leads in the target city via `leadbay_tour_plan`, render\nto a map, draft in-area outreach via `leadbay_prepare_outreach`,\nand optionally persist the selected accounts as a named campaign\nvia `leadbay_create_campaign`. Closes #3630 US1 end-to-end.\n", "arguments": [{ "name": "city", "description": "City or region the user is visiting (e.g. 'Limoges', 'Bay Area'). Used as the geo filter for both Monitor and Discover lookups.", "required": true }, { "name": "date", "description": "When the visit is (e.g. 'May 24', 'next Thursday'). Surfaced in the outreach drafts as 'I'll be in <city> on <date>'.", "required": false }], "expected_calls": ["leadbay_tour_plan", "leadbay_research_lead_by_id", "leadbay_prepare_outreach", "leadbay_create_campaign"], "failure_modes": ["Calls leadbay_followups_map (Monitor-only) instead of leadbay_tour_plan \u2014 loses the Discover (fresh-lead) half that the user explicitly asked for", "Calls leadbay_pull_leads then drops the geo filter \u2014 returns the lens-wide wishlist instead of city-relevant fresh leads", 'Skips the campaign-persist step ("would you like to save these as a tour?") \u2014 leaves the rep with a one-shot map but no follow-up artifact', "Creates a campaign WITHOUT asking the user first \u2014 the persist step is high-intent; offer it, don't assume", "Fabricates lead_ids when seeding the campaign instead of using the ids returned by tour_plan"] },
1300
+ leadbay_plan_tour_in_city: { "name": "leadbay_plan_tour_in_city", "short_description": 'Use whenever the user names a city they\'ll be in and asks who to see\n\u2014 "I\'m in SF next Tuesday, who\'s worth meeting?", "I\'m going to Berlin\n\u2014 who should I visit?", "plan my <city> tour". Any in-person/visit\nintent tied to a place routes here, NOT to `leadbay_pull_leads`. It\nsurfaces follow-ups + fresh Discover leads in the city via\n`leadbay_tour_plan`, ALWAYS offers to plot them on a map (rendering it\non yes), then offers outreach drafts + campaign persistence.\n', "arguments": [{ "name": "city", "description": "City or region the user is visiting (e.g. 'Limoges', 'Bay Area'). Used as the geo filter for both Monitor and Discover lookups.", "required": true }, { "name": "date", "description": "When the visit is (e.g. 'May 24', 'next Thursday'). Surfaced in the outreach drafts as 'I'll be in <city> on <date>'.", "required": false }], "expected_calls": ["leadbay_tour_plan", "leadbay_research_lead_by_id", "leadbay_prepare_outreach", "leadbay_create_campaign"], "failure_modes": ["Calls leadbay_followups_map (Monitor-only) instead of leadbay_tour_plan \u2014 loses the Discover (fresh-lead) half that the user explicitly asked for", "Calls leadbay_pull_leads then drops the geo filter \u2014 returns the lens-wide wishlist instead of city-relevant fresh leads", 'Skips the campaign-persist step ("would you like to save these as a tour?") \u2014 leaves the rep with a one-shot map but no follow-up artifact', "Creates a campaign WITHOUT asking the user first \u2014 the persist step is high-intent; offer it, don't assume", "Fabricates lead_ids when seeding the campaign instead of using the ids returned by tour_plan"] },
1265
1301
  leadbay_prospecting_overview: { "name": "leadbay_prospecting_overview", "short_description": `Orientation for working with Leadbay from any host \u2014 discovery vs.
1266
1302
  follow-up, the outreach loop, outcome recording, imports, pushback /
1267
1303
  snooze, and the connected-outreach-tool registry. Trigger when the
@@ -1283,7 +1319,7 @@ var PROMPT_CATALOG_BULLETS = {
1283
1319
  leadbay_followup_check_in: `- \`leadbay_followup_check_in\`: Follow-up check-in: surface KNOWN leads from the Monitor view needing re-engagement. Trigger on "follow up", "already known leads", "what's overdue", "before my trip", "who should I re-engage". Do NOT trigger on "show me today's leads", "my morning check-in", "run my check-in", "I do this every day", "every morning" \u2014 those go to \`leadbay_daily_check_in\`.`,
1284
1320
  leadbay_import_file: `- \`leadbay_import_file\` (optional args: file, instruction): Import a user-supplied CSV/file into Leadbay through five phases with evidence gates \u2014 scan, derive, resolve identities, preserve & commit, then optionally qualify and report. The job is to maximize how many rows the Leadbay system actually ingests and matches.`,
1285
1321
  leadbay_log_outreach: `- \`leadbay_log_outreach\` (required args: lead_id, summary): Log outreach (an email I sent, a call I made, a meeting I had) on a specific lead. Captures verification so the SDR pipeline trusts the entry.`,
1286
- leadbay_plan_tour_in_city: `- \`leadbay_plan_tour_in_city\` (required args: city; optional args: date): Plan a field sales tour: in one flow, surface follow-ups + fresh Discover leads in the target city via \`leadbay_tour_plan\`, render to a map, draft in-area outreach via \`leadbay_prepare_outreach\`, and optionally persist the selected accounts as a named campaign via \`leadbay_create_campaign\`. Closes #3630 US1 end-to-end.`,
1322
+ leadbay_plan_tour_in_city: `- \`leadbay_plan_tour_in_city\` (required args: city; optional args: date): Use whenever the user names a city they'll be in and asks who to see \u2014 "I'm in SF next Tuesday, who's worth meeting?", "I'm going to Berlin \u2014 who should I visit?", "plan my <city> tour". Any in-person/visit intent tied to a place routes here, NOT to \`leadbay_pull_leads\`. It surfaces follow-ups + fresh Discover leads in the city via \`leadbay_tour_plan\`, ALWAYS offers to plot them on a map (rendering it on yes), then offers outreach drafts + campaign persistence.`,
1287
1323
  leadbay_prospecting_overview: `- \`leadbay_prospecting_overview\`: Orientation for working with Leadbay from any host \u2014 discovery vs. follow-up, the outreach loop, outcome recording, imports, pushback / snooze, and the connected-outreach-tool registry. Trigger when the conversation involves Leadbay leads, prospecting, pipeline, follow-up, outreach, or lens / ICP \u2014 anything from "show me my leads" to "what should I follow up on" to "I'll send via lemlist".`,
1288
1324
  leadbay_qualify_top_n: `- \`leadbay_qualify_top_n\` (optional args: count): Bulk-qualify the top N un-qualified leads in the active lens. Uses leadbay_bulk_qualify_leads with a sensible default budget.`,
1289
1325
  leadbay_refine_audience: `- \`leadbay_refine_audience\` (required args: instruction): Refine the kind of leads Leadbay surfaces beyond firmographics, with a free-text instruction. Handles the clarification round-trip if the new prompt is ambiguous.`,
@@ -6897,7 +6933,7 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
6897
6933
  `;
6898
6934
  var leadbay_adjust_audience = `## WHEN TO USE
6899
6935
 
6900
- 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>".
6936
+ 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>", "restrict this lens to <city/d\xE9partement/r\xE9gion/state>", "only companies in <place>", "exclude <region> from this lens".
6901
6937
 
6902
6938
  **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
6903
6939
 
@@ -6908,7 +6944,7 @@ Prefer when: user wants to change an EXISTING lens's sectors/sizes. If the user
6908
6944
  Examples that SHOULD invoke this tool:
6909
6945
  - "Add fintech to my Joinery lens."
6910
6946
  - "Narrow my audience to manufacturing companies, 50\u2013500 people."
6911
- - "Stop including retail in this lens."
6947
+ - "Restrict my rep's lens to Indre-et-Loire."
6912
6948
 
6913
6949
  Examples that should NOT invoke this tool (sound similar, route elsewhere):
6914
6950
  - "Create a lens called Joinery for fintech."
@@ -6917,9 +6953,10 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
6917
6953
 
6918
6954
  ## RENDER (quick)
6919
6955
 
6920
- On \`applied\`: confirm the lens edited (name) + the sectors/sizes added as
6921
- chips. On \`ambiguous_sectors\` / \`ambiguous_lens\` / \`lens_not_found\`: surface
6922
- the candidates and ask the user to pick, then re-call with the id/exact name.
6956
+ On \`applied\`: confirm the lens edited (name) + the sectors/sizes/locations
6957
+ added as chips. On \`ambiguous_sectors\` / \`ambiguous_locations\` /
6958
+ \`ambiguous_lens\` / \`lens_not_found\`: surface the candidates and ask the user
6959
+ to pick, then re-call with the id/exact name.
6923
6960
 
6924
6961
  ---
6925
6962
 
@@ -6927,7 +6964,9 @@ Restrict (or expand) the lens audience by sector / size. Free-text sectors are a
6927
6964
 
6928
6965
  **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.
6929
6966
 
6930
- WHEN TO USE: when the user wants to see different kinds of leads (sector / size / etc.).
6967
+ **Geography \u2014 scope a sales territory.** Pass \`locations\` (free text like \`["Indre-et-Loire"]\`, \`["Bavaria"]\`, \`["Austin"]\`, or admin-area ids) to restrict the lens to a region, and \`exclude_locations\` to carve one out. Free text auto-resolves via \`/geo/search\` across every admin level \u2014 city, county, *d\xE9partement*, *r\xE9gion*, state, country. Place names go in \`locations\`, **never** in \`sectors\` or \`refine_prompt\`. Unresolved/ambiguous text returns \`status:"ambiguous_locations"\` with candidates \u2014 surface them and re-call the chosen id via the SAME axis it came from: an INCLUDE pick \u2192 \`location_ids\`; an EXCLUDE pick \u2192 \`exclude_locations\` (**NOT** \`location_ids\`, which would include the area the user asked to exclude). The returned \`message\` names the right param per text. This is how a director scopes a rep's territory and then asks for net-new accounts there.
6968
+
6969
+ WHEN TO USE: when the user wants to see different kinds of leads (sector / size / geography / etc.).
6931
6970
 
6932
6971
  WHEN NOT TO USE: to refine BEYOND firmographics \u2014 that's leadbay_refine_prompt.
6933
6972
 
@@ -7207,7 +7246,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
7207
7246
 
7208
7247
  | Host widget | Use when | Field map (from Leadbay payload) |
7209
7248
  |---|---|---|
7210
- | \`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 |
7249
+ | \`places_map_display_v0\` + \`places_search\` (Claude) | \u22652 leads with coords / \`location.city\`, geographic / "in person" / travel intent | **Two-step**: \`places_search\` each lead (query = company + full street address) \u2192 real \`place_id\`/coords, THEN render with \`places_map_display_v0\` (Itinerary mode for a tour). Skipping \`places_search\` \u2192 schematic scatter, not a street map. |
7211
7250
  | \`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") |
7212
7251
  | \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
7213
7252
 
@@ -7525,7 +7564,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
7525
7564
 
7526
7565
  | Host widget | Use when | Field map (from Leadbay payload) |
7527
7566
  |---|---|---|
7528
- | \`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 |
7567
+ | \`places_map_display_v0\` + \`places_search\` (Claude) | \u22652 leads with coords / \`location.city\`, geographic / "in person" / travel intent | **Two-step**: \`places_search\` each lead (query = company + full street address) \u2192 real \`place_id\`/coords, THEN render with \`places_map_display_v0\` (Itinerary mode for a tour). Skipping \`places_search\` \u2192 schematic scatter, not a street map. |
7529
7568
  | \`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") |
7530
7569
  | \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
7531
7570
 
@@ -7781,7 +7820,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
7781
7820
 
7782
7821
  | Host widget | Use when | Field map (from Leadbay payload) |
7783
7822
  |---|---|---|
7784
- | \`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 |
7823
+ | \`places_map_display_v0\` + \`places_search\` (Claude) | \u22652 leads with coords / \`location.city\`, geographic / "in person" / travel intent | **Two-step**: \`places_search\` each lead (query = company + full street address) \u2192 real \`place_id\`/coords, THEN render with \`places_map_display_v0\` (Itinerary mode for a tour). Skipping \`places_search\` \u2192 schematic scatter, not a street map. |
7785
7824
  | \`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") |
7786
7825
  | \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
7787
7826
 
@@ -8397,7 +8436,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
8397
8436
 
8398
8437
  | Host widget | Use when | Field map (from Leadbay payload) |
8399
8438
  |---|---|---|
8400
- | \`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 |
8439
+ | \`places_map_display_v0\` + \`places_search\` (Claude) | \u22652 leads with coords / \`location.city\`, geographic / "in person" / travel intent | **Two-step**: \`places_search\` each lead (query = company + full street address) \u2192 real \`place_id\`/coords, THEN render with \`places_map_display_v0\` (Itinerary mode for a tour). Skipping \`places_search\` \u2192 schematic scatter, not a street map. |
8401
8440
  | \`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") |
8402
8441
  | \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
8403
8442
 
@@ -8475,7 +8514,7 @@ invent a tool that doesn't exist.
8475
8514
  `;
8476
8515
  var leadbay_new_lens = `## WHEN TO USE
8477
8516
 
8478
- 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>".
8517
+ 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>", "create a lens for net-new accounts in <place>", "a lens scoped to <territory>".
8479
8518
 
8480
8519
  **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
8481
8520
 
@@ -8486,7 +8525,7 @@ Prefer when: user wants a brand-new lens (create/make/set up, often 'specialized
8486
8525
  Examples that SHOULD invoke this tool:
8487
8526
  - "Create a lens called Joinery for the fintech sector."
8488
8527
  - "Make me a new audience for healthcare companies, 30\u2013300 people."
8489
- - "Set up a new lens named Nordics SaaS."
8528
+ - "Create a lens for net-new accounts in Indre-et-Loire."
8490
8529
 
8491
8530
  Examples that should NOT invoke this tool (sound similar, route elsewhere):
8492
8531
  - "Add fintech to my Joinery lens."
@@ -8499,7 +8538,8 @@ On \`preview\` (default \u2014 NOTHING created yet): show the lens that WILL be
8499
8538
  created (name + resolved sectors/sizes as chips) and ASK the user to confirm
8500
8539
  via ask_user_input_v0 ("Create this lens?" / "Change something"). Only on
8501
8540
  "yes" re-call with confirm:true. On \`created\`: confirm "Created **<name>**."
8502
- On \`ambiguous_sectors\`: surface the candidate sectors to pick from.
8541
+ On \`ambiguous_sectors\` / \`ambiguous_locations\`: surface the candidates to
8542
+ pick from.
8503
8543
 
8504
8544
  ---
8505
8545
 
@@ -8509,6 +8549,8 @@ Create a brand-new lens (saved audience) and apply its sector/size criteria. Clo
8509
8549
 
8510
8550
  **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\`.
8511
8551
 
8552
+ **Geography \u2014 scope a territory.** Pass \`locations\` (free text like \`["Indre-et-Loire"]\`, \`["Bavaria"]\`, or admin-area ids) to scope the lens to a sales territory, and \`exclude_locations\` to carve one out. Free text auto-resolves via \`/geo/search\` across every admin level (city / county / *d\xE9partement* / *r\xE9gion* / state / country). Like sectors, locations resolve BEFORE the lens is created \u2014 unresolved/ambiguous text returns \`status:"ambiguous_locations"\` with candidates and **does NOT create the lens**. Re-call the chosen id via the SAME axis it came from: an INCLUDE pick \u2192 \`locations\`; an EXCLUDE pick \u2192 \`exclude_locations\` (**NOT** \`locations\`, which would include the area the user asked to exclude). This is how a director spins up a lens for a rep's zone to surface net-new accounts there.
8553
+
8512
8554
  **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.
8513
8555
 
8514
8556
  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>".
@@ -8526,7 +8568,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
8526
8568
 
8527
8569
  | Host widget | Use when | Field map (from Leadbay payload) |
8528
8570
  |---|---|---|
8529
- | \`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 |
8571
+ | \`places_map_display_v0\` + \`places_search\` (Claude) | \u22652 leads with coords / \`location.city\`, geographic / "in person" / travel intent | **Two-step**: \`places_search\` each lead (query = company + full street address) \u2192 real \`place_id\`/coords, THEN render with \`places_map_display_v0\` (Itinerary mode for a tour). Skipping \`places_search\` \u2192 schematic scatter, not a street map. |
8530
8572
  | \`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") |
8531
8573
  | \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
8532
8574
 
@@ -8660,11 +8702,11 @@ email. Do NOT paste the email body into chat prose alongside.
8660
8702
 
8661
8703
  ---
8662
8704
 
8663
- Prepare a single-lead outreach brief: the full \`lead\` block (score, \`split_ai_summary\`, \`location\`, \`size\`, \`phone_numbers\`, \`website\`, \`description\`, \`social_urls\`, \`social_presence\`), the \`recommended_contact\` (always in the post-enrichment shape \u2014 \`contact_id\`, \`first_name\`, \`last_name\`, \`job_title\`, \`email\`, \`phone_number\`, \`linkedin_page\`, \`is_org_contact\` \u2014 with nulls where data isn't yet enriched), \`additional_contacts_count\`, and an \`enrichment\` block describing async state.
8705
+ Prepare a single-lead outreach brief: the full \`lead\` block (score, \`split_ai_summary\`, \`location\`, \`size\`, \`phone_numbers\`, \`website\`, \`description\`, \`social_urls\`, \`social_presence\`), the \`recommended_contact\` in post-enrichment shape (\`contact_id\`, names, \`job_title\`, \`email\`, \`phone_number\`, \`linkedin_page\`, \`is_org_contact\` \u2014 nulls where not yet enriched), \`additional_contacts_count\`, and an \`enrichment\` block describing async state.
8664
8706
 
8665
8707
  Optionally trigger contact enrichment in-flight with \`enrich:true\`. Enrichment is async (~60s). **Self-polling pattern (no separate tool needed):** re-call \`leadbay_prepare_outreach(leadId)\` without \`enrich\`; check \`enrichment.complete\`. When \`complete: true\`, the recommended contact now carries \`email\` and/or \`phone_number\`.
8666
8708
 
8667
- The first call to this tool on a lead records a \`LEAD_VIEWED\`-style prospecting action server-side (one per lead per session \u2014 deduped) so the lead ages out of the Discover "new" view.
8709
+ The first call on a lead records a \`LEAD_VIEWED\`-style prospecting action server-side (deduped, one per lead per session) so it ages out of the Discover "new" view.
8668
8710
 
8669
8711
  IRON LAW \u2014 OUTCOME AFTER OUTREACH. The moment the user reports outreach happened ("I sent it", "she didn't pick up", "left a voicemail", "they replied", a forwarded email thread, a calendar invite), you MUST (1) call leadbay_report_outreach with verification (gmail_message_id, calendar_event_id, or the user's literal one-sentence confirmation as user_confirmed.ref) AND (2) ask the user about the outcome and set epilogue_status to one of the 4 canonical values: EPILOGUE_INTEREST_VALIDATED_OR_MEETING_PLANED ("Meeting booked"), EPILOGUE_COULD_NOT_REACH_STILL_TRYING ("Trying to reach"), EPILOGUE_NOT_INTERESTED_LOST ("Not interested"), EPILOGUE_STILL_CHASING ("In progress"). Use the user-facing labels in dialogue ("What's the outcome \u2014 meeting booked, trying to reach, not interested, or in progress?"); never say "epilogue" out loud. Skipping this step silently de-ranks every future follow-up suggestion because pull_followups depends on honest, current outcomes.
8670
8712
 
@@ -8679,7 +8721,7 @@ WHEN NOT TO USE: across many leads \u2014 use leadbay_enrich_titles for bulk; fo
8679
8721
 
8680
8722
  Route every draft through \`message_compose_v1\` (Claude's email composer). Above it, emit ONE short markdown context paragraph: score callout + sector fit + linked contact name + bare phone/email pills. Do NOT paste the email body into chat prose alongside \u2014 the composer IS the visual.
8681
8723
 
8682
- Variant shape: 1\u20133 entries. Labels describe **strategy** ("Push for alignment", "Reference the M&A signal", "Soft intro \u2014 peer reference"), not tone. \`kind: "email"\` requires \`subject\`; phone/call openers use \`kind: "other"\` with the opener in \`body\`.
8724
+ Variant shape: 1\u20133 entries. Labels describe **strategy** ("Push for alignment", "Reference the M&A signal"), not tone. \`kind: "email"\` requires \`subject\`; phone/call openers use \`kind: "other"\` with the opener in \`body\`.
8683
8725
 
8684
8726
  ## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
8685
8727
 
@@ -8689,7 +8731,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
8689
8731
 
8690
8732
  | Host widget | Use when | Field map (from Leadbay payload) |
8691
8733
  |---|---|---|
8692
- | \`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 |
8734
+ | \`places_map_display_v0\` + \`places_search\` (Claude) | \u22652 leads with coords / \`location.city\`, geographic / "in person" / travel intent | **Two-step**: \`places_search\` each lead (query = company + full street address) \u2192 real \`place_id\`/coords, THEN render with \`places_map_display_v0\` (Itinerary mode for a tour). Skipping \`places_search\` \u2192 schematic scatter, not a street map. |
8693
8735
  | \`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") |
8694
8736
  | \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
8695
8737
 
@@ -9018,7 +9060,7 @@ Trigger phrases: "show me leads", "show me new leads", "show me today's leads",
9018
9060
 
9019
9061
  **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
9020
9062
 
9021
- Do NOT use for: "leads I should follow up with" \u2192 \`leadbay_pull_followups\`; "I'm going to <city>" \u2192 \`leadbay_followups_map\`; "leads I should reach out to" \u2192 \`leadbay_pull_followups\`; "leads to get back to" \u2192 \`leadbay_pull_followups\`; "leads to contact today" \u2192 \`leadbay_pull_followups\`; "should I contact" \u2192 \`leadbay_pull_followups\`; "reconnect with" \u2192 \`leadbay_pull_followups\`; "re-engage" \u2192 \`leadbay_pull_followups\`.
9063
+ Do NOT use for: "leads I should follow up with" \u2192 \`leadbay_pull_followups\`; "I'm going to <city>" \u2192 \`leadbay_tour_plan\`; "I'm in <city> next week \u2014 who's worth meeting" \u2192 \`leadbay_tour_plan\`; "who should I meet in <city>" \u2192 \`leadbay_tour_plan\`; "visiting <city> \u2014 who's worth meeting / seeing" \u2192 \`leadbay_tour_plan\`; "leads I should reach out to" \u2192 \`leadbay_pull_followups\`; "leads to get back to" \u2192 \`leadbay_pull_followups\`; "leads to contact today" \u2192 \`leadbay_pull_followups\`; "should I contact" \u2192 \`leadbay_pull_followups\`; "reconnect with" \u2192 \`leadbay_pull_followups\`; "re-engage" \u2192 \`leadbay_pull_followups\`.
9022
9064
 
9023
9065
  Prefer when: fresh Discover leads; if a lens is named, pass \`lensId\` and pin it
9024
9066
 
@@ -9029,6 +9071,7 @@ Examples that SHOULD invoke this tool:
9029
9071
  Examples that should NOT invoke this tool (sound similar, route elsewhere):
9030
9072
  - "Which leads should I follow up with this week?"
9031
9073
  - "I'm flying to Berlin Thursday \u2014 who should I meet?"
9074
+ - "I'm in San Francisco next Tuesday \u2014 who's worth meeting?"
9032
9075
  - "Show me leads I should reach out to today."
9033
9076
  - "Who should I get back to today?"
9034
9077
  - "Leads I should contact today."
@@ -10226,7 +10269,7 @@ WHEN NOT TO USE: the user wants a lead list (leadbay_pull_leads / leadbay_pull_f
10226
10269
  `;
10227
10270
  var leadbay_tour_plan = `## WHEN TO USE
10228
10271
 
10229
- Trigger phrases: "visiting <city> in <N> days", "field tour in <city>", "plan a tour in <city>", "who should I meet in <city>", "customers plus prospects in <city>", "tour itinerary".
10272
+ Trigger phrases: "visiting <city> in <N> days", "I'm in <city> next week / Tuesday \u2014 who's worth meeting", "I'm going to <city> \u2014 who should I see", "who's worth meeting in <city>", "field tour in <city>", "plan a tour in <city>", "who should I meet in <city>", "customers plus prospects in <city>", "tour itinerary".
10230
10273
 
10231
10274
  **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
10232
10275
 
@@ -10236,6 +10279,7 @@ Prefer when: user wants known accounts plus new discoveries in one geographic it
10236
10279
 
10237
10280
  Examples that SHOULD invoke this tool:
10238
10281
  - "I'm flying to Limoges in 4 days \u2014 give me 3 customers, 3 qualified prospects, and 3 new high-potential."
10282
+ - "I'm in San Francisco next Tuesday. Who's worth meeting?"
10239
10283
  - "Plan my tour next Tuesday in Lyon: known accounts plus discoveries."
10240
10284
  - "Build a mixed itinerary for Berlin \u2014 I want both follow-ups and fresh leads."
10241
10285
 
@@ -10246,16 +10290,18 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
10246
10290
 
10247
10291
  ## RENDER (quick)
10248
10292
 
10249
- Route the union of monitor_leads + discover_leads into
10250
- \`places_map_display_v0\` \u2014 same recipe as followups_map (lat/lng
10251
- split, full address, short notes). Tag each entry's notes with a
10252
- mode badge: "\u2605 Customer" / "\u2605 Qualified" / "\u2726 New" (split Monitor
10253
- by last_monitor_action; Discover = New). Below the widget: one
10254
- intro sentence + per-group chat-prose list with LinkedIn-linked
10255
- contact names.
10293
+ Present the leads grouped by mode (\u2605 Customer / \u2605 Qualified / \u2726 New),
10294
+ then ALWAYS offer to plot them on a map ("Want me to put these on a
10295
+ map?"). On yes (or if the user asked for a map up front), render it:
10296
+ pass \`map_locations\` verbatim into \`places_map_display_v0\`, or on a
10297
+ host without the widget emit one \`### Company \xB7 City, State\` place-card
10298
+ block per lead so the host's address carousel renders. Never a flat
10299
+ prose paragraph. Full recipe below.
10256
10300
 
10257
10301
  ---
10258
10302
 
10303
+ **BEFORE YOU CALL \u2014 clarify scope with a tap-to-answer question.** A field tour is high-intent, so unless the user already said it, ASK first (via \`ask_user_input_v0\` / \`AskUserQuestion\` \u2014 whichever is in your tool set) and wait for the answer before calling this tool: (1) **Who to include?** \u2014 "Mix \u2014 known accounts + fresh prospects" (default) / "Only my known accounts" / "Only fresh new prospects"; (2) **Enrich contacts with no phone/email?** \u2014 "Yes, enrich the top stops" / "No, just plan". Map answers to args: only-known \u2192 \`discover_count: 0\`; only-new \u2192 \`followups_count: 0\`; mix \u2192 defaults; enrich=yes \u2192 after planning, run \`leadbay_enrich_titles\` / \`leadbay_prepare_outreach(enrich:true)\` on top stops missing a channel. Skip the question only if the request already states scope ("plan a tour of just my new leads" \u2192 only-new, don't ask). Do NOT skip straight to building + mapping without this.
10304
+
10259
10305
  Build a single-call mixed-mode itinerary for a field sales tour. Combines \`leadbay_pull_followups\` (Monitor leads in the city \u2014 known accounts) with \`leadbay_pull_leads\` (Discover wishlist \u2014 new prospects, then client-side filtered by city) so the agent can answer the canonical #3630 US1 ask: *"I'm visiting Limoges in 4 days \u2014 propose 3 customers + 3 qualified prospects + 3 new high-potential discoveries."*
10260
10306
 
10261
10307
  **Geo resolution** is identical to \`leadbay_followups_map\`: pass \`city\` (any admin level \u2014 city, state, country, region \u2014 the \`/geo/search\` resolver picks the best match), or a pre-resolved \`city_id\`. Ambiguous matches surface as \`status: "ambiguous_locations"\` + \`location_ambiguities[]\`; pick an id and re-call with \`city_id\`.
@@ -10266,21 +10312,30 @@ Build a single-call mixed-mode itinerary for a field sales tour. Combines \`lead
10266
10312
 
10267
10313
  ---
10268
10314
 
10269
- ## RENDER \u2014 host-native map widget (REQUIRED, preferred)
10315
+ ## RENDER \u2014 present the leads, then OFFER the map (render on yes)
10316
+
10317
+ First present the leads grouped by mode as a tight per-lead list (badge + company + city + best contact). Then **ALWAYS offer the map** as the next step \u2014 "Want me to put these on a map?" \u2014 and render it only when the user accepts (or asked for a map in their original message). The map is mandatory to *offer* on every tour; it is *rendered* on acceptance.
10270
10318
 
10271
- Identical to \`leadbay_followups_map\`. Pass the union \`monitor_leads + discover_leads\` into \`places_map_display_v0\`. The mode information goes in the per-lead \`notes\` string as a **leading badge**:
10319
+ When rendering, use the **two-step host flow** \u2014 this is what produces a real interactive street map instead of a schematic neighborhood scatter:
10272
10320
 
10273
- - \`\u2605 Customer\` \u2014 Monitor lead with an existing \`last_monitor_action\` history (engaged / past interaction).
10274
- - \`\u2605 Qualified\` \u2014 Monitor lead with a high \`ai_agent_lead_score\` or \`score\` but no recent action.
10321
+ 1. **\`places_search\` first, once per lead.** For each entry in \`map_locations\`, call \`places_search\` with a natural query built from the lead's \`name\` + \`address\` (the full street address, e.g. "Brooklyn Brewery, 79 N 11th St, Brooklyn, NY"). This resolves a real \`place_id\` + verified coordinates/ratings from the host's place index. Run them up front (resolve all stops first), as the host's own guidance prescribes.
10322
+ 2. **\`places_map_display_v0\` second**, fed the resolved places. Use **Itinerary mode** for a tour (multi-stop, route order, optional arrival times / stop durations) \u2014 a field-sales day IS an itinerary; Markers mode is only for a plain "show me X on a map". Carry each lead's mode badge (\u2605/\u2726) + one-line pitch from \`map_locations[i].notes\` into the place's note.
10323
+
10324
+ Do NOT skip \`places_search\` and push raw \`latitude\`/\`longitude\` straight into the display widget \u2014 that's exactly what falls back to the schematic relative scatter (dots by neighborhood, no streets). The \`map_locations\` array already gives you everything \`places_search\` needs (\`name\`, full \`address\`, \`latitude\`, \`longitude\`, badge-tagged \`notes\`); do NOT shorten the address to just the city.
10325
+
10326
+ For reference, the badge the server already applied to each \`notes\`:
10327
+
10328
+ - \`\u2605 Customer\` \u2014 Monitor lead with prior engagement history (\`epilogue_status\`, \`last_prospecting_action_at\`, or \`last_monitor_action_at\` set).
10329
+ - \`\u2605 Qualified\` \u2014 Monitor lead with a high score but no recent action.
10275
10330
  - \`\u2726 New\` \u2014 Discover lead from \`discover_leads\`.
10276
10331
 
10277
- Then the rest of the notes string follows the standard recipe: ONE sentence, sector/fit + contact ask, bare phone + email auto-linkified. Example:
10332
+ Example of a server-built \`notes\` string you pass through unchanged:
10278
10333
 
10279
10334
  \`\`\`
10280
10335
  \u2726 New \u2014 Strong mid-size hardware distributor fit. Reach Marie Dupont, Sales Director: +33 5 55 12 34 56, m.dupont@example.fr.
10281
10336
  \`\`\`
10282
10337
 
10283
- Skip any lead whose \`location.pos\` is null (no lat/lng \u2192 no pin) \u2014 list them as a "+ N leads without coordinates" footer below the widget.
10338
+ Leads without coordinates are already omitted from \`map_locations\`. Use \`map_summary.leads_without_coords\` to footnote "+ N leads without coordinates" below the widget \u2014 no need to re-count.
10284
10339
 
10285
10340
  ## Chat prose AFTER the widget (where markdown DOES render)
10286
10341
 
@@ -10288,9 +10343,17 @@ Group the leads into THREE sections (Customers / Qualified Prospects / New Disco
10288
10343
 
10289
10344
  If the user said something like "3+3+3", honor that split. If \`followups_count\` returned fewer Customers than asked, fill from Qualified.
10290
10345
 
10291
- ## RENDER \u2014 fallback for hosts without \`places_map_display_v0\`
10346
+ ## RENDER \u2014 fallback for hosts without \`places_map_display_v0\` (still a map, never a prose list)
10292
10347
 
10293
- Emit per-lead markdown blocks grouped by mode, in the standard place-card-friendly format (\`### **Company** \xB7 City, State\`, mode badge + score, contact line, channel lines). Hosts that auto-detect addresses (Claude.ai web, cowork) build their own carousel from these blocks.
10348
+ When the widget isn't in your tool set (e.g. Claude Desktop), you MUST still produce a map by emitting one place-card block per \`map_locations\` entry in EXACTLY this shape \u2014 the \`### Company \xB7 City, State\` heading with the address is what makes the host auto-detect it and render its own Google-Place-card carousel:
10349
+
10350
+ \`\`\`
10351
+ ### <Company Name> \xB7 <City>, <State>
10352
+
10353
+ <\u2605 Customer | \u2605 Qualified | \u2726 New> \u2014 <one-sentence fit>. Reach **[<Contact>](<LinkedIn URL>)**, <role>. \u260E <bare phone>.
10354
+ \`\`\`
10355
+
10356
+ Group the blocks by mode (Customers \u2192 Qualified \u2192 New). A flat narrative paragraph ("Brooklyn Brewery \u2014 Broadway, 10018 (Midtown). Contact: \u2026") does NOT auto-detect into cards and is WRONG \u2014 the per-lead heading with the city is mandatory.
10294
10357
 
10295
10358
  ## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
10296
10359
 
@@ -10300,7 +10363,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
10300
10363
 
10301
10364
  | Host widget | Use when | Field map (from Leadbay payload) |
10302
10365
  |---|---|---|
10303
- | \`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 |
10366
+ | \`places_map_display_v0\` + \`places_search\` (Claude) | \u22652 leads with coords / \`location.city\`, geographic / "in person" / travel intent | **Two-step**: \`places_search\` each lead (query = company + full street address) \u2192 real \`place_id\`/coords, THEN render with \`places_map_display_v0\` (Itinerary mode for a tour). Skipping \`places_search\` \u2192 schematic scatter, not a street map. |
10304
10367
  | \`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") |
10305
10368
  | \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
10306
10369
 
@@ -10319,7 +10382,7 @@ WHEN TO USE: the user signals a *mixed* tour-planning intent \u2014 they want bo
10319
10382
 
10320
10383
  WHEN NOT TO USE: if the user only wants follow-ups (use \`leadbay_followups_map\`), only wants new leads (use \`leadbay_pull_leads\`), wants research on one specific account (\`leadbay_research_lead_by_id\`), or wants to persist the tour as a campaign artifact (chain into \`leadbay_create_campaign\` after this).
10321
10384
 
10322
- **Response envelope**: \`{city, city_id, monitor_leads, discover_leads, discover_filter_note, _meta}\` on happy path; \`{status: "ambiguous_locations", location_ambiguities, ...}\` when the passed \`city\` matched multiple admin areas.
10385
+ **Response envelope**: \`{city, city_id, monitor_leads, discover_leads, discover_filter_note, map_locations, map_summary, _meta}\` on happy path; \`{status: "ambiguous_locations", location_ambiguities, ...}\` when the passed \`city\` matched multiple admin areas.
10323
10386
 
10324
10387
  ---
10325
10388
  `;
@@ -15664,6 +15727,54 @@ function cityMatches(lead, cityHint) {
15664
15727
  const haystacks = [loc.city, loc.state, loc.country, loc.full].filter((v) => typeof v === "string").map((v) => v.toLowerCase());
15665
15728
  return haystacks.some((h) => h.includes(hint) || hint.includes(h));
15666
15729
  }
15730
+ function toMapLocation(lead, mode) {
15731
+ const pos = lead?.location?.pos;
15732
+ const valid = Array.isArray(pos) && pos.length === 2 && pos.every((n) => typeof n === "number");
15733
+ if (!valid)
15734
+ return null;
15735
+ const loc = lead.location;
15736
+ const c = lead.recommended_contact;
15737
+ const cleanName = (v) => typeof v === "string" && v.trim() && v.trim().toLowerCase() !== "null" ? v.trim() : "";
15738
+ const fullName = c ? [cleanName(c.first_name), cleanName(c.last_name)].filter(Boolean).join(" ") : "";
15739
+ const role = cleanName(c?.job_title) ? `, ${cleanName(c?.job_title)}` : "";
15740
+ const angle = lead.split_ai_summary?.next_step ?? lead.split_ai_summary?.approach_angle ?? lead.short_description ?? "Worth a visit";
15741
+ let reach;
15742
+ if (c && fullName && c.phone_number) {
15743
+ reach = `Reach ${fullName}${role}: ${c.phone_number}${c.email ? `, ${c.email}` : ""}.`;
15744
+ } else if (c && fullName && c.email) {
15745
+ reach = `Reach ${fullName}${role}: ${c.email}.`;
15746
+ } else if (c && fullName) {
15747
+ reach = `Reach ${fullName} (enrich a channel).`;
15748
+ } else {
15749
+ reach = "Enrich a contact to reach this account.";
15750
+ }
15751
+ const notes = `${mode} \u2014 ${angle}. ${reach}`.slice(0, 280);
15752
+ return {
15753
+ name: lead.name,
15754
+ address: loc.full ?? [loc.city, loc.state, loc.country].filter(Boolean).join(", "),
15755
+ latitude: pos[0],
15756
+ longitude: pos[1],
15757
+ notes
15758
+ };
15759
+ }
15760
+ function hasMonitorHistory(lead) {
15761
+ return Boolean(lead?.epilogue_status || lead?.last_prospecting_action_at || lead?.last_monitor_action_at);
15762
+ }
15763
+ function buildMap(monitorLeads, discoverLeads2) {
15764
+ const mapLocations = [
15765
+ ...monitorLeads.map((l) => toMapLocation(l, hasMonitorHistory(l) ? "\u2605 Customer" : "\u2605 Qualified")),
15766
+ ...discoverLeads2.map((l) => toMapLocation(l, "\u2726 New"))
15767
+ ].filter((m) => m !== null);
15768
+ const totalLeads = monitorLeads.length + discoverLeads2.length;
15769
+ return {
15770
+ map_locations: mapLocations,
15771
+ map_summary: {
15772
+ total_leads: totalLeads,
15773
+ leads_with_coords: mapLocations.length,
15774
+ leads_without_coords: totalLeads - mapLocations.length
15775
+ }
15776
+ };
15777
+ }
15667
15778
  var tourPlan = {
15668
15779
  name: "leadbay_tour_plan",
15669
15780
  annotations: {
@@ -15715,6 +15826,20 @@ var tourPlan = {
15715
15826
  type: "string",
15716
15827
  description: "Human-readable summary of the client-side geo filter applied to Discover leads (e.g. 'matched 3/30 by city/state')."
15717
15828
  },
15829
+ map_locations: {
15830
+ type: "array",
15831
+ description: "Pre-shaped entries for `places_map_display_v0` \u2014 pass each one verbatim ({name, address, latitude, longitude, notes}); the mode badge (\u2605 Customer / \u2605 Qualified / \u2726 New) is already in `notes`. Do NOT reshape or re-derive from `location.pos`. One entry per lead with valid coordinates; coordinate-less leads are omitted and counted in `map_summary`.",
15832
+ items: { type: "object" }
15833
+ },
15834
+ map_summary: {
15835
+ type: "object",
15836
+ description: "Deterministic coverage counts so the agent can footnote '+ N leads without coordinates' without re-counting.",
15837
+ properties: {
15838
+ total_leads: { type: "number" },
15839
+ leads_with_coords: { type: "number" },
15840
+ leads_without_coords: { type: "number" }
15841
+ }
15842
+ },
15718
15843
  status: {
15719
15844
  type: "string",
15720
15845
  description: "'ambiguous_locations' when the passed `city` matched multiple admin areas \u2014 pick an id from location_ambiguities and re-call with city_id."
@@ -15731,7 +15856,7 @@ var tourPlan = {
15731
15856
  }
15732
15857
  }
15733
15858
  },
15734
- required: ["monitor_leads", "discover_leads"]
15859
+ required: ["monitor_leads", "discover_leads", "map_locations"]
15735
15860
  },
15736
15861
  execute: async (client, params, ctx) => {
15737
15862
  const followupsCount = params.followups_count ?? DEFAULT_FOLLOWUPS_COUNT;
@@ -15753,6 +15878,12 @@ var tourPlan = {
15753
15878
  monitor_leads: [],
15754
15879
  discover_leads: [],
15755
15880
  discover_filter_note: "City was ambiguous; pick an id and re-call to proceed.",
15881
+ map_locations: [],
15882
+ map_summary: {
15883
+ total_leads: 0,
15884
+ leads_with_coords: 0,
15885
+ leads_without_coords: 0
15886
+ },
15756
15887
  city: params.city ?? null,
15757
15888
  city_id: params.city_id ?? null,
15758
15889
  _meta: {
@@ -15779,6 +15910,7 @@ var tourPlan = {
15779
15910
  monitor_leads: monitorLeads,
15780
15911
  discover_leads: discoverLeads2,
15781
15912
  discover_filter_note: filterNote,
15913
+ ...buildMap(monitorLeads, discoverLeads2),
15782
15914
  _meta: {
15783
15915
  region: client.region,
15784
15916
  latency_ms: client.lastMeta?.latency_ms ?? null
@@ -20415,7 +20547,7 @@ async function resolveLensByName(client, name) {
20415
20547
  return { ok: false, reason: "ambiguous", matches: partial };
20416
20548
  return { ok: false, reason: "not_found", lenses: all };
20417
20549
  }
20418
- function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
20550
+ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes, toAddLocations = [], toExcludeLocations = []) {
20419
20551
  const items = current?.lens_filter?.items ?? [];
20420
20552
  const item = items[0] ?? { criteria: [] };
20421
20553
  const criteria = item.criteria ? [...item.criteria] : [];
@@ -20447,6 +20579,34 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
20447
20579
  });
20448
20580
  }
20449
20581
  }
20582
+ if (toAddLocations.length > 0) {
20583
+ const idx = criteria.findIndex((c) => c.type === "location_ids" && !c.is_excluded);
20584
+ if (idx >= 0) {
20585
+ const cur = criteria[idx];
20586
+ const merged = Array.from(/* @__PURE__ */ new Set([...cur.locations ?? [], ...toAddLocations]));
20587
+ criteria[idx] = { ...cur, locations: merged };
20588
+ } else {
20589
+ criteria.push({
20590
+ type: "location_ids",
20591
+ is_excluded: false,
20592
+ locations: toAddLocations
20593
+ });
20594
+ }
20595
+ }
20596
+ if (toExcludeLocations.length > 0) {
20597
+ const idx = criteria.findIndex((c) => c.type === "location_ids" && c.is_excluded);
20598
+ if (idx >= 0) {
20599
+ const cur = criteria[idx];
20600
+ const merged = Array.from(/* @__PURE__ */ new Set([...cur.locations ?? [], ...toExcludeLocations]));
20601
+ criteria[idx] = { ...cur, locations: merged };
20602
+ } else {
20603
+ criteria.push({
20604
+ type: "location_ids",
20605
+ is_excluded: true,
20606
+ locations: toExcludeLocations
20607
+ });
20608
+ }
20609
+ }
20450
20610
  if (sizes && sizes.length > 0) {
20451
20611
  const normalizedSizes = sizes.map((s) => ({
20452
20612
  min: s.min ?? 0,
@@ -20507,6 +20667,21 @@ var adjustAudience = {
20507
20667
  },
20508
20668
  description: "Company size buckets, e.g. [{min:30,max:300}]"
20509
20669
  },
20670
+ locations: {
20671
+ type: "array",
20672
+ items: { type: "string" },
20673
+ description: "Geographic scope \u2014 free text (e.g. ['Indre-et-Loire', 'Bavaria', 'Austin']) or admin-area ids. Auto-resolved via /geo/search across all admin levels (city / county / d\xE9partement / r\xE9gion / state / country). Place names go HERE, never in sectors/keywords."
20674
+ },
20675
+ location_ids: {
20676
+ type: "array",
20677
+ items: { type: "string" },
20678
+ description: "Explicit admin-area ids (skips /geo/search resolution)"
20679
+ },
20680
+ exclude_locations: {
20681
+ type: "array",
20682
+ items: { type: "string" },
20683
+ description: "Locations to exclude (free text or ids)"
20684
+ },
20510
20685
  lensId: { type: "number", description: "Lens id (escape hatch)" },
20511
20686
  lensName: {
20512
20687
  type: "string",
@@ -20525,17 +20700,22 @@ var adjustAudience = {
20525
20700
  },
20526
20701
  outputSchema: {
20527
20702
  type: "object",
20528
- 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).",
20703
+ description: "Return shapes: 'applied' on success; 'ambiguous_sectors' when free-text sectors matched multiple candidates (re-call with sector_ids); 'ambiguous_locations' when free-text locations didn't resolve to one area \u2014 re-call with the chosen id via the SAME axis it came from (an include pick \u2192 location_ids; an EXCLUDE pick \u2192 exclude_locations, NOT location_ids, which would include it); 'lens_not_found' / 'ambiguous_lens' when a lensName didn't resolve to exactly one lens (re-call with lensId or an exact lensName).",
20529
20704
  properties: {
20530
20705
  status: {
20531
20706
  type: "string",
20532
- description: "'applied', 'ambiguous_sectors', 'lens_not_found', or 'ambiguous_lens'."
20707
+ description: "'applied', 'ambiguous_sectors', 'ambiguous_locations', 'lens_not_found', or 'ambiguous_lens'."
20533
20708
  },
20534
20709
  sector_ambiguities: {
20535
20710
  type: "array",
20536
20711
  description: "Per ambiguous text: {sector_text, matches:[{id, name, score}]}. Agent picks an id and re-calls.",
20537
20712
  items: { type: "object" }
20538
20713
  },
20714
+ location_ambiguities: {
20715
+ type: "array",
20716
+ description: "On 'ambiguous_locations': per text {location_text, matches:[{id, name, country, level, score}]}. Agent picks an id and re-calls via the SAME axis the text came from \u2014 an include text \u2192 location_ids; a text from exclude_locations \u2192 exclude_locations (NOT location_ids, which would include the area the user asked to exclude). The `message` field names the correct param per text.",
20717
+ items: { type: "object" }
20718
+ },
20539
20719
  lenses: {
20540
20720
  type: "array",
20541
20721
  description: "On 'lens_not_found': the user's lenses [{id, name}] to pick from.",
@@ -20619,9 +20799,45 @@ var adjustAudience = {
20619
20799
  message: parts.join(" ")
20620
20800
  };
20621
20801
  }
20802
+ const includeLocTexts = [
20803
+ ...params.locations ?? [],
20804
+ ...params.location_ids ?? []
20805
+ ];
20806
+ const excludeLocTexts = params.exclude_locations ?? [];
20807
+ const includeLocRes = await resolveLocations(client, includeLocTexts);
20808
+ const excludeLocRes = await resolveLocations(client, excludeLocTexts);
20809
+ const locAmbiguities = [
20810
+ ...includeLocRes.ambiguities,
20811
+ ...excludeLocRes.ambiguities
20812
+ ];
20813
+ if (locAmbiguities.length > 0) {
20814
+ const incMatch = includeLocRes.ambiguities.filter((a) => a.matches.length > 0);
20815
+ const incNone = includeLocRes.ambiguities.filter((a) => a.matches.length === 0);
20816
+ const excMatch = excludeLocRes.ambiguities.filter((a) => a.matches.length > 0);
20817
+ const excNone = excludeLocRes.ambiguities.filter((a) => a.matches.length === 0);
20818
+ const quote = (as) => as.map((a) => `"${a.location_text}"`).join(", ");
20819
+ const parts = [];
20820
+ if (incNone.length > 0) {
20821
+ parts.push(`Couldn't find a location matching ${quote(incNone)}. Ask the user to rephrase, then re-call with location_ids=...`);
20822
+ }
20823
+ if (incMatch.length > 0) {
20824
+ parts.push(`${quote(incMatch)} matched multiple areas. Pick from the matches and re-call with location_ids=...`);
20825
+ }
20826
+ if (excNone.length > 0) {
20827
+ parts.push(`Couldn't find a location to exclude matching ${quote(excNone)}. Ask the user to rephrase, then re-call with exclude_locations=... (the chosen id).`);
20828
+ }
20829
+ if (excMatch.length > 0) {
20830
+ parts.push(`${quote(excMatch)} (to exclude) matched multiple areas. Pick from the matches and re-call with exclude_locations=... (the chosen id) \u2014 NOT location_ids, which would include it.`);
20831
+ }
20832
+ return {
20833
+ status: "ambiguous_locations",
20834
+ location_ambiguities: locAmbiguities,
20835
+ message: parts.join(" ")
20836
+ };
20837
+ }
20622
20838
  const lens = await client.request("GET", `/lenses/${startingLensId}`);
20623
20839
  const currentFilter = await client.request("GET", `/lenses/${startingLensId}/filter`);
20624
- const merged = mergeFilter(currentFilter, includeRes.resolved, excludeRes.resolved, params.sizes);
20840
+ const merged = mergeFilter(currentFilter, includeRes.resolved, excludeRes.resolved, params.sizes, includeLocRes.resolved, excludeLocRes.resolved);
20625
20841
  const mergedBody = filterWriteBody(merged);
20626
20842
  const isDefault = lens.is_default || lens.default;
20627
20843
  const isUserLevel = lens.user_id != null;
@@ -21344,6 +21560,16 @@ var newLens = {
21344
21560
  },
21345
21561
  description: "Company size buckets, e.g. [{min:30,max:300}]."
21346
21562
  },
21563
+ locations: {
21564
+ type: "array",
21565
+ items: { type: "string" },
21566
+ description: "Geographic scope \u2014 free text (e.g. ['Indre-et-Loire', 'Bavaria']) or admin-area ids. Auto-resolved via /geo/search across all admin levels (city / county / d\xE9partement / r\xE9gion / state / country). Scopes the lens to a sales territory."
21567
+ },
21568
+ exclude_locations: {
21569
+ type: "array",
21570
+ items: { type: "string" },
21571
+ description: "Locations to exclude \u2014 free text or ids."
21572
+ },
21347
21573
  base: {
21348
21574
  type: "number",
21349
21575
  description: "Lens id to clone from. Defaults to the active/default lens."
@@ -21359,12 +21585,12 @@ var newLens = {
21359
21585
  },
21360
21586
  outputSchema: {
21361
21587
  type: "object",
21362
- 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).",
21588
+ description: "'preview' (default, NOTHING created \u2014 confirm with the user then re-call with confirm:true); 'created' on success; 'ambiguous_sectors' / 'ambiguous_locations' when free-text sectors / locations didn't resolve (re-call with ids \u2014 the lens was NOT created).",
21363
21589
  properties: {
21364
- status: { type: "string", description: "'preview', 'created', 'ambiguous_sectors', or 'orphan_created' (filter write failed + cleanup failed)." },
21590
+ status: { type: "string", description: "'preview', 'created', 'ambiguous_sectors', 'ambiguous_locations', or 'orphan_created' (filter write failed + cleanup failed)." },
21365
21591
  will_create: {
21366
21592
  type: "object",
21367
- description: "On 'preview': what WILL be created \u2014 {name, description, sectors, exclude_sectors, sizes}. Nothing has been written yet."
21593
+ description: "On 'preview': what WILL be created \u2014 {name, description, sectors, exclude_sectors, sizes, locations, exclude_locations}. Nothing has been written yet."
21368
21594
  },
21369
21595
  filter_preview: { type: "object", description: "On 'preview': the FilterPayload that would be applied." },
21370
21596
  lens: {
@@ -21376,6 +21602,11 @@ var newLens = {
21376
21602
  description: "On 'ambiguous_sectors': per text {sector_text, matches:[{id,name,score}]}.",
21377
21603
  items: { type: "object" }
21378
21604
  },
21605
+ location_ambiguities: {
21606
+ type: "array",
21607
+ description: "On 'ambiguous_locations': per text {location_text, matches:[{id,name,country,level,score}]}. Re-call the chosen id via the SAME axis the text came from \u2014 an include text \u2192 locations; a text from exclude_locations \u2192 exclude_locations (NOT locations, which would include the area the user asked to exclude). The `message` field names the correct param per text.",
21608
+ items: { type: "object" }
21609
+ },
21379
21610
  filter_applied: { type: "object", description: "On 'created': the FilterPayload POSTed to the new lens." },
21380
21611
  message: { type: "string" },
21381
21612
  _meta: { type: "object" }
@@ -21402,7 +21633,38 @@ var newLens = {
21402
21633
  message: parts.join(" ")
21403
21634
  };
21404
21635
  }
21405
- const merged = mergeFilter(EMPTY_FILTER, includeRes.resolved, excludeRes.resolved, params.sizes);
21636
+ const includeLocRes = await resolveLocations(client, params.locations ?? []);
21637
+ const excludeLocRes = await resolveLocations(client, params.exclude_locations ?? []);
21638
+ const locAmbiguities = [
21639
+ ...includeLocRes.ambiguities,
21640
+ ...excludeLocRes.ambiguities
21641
+ ];
21642
+ if (locAmbiguities.length > 0) {
21643
+ const incMatch = includeLocRes.ambiguities.filter((a) => a.matches.length > 0);
21644
+ const incNone = includeLocRes.ambiguities.filter((a) => a.matches.length === 0);
21645
+ const excMatch = excludeLocRes.ambiguities.filter((a) => a.matches.length > 0);
21646
+ const excNone = excludeLocRes.ambiguities.filter((a) => a.matches.length === 0);
21647
+ const quote = (as) => as.map((a) => `"${a.location_text}"`).join(", ");
21648
+ const parts = [];
21649
+ if (incNone.length > 0) {
21650
+ parts.push(`Couldn't find a location matching ${quote(incNone)}. Pick a known area and re-call via locations (lens not yet created).`);
21651
+ }
21652
+ if (incMatch.length > 0) {
21653
+ parts.push(`${quote(incMatch)} matched multiple areas. Pick from the matches and re-call with the chosen id in locations.`);
21654
+ }
21655
+ if (excNone.length > 0) {
21656
+ parts.push(`Couldn't find a location to exclude matching ${quote(excNone)}. Pick a known area and re-call via exclude_locations (lens not yet created).`);
21657
+ }
21658
+ if (excMatch.length > 0) {
21659
+ parts.push(`${quote(excMatch)} (to exclude) matched multiple areas. Pick from the matches and re-call with the chosen id in exclude_locations \u2014 NOT locations, which would include it.`);
21660
+ }
21661
+ return {
21662
+ status: "ambiguous_locations",
21663
+ location_ambiguities: locAmbiguities,
21664
+ message: parts.join(" ")
21665
+ };
21666
+ }
21667
+ const merged = mergeFilter(EMPTY_FILTER, includeRes.resolved, excludeRes.resolved, params.sizes, includeLocRes.resolved, excludeLocRes.resolved);
21406
21668
  if (params.confirm !== true) {
21407
21669
  return {
21408
21670
  status: "preview",
@@ -21411,7 +21673,9 @@ var newLens = {
21411
21673
  description: params.description ?? null,
21412
21674
  sectors: includeRes.resolved,
21413
21675
  exclude_sectors: excludeRes.resolved,
21414
- sizes: merged.lens_filter.items[0].criteria.find((c) => c.type === "size") ?? null
21676
+ sizes: merged.lens_filter.items[0].criteria.find((c) => c.type === "size") ?? null,
21677
+ locations: includeLocRes.resolved,
21678
+ exclude_locations: excludeLocRes.resolved
21415
21679
  },
21416
21680
  filter_preview: merged,
21417
21681
  message: `About to create "${params.name}". Confirm with the user, then re-call with confirm:true.`,
@@ -23750,7 +24014,7 @@ function parseWriteEnv(env = process.env) {
23750
24014
  }
23751
24015
 
23752
24016
  // src/http-server.ts
23753
- var VERSION = true ? "0.23.5" : "0.0.0-dev";
24017
+ var VERSION = true ? "0.23.7" : "0.0.0-dev";
23754
24018
  var PORT = Number(process.env.PORT ?? 8080);
23755
24019
  var HOST = process.env.HOST ?? "0.0.0.0";
23756
24020
  var sseSessions = /* @__PURE__ */ new Map();