@leadbay/mcp 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -435,6 +435,606 @@ Render this acknowledgment VERBATIM as the last line of your message:
435
435
  STOP \u2014 awaiting user decision. I will not take any further action until you tell me what to do next.
436
436
  \`\`\`
437
437
 
438
+ Do not propose a next action. Do not call any more tools. Hand control back to the user.
439
+ `;
440
+ var leadbay_getting_started = `
441
+ ## MEMORY
442
+
443
+ Before responding, glance at any \`_meta.agent_memory.summary\` returned by tool calls earlier in this session and reflect its top signals in your reasoning ("Filtering by your stated preference for healthcare"). After any material new signal from the user this conversation (sector, region, deal size, communication style, qualification rule, explicit retraction, or recurrence / scheduling preference such as "I do this every day" or "remind me every morning"), call \`leadbay_agent_memory_capture\` to persist it: \`source:"user_stated"\` if literal, \`source:"inferred"\` with confidence <=6 if inferred.
444
+
445
+
446
+ Walk me through Leadbay. Treat these the same way: "I'm new here", "how do I
447
+ use this?", "getting started", "show me how Leadbay works", "give me a tour",
448
+ "I just installed this".
449
+
450
+ This is a GUIDED WALKTHROUGH, not an explainer. The user learns by clicking,
451
+ and every click runs a real Leadbay call against their own account. By the end
452
+ they will have actually checked their account, pulled leads, had a first email
453
+ drafted to the best of them, and revealed the person to send it to.
454
+
455
+ If the user wants orientation PROSE without doing anything \u2014 "explain how
456
+ Leadbay works", "what's the difference between discovery and follow-up" \u2014
457
+ this is the wrong prompt. Use \`leadbay_prospecting_overview\` instead.
458
+
459
+ If their problem is **setup** rather than usage \u2014 the connector isn't installed
460
+ yet, they can't sign in, their Leadbay tools aren't appearing, or they're asking
461
+ how to run this on another host \u2014 this walkthrough cannot help them. It assumes
462
+ a working connection, and GATE 1 is what proves it. Point them at the setup
463
+ guide instead of guessing at install steps:
464
+ <https://docs.leadbay.app/doc/leadbay-mcp/quickstart>
465
+
466
+ GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
467
+
468
+ 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.
469
+
470
+
471
+ # Resilience rules for Leadbay long-running tools
472
+
473
+ These four rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
474
+
475
+ ## Rule 1 \u2014 Pin the lens
476
+
477
+ After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into your working memory and **pass it explicitly as the \`lensId\` argument to every subsequent call** in this session \u2014 including any re-pulls, bulk qualifies, or research calls that accept it. (Field-name caveat: the response nests it as \`lens.id\`; the parameter on subsequent calls is \`lensId\`.) The active lens can shift between calls (5-minute client cache + backend \`last_requested_lens\` can change if the user touches the web UI). A lens shift mid-workflow throws away your top-10 work.
478
+
479
+ ## Rule 2 \u2014 Prefer async for bulk operations
480
+
481
+ \`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\`, which returns \`{status:'running', qualify_id}\` immediately. Then poll \`leadbay_qualify_status\` (or \`leadbay_import_status\`) every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
482
+
483
+ ## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
484
+
485
+ \`leadbay_research_lead_by_id\` is composite and reads many sub-resources. Calling it on 10 leads in parallel can saturate the transport and produce \`"Tool permission stream closed"\` errors that look like permission failures but are really backpressure. **Call it sequentially**, or at most 3 in parallel. If one call fails with a stream/timeout error, retry that one call once before moving on; on a second failure, note the lead and continue \u2014 do not abandon the remaining leads.
486
+
487
+ ## Rule 4 \u2014 Retry, don't replan
488
+
489
+ If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any other transport-level error (distinct from a Leadbay-issued error payload), the work may still be running server-side. Do this in order:
490
+
491
+ 1. For bulk tools \u2014 retry with \`wait_for_completion:false\` and poll the status tool with the returned id. Don't re-pull leads; that can shift the lens.
492
+ 2. For single-lead tools \u2014 retry the same call once. If it still fails, record the lead id and continue with the rest of the workflow.
493
+ 3. **Do not** switch strategies (e.g. "the endpoint is broken, let me re-pull from scratch"). The earlier work is still valid; the timeout was the wire.
494
+
495
+ If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
496
+
497
+
498
+ # THE ONE-FORWARD-OPTION RULE \u2014 the structural contract of this walkthrough
499
+
500
+ Every gate presents **exactly ONE way forward, plus a way out**. Two options,
501
+ never more:
502
+
503
+ 1. **The action** \u2014 the single next step of the tour.
504
+ 2. **The exit** \u2014 \`I'm done for now\`, which ends the walkthrough politely.
505
+
506
+ This is deliberate. A first-run user does not yet know enough to choose between
507
+ *paths* \u2014 a menu of alternatives makes them stall. One forward move makes the
508
+ next step obvious, and the click is what teaches them the tool. The exit exists
509
+ so the tour is never a trap, and because your host's choice widget requires 2\u20134
510
+ options: a lone option is rejected or silently degrades to prose, which kills
511
+ the whole feature.
512
+
513
+ **Never add a third option**, and never turn the exit into an alternative route
514
+ ("show me my lenses instead") \u2014 that reintroduces the choice this rule exists
515
+ to remove.
516
+
517
+ **The gate IS the widget.** Call your host's choice widget with these two
518
+ options. **Never render a gate as a prose question** \u2014 "say the word and I'll
519
+ check it" is a defect, not a gate: the user gets no button and the walkthrough
520
+ becomes a conversation they have to drive themselves.
521
+
522
+ **EVERY GATE IS TWO BEATS \u2014 EXPLAIN, THEN ASK.** This is a tutorial, so the
523
+ user must understand what they're about to do *before* they click:
524
+
525
+ 1. **Explain** \u2014 one or two plain sentences saying what this step does and why
526
+ it matters. Never jargon. This is the teaching half; skipping it turns the
527
+ walkthrough into a series of unexplained buttons.
528
+ 2. **Ask** \u2014 fire the widget. **Then STOP and wait for the click.**
529
+
530
+ **NEVER run a step's tool without firing its widget first and receiving the
531
+ user's click.** Calling \`leadbay_pull_leads\` because the walkthrough "obviously
532
+ goes there next" defeats the entire feature \u2014 the click IS the lesson. The one
533
+ exception is when the user's own message already told you to do it (e.g. "walk
534
+ me through it and just run everything"); then follow what they asked.
535
+
536
+ **Each gate ships its own widget payload \u2014 use it, don't rewrite it.** Every
537
+ step in the manifest carries \`explain\` (what to say) and \`next_steps\`
538
+ (\`{question, options[]}\`, already the widget's shape). Map \`next_steps\` into
539
+ your host's widget VERBATIM \u2014 same question, same two options, same labels and
540
+ descriptions. Do not reword them, do not merge two gates into one widget, and
541
+ do not add a third option.
542
+
543
+ Typing works as an escape hatch too. If the user types
544
+ something off-script ("actually just show me my lenses"), abandon the
545
+ walkthrough and serve what they asked. Never re-fire a gate the user has
546
+ already declined in prose.
547
+
548
+ **ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
549
+
550
+ **If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
551
+
552
+ **One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
553
+ - Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
554
+ - Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
555
+
556
+ Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
557
+ - \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
558
+ - \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
559
+
560
+ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
561
+
562
+ ---
563
+
564
+
565
+
566
+ # THE OPENING \u2014 SHORT, THEN STRAIGHT INTO GATE 1
567
+
568
+ **A short paragraph, then the widget** \u2014 3\u20134 sentences, all in your FIRST
569
+ message. In the user's own language, no jargon, cover:
570
+
571
+ 1. **What Leadbay is** \u2014 it brings you a fresh batch of companies worth
572
+ selling to every day, rather than you going hunting for them.
573
+ 2. **How it knows what to send** \u2014 you describe who you sell to (that
574
+ description is your **lens**), and it goes and finds companies matching it,
575
+ getting sharper as you engage with what it sends.
576
+ 3. **What this walkthrough will do** \u2014 four quick steps, each a real action on
577
+ their own account, ending with leads in hand, a first email already written,
578
+ and the person to send it to.
579
+ 4. **One line handing off to the first step** \u2014 e.g. "First, let's see which
580
+ account you're on."
581
+
582
+ Then **fire GATE 1's widget immediately, in the same message**, and stop.
583
+
584
+ Keep it to a paragraph. Do NOT walk through the four steps one at a time here
585
+ \u2014 each gate explains itself when its turn arrives, and turning the opening
586
+ into a syllabus buries the first button under text nobody reads.
587
+
588
+ Call no tool in the opening. The widget is the whole ask.
589
+
590
+ # GATE 1 \u2014 "Check my account"
591
+
592
+ The opening paragraph above IS this gate's explanation \u2014 don't add another one
593
+ on top of it. Just hand off in a line and fire the widget.
594
+
595
+ **Why it's useful**, if you say anything at all: this is where they can see at
596
+ a glance how much they've used this week and what's left \u2014 so a batch that
597
+ comes back small later has a visible reason rather than feeling broken.
598
+
599
+ **Fire the widget** \u2014 question \`Let's start with your account status.\`, first option labelled \`Check my account\`, description \`Check my Leadbay account status.\` Second option: \`I'm done for now\` / \`Stop the walkthrough here.\` **Wait for the click.**
600
+
601
+ On click: call \`leadbay_account_status\` (it takes no arguments).
602
+
603
+ **Show them their actual account \u2014 this is the payoff of the click.** Lead with
604
+ one line on who they're signed in as and their organization, then render their
605
+ **quota windows in full**, exactly as the web app shows them: Daily / Weekly /
606
+ Monthly, each with a \`\u25B0\u25B1\` gauge, % used, $ spent against the cap, and when it
607
+ resets \u2014 plus the per-resource breakdown underneath. A one-line "you're
608
+ connected as X" is an under-delivery: they clicked a button labelled *check my
609
+ account status*, so show them the status.
610
+
611
+ ## RENDERING \u2014 quota windows (percentage + $, like the frontend)
612
+
613
+ Mirror the Leadbay web quota widget: three windows side by side \u2014 **Daily**,
614
+ **Weekly**, **Monthly** \u2014 each headlined by a **% used** gauge and a **$ spend /
615
+ $ cap** figure, with a per-resource usage breakdown underneath. **Never speak in
616
+ raw "credits"** for quota \u2014 the unit is a percentage and a dollar spend.
617
+
618
+ **Include the quota whenever it is readable** \u2014 as part of the default account
619
+ answer, even when the user only asked "what account am I connected to?". The
620
+ sole reason to omit it is the silence gate below (unreadable quota, or an
621
+ unlimited account); it is NOT gated on the user explicitly asking for quota.
622
+
623
+ **Silence gate (check FIRST).** Render NOTHING about quota when any of these
624
+ holds \u2014 do not mention quota at all, do not say "unreadable", never tell the user
625
+ to reconnect:
626
+ - \`quota\` is null, OR \`quota_error\` is set (a 401/403 backend quirk for plan-less
627
+ orgs \u2014 the same token read user/org fine), OR
628
+ - \`organization.unlimited_credits\` is true (internal/unlimited account \u2014 stay
629
+ silent on quota; never announce "unlimited").
630
+
631
+ **Pick the group (for DISPLAY only).** Prefer \`quota.user\` (present for every
632
+ caller). Use \`quota.org\` only when \`quota.user\` is absent (admins receive both \u2014
633
+ still show the caller's own \`user\` view). Call the chosen group \`<group>\` below.
634
+
635
+ **Exception \u2014 lens-refill pre-checks read the refill row, ORG-first.** This
636
+ user-preference is for the display gauge ONLY. When you pre-check the
637
+ \`LENS_EXTRA_REFILL\` resource before \`leadbay_extend_lens\`, look for the row in
638
+ **\`quota.org.resources[]\` first** (admins get the org group, and the refill
639
+ quota is org-scoped there); when \`quota.org\` is absent \u2014 non-admin callers only
640
+ receive the \`user\` group \u2014 fall back to **\`quota.user.resources[]\`**. Match the
641
+ resource type case-insensitively (\`LENS_EXTRA_REFILL\` / \`lens_extra_refill\`).
642
+ Skipping the \`user\` fallback for non-admins would make the row invisible even
643
+ when the quota data exists, so the agent burns the write and hits the very 429
644
+ this pre-check exists to avoid.
645
+
646
+ **Per window (fixed order: daily \u2192 weekly \u2192 monthly).** Match entries by
647
+ \`window_type\` (\`"daily"\` / \`"weekly"\` / \`"monthly"\`).
648
+
649
+ **Headline \u2014 when \`<group>.spend[]\` has an entry for the window (the % gauge):**
650
+ - \`pct = round(current_units / max_units \xD7 100)\` (both are dollar_cents).
651
+ - \`$used = (current_units / 100).toFixed(2)\`, \`$cap = (max_units / 100).toFixed(2)\`.
652
+ - 10-segment bar in a SINGLE inline-code span (backticks give it contrast):
653
+ \`filled = round(pct / 10)\` clamped 0..10; \`bar = "\u25B0"\xD7filled + "\u25B1"\xD7(10 \u2212 filled)\`.
654
+ Use ONLY \`\u25B0\`/\`\u25B1\` \u2014 do NOT use the \`\u2756\` glyph (that identity belongs to lead
655
+ discovery, not quota).
656
+ - Line: **\`<Window>\`** \`\` \`\u25B0\u25B0\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\` \`\` \`<pct>% used \xB7 $<used> / $<cap> \xB7 resets <resets_at, relative>\`.
657
+ e.g. \`**Daily** \` + \`\` \`\u25B0\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\` \`\` + \` 7% used \xB7 $0.84 / $12.00 \xB7 resets in ~7 h\`.
658
+
659
+ **Fallback \u2014 when \`<group>.spend[]\` is empty** (internal / free orgs have no
660
+ OVERALL_SPEND quota): no gauge. Render the per-window resource breakdown as a
661
+ compact table instead \u2014 one row per resource in \`<group>.resources[]\` for that
662
+ window: the friendly label + \`count\` (append \`/ <max_units>\` only when
663
+ \`max_units\` is a number). This is the pre-existing behavior, preserved.
664
+
665
+ **Resource labels (look up case-insensitively \u2014 lower-case \`resource_type\`
666
+ first).** Localize to \`user.language\` (FR canonical shown; English in parens):
667
+ - \`llm_completion\` \u2192 **G\xE9n\xE9rations par IA** (AI generations)
668
+ - \`ai_rescore\` \u2192 **Leads qualifi\xE9s** (qualified leads)
669
+ - \`web_fetch\` \u2192 **Informations web** (web insights)
670
+ - \`contact_enrichment_phone\` \u2192 **T\xE9l\xE9phones enrichis** (phones enriched)
671
+ - \`contact_enrichment_email\` \u2192 **E-mails enrichis** (emails enriched)
672
+
673
+ Skip any resource type not in this map silently \u2014 never dump the raw
674
+ \`resource_type\` string at the user.
675
+
676
+ **\`resets_at\`.** Show as a relative countdown ("resets in ~7 h", "resets in 3
677
+ days"), computed against now \u2014 mirroring the widget's "r\xE9initialis\xE9 dans X". The
678
+ raw value is an ISO-8601 timestamp.
679
+
680
+ **Top-up (optional, subordinate).** When \`quota.topup\` is present, you MAY add one
681
+ small line below the windows: \`Top-up: $<remaining_cents/100> of $<total_credit_cents/100> left\`.
682
+ Keep it secondary \u2014 the three window gauges are the headline. Omit when null.
683
+
684
+ **Legend** (once, below): \`\` \`\u25B0\` used \xB7 \`\u25B1\` remaining \`\`.
685
+
686
+
687
+ **Then explain what they're looking at \u2014 one or two plain lines, no jargon.**
688
+ A first-run user has never seen these numbers and won't know whether they're
689
+ good, bad, or something to worry about. Say, in your own words:
690
+
691
+ - **What it counts** \u2014 the AI work Leadbay does on their behalf: researching
692
+ companies on the web and qualifying leads against their criteria. Not
693
+ "credits", and not something they spend by clicking around.
694
+ - **Why it matters to them** \u2014 it paces how many fresh leads arrive. Heavy use
695
+ now means Leadbay queues up a bigger batch for next time; and if a batch ever
696
+ comes back smaller than expected, this is where they'd see why. Each window
697
+ refills on its own at the reset time already shown.
698
+
699
+ Keep it to a sentence or two, in their language. Do NOT lecture, do NOT explain
700
+ every resource row one by one, and do NOT turn this into a pricing pitch \u2014 if a
701
+ window is genuinely exhausted the tool's own guidance covers wait-vs-top-up.
702
+
703
+ **When the silence gate above applies, skip this explanation too** \u2014 there is
704
+ nothing on screen to explain, and describing an absent gauge just confuses.
705
+
706
+ **Two things this gate must NOT do** (both are pinned regressions):
707
+
708
+ - **Say nothing about quota when the silence gate above applies** \u2014 \`quota\` is
709
+ null, \`quota_error\` is set, or the org has \`unlimited_credits\`. A brand-new
710
+ org often has no billing plan yet, so the quota read fails. That is NOT an
711
+ error worth showing: do not mention quota, do not mention a 401, and above
712
+ all do NOT tell the user to log in again or reconnect \u2014 their token is fine,
713
+ the very same response just read their account. In that case fall back to the
714
+ short user + org line and move on to GATE 2 without comment.
715
+ - **Do not volunteer the lens.** The response deliberately withholds the lens
716
+ unless the user asked about it, so there is nothing to report. Don't reach
717
+ for another tool to find it either. The lens shows up naturally at GATE 2.
718
+
719
+ # GATE 2 \u2014 "Pull today's leads"
720
+
721
+ **Explain first \u2014 this is where you teach the LENS.** Leadbay keeps a *lens*:
722
+ their description of who they sell to. Every day it goes and finds fresh
723
+ companies matching it. This click pulls today's batch.
724
+
725
+ **Why it's useful:** it replaces the hour spent digging through directories and
726
+ LinkedIn looking for someone worth calling \u2014 the list is already waiting, and
727
+ already scored, when they sit down. And it sharpens itself: the leads they
728
+ like, contact or skip teach the lens what a good fit looks like, so tomorrow's
729
+ batch lands closer than today's.
730
+
731
+ **Then fire the widget** \u2014 question \`Now let's see today's leads. Ready?\`, first option labelled \`Pull today's leads\`, description \`Pull today's leads from your lens.\` Second option: \`I'm done for now\` / \`Stop the walkthrough here.\` **Wait for the click.**
732
+
733
+ On click: call \`leadbay_pull_leads\` with **no arguments** (it resolves the
734
+ user's default lens itself).
735
+
736
+ Capture \`lens.id\` from the response and pass it as an explicit \`lensId\` on
737
+ every later call in this walkthrough, so gate 4 enriches the same lens the
738
+ user just looked at. Pin the TOP-SCORING lead's id and name too \u2014 gate 3 drafts
739
+ to it, and gate 4 reveals its contact.
740
+
741
+ Render the batch with the canonical layout:
742
+
743
+ ## RENDERING \u2014 markdown table, three columns, score-bar driven
744
+
745
+ Present the response as a markdown table **in the exact order the tool returned the leads** \u2014 this is the Discover-tab order (the backend orders by new-today first, then status, then score). Do **not** re-sort the rows (in particular, do NOT re-order by \`score\`); render them top-to-bottom as received so the list matches what the user sees in the Leadbay UI. Exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
746
+
747
+ ## Score-bar (10-segment, inline-code wrapped)
748
+
749
+ Wrap a 10-glyph bar in a SINGLE inline-code span (backticks). The inline-code styling is what gives the bar contrast in most chat renderers \u2014 HTML \`<span>\` is stripped inside table cells.
750
+
751
+ Glyphs (use these exact characters; do not substitute):
752
+
753
+ - \`\u25B0\` \u2014 firmographic-only fill
754
+ - \`\u2756\` \u2014 AI-booster cap (placed at the RIGHT END of the filled run, never the front)
755
+ - \`\u25B1\` \u2014 empty
756
+
757
+ Computation:
758
+
759
+ \`\`\`
760
+ total_filled = round(score / 10), clamped to 0..10
761
+ ai_segments = round(qualification_summary.avg_qualification_boost / 3.3),
762
+ clamped to [0, total_filled]
763
+ normal_filled = total_filled \u2212 ai_segments
764
+ bar = "\u25B0" \xD7 normal_filled
765
+ + "\u2756" \xD7 ai_segments
766
+ + "\u25B1" \xD7 (10 \u2212 total_filled)
767
+ \`\`\`
768
+
769
+ If \`qualification_summary.answered == 0\` or \`avg_qualification_boost\` is null, set \`ai_segments = 0\` (no \u2756). Always wrap the bar in backticks. Print the legend \`\` \`\u25B0\` firmographic \xB7 \`\u2756\` AI booster cap \xB7 \`\u25B1\` unfilled \`\` once below the table.
770
+
771
+
772
+ **Column 1 \u2014 Company**
773
+
774
+ - Line 1: the 10-segment score bar in inline-code backticks (see the score-bar snippet above for the algorithm).
775
+ - Insert \`<br>\` between lines.
776
+ - Line 2: linked company name + \` \xB7 \` + short location + \` \xB7 \` + compact size.
777
+ - Link target: \`website\` (prefix \`https://\` if it's a bare hostname). Don't synthesize an app deep-link.
778
+ - Location: shorten "City of New York" \u2192 "NYC"; otherwise "City ST"; state alone only when city missing.
779
+ - Size: \`"Xk+"\` when \`size.min >= 1000\`, \`"min\u2013max"\` otherwise.
780
+
781
+ **Column 2 \u2014 Why it fits**
782
+
783
+ - One sentence, \u2264 20 words.
784
+ - Synthesize from (in priority order, whichever is present) the lead's \`short_description\`, top 2 \`tags[].display_name\`, and the gist of \`qualification_summary.best_response_excerpt\`. The trim payload does NOT carry the longer \`description\` field \u2014 for that, agent must call \`leadbay_research_lead_by_id\` or \`leadbay_research_lead_by_name_fuzzy\`.
785
+ - Do NOT append \`(boost N)\` \u2014 the \u2756 cap in column 1 already carries that signal.
786
+ - No bullet lists, no line breaks inside the cell.
787
+
788
+ **Column 3 \u2014 Contact**
789
+
790
+ \`[Contact name](LINK) \xB7 short job title\`. The \`[Contact name](LINK)\` markdown link wrapping is mandatory \u2014 never render the name as plain text. See linking/contact-linkedin for the URL priority (real profile \u2192 constructed people-search) and the \xB0-flag fallback.
791
+
792
+ **Hide from the user (never include in any cell):** \`id\`, \`location.pos\`, \`location.country\` (unless city/state both missing), \`sector_id\`, \`is_hq\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`highlighted_fields\`, \`custom_fields\`, \`contacts_count\` when 0, \`notes_count\` / \`epilogue_actions_count\` / \`prospecting_actions_count\` when 0, \`stale_at\`, \`deal_insights\`, \`social_presence\` booleans (except as the \xB0-flag signal), \`need_attention\` flags, any field whose value is the string \`"null"\`.
793
+
794
+ ## Linking a contact's name
795
+
796
+ **MANDATORY: every contact name in your output \u2014 table cells, prose, headers, "Reach <Name>" callouts \u2014 MUST be wrapped in markdown link syntax \`[Name](URL)\`. Never render a contact name as bare text. A plain-text name is a broken contact card; the underlined name is the user's primary affordance for "take me to this person's profile". No "no URL available" exception \u2014 the search URL below is always constructable from name + company.**
797
+
798
+ URL priority (first applicable wins):
799
+
800
+ 1. **Real profile** \u2014 \`contact.linkedin_page\` when it's a string starting with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it).
801
+ 2. **Constructed people-search** \u2014 \`https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>\`. URL-encode params. Strip Inc / LLC / Corp / Ltd / GmbH / Co / S.A. / S.L. / PLC / AG / SAS / SARL suffixes from the company. Append a trailing \` \xB0\` to the rendered name ONLY when this fallback is in use AND \`social_presence.linkedin == false\`. Never append \`\xB0\` when a real \`linkedin_page\` was used.
802
+
803
+ Never link a person's name to the company's LinkedIn page (and vice versa) \u2014 the two surfaces are different and conflating them quietly degrades the workflow.
804
+
805
+ ## Linking the company
806
+
807
+ Use the lead's \`website\` as the company-name link target \u2014 prefix \`https://\` if the value is a bare hostname. (The MCP does NOT synthesize a Leadbay-app deep-link URL; the team has not standardized one. Linking to \`website\` is always real data.)
808
+
809
+ When the response carries \`social_urls\` (the post-fix multi-platform URL block on rich-lead responses), render every non-null platform as a pill chip in the company-info row. Iterate over \`social_urls\`'s keys \u2014 never hardcode a fixed list \u2014 and emit each as \`[<platform-label>](<url>)\`. Skip platforms whose URL is null.
810
+
811
+ \`social_presence\` carries booleans for the same 6 platforms (crunchbase, facebook, instagram, linkedin, tiktok, twitter) \u2014 useful when you only care that the company has a profile somewhere. Use it as the \xB0-flag signal in the contact people-search fallback (see linking/contact-linkedin).
812
+
813
+
814
+
815
+ ## Branch \u2014 the batch came back empty
816
+
817
+ A brand-new account often reads empty for the first minute while the backend
818
+ computes the lens wishlist. Check \`computing_wishlist\` / \`computing_scores\`:
819
+
820
+ - **Either is true** \u2192 the lens is still building. Say exactly that, in the
821
+ user's terms: "your lens is still building your first batch \u2014 that's normal
822
+ on a new account, it takes about a minute." The tool's \`next_steps\` payload
823
+ carries a **two-option** warm-up widget ("Re-pull in ~30s" / "Refine
824
+ audience") \u2014 render it VERBATIM. This is the ONE place a gate carries two
825
+ options, because the server built the payload and a re-pull genuinely has a
826
+ real alternative. On "Re-pull in ~30s", wait ~30s and return to GATE 2.
827
+ **NEVER say "no leads found."**
828
+ - **Both false** \u2192 the lens is genuinely empty or too narrow, and \`next_steps\`
829
+ is \`null\`. Say so honestly, offer to widen the audience, and end the
830
+ walkthrough here. There is nothing to enrich.
831
+
832
+ # GATE 3 \u2014 "Draft the first email"
833
+
834
+ **Explain first \u2014 and name the company.** Take the TOP-SCORING lead from
835
+ GATE 2 and say its name out loud, so this is an offer about a real company
836
+ rather than an abstraction. Leadbay already worked out *why* that company fits
837
+ them, so it can write the first email instead of leaving them at a blank page.
838
+
839
+ **Why it's useful:** finding companies was never the hard part. Writing the
840
+ twentieth opener of the day is where prospecting actually dies. This turns a
841
+ row in a table into something they could send in a minute.
842
+
843
+ Say plainly that this only **drafts** \u2014 nothing is sent, and they see it first.
844
+
845
+ **Then fire the widget** \u2014 question \`Want me to draft the first email to your top lead?\`, first option labelled \`Draft the first email\`, description \`Write a first email to the best company in today's batch. Nothing is sent.\` Second option: \`I'm done for now\` / \`Stop the walkthrough here.\` **Wait for the click.**
846
+
847
+ On click: call \`leadbay_prepare_outreach\` with \`leadId\` = the top lead's id,
848
+ **and nothing else**.
849
+
850
+ **This gate spends NOTHING. Never pass \`enrich: true\`** \u2014 that launches a paid
851
+ contact reveal off the back of a *draft* click. They agreed to see an email
852
+ written, not to spend. GATE 4 is where the reveal gets asked for, on its own
853
+ terms.
854
+
855
+ \`recommended_contact\` comes back in its post-enrichment shape with \`email\` and
856
+ \`phone\` still **null**. That is expected, not a failure \u2014 and it's exactly the
857
+ hook for the next gate: an email written, and nobody to send it to yet. Don't
858
+ apologise for it, and don't reach for another tool to fill it in.
859
+
860
+ **Render the draft through \`message_compose_v1\`** \u2014 \`kind: "email"\`, a
861
+ \`summary_title\` naming the company, and 2\u20133 variants whose labels name the
862
+ **strategy** ("Lead with the growth signal", "Ask about their current setup"),
863
+ never the tone. Do NOT also paste the body into chat prose; the composer *is*
864
+ the answer. If the host exposes no composer, fall back to the canonical
865
+ prepare-outreach layout: one context line, then subject + body as a quoted
866
+ block.
867
+
868
+ **Address it to the job TITLE** \u2014 "the Head of Operations at <Company>". You do
869
+ not have a name yet, and inventing one is fabrication.
870
+
871
+ Add one line on *why this company was the pick* \u2014 its score and the fit reason
872
+ from the lead's summary \u2014 so the draft reads as reasoned rather than generated.
873
+
874
+ # GATE 4 \u2014 "Find who to email"
875
+
876
+ **Explain first \u2014 point at the gap the draft just opened.** They have an email
877
+ ready and nobody to send it to: it's addressed to a job title, not a person.
878
+ That's what this step fixes. Leadbay can find *which roles* exist at that
879
+ company, then reveal the actual human and how to reach them.
880
+
881
+ **Why it's useful:** they ask for the operations director by name instead of
882
+ pitching whoever answers the switchboard \u2014 the difference between a
883
+ conversation and a dead end.
884
+
885
+ Say plainly that the first look is **free**, and that revealing the contact
886
+ costs credits and needs their say-so.
887
+
888
+ **First, check \`leadbay_enrich_titles\` is in your tool set.** On a read-only
889
+ deployment it is not registered, and a gate whose tool cannot run is a dead
890
+ end. If it's missing: don't fire this widget, say plainly that revealing
891
+ contacts isn't enabled on this connection, note the draft is still theirs, and
892
+ go straight to the closing. Ending one step early beats offering a button that
893
+ does nothing.
894
+
895
+ **Then fire the widget** \u2014 question \`Want to find out who to send that email to?\`, first option labelled \`Find who to email\`, description \`See the roles at that company. Free \u2014 no contact details revealed yet.\` Second option: \`I'm done for now\` / \`Stop the walkthrough here.\` **Wait for the click.**
896
+
897
+ This gate runs in **TWO BEATS**. Do not collapse them.
898
+
899
+ ## BEAT 1 \u2014 the free look (spends nothing)
900
+
901
+ On click: call \`leadbay_enrich_titles\` with \`leadIds\` = **the one lead you
902
+ drafted for at GATE 3** and \`lensId\` = the pinned lens id.
903
+
904
+ **This call must spend NOTHING.** Omit \`titles\` entirely: that returns
905
+ \`mode:"discover"\`, the free preview of which job titles exist at that company.
906
+ Do NOT pass \`titles\`, \`confirm=true\`, \`email=true\` or \`phone=true\` on this call
907
+ \u2014 any one of them launches the paid reveal before the user has chosen anything.
908
+
909
+ Present the discovered titles and say plainly: "nothing spent yet."
910
+
911
+ ## BEAT 2 \u2014 reveal the person the draft is for (spends credits)
912
+
913
+ Name the title the GATE 3 draft is addressed to, and tell them the cost
914
+ **before** they decide: one credit per contact revealed \u2014 here that's **one
915
+ contact, one credit**. Then ask them to confirm.
916
+
917
+ **Wait for an explicit confirmation.** Silence is not consent, and neither is
918
+ "they clicked the gate earlier" \u2014 the gate click bought the free look, not the
919
+ reveal.
920
+
921
+ Once confirmed, call \`leadbay_enrich_titles\` AGAIN with
922
+ \`leadIds: [<the drafted lead's id>]\` \u2014 **the array, always, even for one lead**
923
+ \u2014 plus the chosen \`titles\`, \`confirm: true\` and \`email: true\`. That's the real,
924
+ paid reveal.
925
+
926
+ \`leadIds\` is the only key this tool reads for scope. A singular \`leadId\` is not
927
+ a parameter: it is silently ignored, and the call then falls back to the
928
+ account's **default wishlist selection** while \`confirm\`/\`email\` are set \u2014 so
929
+ it would reveal and charge for the whole batch instead of the one lead the user
930
+ agreed to.
931
+
932
+ It returns a \`bulk_id\` and runs async \u2014 poll \`leadbay_bulk_enrich_status\`
933
+ with that id (\`include_contacts=true\`) until \`all_done\`, or until the resolved
934
+ count plateaus across a few spaced polls. Then report the contact that actually
935
+ resolved: name, title, and the email/phone that came back. Contacts sometimes
936
+ don't resolve; say so honestly rather than implying success.
937
+
938
+ **Then close the loop** \u2014 one line: one credit per contact revealed, so this
939
+ cost one. And say the thing that makes it land: the draft from GATE 3 now has a
940
+ real person and a real address to go to. This is the moment GATE 1's quota
941
+ numbers stop being abstract, because they just watched them move and got
942
+ something for it. Don't turn it into a pricing pitch.
943
+
944
+ If they decline the reveal, that's fine \u2014 keep the draft and the title, and
945
+ let it go without pushing \u2014 the tour is done either way.
946
+
947
+ # HOW THE TOUR ENDS \u2014 THREE ENDINGS, PICK THE RIGHT ONE
948
+
949
+ This is the ONLY place that says what to do when the walkthrough stops. There
950
+ is no other closing section: work out which of these three happened, then do
951
+ that one in full, in the order written.
952
+
953
+ **The buttons disappear when the walkthrough ends.** If it stops without
954
+ telling the user what to *type*, they learned to click through a tutorial and
955
+ nothing about using Leadbay tomorrow. That is what the cheat-sheet is for.
956
+
957
+ ## ENDING A \u2014 they finished all four gates
958
+
959
+ 1. Render the \`keep_going\` cheat-sheet (below).
960
+ 2. Then the setup-guide link (below).
961
+
962
+ ## ENDING B \u2014 they picked \`I'm done for now\`
963
+
964
+ **All three beats, in this order. The offer is the LAST thing you say.**
965
+
966
+ 1. One short line acknowledging the stop \u2014 "No problem, we'll leave it there."
967
+ 2. The \`keep_going\` cheat-sheet, then the setup-guide link (below).
968
+ 3. **The 1:1 offer \u2014 REQUIRED, and it goes last.** Ending B without it is
969
+ incomplete: they stopped right before the setup work a call actually helps
970
+ with, which makes this the one moment the offer is welcome rather than
971
+ pushy. Say, in your own words, one sentence and the link:
972
+
973
+ > If you want a hand tuning this to your own market, Zoe on our team runs 1:1
974
+ > sessions: <https://calendly.com/zoe-leadbay/demo-leadbay>
975
+
976
+ That length is the rule, not a suggestion \u2014 **one sentence**. Listing
977
+ everything Zoe could help with turns an offer into promotional copy, which
978
+ is exactly what a user who just said "I'm done" doesn't want.
979
+
980
+ Keep it to **one sentence and the link**. Never re-open the walkthrough,
981
+ never re-fire the gate they just declined, and never argue for finishing the
982
+ tour.
983
+
984
+ **On this path the offer is the last PROSE you write.** The STOP block below
985
+ still closes the message \u2014 it is a machine marker, not something the user
986
+ reads as content, so it does not displace the offer. What must never happen
987
+ is the offer being dropped or pushed above the cheat-sheet to make room.
988
+
989
+ ## ENDING C \u2014 they typed something off-script
990
+
991
+ Serve what they actually asked for. **No cheat-sheet, no setup link, no 1:1
992
+ offer** \u2014 they're already off doing what they wanted, and any of it on top of
993
+ their real question is exactly the interruption they were avoiding.
994
+
995
+ ## The cheat-sheet (endings A and B)
996
+
997
+ Render the manifest's \`keep_going\` rows as a compact two-column markdown table,
998
+ titled something like **"Next time, just ask"**. Keep the phrases VERBATIM \u2014
999
+ each one is taken from that tool's own trigger list, so it's a phrase that
1000
+ genuinely routes. Do not invent extra rows, and do not reword the phrases into
1001
+ something that sounds nicer but doesn't match.
1002
+
1003
+ | What you want | Just say |
1004
+ |---|---|
1005
+ | Today's fresh leads | "Show me today's leads" |
1006
+ | Who to follow up with | "What should I follow up on" |
1007
+ | The story on one company | "Research <Company>" |
1008
+ | An email to a contact | "Draft outreach for <Contact>" |
1009
+ | Change who you target | "Narrow the audience to <sector>" |
1010
+ | Switch target audience | "Show me my lenses" |
1011
+
1012
+ Add one closing line in your own words: they don't need to remember exact
1013
+ wording \u2014 plain language works, and this is just a starting point.
1014
+
1015
+ ## The setup guide (endings A and B)
1016
+
1017
+ One plain link, for the things the four gates didn't cover \u2014 installing Leadbay
1018
+ on another machine, adding a teammate, signing back in later:
1019
+ <https://docs.leadbay.app/doc/leadbay-mcp/quickstart>
1020
+
1021
+ **Once, here, and nowhere else.** Never drop that link between gates: a link
1022
+ mid-tour is an invitation to leave the thing they're in the middle of doing.
1023
+
1024
+ # STOP
1025
+
1026
+ IRON LAW \u2014 the walkthrough **drafts** an email at GATE 3 but never **sends**
1027
+ one. The draft stays in the chat for the user to read and judge; nothing
1028
+ leaves. Never send it, never offer to send it on their behalf, and never call
1029
+ \`leadbay_report_outreach\` \u2014 logging an outreach that never happened poisons the
1030
+ human team's pipeline.
1031
+
1032
+ Render this acknowledgment VERBATIM as the last line of your message:
1033
+
1034
+ \`\`\`
1035
+ STOP \u2014 awaiting user decision. I will not take any further action until you tell me what to do next.
1036
+ \`\`\`
1037
+
438
1038
  Do not propose a next action. Do not call any more tools. Hand control back to the user.
439
1039
  `;
440
1040
  var leadbay_import_file = `
@@ -643,7 +1243,29 @@ Map my answers to the \`leadbay_tour_plan\` call:
643
1243
 
644
1244
  # PHASE 2 \u2014 BUILD THE ITINERARY
645
1245
 
646
- 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\`.
1246
+ **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
1247
+
1248
+ **On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
1249
+
1250
+ \`axis: "include"\`:
1251
+
1252
+ - \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
1253
+ - \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
1254
+ - \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
1255
+ - \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
1256
+
1257
+ \`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
1258
+
1259
+ On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
1260
+
1261
+ **Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
1262
+
1263
+ Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
1264
+
1265
+
1266
+ **Gate before calling.** If \`{{arg:city}}\` is a country name or a supra-national scope rather than a city, do NOT call \`leadbay_tour_plan\` with it \u2014 a tour of an entire country is not an itinerary, and the value would resolve to a same-named village. Tell me the workspace already covers one country and ask which city or region I'm actually visiting. Otherwise:
1267
+
1268
+ 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\`. If it is \`status: "country_level_location"\`, do NOT retry with a spelling variant and do NOT re-call without \`city\` \u2014 a tour with no city is arbitrary nationwide leads, not an itinerary. Ask me which city or region I am visiting.
647
1269
 
648
1270
  Split the returned \`monitor_leads\` into two buckets client-side using their engagement-history fields:
649
1271
 
@@ -1010,8 +1632,76 @@ Recommend the single most-promising lead from this batch and offer to research i
1010
1632
  var leadbay_refine_audience = `
1011
1633
  Refine the Leadbay audience prompt to: {{arg:instruction}}
1012
1634
 
1013
- # PHASE 1 \u2014 REFINE
1014
- Call \`leadbay_refine_prompt\` with \`prompt=<the instruction above>\`.
1635
+ # PHASE 0 \u2014 GATE: RESOLVE THE REGION, STRIP THE COUNTRY, THEN CLASSIFY (may end the run)
1636
+ A refine prompt shapes the KIND of company, never WHERE it is. Before any tool call:
1637
+
1638
+ **Step 1 \u2014 if a COUNTRY is named at all, find out which country this workspace serves,
1639
+ and do it FIRST.** Every later step turns on whether the country I named is this
1640
+ workspace's own, and you cannot tell that from my message: "French hospitals across
1641
+ France" is a redundant clause on an FR backend and an unsupported ask on a US one, and
1642
+ the language I write in says nothing about it. Do NOT guess from the country I named,
1643
+ from my language, or from the fact that the request sounds plausible \u2014 strip first and
1644
+ you will have already decided, silently and possibly wrongly, that the country was
1645
+ redundant. Every Leadbay tool result carries the fact at \`_meta.region\`
1646
+ (\`us\` | \`fr\` | \`custom\`); if no call this session has returned one, call
1647
+ \`leadbay_account_status\` \u2014 read-only, writes nothing \u2014 and read \`_meta.region\` from it.
1648
+ \`custom\` means the backend's country is unknown: claim nothing about which country it
1649
+ holds. Only a place BELOW country level ("in Paris", "Texas") skips this step.
1650
+
1651
+ **Step 2 \u2014 now strip, and do not stop.** With the region known, if my instruction names
1652
+ this workspace's own country or a whole-country scope ("nationwide", "the whole US",
1653
+ "partout en France"), remove that phrase and KEEP THE REST. It is redundant, never a
1654
+ filter \u2014 but it is almost never the whole instruction. "Hospitals running their own IT
1655
+ nationwide" is a refinement about hospitals; "hospitals in Paris, France" is Paris plus
1656
+ hospitals. Losing the rest because a country rode along is the worse error of the two.
1657
+ A country that is NOT this workspace's own is not stripped \u2014 it is the whole answer, and
1658
+ Step 3 handles it.
1659
+
1660
+ **Step 3 \u2014 classify what REMAINS**, and act on every part of it:
1661
+
1662
+ - **Nothing remains** (the country was the entire instruction) \u2192 **STOP HERE. Call
1663
+ NOTHING.** Do not continue to PHASE 1: \`leadbay_refine_prompt\` would overwrite my
1664
+ qualitative audience prompt and kick off an intelligence recompute to express a scope
1665
+ this workspace already has. Tell me there is nothing to set because the workspace
1666
+ already covers exactly that, offer the axes that do narrow an audience (sector, size,
1667
+ or a sub-country region / state / county / city), and end your turn.
1668
+ - **A DIFFERENT country** ("partout en France" on a US workspace) \u2192 **STOP HERE too, but
1669
+ do not say "there is nothing to set" \u2014 that is false.** The ask is UNSUPPORTED, not
1670
+ already-satisfied: this workspace holds only its own country's companies, so there are
1671
+ no leads there to scope to. Say so plainly, do not offer an unfiltered view as if it
1672
+ answered the request, and end your turn. If a qualitative part rode along with it, say
1673
+ it cannot be applied to a country that is not here either.
1674
+ - **A supra-national scope** ("EU-wide", "EMEA") \u2192 stop as well: name what the workspace
1675
+ covers and ask whether I want that instead, rather than assuming it.
1676
+ - **A sub-country place** ("prospects in Texas", "restrict to Indre-et-Loire") \u2192 a place
1677
+ is not a qualitative refinement: route it to \`leadbay_adjust_audience({locations: [...]})\`
1678
+ and say why. If a qualitative part ALSO remains, continue to PHASE 1 with that part \u2014
1679
+ do not drop half the request.
1680
+ - **A qualitative refinement** \u2192 continue to PHASE 1, passing the STRIPPED text and never
1681
+ the raw instruction.
1682
+
1683
+ **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
1684
+
1685
+ **On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
1686
+
1687
+ \`axis: "include"\`:
1688
+
1689
+ - \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
1690
+ - \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
1691
+ - \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
1692
+ - \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
1693
+
1694
+ \`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
1695
+
1696
+ On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
1697
+
1698
+ **Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
1699
+
1700
+ Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
1701
+
1702
+
1703
+ # PHASE 1 \u2014 REFINE (only when PHASE 0 classified the instruction as qualitative)
1704
+ Call \`leadbay_refine_prompt\` with \`prompt=<the STRIPPED instruction from PHASE 0, Step 1>\` \u2014 the text with any country phrase removed, never the raw instruction.
1015
1705
 
1016
1706
  # PHASE 2 \u2014 CLARIFICATION ROUND-TRIP (if needed)
1017
1707
 
@@ -1148,7 +1838,59 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
1148
1838
 
1149
1839
  # PHASE 1 \u2014 INTERPRET INTENT INTO A LENS
1150
1840
 
1151
- Call \`leadbay_refine_prompt({user_prompt: "{{arg:audience}}"})\`. This handles the clarification protocol natively \u2014 if the system needs more info (e.g. industry disambiguation, geography precision), it returns \`status: "clarification_needed"\` with options. Surface those to me; on my answer, re-call \`leadbay_refine_prompt\` until the prompt converges.
1841
+ **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
1842
+
1843
+ **On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
1844
+
1845
+ \`axis: "include"\`:
1846
+
1847
+ - \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
1848
+ - \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
1849
+ - \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
1850
+ - \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
1851
+
1852
+ \`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
1853
+
1854
+ On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
1855
+
1856
+ **Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
1857
+
1858
+ Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
1859
+
1860
+
1861
+ **Before calling, find out which country this workspace serves.** You cannot tell from
1862
+ my \`audience\` argument: "plumbers across France" is a redundant clause on an FR backend
1863
+ and an unsupported ask on a US one, and this prompt hands you nothing that says which.
1864
+ Guessing here creates a lens plus per-rep campaigns in the wrong country. Every Leadbay
1865
+ tool result carries it at \`_meta.region\` (\`us\` | \`fr\` | \`custom\`); if no call this
1866
+ session has returned one, call \`leadbay_account_status\` first \u2014 read-only, writes
1867
+ nothing \u2014 and read \`_meta.region\` from it. On \`custom\` the backend's country is unknown,
1868
+ so claim nothing about it: ask me which country this workspace covers before creating
1869
+ anything.
1870
+
1871
+ **Then classify any country in EITHER free-text argument \u2014 \`audience\` AND \`rep_split\`.**
1872
+ Both reach the workspace, by different routes: \`audience\` becomes the lens, \`rep_split\`
1873
+ becomes the campaigns in PHASE 3. "Split France to Alice and Germany to Bob" partitions a
1874
+ single-country cohort along an axis that does not exist here, and PHASE 3 will persist
1875
+ those campaigns without ever looking again. The three cases do NOT get the same
1876
+ treatment:
1877
+
1878
+ - **This workspace's own country** ("plumbers across the US" on a US workspace) \u2192 drop only that clause and keep everything else. Say you dropped it, then continue: the lens covers the whole workspace anyway. **Unless dropping it leaves NOTHING** \u2014 \`audience: "France"\` on an FR workspace is entirely that clause, and what remains is an empty string. Do NOT continue into PHASE 1 with it: \`leadbay_refine_prompt({user_prompt: ""})\` would overwrite my refinement prompt with nothing and \`leadbay_create_lens\` + \`leadbay_promote_lens\` would then persist and ACTIVATE a scopeless lens, to express something this workspace already is. Write nothing at all: tell me the workspace already covers exactly that, and ask for a real sector, size, or sub-country criterion before anything is created. Same for \`rep_split\` \u2014 if the sanitized split is empty, there is no split to make.
1879
+ - **A different country** ("plumbers across France" on a US workspace) \u2192 **STOP. Create nothing.** Do NOT drop the country and build a lens for this workspace instead \u2014 that would hand me a US lens, plus campaigns, presented as the answer to a France request. Say this workspace holds only its own country's companies, so the ask cannot be filled here, and end your turn.
1880
+ - **A supra-national scope** ("plumbers across EMEA") \u2192 also stop: name what the workspace covers and ask whether I want that instead, rather than assuming it.
1881
+
1882
+ Keep any sub-country place (state, *r\xE9gion*, *d\xE9partement*, county, city) exactly as-is \u2014
1883
+ those are real splits and real audience clauses.
1884
+
1885
+ For \`rep_split\` specifically, apply the same verdict to the SPLIT AXIS: the home country
1886
+ is not a split (every lead is in it, so one rep would get everything and the others
1887
+ nothing) \u2014 say so and ask me to split by region / sector / size instead. A different
1888
+ country or a supra-national scope is not a split either, and there is no cohort to give
1889
+ that rep: stop rather than silently handing them an empty campaign or, worse, a slice of
1890
+ the home country's leads labelled with another country's name. Carry only the sanitized
1891
+ split into PHASE 3.
1892
+
1893
+ Call \`leadbay_refine_prompt({user_prompt: "<my audience with the home-country clause removed>"})\` \u2014 pass the SANITIZED text, not the raw argument, or the country label reaches the lens anyway and fences it to a same-named village. This handles the clarification protocol natively \u2014 if the system needs more info (e.g. industry disambiguation, geography precision), it returns \`status: "clarification_needed"\` with options. Surface those to me; on my answer, re-call \`leadbay_refine_prompt\` until the prompt converges.
1152
1894
 
1153
1895
  When the prompt has converged, call \`leadbay_create_lens({user_prompt: <refined>, name: "<short descriptive name>"})\` to create a draft lens, then \`leadbay_promote_lens({lensId})\` to make it the active lens.
1154
1896
 
@@ -1162,7 +1904,7 @@ Then ask me ONCE: "Which of these should we drop?" If I name leads to drop, excl
1162
1904
 
1163
1905
  # PHASE 3 \u2014 DECIDE THE CAMPAIGN SHAPE
1164
1906
 
1165
- If I provided a \`rep_split\` ("one campaign per rep: John gets Tulsa, Sarah gets OKC"), partition the validated leads accordingly. If I didn't, ask ONCE: "Create one campaign for the whole batch, or split per rep / region / sector?" \u2014 surface 2-4 options via your host's choice widget (\`ask_user_input_v0\` or \`AskUserQuestion\`) when available, else as a bulleted list.
1907
+ If I provided a \`rep_split\` ("one campaign per rep: John gets Tulsa, Sarah gets OKC"), partition the validated leads by the SANITIZED split from PHASE 1 \u2014 never by the raw argument, and never along a country axis it classified as unusable. If I didn't, ask ONCE: "Create one campaign for the whole batch, or split per rep / region / sector?" \u2014 surface 2-4 options via your host's choice widget (\`ask_user_input_v0\` or \`AskUserQuestion\`) when available, else as a bulleted list.
1166
1908
 
1167
1909
  For each campaign-shape decision, derive a name. Templates:
1168
1910
  - Whole batch: \`"<lens-name> \u2013 <YYYY-MM-DD>"\`
@@ -1296,7 +2038,7 @@ Call \`leadbay_account_status\` for my quota and active lens.
1296
2038
 
1297
2039
  Say that scope in one line up front, so nobody reads the ranking as a money sort. If I ask for a cash-ranked plan, tell me plainly that it needs my invoicing extract and that the MCP has no path to it today \u2014 then deliver this plan anyway rather than stopping.
1298
2040
 
1299
- **DELIVER FIRST, ASK ALONGSIDE \u2014 never gate the plan on a missing input.** Only ONE thing can stop you before you have shipped a ranked list of real accounts: not knowing **whose** plan this is (a company-identity mismatch you genuinely cannot resolve). Everything else is a question you carry *next to* the delivered plan, not a reason to withhold it:
2041
+ **DELIVER FIRST, ASK ALONGSIDE \u2014 never gate the plan on a missing input.** Only TWO things can stop you before you have shipped a ranked list of real accounts: not knowing **whose** plan this is (a company-identity mismatch you genuinely cannot resolve), and a \`territory\` naming a country that is NOT this workspace's own \u2014 or a supra-national scope (see the country branch below, which overrides this rule for that one case). The second is an exception for the same reason as the first: both would ship a plan about the wrong companies. Delivering a whole-workspace plan under a "France" heading is not a partial answer, it is a wrong one. Everything else is a question you carry *next to* the delivered plan, not a reason to withhold it:
1300
2042
 
1301
2043
  - **No benchmark?** Costs nothing here \u2014 the money column is OMITTED regardless. Pull, qualify, rank by the Leadbay signal, deliver, and mention what a cash-ranked version would need.
1302
2044
  - **No Tier-1 threshold?** Not a blocker. Deliver, and ask alongside.
@@ -1313,7 +2055,45 @@ If I gave a \`territory\`, scope discovery to it now, and **make sure the scopin
1313
2055
  \u26A0 **Location criteria MERGE \u2014 they do not replace.** \`adjust_audience\` unions the new \`location_ids\` into any existing include-location criterion (and \`pull_followups\` merges its \`city\` shortcut the same way). So asking for "R\xE9gion Ouest" on a lens already scoped to Paris yields **Paris OR R\xE9gion Ouest** while your header claims R\xE9gion Ouest. Before adding a territory, check the current filter: if it already carries locations you were not asked to keep, clear or replace them (or build a fresh territory-only lens for this one-off plan) rather than stacking a union.
1314
2056
  - **If a new lens is genuinely warranted: \`leadbay_new_lens\` is a two-step call.** It returns \`status:"preview"\` and creates NOTHING unless you re-call the same args with \`confirm:true\`. So: preview \u2192 confirm \u2192 take \`lens.id\` from the \`created\` response \u2192 pass that id as \`lensId\` on every subsequent pull. Never continue on the previous active lens after previewing a new one; that delivers the old audience under a new heading.
1315
2057
 
1316
- A place name goes to \`locations\`, never to \`sectors\` or a refine prompt.
2058
+ If the \`territory\` I named is a country, which one decides what you do:
2059
+
2060
+ - **This workspace's own country** \u2192 make no scope CHANGE, but do not claim national
2061
+ coverage until you have READ the lens. \`leadbay_pull_leads\` keeps applying my ACTIVE
2062
+ lens, and this prompt already warns that lens may be scoped to a city, a sector or a
2063
+ rep patch. On an FR tenant whose active lens is Paris-only, a \`territory: "France"\`
2064
+ plan is a Paris plan \u2014 and "covers all of France" printed above it is exactly the
2065
+ confidently wrong deliverable this whole gate exists to stop, this time in my own
2066
+ header rather than in a filter.
2067
+ **Read the \`lens://<id>/definition\` resource** \u2014 that is the only place a lens's
2068
+ \`location_ids\` are visible. \`leadbay_pull_leads\` returns only \`lens: {id}\`, not the
2069
+ filter, and \`active_filters\` describes the separately-persisted MONITOR filter, not
2070
+ the Discover lens; neither can settle this and neither is a substitute (same rule as
2071
+ the Monitor-mirroring section below). Then say ONE of: the lens really is
2072
+ workspace-wide, or it is scoped to \`<the places its filter names>\` \u2014 offering to clear
2073
+ that scope if national is what I meant. If you genuinely cannot read the definition,
2074
+ say the scope is unverified rather than calling it national. Then offer sector / size
2075
+ / sub-country region as the axes that would actually narrow it.
2076
+ - **A different country, or a supra-national scope** \u2192 do NOT simply drop the scope and build the plan anyway. An unfiltered plan is this workspace's own accounts, which is not an answer to a request about somewhere else \u2014 delivering it under my heading would be a confidently wrong plan. Say the ask cannot be filled from this workspace and stop. **This is the one case that overrides DELIVER FIRST above**: shipping the plan anyway is the failure, not the fix.
2077
+
2078
+ **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
2079
+
2080
+ **On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
2081
+
2082
+ \`axis: "include"\`:
2083
+
2084
+ - \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
2085
+ - \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
2086
+ - \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
2087
+ - \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
2088
+
2089
+ \`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
2090
+
2091
+ On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
2092
+
2093
+ **Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
2094
+
2095
+ Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
2096
+
1317
2097
 
1318
2098
  # PHASE 1 \u2014 THE FIVE QUALIFICATION QUESTIONS
1319
2099
 
@@ -1745,9 +2525,16 @@ var PROMPT_META = {
1745
2525
  leadbay_daily_check_in: { "name": "leadbay_daily_check_in", "short_description": 'Morning DISCOVERY workflow \u2014 new leads from the lens wishlist. Trigger\non "show me leads", "what\'s new today", "let\'s prospect", "run my check-in",\n"my morning check-in", "I do this every day", "every morning". Recurrence\nlanguage always means this prompt. Do NOT trigger on follow-up phrasings\n("follow up", "before my trip") \u2014 those go to `leadbay_followup_check_in`.\n', "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_research_lead_by_id", "leadbay_bulk_qualify_leads", "leadbay_enrich_contacts"], "failure_modes": ["Calls leadbay_report_outreach without explicit user authorization", "Surfaces fewer than 10 leads when more are available, or fails to top up via leadbay_qualify_top_n when the batch is short", `Replaces the canonical pull_leads table layout with prose per row (the per-tool RENDERING block is the structural contract; "Today's nudges" goes above it, not in place of it)`, "Skips the nudge paragraph entirely \u2014 the table alone is fine but adding the nudge is the value-add", `Skips deep research on promising leads (Phase 4) \u2014 the agent must call leadbay_research_lead_by_id on each when the user's intent is to research specific leads; Phase 4 is intentionally skipped for batch-view requests ("show me today's leads", "run my morning check-in") per the Phase 4 skip gate`, "Triggers contact enrichment without asking the user first (it consumes quota)", "Skips the STOP byproduct and proposes next actions on its own", 'Fires 10 parallel leadbay_research_lead_by_id calls and treats "stream closed" errors as terminal \u2014 must serialize and retry singletons', "Re-pulls leadbay_pull_leads without passing the captured lensId, allowing a backend lens shift to discard the Phase 2 batch", 'Treats a "Request timed out" from leadbay_bulk_qualify_leads as terminal instead of retrying with wait_for_completion:false + qualify_status polling', 'Triggers on a follow-up query (e.g., "leads I should follow up with") that should have routed to `leadbay_followup_check_in` \u2014 the two entry points are different data sources (Discover wishlist vs Monitor view) per \xA71.6'] },
1746
2526
  leadbay_extend_my_lens: { "name": "leadbay_extend_my_lens", "short_description": "Add more leads to the current lens on demand \u2014 for users whose appetite\nexceeds the standard daily fill. The agent picks seeds silently from\nwhat's already on the lens, fires the extra refill, and surfaces the\nqueue confirmation. The user never reviews the seed list.\n", "arguments": [{ "name": "extra_count", "description": "How many extra leads to add. Optional. Omit to use the backend default.", "required": false }], "expected_calls": ["leadbay_account_status", "leadbay_seed_candidates", "leadbay_extend_lens", "leadbay_pull_leads"], "failure_modes": ["Surfaces the seed candidate list to the user instead of picking silently \u2014 the user asked for MORE LEADS, not a candidate review meeting", "Skips the seeded path and calls `leadbay_extend_lens` with no `seed_lead_ids`, losing the bias signal the recommender needs", "On 429, silently retries instead of surfacing the three options (smaller / wait / upgrade) via your host's choice widget (`ask_user_input_v0` or `AskUserQuestion`)", "Forgets to pre-check `LENS_EXTRA_REFILL` quota in `leadbay_account_status` and burns a wasted API call", "Skips the post-queue pull-leads suggestion, so the user doesn't see what just got added"] },
1747
2527
  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)'] },
2528
+ leadbay_getting_started: { "name": "leadbay_getting_started", "short_description": `Guided first-run walkthrough \u2014 four clicks that actually use Leadbay: check
2529
+ the account, pull today's leads, draft a first email to the top one, then
2530
+ reveal who to send it to. Use when the user is new or asks to be SHOWN how
2531
+ Leadbay works ("walk me through Leadbay", "I'm new", "how do I use this",
2532
+ "give me a tour"). Don't use it for orientation prose with no clicking \u2014
2533
+ that's leadbay_prospecting_overview.
2534
+ `, "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_prepare_outreach", "leadbay_enrich_titles", "leadbay_bulk_enrich_status"], "failure_modes": ['Presents a gate as prose ("let me know if you want me to pull your leads") instead of CALLING the host choice widget \u2014 the click IS the lesson, and prose turns the walkthrough into a lecture', "Runs a step's tool WITHOUT firing that step's widget first and waiting for the click \u2014 the walkthrough becomes an automated demo the user only watches, which is the exact opposite of learning by doing", "Fires the widget without the EXPLAIN beat, so the user gets an unexplained button and learns nothing about what a lens or an enrichment actually is", 'Answers gate 1 with a bare "you\'re connected as X at Y" when the quota IS readable \u2014 the user clicked a button labelled `check my account status`, so the quota windows (Daily/Weekly/Monthly gauges, % used, $ spent, resets) ARE the answer, not an optional extra', 'Renders quota as raw "credits" instead of the web app\'s percentage + dollar-spend gauges, or dumps raw `resource_type` strings the user has never seen', "Opens with a wall of text \u2014 previewing all four steps, explaining lenses up front, or writing several paragraphs before the first widget. The opening is TWO lines then the button; a first-run user wants to see it work, not read a syllabus", `Ends the first message without firing gate 1's widget, leaving the user to reply "ok" before anything happens`, "Rewrites the gate's own `next_steps` payload (its `question`, `label` or `description`) instead of mapping it into the widget verbatim, or merges two gates into a single multi-option widget", 'Fires a THIRD option, or turns the exit into an alternative route ("show me my lenses instead") \u2014 each gate carries exactly one forward action plus the `I\'m done for now` exit, never a menu of paths', `Fires a single-option widget \u2014 the host requires 2\u20134 options, so a lone option is rejected or silently degrades to prose ("say the word and I'll check it"), which is the exact defect this rule exists to prevent`, 'Launches the PAID reveal at gate 4 BEFORE the user has picked leads and confirmed \u2014 beat 1 must be the free `mode:"discover"` preview (no `titles`, no `confirm`, no `email`, no `phone`); the gate click bought the free look, not the reveal, and silence is never consent', "Stops at the free preview after the user DID pick leads and confirm \u2014 they asked for real contact details, so the second call must actually run with `confirm:true` and the chosen titles", "Reports the enrichment without polling `leadbay_bulk_enrich_status` to completion, so it claims contacts it never actually saw resolve", `Reveals contacts and never says what it cost \u2014 the user just spent credits and deserves the one-line "N contacts = N credits", which is also what makes gate 1's quota numbers concrete`, 'Reports "no leads" on an empty batch while `computing_wishlist` / `computing_scores` is true \u2014 the lens is still building; render the tool\'s own two-option warm-up widget verbatim and pause', "Rewords, reorders or prose-ifies the `next_steps` payload from `leadbay_pull_leads` instead of mapping `options[]` into the widget verbatim", "Runs all four steps in one turn without waiting for the user's click between gates \u2014 the walkthrough is a sequence of gates, not a script to recite", "Skips `leadbay_pull_leads` and jumps straight to enrichment, leaving gate 4 with no `leadIds` to scope", "Passes a singular `leadId` to `leadbay_enrich_titles` on the confirmed reveal \u2014 that key does not exist on this tool, so it is dropped and the paid call falls back to the whole default wishlist selection, charging for far more than the one lead the user agreed to. it is always the `leadIds` ARRAY, even for a single lead", "Drops the pinned `lens.id` between gates, so gate 4 enriches against a different lens than the one the user just saw", "Ends the completed walkthrough without the `keep_going` cheat-sheet \u2014 the buttons disappear with the tour, so a user who was never told what to TYPE learned to click a tutorial and nothing about using Leadbay tomorrow", "Invents phrases for the cheat-sheet, or rewords them into something that sounds nicer but doesn't match the tool's real triggers \u2014 teaching a phrase that doesn't route is worse than teaching none", "SENDS the gate 3 draft, or offers to send it \u2014 the walkthrough drafts and stops there; the email is the user's to judge, and nothing leaves the chat", "Passes `enrich:true` to `leadbay_prepare_outreach` at gate 3 \u2014 that launches a PAID contact reveal off the back of a DRAFT click, spending credits the user never agreed to", "Invents a contact NAME for the gate 3 draft \u2014 `recommended_contact` still has null email/name at that point, so the draft is addressed to the job TITLE; a fabricated name is the one thing that makes the whole draft untrustworthy", "Treats the null email at gate 3 as a failure \u2014 apologising for it, retrying, or calling another tool to fill it in. It is the setup for gate 4 \u2014 an email written, nobody to send it to yet", "Pastes the drafted email into chat prose alongside `message_compose_v1` instead of letting the composer BE the answer", "Enriches leads other than the one it drafted for at gate 3 \u2014 gate 4 reveals the person that email is going to, so it is scoped to that ONE lead, one contact, one credit", "Renders the cheat-sheet on the exit and stops there, dropping the 1:1 offer \u2014 the observed failure is that the agent feels finished once the table is on screen, so the user who just stepped out never hears about the help that would bring them back. ENDING B is not complete without the offer, and the offer goes LAST", "Treats the exit click as ENDING C (typed off-script) and closes in silence, or treats a typed request as ENDING B and buries their real answer under a cheat-sheet and a booking link", "Turns the exit offer into a pitch \u2014 several sentences, a re-opened gate, or an argument for finishing the tour. They said they were done; it is one line and a link", "Fires the 1:1 offer mid-tour, or at a user who left by TYPING a different request \u2014 a booking link on top of their real question is an interruption, not an offer", "Runs the four gates at a user whose actual problem is SETUP \u2014 the connector isn't installed, they can't sign in, or their Leadbay tools aren't appearing. The tour assumes a working connection and cannot fix any of it; the setup guide can", "Pastes the setup-guide link mid-tour, between gates, instead of once at the closing \u2014 a link in the middle of the walkthrough invites the user to leave the thing they're doing"] },
1748
2535
  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"] },
1749
2536
  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"] },
1750
- 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"] },
2537
+ 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. A country is not a city: this workspace already covers exactly one country, and a country name here silently fences the tour to a same-named village. Do NOT omit the argument to recover \u2014 a city-less tour returns arbitrary leads from across the whole workspace, which is not an itinerary. Ask which city or region the visit is to.", "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"] },
1751
2538
  leadbay_prospecting_overview: { "name": "leadbay_prospecting_overview", "short_description": `Orientation for working with Leadbay from any host \u2014 discovery vs.
1752
2539
  follow-up, the outreach loop, outcome recording, imports, pushback /
1753
2540
  snooze, and the connected-outreach-tool registry. Trigger when the
@@ -1758,8 +2545,8 @@ should I follow up on" to "I'll send via lemlist".
1758
2545
  leadbay_qualify_top_n: { "name": "leadbay_qualify_top_n", "short_description": "Bulk-qualify the top N un-qualified leads in the active lens. Uses\nleadbay_bulk_qualify_leads with a sensible default budget.\n", "arguments": [{ "name": "count", "description": "How many leads to qualify (default 10, max 25). Higher counts may take 5+ minutes.", "required": false }], "expected_calls": ["leadbay_bulk_qualify_leads", "leadbay_qualify_status", "leadbay_pull_leads", "leadbay_research_lead_by_id"], "failure_modes": ["Picks a count larger than the user asked for (or larger than the max 25)", "Glosses over still-running leads in the summary instead of naming them", "Recommends a lead from the existing qualified pool instead of one from this batch's actual results", 'Replaces the canonical pull_leads table with prose when rendering the newly-qualified batch (the per-tool RENDERING block is the structural contract; "standouts" commentary sits above it)', "Expands the qualify-status sentence into a card or table instead of the one-line status-inline render"] },
1759
2546
  leadbay_refine_audience: { "name": "leadbay_refine_audience", "short_description": "Refine the kind of leads Leadbay surfaces beyond firmographics, with a\nfree-text instruction. Handles the clarification round-trip if the new\nprompt is ambiguous.\n", "arguments": [{ "name": "instruction", "description": "The refinement (e.g. 'focus on hospitals running their own IT'). Set to plain English.", "required": true }], "expected_calls": ["leadbay_refine_prompt", "leadbay_account_status"], "failure_modes": ["Calls leadbay_answer_clarification on the user's behalf instead of surfacing the clarification verbatim", "Glosses over the clarification options instead of presenting them as offered", "Promises immediate effect when status='applied' actually triggers an async intelligence recompute"] },
1760
2547
  leadbay_research_a_domain: { "name": "leadbay_research_a_domain", "short_description": "Resolve a company by name or domain across the user's visible Discover,\nMonitor, and Activate corpus, then return everything Leadbay knows about it.\n", "arguments": [{ "name": "domain", "description": "Company name or domain (for example 'Acme Corporation' or 'acme.com'). The legacy argument key remains `domain` for client compatibility.", "required": true }], "expected_calls": ["leadbay_research_lead_by_name_fuzzy"], "failure_modes": ["Fabricates qualification answers not present in any tool response", "Calls leadbay_import_and_qualify before searching the existing visible corpus", "Treats the active lens as the entire search universe when the user did not request a lens scope", "Imports a missing company without the user's explicit permission", "Renders the research result as a freeform narrative instead of the canonical research-company-card layout (the card with header score bar, pill row, signal sections, contacts table is the structural contract; commentary belongs ABOVE or BELOW it)"] },
1761
- leadbay_setup_team_prospecting: { "name": "leadbay_setup_team_prospecting", "short_description": "Manager-led prospecting setup: conversationally turn a natural-language\naudience ask into a Leadbay lens, validate the candidate leads, and\npersist them as one or more named campaigns the rep(s) can work\nthrough. Closes #3630 US3 end-to-end (within the current\ncreator-scoped campaign visibility model).\n", "arguments": [{ "name": "audience", "description": "Natural-language audience description (e.g. 'plumbing companies with 10-50 employees in Seine-Maritime'). The lens-creation step (`leadbay_refine_prompt` \u2192 `leadbay_create_lens`) interprets it.", "required": true }, { "name": "rep_split", "description": "Optional: how to split the validated leads into per-rep campaigns. Free text \u2014 e.g. 'split by city' or 'one campaign per rep: John gets Tulsa, Sarah gets OKC'.", "required": false }], "expected_calls": ["leadbay_refine_prompt", "leadbay_create_lens", "leadbay_promote_lens", "leadbay_pull_leads", "leadbay_research_lead_by_id", "leadbay_create_campaign", "leadbay_add_leads_to_campaign"], "failure_modes": ["Skips the validation step \u2014 creates a campaign of unvetted leads from a freshly-created lens without giving the manager a chance to drop weak fits", "Creates ONE campaign for all reps without asking about the split \u2014 the user explicitly mentioned per-rep distribution and the prompt should honor it", "Pretends the backend supports cross-user assignment \u2014 campaigns are owned by the caller (creator-scoped). Surface this honestly instead of fabricating an assignment model", "Asks ALL clarifying questions inline before tool calls \u2014 instead, run the lens refinement loop with `leadbay_refine_prompt` which handles the clarification protocol natively"] },
1762
- leadbay_top_accounts_to_activate: { "name": "leadbay_top_accounts_to_activate", "short_description": 'Build a ranked account-conquest plan from Leadbay data \u2014 the accounts worth\nactivating, each with a motif, a pitch and a checklist, ranked by the\nstrongest Leadbay signal. Every figure carries its source, and anything\nLeadbay can\'t measure is shown as OMITTED rather than estimated. Uses\n`leadbay_bulk_qualify_leads` and `leadbay_enrich_titles`. Trigger on\n"top 50 accounts to activate", "who should we go after".\n', "arguments": [{ "name": "count", "description": "Optional: how many accounts the plan should hold (default 50).", "required": false }, { "name": "territory", "description": "Optional: restrict the plan to a territory (e.g. 'Indre-et-Loire', 'R\xE9gion Ouest'). Sets geography on the Discover lens.", "required": false }], "expected_calls": ["leadbay_account_status", "leadbay_get_qualification_questions", "leadbay_pull_leads", "leadbay_pull_followups", "leadbay_bulk_qualify_leads", "leadbay_qualify_status", "leadbay_scan_portfolio_signals", "leadbay_enrich_titles", "leadbay_bulk_enrich_status", "leadbay_account_history", "leadbay_artifact_kit", "leadbay_new_lens", "leadbay_adjust_audience"], "failure_modes": ["Invents, estimates or proxies a revenue-realized figure \u2014 the single worst failure. Leadbay does not hold what an account buys, and headcount, sector and lead score are NOT proxies for it.", "Sorts by cash-to-capture, synthesizing a revenue figure per account purely to make that ranking work. Leadbay has no revenue data: rank by the Leadbay signal, say so in the header, and never invent the key.", "Emits \u20AC figures with no provenance class, so modelled numbers read as measured fact in front of a paying client.", "Skips the PROVENANCE LEDGER, or drops un-sourceable fields from it instead of rendering them as OMITTED \u2014 which hides the gap.", "Fabricates registry/TAM counts (France or regional company counts) instead of querying the registry or marking the figure NOT COMPUTED. Leadbay does not proxy SIRENE.", "Invents the five qualification questions from this prompt's own recommendations instead of reading the org's actual questions via leadbay_get_qualification_questions.", "Leaves the deck's live layer dead \u2014 qualification and enrichment handles never wired in, so the pills and contacts stay empty while the deck still looks finished.", "Invents lead ids to make the qualification pills appear populated.", "Fabricates a plausible-sounding signal ('just won a public tender') for an account whose signals were never read. No signal read means an explicit dash.", "Assigns a motif outside the closed set of six, or assigns SAUVETAGE / PLAN DE COMPTE / MONT\xC9E EN GAMME / R\xC9VEIL from a lead score or sector when order history was never available.", "Labels Monitor membership as 'is a client' \u2014 Monitor is a Leadbay view whose membership is decided by lens scoring, not by whether the company ever bought anything.", "Launches paid enrichment on the whole plan without consent. Asking for a plan is not authorization to spend on 50 accounts.", "Re-launches enrichment from inside the built deck when a bulk handle already exists this session \u2014 double-spends the user's quota.", "Forces the interactive deck without offering it first, or ships the deck INSTEAD of a chat answer that stands on its own.", "Refuses the task because revenue data is missing, instead of delivering the conquest plan and naming what a cash-ranked version would need.", "Ends the turn without a ranked list of real accounts \u2014 gating the whole plan on a NON-blocking question (the territory, a missing lens, or a 3-vs-5 qualification-question gap) so the user gets a plan-of-a-plan. Only an unresolvable company-identity mismatch may stop delivery; every other open question rides alongside the delivered plan.", "Stops after the discovery contact preview to wait for enrichment consent, delivering no plan that turn \u2014 the ranked plan ships first; the paid reveal is offered alongside it.", "Renders a contact channel enrichment never returned (e.g. a phone link when only email was approved and revealed) instead of showing the returned channels and marking the rest omitted."] },
2548
+ leadbay_setup_team_prospecting: { "name": "leadbay_setup_team_prospecting", "short_description": "Manager-led prospecting setup: conversationally turn a natural-language\naudience ask into a Leadbay lens, validate the candidate leads, and\npersist them as one or more named campaigns the rep(s) can work\nthrough. Closes #3630 US3 end-to-end (within the current\ncreator-scoped campaign visibility model).\n", "arguments": [{ "name": "audience", "description": "Natural-language audience description (e.g. 'plumbing companies with 10-50 employees in Seine-Maritime'). The lens-creation step (`leadbay_refine_prompt` \u2192 `leadbay_create_lens`) interprets it. A country name is not a scope here \u2014 this workspace already covers exactly one country, so drop it and keep the rest of the description; a DIFFERENT country cannot be targeted at all.", "required": true }, { "name": "rep_split", "description": "Optional: how to split the validated leads into per-rep campaigns. Free text \u2014 e.g. 'split by city' or 'one campaign per rep: John gets Tulsa, Sarah gets OKC'. Splitting by country is not a split \u2014 the workspace is single-country.", "required": false }], "expected_calls": ["leadbay_refine_prompt", "leadbay_create_lens", "leadbay_promote_lens", "leadbay_pull_leads", "leadbay_research_lead_by_id", "leadbay_create_campaign", "leadbay_add_leads_to_campaign"], "failure_modes": ["Skips the validation step \u2014 creates a campaign of unvetted leads from a freshly-created lens without giving the manager a chance to drop weak fits", "Creates ONE campaign for all reps without asking about the split \u2014 the user explicitly mentioned per-rep distribution and the prompt should honor it", "Pretends the backend supports cross-user assignment \u2014 campaigns are owned by the caller (creator-scoped). Surface this honestly instead of fabricating an assignment model", "Asks ALL clarifying questions inline before tool calls \u2014 instead, run the lens refinement loop with `leadbay_refine_prompt` which handles the clarification protocol natively"] },
2549
+ leadbay_top_accounts_to_activate: { "name": "leadbay_top_accounts_to_activate", "short_description": 'Build a ranked account-conquest plan from Leadbay data \u2014 the accounts worth\nactivating, each with a motif, a pitch and a checklist, ranked by the\nstrongest Leadbay signal. Every figure carries its source, and anything\nLeadbay can\'t measure is shown as OMITTED rather than estimated. Uses\n`leadbay_bulk_qualify_leads` and `leadbay_enrich_titles`. Trigger on\n"top 50 accounts to activate", "who should we go after".\n', "arguments": [{ "name": "count", "description": "Optional: how many accounts the plan should hold (default 50).", "required": false }, { "name": "territory", "description": "Optional: restrict the plan to a territory (e.g. 'Indre-et-Loire', 'R\xE9gion Ouest'). Sets geography on the Discover lens. A country is not a territory \u2014 this workspace already covers exactly one country.", "required": false }], "expected_calls": ["leadbay_account_status", "leadbay_get_qualification_questions", "leadbay_pull_leads", "leadbay_pull_followups", "leadbay_bulk_qualify_leads", "leadbay_qualify_status", "leadbay_scan_portfolio_signals", "leadbay_enrich_titles", "leadbay_bulk_enrich_status", "leadbay_account_history", "leadbay_artifact_kit", "leadbay_new_lens", "leadbay_adjust_audience"], "failure_modes": ["Invents, estimates or proxies a revenue-realized figure \u2014 the single worst failure. Leadbay does not hold what an account buys, and headcount, sector and lead score are NOT proxies for it.", "Sorts by cash-to-capture, synthesizing a revenue figure per account purely to make that ranking work. Leadbay has no revenue data: rank by the Leadbay signal, say so in the header, and never invent the key.", "Emits \u20AC figures with no provenance class, so modelled numbers read as measured fact in front of a paying client.", "Skips the PROVENANCE LEDGER, or drops un-sourceable fields from it instead of rendering them as OMITTED \u2014 which hides the gap.", "Fabricates registry/TAM counts (France or regional company counts) instead of querying the registry or marking the figure NOT COMPUTED. Leadbay does not proxy SIRENE.", "Invents the five qualification questions from this prompt's own recommendations instead of reading the org's actual questions via leadbay_get_qualification_questions.", "Leaves the deck's live layer dead \u2014 qualification and enrichment handles never wired in, so the pills and contacts stay empty while the deck still looks finished.", "Invents lead ids to make the qualification pills appear populated.", "Fabricates a plausible-sounding signal ('just won a public tender') for an account whose signals were never read. No signal read means an explicit dash.", "Assigns a motif outside the closed set of six, or assigns SAUVETAGE / PLAN DE COMPTE / MONT\xC9E EN GAMME / R\xC9VEIL from a lead score or sector when order history was never available.", "Labels Monitor membership as 'is a client' \u2014 Monitor is a Leadbay view whose membership is decided by lens scoring, not by whether the company ever bought anything.", "Launches paid enrichment on the whole plan without consent. Asking for a plan is not authorization to spend on 50 accounts.", "Re-launches enrichment from inside the built deck when a bulk handle already exists this session \u2014 double-spends the user's quota.", "Forces the interactive deck without offering it first, or ships the deck INSTEAD of a chat answer that stands on its own.", "Refuses the task because revenue data is missing, instead of delivering the conquest plan and naming what a cash-ranked version would need.", "Ends the turn without a ranked list of real accounts \u2014 gating the whole plan on a NON-blocking question (a MISSING territory, a missing lens, or a 3-vs-5 qualification-question gap) so the user gets a plan-of-a-plan. Only two things may stop delivery: an unresolvable company-identity mismatch, and a territory naming a foreign or supra-national scope. Every other open question rides alongside the delivered plan.", "Stops after the discovery contact preview to wait for enrichment consent, delivering no plan that turn \u2014 the ranked plan ships first; the paid reveal is offered alongside it.", "Renders a contact channel enrichment never returned (e.g. a phone link when only email was approved and revealed) instead of showing the returned channels and marking the rest omitted."] },
1763
2550
  leadbay_work_campaign: { "name": "leadbay_work_campaign", "short_description": "Work a campaign as a real outreach session: pick the campaign,\nassess what the user has (phones / emails / coords), then PROPOSE\nthe right session mode (call sheet, email sheet, enrich titles\nfirst, map). After they pick, render \u2014 and as they dictate\noutcomes per lead, record both note + epilogue via\n`leadbay_report_outreach` in one round trip.\n", "arguments": [{ "name": "campaign", "description": "Campaign name (fuzzy match against your own campaigns) or campaign UUID. Omit to list and pick interactively.", "required": false }, { "name": "mode", "description": "Optional: skip the readiness-assessment proposal and jump directly into 'call_sheet' / 'email_sheet' / 'map' / 'enrich_first'. Omit (recommended) and let the prompt propose based on the data.", "required": false }], "expected_calls": ["leadbay_list_campaigns", "leadbay_campaign_call_sheet", "leadbay_enrich_titles", "leadbay_report_outreach"], "failure_modes": ["Renders the call sheet immediately without proposing the right mode \u2014 if 60% of leads have no contacts, calling is futile; enrich first. Always assess `readiness` first.", "Auto-renders the map widget without asking \u2014 maps are intrusive when the user just wants to scroll a list. Map mode is a proposed option, not a default.", "Proposes map mode after the user has previously said they don't like maps \u2014 check conversation memory before adding 'View on a map' to the options list.", "Calls `leadbay_campaign_progression` instead of `leadbay_campaign_call_sheet` \u2014 progression has counts but no phones / LinkedIn / call-ready data; the user can't actually dial from progression rows.", "Renders contacts WITHOUT making the phone number a `[bare](tel:URL)` link \u2014 on mobile that breaks one-tap calling, which is the whole point of the cheat sheet.", "Records outreach WITHOUT epilogue_status \u2014 leaves the lead's pipeline state unchanged; the rep then sees the same lead surfaced again next session.", "Records outreach WITHOUT verification \u2014 verification.source/ref is REQUIRED. For calls, pass `{source: 'user_confirmed', ref: <user's exact words>}`.", "Loops through ALL leads in a 50-lead campaign before recording any outreach \u2014 the call-then-record loop must be per-lead, not batched."] }
1764
2551
  };
1765
2552
  var PROMPT_CATALOG_HEADER = `This server exposes the following workflow prompts via \`prompts/list\` and \`prompts/get\`. Some MCP clients render them as slash commands; if your client does not, you (the agent) should invoke them directly via \`prompts/get\` when the user's request matches one of the triggers described below.`;
@@ -1768,6 +2555,7 @@ var PROMPT_CATALOG_BULLETS = {
1768
2555
  leadbay_daily_check_in: `- \`leadbay_daily_check_in\`: Morning DISCOVERY workflow \u2014 new leads from the lens wishlist. Trigger on "show me leads", "what's new today", "let's prospect", "run my check-in", "my morning check-in", "I do this every day", "every morning". Recurrence language always means this prompt. Do NOT trigger on follow-up phrasings ("follow up", "before my trip") \u2014 those go to \`leadbay_followup_check_in\`.`,
1769
2556
  leadbay_extend_my_lens: `- \`leadbay_extend_my_lens\` (optional args: extra_count): Add more leads to the current lens on demand \u2014 for users whose appetite exceeds the standard daily fill. The agent picks seeds silently from what's already on the lens, fires the extra refill, and surfaces the queue confirmation. The user never reviews the seed list.`,
1770
2557
  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\`.`,
2558
+ leadbay_getting_started: `- \`leadbay_getting_started\`: Guided first-run walkthrough \u2014 four clicks that actually use Leadbay: check the account, pull today's leads, draft a first email to the top one, then reveal who to send it to. Use when the user is new or asks to be SHOWN how Leadbay works ("walk me through Leadbay", "I'm new", "how do I use this", "give me a tour"). Don't use it for orientation prose with no clicking \u2014 that's leadbay_prospecting_overview.`,
1771
2559
  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.`,
1772
2560
  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.`,
1773
2561
  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.`,
@@ -1791,29 +2579,28 @@ function substitutePlaceholders(body, substitutions) {
1791
2579
  }
1792
2580
  return out;
1793
2581
  }
2582
+ function promptArguments(name) {
2583
+ return PROMPT_META[name].arguments.map(
2584
+ (argument) => ({ ...argument })
2585
+ );
2586
+ }
1794
2587
  var CATALOG = [
1795
2588
  {
1796
2589
  name: "leadbay_daily_check_in",
1797
2590
  description: PROMPT_META.leadbay_daily_check_in.short_description,
1798
- arguments: [],
2591
+ arguments: promptArguments("leadbay_daily_check_in"),
1799
2592
  render: () => [userMessage(leadbay_daily_check_in)]
1800
2593
  },
1801
2594
  {
1802
2595
  name: "leadbay_prospecting_overview",
1803
2596
  description: PROMPT_META.leadbay_prospecting_overview.short_description,
1804
- arguments: [],
2597
+ arguments: promptArguments("leadbay_prospecting_overview"),
1805
2598
  render: () => [userMessage(leadbay_prospecting_overview)]
1806
2599
  },
1807
2600
  {
1808
2601
  name: "leadbay_research_a_domain",
1809
2602
  description: PROMPT_META.leadbay_research_a_domain.short_description,
1810
- arguments: [
1811
- {
1812
- name: "domain",
1813
- description: "Company name or domain (for example 'Acme Corporation' or 'acme.com'). The legacy argument key remains `domain` for client compatibility.",
1814
- required: true
1815
- }
1816
- ],
2603
+ arguments: promptArguments("leadbay_research_a_domain"),
1817
2604
  render: (args) => [
1818
2605
  userMessage(
1819
2606
  substitutePlaceholders(leadbay_research_a_domain, {
@@ -1825,18 +2612,7 @@ var CATALOG = [
1825
2612
  {
1826
2613
  name: "leadbay_import_file",
1827
2614
  description: PROMPT_META.leadbay_import_file.short_description,
1828
- arguments: [
1829
- {
1830
- name: "file",
1831
- description: "Path or user-visible name of the CSV/file to import. If omitted, use the file the user attached or referenced.",
1832
- required: false
1833
- },
1834
- {
1835
- name: "instruction",
1836
- description: "Additional user goal, e.g. 'then qualify the leads', 'preserve owner phone as a custom field', or 'only import restaurants in Manhattan'.",
1837
- required: false
1838
- }
1839
- ],
2615
+ arguments: promptArguments("leadbay_import_file"),
1840
2616
  render: (args) => [
1841
2617
  userMessage(
1842
2618
  substitutePlaceholders(leadbay_import_file, {
@@ -1849,13 +2625,7 @@ var CATALOG = [
1849
2625
  {
1850
2626
  name: "leadbay_refine_audience",
1851
2627
  description: PROMPT_META.leadbay_refine_audience.short_description,
1852
- arguments: [
1853
- {
1854
- name: "instruction",
1855
- description: "The refinement (e.g. 'focus on hospitals running their own IT'). Set to plain English.",
1856
- required: true
1857
- }
1858
- ],
2628
+ arguments: promptArguments("leadbay_refine_audience"),
1859
2629
  render: (args) => [
1860
2630
  userMessage(
1861
2631
  substitutePlaceholders(leadbay_refine_audience, {
@@ -1867,18 +2637,7 @@ var CATALOG = [
1867
2637
  {
1868
2638
  name: "leadbay_log_outreach",
1869
2639
  description: PROMPT_META.leadbay_log_outreach.short_description,
1870
- arguments: [
1871
- {
1872
- name: "lead_id",
1873
- description: "The lead UUID. Get it from leadbay_pull_leads or leadbay_research_lead_by_id.",
1874
- required: true
1875
- },
1876
- {
1877
- name: "summary",
1878
- description: "1-2 sentences describing what I did (e.g. 'Sent intro email to CTO citing recent Hornsea contract').",
1879
- required: true
1880
- }
1881
- ],
2640
+ arguments: promptArguments("leadbay_log_outreach"),
1882
2641
  render: (args) => [
1883
2642
  userMessage(
1884
2643
  substitutePlaceholders(leadbay_log_outreach, {
@@ -1891,18 +2650,7 @@ var CATALOG = [
1891
2650
  {
1892
2651
  name: "leadbay_plan_tour_in_city",
1893
2652
  description: PROMPT_META.leadbay_plan_tour_in_city.short_description,
1894
- arguments: [
1895
- {
1896
- name: "city",
1897
- description: "City or region the user is visiting (e.g. 'Limoges', 'Bay Area'). Used as the geo filter for both Monitor and Discover lookups.",
1898
- required: true
1899
- },
1900
- {
1901
- name: "date",
1902
- description: "When the visit is (e.g. 'May 24', 'next Thursday'). Surfaced in the outreach drafts as 'I'll be in <city> on <date>'.",
1903
- required: false
1904
- }
1905
- ],
2653
+ arguments: promptArguments("leadbay_plan_tour_in_city"),
1906
2654
  render: (args) => [
1907
2655
  userMessage(
1908
2656
  substitutePlaceholders(leadbay_plan_tour_in_city, {
@@ -1916,28 +2664,7 @@ var CATALOG = [
1916
2664
  {
1917
2665
  name: "leadbay_build_campaign",
1918
2666
  description: PROMPT_META.leadbay_build_campaign.short_description,
1919
- arguments: [
1920
- {
1921
- name: "audience",
1922
- description: "Optional: a fresh audience to target (e.g. 'dental clinics in Texas'). Omit to build from your ACTIVE lens \u2014 the default.",
1923
- required: false
1924
- },
1925
- {
1926
- name: "campaign_name",
1927
- description: "Optional: a name for the campaign. Omit and one is derived from the lens/audience + date (or the backend AI-names it).",
1928
- required: false
1929
- },
1930
- {
1931
- name: "count",
1932
- description: "Optional: how many fully-actionable leads to build (default 20). The loop keeps discovering, qualifying and enriching until this many in-ICP leads each have a reachable target-title contact \u2014 or the lens is exhausted. Higher counts take longer and consume more quota.",
1933
- required: false
1934
- },
1935
- {
1936
- name: "job_titles",
1937
- description: "Optional: the exact buyer job titles to enrich, comma-separated (e.g. 'VP Sales, Head of Growth, Director of Business Development'). Omit and the buyer persona is derived from what you sell. A lead only counts toward the target when it has a reachable contact matching one of these titles.",
1938
- required: false
1939
- }
1940
- ],
2667
+ arguments: promptArguments("leadbay_build_campaign"),
1941
2668
  render: (args) => {
1942
2669
  const n = args.count ?? "20";
1943
2670
  return [
@@ -1955,18 +2682,7 @@ var CATALOG = [
1955
2682
  {
1956
2683
  name: "leadbay_setup_team_prospecting",
1957
2684
  description: PROMPT_META.leadbay_setup_team_prospecting.short_description,
1958
- arguments: [
1959
- {
1960
- name: "audience",
1961
- description: "Natural-language audience description (e.g. 'plumbing companies with 10-50 employees in Seine-Maritime').",
1962
- required: true
1963
- },
1964
- {
1965
- name: "rep_split",
1966
- description: "Optional: how to split validated leads into per-rep campaigns. Free text (e.g. 'split by city', 'one campaign per rep').",
1967
- required: false
1968
- }
1969
- ],
2685
+ arguments: promptArguments("leadbay_setup_team_prospecting"),
1970
2686
  render: (args) => [
1971
2687
  userMessage(
1972
2688
  substitutePlaceholders(leadbay_setup_team_prospecting, {
@@ -1980,18 +2696,7 @@ var CATALOG = [
1980
2696
  {
1981
2697
  name: "leadbay_work_campaign",
1982
2698
  description: PROMPT_META.leadbay_work_campaign.short_description,
1983
- arguments: [
1984
- {
1985
- name: "campaign",
1986
- description: "Campaign name (fuzzy match) or campaign UUID. Omit to list and pick interactively.",
1987
- required: false
1988
- },
1989
- {
1990
- name: "mode",
1991
- description: "Optional: skip readiness proposal and jump to 'call_sheet', 'email_sheet', 'map', or 'enrich_first'. Omit to let the prompt propose based on campaign data.",
1992
- required: false
1993
- }
1994
- ],
2699
+ arguments: promptArguments("leadbay_work_campaign"),
1995
2700
  render: (args) => [
1996
2701
  userMessage(
1997
2702
  substitutePlaceholders(leadbay_work_campaign, {
@@ -2004,13 +2709,7 @@ var CATALOG = [
2004
2709
  {
2005
2710
  name: "leadbay_qualify_top_n",
2006
2711
  description: PROMPT_META.leadbay_qualify_top_n.short_description,
2007
- arguments: [
2008
- {
2009
- name: "count",
2010
- description: "How many leads to qualify (default 10, max 25). Higher counts may take 5+ minutes.",
2011
- required: false
2012
- }
2013
- ],
2712
+ arguments: promptArguments("leadbay_qualify_top_n"),
2014
2713
  render: (args) => {
2015
2714
  const n = args.count ?? "10";
2016
2715
  return [
@@ -2025,29 +2724,35 @@ var CATALOG = [
2025
2724
  {
2026
2725
  name: "leadbay_top_accounts_to_activate",
2027
2726
  description: PROMPT_META.leadbay_top_accounts_to_activate.short_description,
2028
- arguments: [
2029
- {
2030
- name: "count",
2031
- description: "Optional: how many accounts the plan should hold (default 50).",
2032
- required: false
2033
- },
2034
- {
2035
- name: "territory",
2036
- description: "Optional: restrict the plan to a territory (e.g. 'Indre-et-Loire'). Sets geography on the Discover lens via `locations`.",
2037
- required: false
2038
- }
2039
- ],
2727
+ arguments: promptArguments("leadbay_top_accounts_to_activate"),
2040
2728
  render: (args) => {
2041
2729
  const n = args.count ?? "50";
2042
2730
  return [
2043
2731
  userMessage(
2044
2732
  substitutePlaceholders(leadbay_top_accounts_to_activate, {
2045
2733
  count_or_default: n,
2046
- territory_block: args.territory ? `Scope the plan to **${args.territory}** \u2014 pass it as \`locations\` on the lens, never as a sector.` : ""
2734
+ // The country caveat is INSIDE the substituted string, not only in
2735
+ // the prompt body, because this sentence is the FIRST instruction
2736
+ // the agent reads and the body's country branch is ~35 lines below
2737
+ // it. Rendered with `territory: "France"`, the old wording told the
2738
+ // agent in its opening paragraph to pass a country as `locations` —
2739
+ // the exact call this prompt later forbids (product#3951). The
2740
+ // audit could not see it either: it reads prompts.generated.ts,
2741
+ // where this is still an unexpanded `{{arg:territory_block}}`.
2742
+ territory_block: args.territory ? `Scope the plan to **${args.territory}** \u2014 but ONLY if it names a place INSIDE this workspace's country (state / r\xE9gion / d\xE9partement / county / city): pass that as \`locations\` on the lens, never as a sector. If **${args.territory}** is a country or a supra-national area (EU, EMEA), it is NOT a location filter \u2014 do not pass it as \`locations\` at all; follow the country branch below instead.` : ""
2047
2743
  })
2048
2744
  )
2049
2745
  ];
2050
2746
  }
2747
+ },
2748
+ {
2749
+ // Guided first-run walkthrough (issue #3952). No arguments — the tour is
2750
+ // the same for every new user, and asking a brand-new user to parameterize
2751
+ // their own onboarding defeats the point.
2752
+ name: "leadbay_getting_started",
2753
+ description: PROMPT_META.leadbay_getting_started.short_description,
2754
+ arguments: promptArguments("leadbay_getting_started"),
2755
+ render: () => [userMessage(leadbay_getting_started)]
2051
2756
  }
2052
2757
  ];
2053
2758
  function listPrompts() {
@@ -2088,7 +2793,7 @@ var REGIONS = {
2088
2793
  };
2089
2794
  var API_VERSION = "1.6";
2090
2795
  var API_PREFIX = `/${API_VERSION}`;
2091
- function httpsRequest(method, url, headers, body) {
2796
+ function httpsRequest(method, url, headers, body, timeoutMs) {
2092
2797
  return new Promise((resolve, reject) => {
2093
2798
  const start = Date.now();
2094
2799
  const parsed = new URL(url);
@@ -2096,6 +2801,11 @@ function httpsRequest(method, url, headers, body) {
2096
2801
  if (body !== void 0) {
2097
2802
  reqHeaders["Content-Length"] = Buffer.byteLength(body);
2098
2803
  }
2804
+ let deadline;
2805
+ const clearDeadline = () => {
2806
+ if (deadline !== void 0)
2807
+ clearTimeout(deadline);
2808
+ };
2099
2809
  const req = https.request({
2100
2810
  hostname: parsed.hostname,
2101
2811
  port: 443,
@@ -2106,6 +2816,7 @@ function httpsRequest(method, url, headers, body) {
2106
2816
  const chunks = [];
2107
2817
  res.on("data", (chunk) => chunks.push(chunk));
2108
2818
  res.on("end", () => {
2819
+ clearDeadline();
2109
2820
  resolve({
2110
2821
  status: res.statusCode ?? 0,
2111
2822
  body: Buffer.concat(chunks).toString("utf8"),
@@ -2114,15 +2825,27 @@ function httpsRequest(method, url, headers, body) {
2114
2825
  });
2115
2826
  });
2116
2827
  });
2117
- req.on("error", reject);
2828
+ if (timeoutMs !== void 0 && timeoutMs > 0) {
2829
+ deadline = setTimeout(() => {
2830
+ req.destroy?.();
2831
+ const err = new Error(`Request timed out after ${timeoutMs}ms: ${method} ${url}`);
2832
+ err.code = "TIMEOUT";
2833
+ reject(err);
2834
+ }, timeoutMs);
2835
+ deadline.unref?.();
2836
+ }
2837
+ req.on("error", (e) => {
2838
+ clearDeadline();
2839
+ reject(e);
2840
+ });
2118
2841
  if (body !== void 0)
2119
2842
  req.write(body);
2120
2843
  req.end();
2121
2844
  });
2122
2845
  }
2123
2846
  function createClient(config = {}) {
2124
- const region = config.region ?? "us";
2125
- const baseUrl = config.baseUrl ?? REGIONS[region];
2847
+ const region = config.baseUrl ? config.region : config.region ?? "us";
2848
+ const baseUrl = config.baseUrl ?? REGIONS[region ?? "us"];
2126
2849
  if (!baseUrl) {
2127
2850
  throw new Error(`Leadbay: unknown region "${region}". Supported: ${Object.keys(REGIONS).join(", ")}. Or pass an explicit baseUrl.`);
2128
2851
  }
@@ -2217,7 +2940,7 @@ function findMockFixture(method, basePath) {
2217
2940
  }
2218
2941
  return null;
2219
2942
  }
2220
- var LeadbayClient = class {
2943
+ var LeadbayClient = class _LeadbayClient {
2221
2944
  token;
2222
2945
  _baseUrl;
2223
2946
  _region;
@@ -2267,16 +2990,34 @@ var LeadbayClient = class {
2267
2990
  // Last response metadata — composites can read this after a request to
2268
2991
  // surface latency/region/retry_after to the agent in their `_meta` block.
2269
2992
  _lastMeta = null;
2993
+ /**
2994
+ * Derive the region from a base URL, comparing the NORMALIZED form.
2995
+ *
2996
+ * The trailing slash matters: `LEADBAY_BASE_URL=https://api-fr.leadbay.app/`
2997
+ * is an ordinary way to spell an env var, and comparing it raw labelled that
2998
+ * tenant "custom". Since createClient stopped forcing "us" onto a supplied
2999
+ * baseUrl, that mislabel reaches the single-country guard, which then reports
3000
+ * country_indeterminate instead of correctly classifying France as this
3001
+ * workspace's own country (product#3951).
3002
+ */
3003
+ static regionFromBaseUrl(baseUrl) {
3004
+ const normalized = baseUrl.replace(/\/+$/, "");
3005
+ if (normalized === REGIONS.us.replace(/\/+$/, ""))
3006
+ return "us";
3007
+ if (normalized === REGIONS.fr.replace(/\/+$/, ""))
3008
+ return "fr";
3009
+ return "custom";
3010
+ }
2270
3011
  constructor(baseUrl, token, region) {
2271
3012
  if (typeof baseUrl === "object") {
2272
3013
  const opts = baseUrl;
2273
3014
  this._baseUrl = opts.baseUrl.replace(/\/+$/, "");
2274
3015
  this.token = opts.bearer ?? null;
2275
- this._region = opts.region ?? (opts.baseUrl === REGIONS.us ? "us" : opts.baseUrl === REGIONS.fr ? "fr" : "custom");
3016
+ this._region = opts.region ?? _LeadbayClient.regionFromBaseUrl(opts.baseUrl);
2276
3017
  } else {
2277
3018
  this._baseUrl = baseUrl.replace(/\/+$/, "");
2278
3019
  this.token = token ?? null;
2279
- this._region = region ?? (baseUrl === REGIONS.us ? "us" : baseUrl === REGIONS.fr ? "fr" : "custom");
3020
+ this._region = region ?? _LeadbayClient.regionFromBaseUrl(baseUrl);
2280
3021
  }
2281
3022
  }
2282
3023
  get baseUrl() {
@@ -2304,7 +3045,7 @@ var LeadbayClient = class {
2304
3045
  // one the client was constructed with.
2305
3046
  setBaseUrl(baseUrl, region) {
2306
3047
  this._baseUrl = baseUrl.replace(/\/+$/, "");
2307
- this._region = region ?? (baseUrl === REGIONS.us ? "us" : baseUrl === REGIONS.fr ? "fr" : "custom");
3048
+ this._region = region ?? _LeadbayClient.regionFromBaseUrl(baseUrl);
2308
3049
  this.clearTenantScopedCaches();
2309
3050
  }
2310
3051
  setToken(token) {
@@ -2370,8 +3111,8 @@ var LeadbayClient = class {
2370
3111
  // are idempotent, so retrying them is safe. The 250ms backoff releases the
2371
3112
  // concurrency slot first (release → sleep → re-acquire) so a wave of 401s
2372
3113
  // doesn't pin all MAX_CONCURRENT slots in setTimeout and stall the queue.
2373
- httpsRequestWithRetry = async (method, url, headers, body) => {
2374
- const res = await httpsRequest(method, url, headers, body);
3114
+ httpsRequestWithRetry = async (method, url, headers, body, timeoutMs) => {
3115
+ const res = await httpsRequest(method, url, headers, body, timeoutMs);
2375
3116
  if (res.status === 401 && method.toUpperCase() === "GET") {
2376
3117
  this.releaseSemaphore();
2377
3118
  try {
@@ -2379,7 +3120,7 @@ var LeadbayClient = class {
2379
3120
  } finally {
2380
3121
  await this.acquireSemaphore();
2381
3122
  }
2382
- return httpsRequest(method, url, headers, body);
3123
+ return httpsRequest(method, url, headers, body, timeoutMs);
2383
3124
  }
2384
3125
  return res;
2385
3126
  };
@@ -2400,7 +3141,7 @@ var LeadbayClient = class {
2400
3141
  if (body) {
2401
3142
  headers["Content-Type"] = "application/json";
2402
3143
  }
2403
- const res = await (retryOn401 ? this.httpsRequestWithRetry : httpsRequest)(method, url, headers, body ? JSON.stringify(body) : void 0);
3144
+ const res = await (retryOn401 ? this.httpsRequestWithRetry : httpsRequest)(method, url, headers, body ? JSON.stringify(body) : void 0, opts?.timeoutMs);
2404
3145
  this._lastMeta = {
2405
3146
  region: this._region,
2406
3147
  endpoint: `${method} ${path}`,
@@ -2585,13 +3326,22 @@ var LeadbayClient = class {
2585
3326
  }
2586
3327
  // /me cache (60s TTL). Separate from resolveOrgId() which still works for
2587
3328
  // legacy callers (it now delegates here).
2588
- async resolveMe(force = false) {
3329
+ //
3330
+ // `opts.timeoutMs` bounds each underlying attempt and CANCELS it. Callers that
3331
+ // give up on this read with their own `Promise.race` must pass it: abandoning
3332
+ // the promise doesn't stop the request, so against a silent backend (handshake
3333
+ // completes, nothing ever comes back) the socket and its API-semaphore slot
3334
+ // stay held for the life of the process. Racing bounds the caller's wait; only
3335
+ // the deadline bounds the resource.
3336
+ async resolveMe(force = false, opts) {
2589
3337
  const now = Date.now();
2590
3338
  if (!force && this.mePayload !== null && this.mePayloadCachedAt !== null && now - this.mePayloadCachedAt < ME_CACHE_TTL_MS) {
2591
3339
  return this.mePayload;
2592
3340
  }
2593
3341
  const seqAtStart = ++this.telemetryStateSeq;
2594
- const me = await this.request("GET", "/users/me");
3342
+ const me = await this.request("GET", "/users/me", void 0, {
3343
+ timeoutMs: opts?.timeoutMs
3344
+ });
2595
3345
  this.mePayload = me;
2596
3346
  this.mePayloadCachedAt = now;
2597
3347
  if (this.telemetryStateSeq === seqAtStart && me.telemetry_enabled !== void 0) {
@@ -2617,7 +3367,12 @@ var LeadbayClient = class {
2617
3367
  //
2618
3368
  // Returns the observed preference: true/false, or undefined when the backend
2619
3369
  // omitted the field (older backend → caller treats as enabled default).
2620
- async fetchTelemetryEnabled() {
3370
+ //
3371
+ // `opts.timeoutMs` bounds and CANCELS each attempt — same reasoning as
3372
+ // resolveMe(): the hosted SSE refresh fires this off behind its own timer and
3373
+ // stops waiting, so without a deadline a dark region leaves the request (and
3374
+ // the semaphore slot the caller is explicitly waiting on) held forever.
3375
+ async fetchTelemetryEnabled(opts) {
2621
3376
  const seqAtStart = ++this.telemetryStateSeq;
2622
3377
  if (process.env.LEADBAY_MOCK === "1") {
2623
3378
  const metaBefore = this._lastMeta;
@@ -2638,7 +3393,7 @@ var LeadbayClient = class {
2638
3393
  }
2639
3394
  await this.acquireSemaphore();
2640
3395
  try {
2641
- const res = await this.httpsRequestWithRetry("GET", `${this._baseUrl}${API_PREFIX}/users/me`, { Authorization: `Bearer ${this.token}` }, void 0);
3396
+ const res = await this.httpsRequestWithRetry("GET", `${this._baseUrl}${API_PREFIX}/users/me`, { Authorization: `Bearer ${this.token}` }, void 0, opts?.timeoutMs);
2642
3397
  if (res.status < 200 || res.status >= 300) {
2643
3398
  throw this.mapErrorResponse(res.status, res.body, "/users/me", res.headers);
2644
3399
  }
@@ -2661,6 +3416,14 @@ var LeadbayClient = class {
2661
3416
  this.mePayload = null;
2662
3417
  this.mePayloadCachedAt = null;
2663
3418
  }
3419
+ // Warm the /users/me cache from a payload the caller already fetched, so the
3420
+ // next resolveMe() is a cache hit (no extra round trip). Used by the hosted
3421
+ // HTTP auth probe: it validates the token with a fail-fast /users/me request
3422
+ // and seeds the result here, so the telemetry path's resolveMe() reuses it.
3423
+ seedMe(me) {
3424
+ this.mePayload = me;
3425
+ this.mePayloadCachedAt = Date.now();
3426
+ }
2664
3427
  // Synchronous read of the last-cached telemetry preference, without a fetch.
2665
3428
  // Returns undefined when /users/me hasn't been resolved (or was invalidated).
2666
3429
  // The hosted telemetry suppression predicate reads this AT CAPTURE TIME so a
@@ -7231,6 +7994,7 @@ var COMPOSITE_FILE_TOOL_NAMES = /* @__PURE__ */ new Set([
7231
7994
  "leadbay_followups_map",
7232
7995
  "leadbay_get_lead_custom_fields",
7233
7996
  "leadbay_get_qualification_questions",
7997
+ "leadbay_getting_started",
7234
7998
  "leadbay_import_and_qualify",
7235
7999
  "leadbay_import_leads",
7236
8000
  "leadbay_import_status",
@@ -7679,7 +8443,7 @@ Trigger phrases: "narrow the audience to <sector>", "add <sector> to my <name> l
7679
8443
 
7680
8444
  **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
7681
8445
 
7682
- 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\`.
8446
+ Do NOT use for: "companies anywhere in this workspace's OWN country / nationwide (a foreign country is unsupported, not unfiltered \u2014 call nothing)" \u2192 \`leadbay_pull_leads\`; "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\`.
7683
8447
 
7684
8448
  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.
7685
8449
 
@@ -7692,6 +8456,7 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
7692
8456
  - "Create a lens called Joinery for fintech."
7693
8457
  - "Show me my lenses."
7694
8458
  - "Focus on hospitals that run their own IT."
8459
+ - "Show me companies anywhere in the US."
7695
8460
 
7696
8461
  ## RENDER (quick)
7697
8462
 
@@ -7706,7 +8471,29 @@ Restrict (or expand) the lens audience by sector / size. Free-text sectors are a
7706
8471
 
7707
8472
  **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.
7708
8473
 
7709
- **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.
8474
+ **Geography \u2014 scope a sales territory.** Pass \`locations\` (free text like \`["Indre-et-Loire"]\`, \`["Texas"]\`, \`["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\` at any level from state down to city \u2014 state, *r\xE9gion*, *d\xE9partement*, county, city. 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.
8475
+
8476
+ **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
8477
+
8478
+ **On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
8479
+
8480
+ \`axis: "include"\`:
8481
+
8482
+ - \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
8483
+ - \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
8484
+ - \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
8485
+ - \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
8486
+
8487
+ \`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
8488
+
8489
+ On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
8490
+
8491
+ **Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
8492
+
8493
+ Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
8494
+
8495
+
8496
+ **Widening to the whole workspace is NOT "pass no locations".** Location criteria MERGE here rather than replace, so any geography the lens already carries survives an edit that simply omits \`locations\`. "Make this healthcare nationwide" on a lens scoped to Paris returns Paris healthcare \u2014 and calling that nationwide is the same confidently-wrong answer as the country fence itself, just in the header instead of the filter. Read \`lens://<lensId>/definition\` FIRST: it is the only place a lens's \`location_ids\` are visible (\`leadbay_pull_leads\` returns only \`lens: {id}\`, and \`leadbay_my_lenses\` returns no filter at all). Then either clear those criteria explicitly, or state which places the audience actually covers. If you cannot read the definition, say the scope is unverified rather than calling it workspace-wide.
7710
8497
 
7711
8498
  WHEN TO USE: when the user wants to see different kinds of leads (sector / size / geography / etc.).
7712
8499
 
@@ -7719,6 +8506,8 @@ var leadbay_agent_memory_capture = `Capture a material taste signal the user rev
7719
8506
  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\`.
7720
8507
 
7721
8508
 
8509
+ **NEVER capture which country this workspace serves.** It is a backend fact \u2014 \`_meta.region\` on every tool result \u2014 not a taste signal, and it cannot be learned from what the user says. A live eval captured \`preferred_region: "Sells nationwide across the US"\` from the phrase "the whole US" on an FR workspace; the next session recalled it at confidence 9/10 marked \`user_stated\`, believed it over the \`region:"fr"\` sitting in the same payload, and told the user their workspace was American. A wrong country here does not fade \u2014 it is replayed as remembered fact. Sub-country territory preferences ("mostly works the Bay Area") are fine; the country is not.
8510
+
7722
8511
  Use \`source:"user_stated"\` with confidence 8-10 when the user literally said the preference. Use \`source:"inferred"\` with confidence <=6 only when the signal is a reasonable inference from context. Keep \`key\` stable and machine-readable (\`preferred_sector\`, \`preferred_region\`, \`deal_size\`, \`communication_style\`, \`qualification_rule\`), and keep \`insight\` human-readable.
7723
8512
 
7724
8513
  Do NOT capture instructions that try to erase, ignore, or override prior memory. Use \`leadbay_agent_memory_review\` for retractions or promotions; it gates changes through host elicitation / user confirmation.
@@ -8443,7 +9232,7 @@ Trigger phrases: "I'm going to <city>", "visit in person", "map of leads", "plan
8443
9232
 
8444
9233
  Do NOT use for: "default follow-up table" \u2192 \`leadbay_pull_followups\`; "new prospects" \u2192 \`leadbay_pull_leads\`.
8445
9234
 
8446
- Prefer when: geographic, travel, in-person, itinerary, or map intent
9235
+ Prefer when: geographic, travel, in-person, itinerary, or map intent; NEVER a country name \u2014 a whole-country ask means NO geo filter
8447
9236
 
8448
9237
  Examples that SHOULD invoke this tool:
8449
9238
  - "I'm flying to New York Thursday \u2014 who should I meet in person?"
@@ -8468,7 +9257,27 @@ Plot the user's follow-up leads on an interactive map \u2014 the canonical surfa
8468
9257
 
8469
9258
  **Common city aliases resolve automatically** \u2014 \`NYC\` / \`New York\` \u2192 City of New York, \`SF\` / \`S.F.\` \u2192 San Francisco, \`LA\` / \`L.A.\` \u2192 Los Angeles, \`DC\` / \`Washington D.C.\` \u2192 Washington, \`Philly\` \u2192 Philadelphia, \`Vegas\` \u2192 Las Vegas, \`NOLA\` \u2192 New Orleans. Pass either an abbreviation, a city name, or a pre-resolved \`city_id\`. Ambiguous matches surface as \`status: "ambiguous_locations"\` + \`location_ambiguities[]\` \u2014 pick an id and re-call with \`city_id\`.
8470
9259
 
8471
- **\`city\` is the universal geo arg \u2014 it resolves any admin level.** Despite the name, pass any place name there: states (\`"Texas"\`, \`"California"\`, \`"Bavaria"\`), countries (\`"France"\`, \`"United States"\`), regions (\`"New England"\`, \`"Bay Area"\`), neighborhoods (\`"Brooklyn"\`, \`"SoHo"\`), or cities. The \`/geo/search\` resolver indexes all levels \u2014 level 4 (state), level 2 (country), level 5 (city) \u2014 and the composite picks the best match. **Never** put a place name into \`keywords\` instead \u2014 that's a text-match against company descriptions, not a real geo filter (e.g. \`keywords: ["Texas"]\` returns \u22480 hits even when the user has dozens of Texas leads). If \`keywords: ["<PlaceName>"]\` returned empty, the correct next call is \`city: "<PlaceName>"\`, NOT the unfiltered Monitor view.
9260
+ **\`city\` is the universal SUB-country geo arg.** Despite the name, pass any place name BELOW country level: states (\`"Texas"\`, \`"California"\`), regions (\`"New England"\`, \`"Bay Area"\`), counties, neighborhoods (\`"Brooklyn"\`, \`"SoHo"\`), or cities \u2014 the \`/geo/search\` resolver indexes every level it returns and the composite picks the best match. A COUNTRY name is the one thing it must never receive (rule below). And \`keywords: ["Texas"]\` returns \u22480 hits even when the user has dozens of Texas leads \u2014 that's a text-match against company descriptions, not a geo filter. If \`keywords: ["<PlaceName>"]\` returned empty, the correct next call is \`city: "<PlaceName>"\`, NOT the unfiltered Monitor view.
9261
+
9262
+ **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
9263
+
9264
+ **On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
9265
+
9266
+ \`axis: "include"\`:
9267
+
9268
+ - \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
9269
+ - \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
9270
+ - \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
9271
+ - \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
9272
+
9273
+ \`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
9274
+
9275
+ On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
9276
+
9277
+ **Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
9278
+
9279
+ Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
9280
+
8472
9281
 
8473
9282
  ---
8474
9283
 
@@ -8797,33 +9606,134 @@ render verbatim.
8797
9606
  `;
8798
9607
  var leadbay_get_quota = `Read quota / spend across daily, weekly, and monthly windows. The response has two scope groups: **\`user\`** (present for every caller) and **\`org\`** (admin-only \u2014 \`null\` for non-admins). **Read from \`user\` first**, falling back to \`org\` only when \`user\` is absent. Each group carries \`spend[]\` (the dollar-spend gauge: \`current_units\` / \`max_units\` in dollar_cents \u2192 % used = the ratio, $ = \`/100\`) and \`resources[]\` (per-resource usage: \`{resource_type, count (used), max_units (cap or null), window_type, resets_at}\`). \`spend[]\` is empty for orgs with no OVERALL_SPEND quota \u2014 fall back to the \`resources[]\` counts then. There is also a top-level \`topup\` ({remaining_cents, total_credit_cents}) when present. Resource types may arrive lowercase (\`lens_extra_refill\`) or uppercase \u2014 match case-insensitively. Present quota as a percentage / dollar figure, never raw "credits".
8799
9608
 
8800
- WHEN TO USE: after a 429 error, to explain to the user which window was hit and when it resets.
9609
+ WHEN TO USE: after a 429 error, to explain to the user which window was hit and when it resets.
9610
+
9611
+ WHEN NOT TO USE: as a pre-flight gate before bulk operations \u2014 operations themselves return 429 with hints; this tool is for diagnostics, not gating.
9612
+ `;
9613
+ var leadbay_get_selection_ids = `List the lead ids currently in the user's selection (the transient set that bulk operations like enrichment act on).
9614
+
9615
+ WHEN TO USE: to verify the selection state before/after bulk ops if a composite call has misbehaved.
9616
+
9617
+ WHEN NOT TO USE: in the normal flow \u2014 leadbay_enrich_titles manages selection lifecycle automatically (select \u2192 action \u2192 clear).
9618
+ `;
9619
+ var leadbay_get_taste_profile = `Get the user's Ideal Buyer Profile, purchase-intent tags, and qualification questions. The result is cached on the client. Returns an operator \`hint\` when no profile is configured yet.
9620
+
9621
+ WHEN TO USE: at the very start of a session to understand what kind of leads the user is looking for.
9622
+
9623
+ WHEN NOT TO USE: per-lead \u2014 leadbay_research_lead_by_id already includes the per-lead qualification answers (which are scored against these org-level questions).
9624
+ `;
9625
+ var leadbay_get_user_prompt = `Read the org's intelligence-refinement prompt (free-text instruction that steers lead recommendations beyond firmographics). Returns \`{prompt: null, set: false}\` when none is configured (the backend returns 204 in that case).
9626
+
9627
+ WHEN TO USE: to know what's currently steering the agent's recommendations before suggesting a refine.
9628
+
9629
+ WHEN NOT TO USE: to set/change the prompt \u2014 use leadbay_refine_prompt.
9630
+ `;
9631
+ var leadbay_get_web_fetch = `Read the AI-generated web-research summary for a lead \u2014 company profile, business signals, prospecting clues, each with sources and "hot" flags marking high-signal recent items. The content is dictioned by emoji-prefixed section labels in the raw API.
9632
+
9633
+ WHEN TO USE: when the agent already qualified this lead and wants the underlying research to reason from.
9634
+
9635
+ WHEN NOT TO USE: as the first read on a lead \u2014 the leadbay_research_lead_by_id composite bundles this with qualification answers and reshapes the dict into a stable array form.
9636
+ `;
9637
+ var leadbay_getting_started2 = `## WHEN TO USE
9638
+
9639
+ Trigger phrases: "walk me through leadbay", "I'm new", "how do I use this", "getting started", "show me how this works", "give me a tour", "help me get started", "I just installed this".
9640
+
9641
+ **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
9642
+
9643
+ Do NOT use for: "show me today's leads" \u2192 \`leadbay_pull_leads\`; "which audiences do I have" \u2192 \`leadbay_my_lenses\`; "where am I / what's my plan and quota" \u2192 \`leadbay_account_status\`.
9644
+
9645
+ Prefer when: the user has never used Leadbay, or asks to be SHOWN rather than told \u2014 the walkthrough runs real calls on their own account
9646
+
9647
+ Examples that SHOULD invoke this tool:
9648
+ - "Walk me through Leadbay."
9649
+ - "I'm new here \u2014 how do I use this?"
9650
+ - "Can you show me how this works?"
9651
+
9652
+ Examples that should NOT invoke this tool (sound similar, route elsewhere):
9653
+ - "Show me today's leads."
9654
+ - "Explain the difference between discovery and follow-up."
9655
+ - "Which of my lenses is active right now?"
9656
+
9657
+ ## RENDER (quick)
9658
+
9659
+ Not a data table. Run the walkthrough ONE gate at a time: fire your host's
9660
+ choice widget with that step's forward option + exit, wait for the click, make that
9661
+ step's tool call, then advance. Never dump all four steps at once, and never
9662
+ render a gate as a prose question.
9663
+
9664
+ ---
9665
+
9666
+ Returns the **guided first-run walkthrough** \u2014 a short script the agent drives so a brand-new user learns Leadbay by *doing*, not by reading. Makes no backend call and mutates nothing; the content is static and version-locked.
9667
+
9668
+ Every click in the walkthrough runs a real Leadbay call against the user's own account. By the end they have confirmed which account they're on, pulled today's leads, had a first email drafted to the best of them, and revealed the person to send it to. Every gate calls a real Leadbay tool \u2014 the tour ends where Leadbay's own value ends.
9669
+
9670
+ For orientation **prose** with no clicking \u2014 "explain how Leadbay works", "what's the difference between discovery and follow-up" \u2014 this tool is the wrong answer; that's the \`leadbay_prospecting_overview\` prompt.
9671
+
9672
+ And when the problem is **setup** rather than usage \u2014 the connector isn't installed, they can't sign in, their Leadbay tools aren't appearing, or they want to run this on another host \u2014 the walkthrough can't help either: it assumes a working connection, and step 1 is what proves it. Send them to the setup guide the manifest carries as \`docs_url\`: <https://docs.leadbay.app/doc/leadbay-mcp/quickstart>. Its \`docs_note\` names the only two moments the link should appear \u2014 that pre-check, and once at the closing. Never between gates.
9673
+
9674
+ ## THE ONE-FORWARD-OPTION RULE
9675
+
9676
+ Every gate carries **exactly one way forward, plus a way out** \u2014 two options, never more: the action, and \`I'm done for now\`.
9677
+
9678
+ A first-run user doesn't yet know enough to choose between PATHS \u2014 a menu of alternatives makes them stall. One forward move makes the next step obvious, and the click is what teaches the tool. The exit keeps the tour from being a trap and satisfies the host widget's 2\u20134 option requirement: a lone option is rejected or silently degrades to prose. Never add a third option, and never turn the exit into an alternative route. Typing works too: if they type something off-script, abandon the walkthrough and serve what they asked.
9679
+
9680
+ ## What it returns
9681
+
9682
+ \`\`\`
9683
+ {
9684
+ version, intro, one_option_rule, docs_url, docs_note,
9685
+ calendly_url, exit_offer,
9686
+ steps: [ { n, gate_label, gate_description, calls, args, ... } ],
9687
+ keep_going, stop
9688
+ }
9689
+ \`\`\`
9690
+
9691
+ Per step: \`gate_label\` / \`gate_description\` are the widget's forward option, \`calls\` is the tool to invoke on click (or \`null\`), and \`args\` is the literal argument shape. Render each \`gate_label\` verbatim \u2014 don't reword them.
9692
+
9693
+ | Step | Gate | Calls |
9694
+ |---|---|---|
9695
+ | 1 | Check my account | \`leadbay_account_status\` (no args) |
9696
+ | 2 | Pull today's leads | \`leadbay_pull_leads\` (no args) |
9697
+ | 3 | Draft the first email | \`leadbay_prepare_outreach\` \u2014 \`leadId\` ONLY, never \`enrich\` |
9698
+ | 4 | Find who to email | \`leadbay_enrich_titles\` \u2014 free preview, then a consented paid reveal |
9699
+
9700
+ Steps 1, 2 and 3 carry \`branches[]\`, and steps 3 and 4 carry \`spend\` (+ \`quota_note\` on 4). Every step also carries \`explain\` (say this BEFORE firing) and \`next_steps\` (\`{question, options[]}\` \u2014 already the widget's shape, map it verbatim).
8801
9701
 
8802
- WHEN NOT TO USE: as a pre-flight gate before bulk operations \u2014 operations themselves return 429 with hints; this tool is for diagnostics, not gating.
8803
- `;
8804
- var leadbay_get_selection_ids = `List the lead ids currently in the user's selection (the transient set that bulk operations like enrichment act on).
9702
+ When the user picks \`I'm done for now\`, don't just go quiet \u2014 **\`exit_offer\`** says what to do: one short line offering a 1:1 with Zoe (lens tuning, CRM wiring, automating the daily run) plus **\`calendly_url\`**, then stop. One sentence and the link, never a pitch, never a re-opened gate. Only on the EXIT click: if they left by *typing* a different request, skip it and serve what they asked.
8805
9703
 
8806
- WHEN TO USE: to verify the selection state before/after bulk ops if a composite call has misbehaved.
9704
+ The manifest also carries **\`keep_going\`**: the closing cheat-sheet of *what you want \u2192 what you say*. The buttons vanish when the tour ends, so render these rows as a small two-column table at the finish, phrases **verbatim**. Each one is lifted from that tool's own trigger list, so it genuinely routes \u2014 inventing or prettifying a phrase teaches the user something that won't work. Add \`docs_url\` beneath it as one plain link, for what the gates didn't cover.
8807
9705
 
8808
- WHEN NOT TO USE: in the normal flow \u2014 leadbay_enrich_titles manages selection lifecycle automatically (select \u2192 action \u2192 clear).
8809
- `;
8810
- var leadbay_get_taste_profile = `Get the user's Ideal Buyer Profile, purchase-intent tags, and qualification questions. The result is cached on the client. Returns an operator \`hint\` when no profile is configured yet.
9706
+ ## Three hard rules the manifest encodes
8811
9707
 
8812
- WHEN TO USE: at the very start of a session to understand what kind of leads the user is looking for.
9708
+ **Step 1 shows the real account, and is silent about two things.** The click is labelled *check my account status*, so deliver it: user + org, then the **full quota windows** the way the web app renders them \u2014 Daily / Weekly / Monthly with a \`\u25B0\u25B1\` gauge, % used, $ spent against the cap, resets countdown, and the per-resource breakdown. Never raw "credits". But apply the silence gate first: when \`quota\` is null, \`quota_error\` is set, or the org has \`unlimited_credits\`, say **nothing** about quota \u2014 never mention a 401, never suggest logging in again (the token is fine, the same response just read their account), and never announce "unlimited". And **never volunteer the lens**: the response withholds it unless the user asked, so there is nothing to report and no other tool to reach for. Both are pinned regressions (WORKFLOWS #30 / #31).
8813
9709
 
8814
- WHEN NOT TO USE: per-lead \u2014 leadbay_research_lead_by_id already includes the per-lead qualification answers (which are scored against these org-level questions).
8815
- `;
8816
- var leadbay_get_user_prompt = `Read the org's intelligence-refinement prompt (free-text instruction that steers lead recommendations beyond firmographics). Returns \`{prompt: null, set: false}\` when none is configured (the backend returns 204 in that case).
9710
+ **Step 3 drafts, and spends nothing.** Call \`leadbay_prepare_outreach\` with \`leadId\` alone \u2014 **never \`enrich: true\`**, which launches a paid contact reveal off the back of a *draft* click. \`recommended_contact\` returns with \`email\`/\`phone\` null; that is expected, and it is the hook for step 4. Render through \`message_compose_v1\` (2\u20133 strategy-labelled variants), address it to the job TITLE \u2014 no name exists yet, and inventing one is fabrication \u2014 and never send it or offer to.
8817
9711
 
8818
- WHEN TO USE: to know what's currently steering the agent's recommendations before suggesting a refine.
9712
+ **Step 4 runs in two beats \u2014 free first, paid only on consent.** Scoped to the ONE lead step 3 drafted for. Beat 1 omits \`titles\` and returns \`mode:"discover"\`, the free list of job titles at that company; say plainly that nothing has been spent. Beat 2 names the title the draft is addressed to, states the cost BEFORE they decide (one contact, one credit), and only on confirmation calls again with \`titles\` + \`confirm:true\` + \`email:true\` \u2014 polled via \`leadbay_bulk_enrich_status\` until done, reporting only what actually resolved. The gate click bought the free look, not the reveal: never launch without an explicit confirm.
8819
9713
 
8820
- WHEN NOT TO USE: to set/change the prompt \u2014 use leadbay_refine_prompt.
8821
- `;
8822
- var leadbay_get_web_fetch = `Read the AI-generated web-research summary for a lead \u2014 company profile, business signals, prospecting clues, each with sources and "hot" flags marking high-signal recent items. The content is dictioned by emoji-prefixed section labels in the raw API.
9714
+ ## Empty first batch is normal, not an error
8823
9715
 
8824
- WHEN TO USE: when the agent already qualified this lead and wants the underlying research to reason from.
9716
+ A brand-new lens reads empty for the first minute while the backend computes its wishlist. When \`leadbay_pull_leads\` returns no leads but \`computing_wishlist\` / \`computing_scores\` is true, the lens is warming up: render that tool's own two-option warm-up payload verbatim and pause. **Never report "no leads found"** in that state.
8825
9717
 
8826
- WHEN NOT TO USE: as the first read on a lead \u2014 the leadbay_research_lead_by_id composite bundles this with qualification answers and reshapes the dict into a stable array form.
9718
+ ## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
9719
+
9720
+ 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.
9721
+
9722
+ **The Big Three** \u2014 when a tool result fits, route there:
9723
+
9724
+ | Host widget | Use when | Field map (from Leadbay payload) |
9725
+ |---|---|---|
9726
+ | \`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. |
9727
+ | \`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") |
9728
+ | \`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. |
9729
+
9730
+ 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.
9731
+
9732
+ **Rules:**
9733
+ - 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.
9734
+ - Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
9735
+ - 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.
9736
+ - One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
8827
9737
  `;
8828
9738
  var leadbay_import_and_qualify = `Import + qualify leads in one call. Pass either \`domains: [{domain, name?}]\` (Mode A) OR \`records[]\` with \`mappings\` (Mode B). At least one mapped field must be LEADBAY_ID, CRM_ID, SIREN, LEAD_NAME, or LEAD_WEBSITE. Discover the org's mappable surface via \`leadbay_list_mappable_fields\`. For messy files, prefer the \`leadbay_import_file\` prompt which walks an agent through scan \u2192 resolve \u2192 preserve \u2192 commit phases.
8829
9739
 
@@ -9163,7 +10073,29 @@ WHEN NOT TO USE: in normal flow \u2014 composites auto-resolve the active lens v
9163
10073
  `;
9164
10074
  var leadbay_list_locations = `Search the geo / admin-area taxonomy by free-text name and return the matching admin_area ids. This is the primary way to turn a user's "leads in Berlin" / "filter to Lyon" intent into the \`{type: "location_ids", locations: [<id>]}\` shape that the backend filter expects.
9165
10075
 
9166
- The response has two arrays: \`results\` (top-10 prefix matches ranked by relevance) and \`parents\` (the admin-area chain referenced by \`results[].parent_ids\`, useful for disambiguation breadcrumbs). Each entry: \`{id, country, level, name, parent_ids}\`. The \`level\` is the admin depth \u2014 **5** = region, **6** = county, **7** = township-area, **8** = city/town.
10076
+ The response has two arrays: \`results\` (top-10 prefix matches ranked by relevance) and \`parents\` (the admin-area chain referenced by \`results[].parent_ids\`, useful for disambiguation breadcrumbs). Each entry: \`{id, country, level, name, parent_ids}\`. The \`level\` is the admin depth \u2014 **5** = region, **6** = county, **7** = township-area, **8** = city/town. Country nodes are NOT in this index, so searching a country name cannot return that country \u2014 it returns whatever same-named town the trigram matcher finds (measured: \`France\` \u2192 the commune of Francs, \`United States\` \u2192 Statesboro). Passing such an id onward fences the caller to one village, so this tool refuses a country query outright and returns \`status: "country_level_location"\` with an empty \`results\`.
10077
+
10078
+ **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
10079
+
10080
+ **On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
10081
+
10082
+ \`axis: "include"\`:
10083
+
10084
+ - \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
10085
+ - \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
10086
+ - \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
10087
+ - \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
10088
+
10089
+ \`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
10090
+
10091
+ On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
10092
+
10093
+ **Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
10094
+
10095
+ Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
10096
+
10097
+
10098
+ **The include-axis recovery above does NOT apply to this tool.** "Omit the geo argument and the result covers everything" describes a tool that READS leads and can widen. This one resolves names to ids: \`q\` is REQUIRED, and the empty-\`q\` path returns no matches rather than workspace-wide data \u2014 so re-calling without it either fails validation or produces an empty lookup that would then be reported as full coverage. There is no country id to hand out and nothing to retry. Look up a place INSIDE the workspace instead; or, if the whole workspace was meant, skip this tool entirely \u2014 the tools that consume these ids just omit their geo argument.
9167
10099
 
9168
10100
  WHEN TO USE: to resolve a free-text city/region name before passing it to a \`location_ids\` filter (e.g. on \`leadbay_pull_followups({set_filter})\` or \`leadbay_adjust_audience\`). The composite \`leadbay_pull_followups\` accepts \`city: <free-text>\` directly and runs this resolver internally \u2014 prefer that path; reach for this granular tool only when you need to surface candidates to the user before committing.
9169
10101
 
@@ -9360,7 +10292,7 @@ Trigger phrases: "create a lens", "create a new lens called <name>", "create a l
9360
10292
 
9361
10293
  **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
9362
10294
 
9363
- 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\`.
10295
+ Do NOT use for: "companies anywhere in this workspace's OWN country / nationwide (a foreign country is unsupported, not unfiltered \u2014 call nothing)" \u2192 \`leadbay_pull_leads\`; "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\`.
9364
10296
 
9365
10297
  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).
9366
10298
 
@@ -9373,6 +10305,7 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
9373
10305
  - "Add fintech to my Joinery lens."
9374
10306
  - "Show me my lenses."
9375
10307
  - "I want more leads on this lens."
10308
+ - "Show me companies anywhere in the US."
9376
10309
 
9377
10310
  ## RENDER (quick)
9378
10311
 
@@ -9392,7 +10325,29 @@ Create a brand-new lens (saved audience) and apply its sector/size criteria. Clo
9392
10325
 
9393
10326
  **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\`.
9394
10327
 
9395
- **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.
10328
+ **Geography \u2014 scope a territory.** Pass \`locations\` (free text like \`["Indre-et-Loire"]\`, \`["Texas"]\`, 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\` at any level from state down to city (state / *r\xE9gion* / *d\xE9partement* / county / city). 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.
10329
+
10330
+ **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
10331
+
10332
+ **On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
10333
+
10334
+ \`axis: "include"\`:
10335
+
10336
+ - \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
10337
+ - \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
10338
+ - \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
10339
+ - \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
10340
+
10341
+ \`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
10342
+
10343
+ On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
10344
+
10345
+ **Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
10346
+
10347
+ Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
10348
+
10349
+
10350
+ **A new lens is a CLONE, and inherits the base lens's geography.** \`base\` defaults to the ACTIVE lens, so this applies even when no base was named. A criteria-less clone inherits the base audience wholesale, and adding sectors does not clear the base's location criteria either \u2014 so "nationwide healthcare" built on a Paris-scoped active lens creates a Paris healthcare lens under a nationwide name. Omitting \`locations\` is therefore not the same as having no geography. Read \`lens://<base>/definition\` before describing a new lens as workspace-wide, and say the scope is unverified if you cannot.
9396
10351
 
9397
10352
  **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.
9398
10353
 
@@ -9715,20 +10670,18 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
9715
10670
  `;
9716
10671
  var leadbay_pull_followups = `## WHEN TO USE
9717
10672
 
9718
- Trigger phrases: "what should I follow up on", "leads I've already worked", "what's overdue", "leads in <city / state / country>", "reach out to today", "should reach out to", "get back to", "contact today", "reconnect with", "re-engage", "leads to contact", "who should I ping".
10673
+ Trigger phrases: "what should I follow up on", "leads I've already worked", "what's overdue", "stale leads", "leads in <city / state / region>", "reach out to today", "should reach out to", "get back to", "contact today", "reconnect with", "re-engage", "leads to contact", "who should I ping".
9719
10674
 
9720
10675
  **Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
9721
10676
 
9722
10677
  Do NOT use for: "new leads / today's prospects" \u2192 \`leadbay_pull_leads\`; "map / trip / in person" \u2192 \`leadbay_followups_map\`.
9723
10678
 
9724
- Prefer when: known Monitor leads; pass \`city\` or \`set_filter\` for geo/sector/recency
10679
+ Prefer when: known Monitor leads; pass \`city\` or \`set_filter\` for geo/sector/recency; NEVER a country name \u2014 a whole-country ask means NO geo filter
9725
10680
 
9726
10681
  Examples that SHOULD invoke this tool:
9727
10682
  - "What should I follow up on this week?"
9728
10683
  - "What's overdue in my pipeline?"
9729
10684
  - "Show me leads I should reach out to today."
9730
- - "Who should I get back to today?"
9731
- - "Leads I should contact today."
9732
10685
 
9733
10686
  Examples that should NOT invoke this tool (sound similar, route elsewhere):
9734
10687
  - "Show me today's new leads."
@@ -9745,7 +10698,7 @@ table. Detail + status priority below.
9745
10698
 
9746
10699
  ---
9747
10700
 
9748
- Pull KNOWN leads from the user's Monitor view \u2014 the re-engagement entry point. Use when the user asks "what should I follow up on", "leads I haven't contacted", "leads in [city]", "before my trip", or any phrasing implying pre-existing pipeline context. For NEW leads from Discover, use \`leadbay_pull_leads\`.
10701
+ Pull KNOWN leads from the user's Monitor view \u2014 the re-engagement entry point.
9749
10702
 
9750
10703
  Backend: wraps \`GET /1.6/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.6/monitor/filter\`. The Monitor filter is a single \`FilterItem\` per user \u2014 refreshing restores it.
9751
10704
 
@@ -9753,24 +10706,44 @@ Backend: wraps \`GET /1.6/monitor?personal=&liked=&filtered=&count=&page=\` plus
9753
10706
 
9754
10707
  Practical mapping from user phrasing to criterion:
9755
10708
 
9756
- | User phrase | Criterion |
9757
- |--------------------------------------|----------------------------------------------------------------------|
9758
- | "leads in Lyon" | \`{type: "location_ids", locations: [<admin_area_id>]}\` |
9759
- | "healthcare staffing" | \`{type: "keywords", keywords: ["healthcare", "staffing"]}\` |
9760
- | "leads I haven't touched in 30 days" | \`{type: "last_action_date", last_days: 30}\` |
9761
- | "leads I liked" | \`{type: "liked"}\` |
9762
- | "leads 50\u2013200 employees" | \`{type: "size", sizes: [{min: 50, max: 200}]}\` |
9763
- | "Y Combinator companies" | \`{type: "yc"}\` |
10709
+ | User phrase | Criterion |
10710
+ |---|---|
10711
+ | "leads in Lyon" | \`{type: "location_ids", locations: [<admin_area_id>]}\` |
10712
+ | "healthcare staffing" | \`{type: "keywords", keywords: ["healthcare", "staffing"]}\` |
10713
+ | "leads I haven't touched in 30 days" | \`{type: "last_action_date", last_days: 30}\` |
10714
+ | "leads I liked" | \`{type: "liked"}\` |
10715
+ | "leads 50\u2013200 employees" | \`{type: "size", sizes: [{min: 50, max: 200}]}\` |
10716
+ | "Y Combinator companies" | \`{type: "yc"}\` |
9764
10717
 
9765
10718
  Geo filtering needs \`admin_area_id\` resolution \u2014 backend rejects free-text in \`location_ids\`. Pass \`city: "<free-text>"\` and the composite calls \`/geo/search\` internally, picks the best match, merges its id into \`set_filter\`. Ambiguous matches return \`status: "ambiguous_locations"\` + \`location_ambiguities[]\` \u2014 pick an id and re-call with \`city_id\`.
9766
10719
 
9767
- **Place names go through \`city\`, NEVER \`keywords\`.** Any geographic token the user names \u2014 cities (\`"Berlin"\`), states/regions (\`"Texas"\`, \`"Bavaria"\`), countries (\`"France"\`), neighborhoods (\`"Brooklyn"\`) \u2014 resolves via \`/geo/search\` (all admin levels). A place name in \`keywords\` becomes a TEXT-MATCH against company descriptions (\u22480 hits), not a real filter. If a place resolves ambiguously, surface the choices \u2014 never silently fall back to keyword search or the unfiltered view.
10720
+ In \`keywords\` a place name is a TEXT-MATCH on company descriptions (\u22480 hits), not a filter \u2014 never fall back to it, nor to the unfiltered view, when a place is ambiguous.
9768
10721
 
9769
- **Pushback exclusion.** Leads with active pushback (\`pushback_status\` set, \`pushback_until > today\`) are excluded client-side; \`total_excluded_by_pushback\` reports how many rows were dropped.
10722
+ **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
10723
+
10724
+ **On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
10725
+
10726
+ \`axis: "include"\`:
10727
+
10728
+ - \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
10729
+ - \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
10730
+ - \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
10731
+ - \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
9770
10732
 
9771
- WHEN TO USE: re-engaging pipeline ("what should I follow up on", "stale leads"), filtering monitored leads by city / sector / recency / action type / liked. The canonical orchestrator is the \`leadbay_followup_check_in\` prompt.
10733
+ \`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
9772
10734
 
9773
- WHEN NOT TO USE: for NEW leads \u2014 that's \`leadbay_pull_leads\` (Discover).
10735
+ On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
10736
+
10737
+ **Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
10738
+
10739
+ Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
10740
+
10741
+
10742
+ **A whole-workspace read also needs \`filtered:false\`.** Omitting \`city\` does not widen this tool \u2014 \`filtered\` defaults to true, so a filter persisted earlier still applies and its stale cohort reads as everything. If other criteria were requested, re-send them in \`set_filter\` instead; \`active_filters\` reports what applied.
10743
+
10744
+ **Pushback exclusion.** Leads with active pushback (\`pushback_status\` set, \`pushback_until > today\`) are excluded client-side; \`total_excluded_by_pushback\` reports how many rows were dropped.
10745
+
10746
+ The canonical orchestrator for a re-engagement pass is the \`leadbay_followup_check_in\` prompt.
9774
10747
 
9775
10748
  **Anti-confusion guardrail.** Iterating \`pull_leads\` pages looking for \`prospecting_actions_count > 0\` or \`notes_count > 0\` rows is the wrong entry point \u2014 the two read different tables. Leads with follow-up history live in \`pull_followups\`.
9776
10749
 
@@ -9792,16 +10765,16 @@ Markdown table with FOUR columns, sorted by \`last_monitor_action_at\` desc. **N
9792
10765
 
9793
10766
  **Active-filters line** ABOVE the table, \` \xB7 \`-separated chips from \`active_filters.criteria\`:
9794
10767
 
9795
- | Criterion type | Chip |
9796
- |-----------------------|----------------------------|
9797
- | \`location_ids\` | \u{1F4CD} \\<resolved name\\> |
9798
- | \`sector_ids\` | \u{1F3F7} \\<sector name\\> |
9799
- | \`keywords\` | \u{1F50D} \\<keyword\\> |
9800
- | \`size\` | \u{1F465} \\<min\\>\u2013\\<max\\> |
9801
- | \`last_action_date\` | \u{1F4C5} \\<window\\> |
9802
- | \`last_action\` | \u{1F3AF} \\<action types\\> |
9803
- | \`liked\` / \`yc\` | \u2B50 liked / \u{1F3C5} YC |
9804
- | \`custom_field*\` | \u2699 \\<field name\\> |
10768
+ | Criterion type | Chip |
10769
+ | --- | --- |
10770
+ | \`location_ids\` | \u{1F4CD} \\<resolved name\\> |
10771
+ | \`sector_ids\` | \u{1F3F7} \\<sector name\\> |
10772
+ | \`keywords\` | \u{1F50D} \\<keyword\\> |
10773
+ | \`size\` | \u{1F465} \\<min\\>\u2013\\<max\\> |
10774
+ | \`last_action_date\` | \u{1F4C5} \\<window\\> |
10775
+ | \`last_action\` | \u{1F3AF} \\<action types\\> |
10776
+ | \`liked\` / \`yc\` | \u2B50 liked / \u{1F3C5} YC |
10777
+ | \`custom_field*\` | \u2699 \\<field name\\> |
9805
10778
 
9806
10779
  Render \`*No filters applied.*\` when empty.
9807
10780
 
@@ -9888,21 +10861,19 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
9888
10861
 
9889
10862
 
9890
10863
 
9891
- Always include at least one filter-modification offer (users think in filters: by city, by recency, by action type). Filter modification goes through \`set_filter: FilterItem\` which the composite POSTs to \`/monitor/filter\` server-side.
9892
-
9893
- | Observation | Suggest | Calls |
9894
- |-----------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------|
9895
- | Always (top of menu) | "Prep outreach for [top row's contact]" | leadbay_prepare_outreach(leadId) |
9896
- | User named a city / sector / timeframe | "Refilter by [their phrase]" | leadbay_pull_followups(set_filter: { criteria: [...] }) |
9897
- | \`pagination.has_more == true\` | "Pull the next page" | leadbay_pull_followups(page = current + 1) |
9898
- | \u22653 rows \u2728 (never-touched) | "Surface only never-touched leads" | set_filter with \`last_action_date.last_days = 0\` |
9899
- | \u22653 rows \u26A1 (Trying to reach) | "Focus on overdue commitments" | set_filter with \`last_action.types = ["EPILOGUE_COULD_NOT_REACH_STILL_TRYING"]\` |
9900
- | User planning a trip / in a city | "Group by city for trip planning" | leadbay_pull_followups({city: "<their city>"}) \u2014 composite resolves admin_area_id via /geo/search |
9901
- | All rows last action > 60d | "Re-qualify \u2014 context may have changed" | leadbay_bulk_qualify_leads([leadId, ...]) |
9902
- | One obvious priority row | "Take me to that lead's full brief" | leadbay_prepare_outreach(leadId) / leadbay_research_lead_by_id(leadId) |
9903
- | User wants to defer a lead | "Snooze [Company] for 3 / 6 / 12 months" | leadbay_set_pushback({ lead_ids:[leadId], status:"3" }) |
9904
- | User completed outreach mid-flow | "Log the outreach + record the outcome" | leadbay_report_outreach |
9905
- | Discovery mode might fit better | "Looking for NEW leads instead? Switch to discovery." | leadbay_pull_leads |
10864
+ | Observation | Suggest | Calls |
10865
+ |---|---|---|
10866
+ | Always (top of menu) | "Prep outreach for [top row's contact]" | leadbay_prepare_outreach(leadId) |
10867
+ | User named a city / sector / timeframe | "Refilter by [their phrase]" | leadbay_pull_followups(set_filter: { criteria: [...] }) |
10868
+ | \`pagination.has_more == true\` | "Pull the next page" | leadbay_pull_followups(page = current + 1) |
10869
+ | \u22653 rows \u2728 (never-touched) | "Surface only never-touched leads" | set_filter with \`last_action_date.last_days = 0\` |
10870
+ | \u22653 rows \u26A1 (Trying to reach) | "Focus on overdue commitments" | set_filter with \`last_action.types = ["EPILOGUE_COULD_NOT_REACH_STILL_TRYING"]\` |
10871
+ | User planning a trip / in a city | "Group by city for trip planning" | leadbay_pull_followups({city: "<their city>"}) \u2014 composite resolves admin_area_id via /geo/search |
10872
+ | All rows last action > 60d | "Re-qualify \u2014 context may have changed" | leadbay_bulk_qualify_leads([leadId, ...]) |
10873
+ | One obvious priority row | "Take me to that lead's full brief" | leadbay_prepare_outreach(leadId) / leadbay_research_lead_by_id(leadId) |
10874
+ | User wants to defer a lead | "Snooze [Company] for 3 / 6 / 12 months" | leadbay_set_pushback({ lead_ids:[leadId], status:"3" }) |
10875
+ | User completed outreach mid-flow | "Log the outreach + record the outcome" | leadbay_report_outreach |
10876
+ | Discovery mode might fit better | "Looking for NEW leads instead? Switch to discovery." | leadbay_pull_leads |
9906
10877
  Always offer at least one of: prep outreach, refilter, pushback. Pushback is the canonical way to honor "not now" / "next quarter" \u2014 leads with active pushback are excluded from this view until expiry.
9907
10878
  `;
9908
10879
  var leadbay_pull_leads = `## WHEN TO USE
@@ -10747,7 +11718,7 @@ Trigger phrases: "which of my leads <did X>", "find leads that <raised / acquire
10747
11718
 
10748
11719
  Do NOT use for: "research one named company" \u2192 \`leadbay_research_lead_by_name_fuzzy\`; "everything about lead <UUID>" \u2192 \`leadbay_research_lead_by_id\`; "qualify my next N leads (they aren't researched yet)" \u2192 \`leadbay_bulk_qualify_leads\`; "just list my follow-ups" \u2192 \`leadbay_pull_followups\`.
10749
11720
 
10750
- Prefer when: user wants to FILTER a known portfolio by a web-research signal in bulk \u2014 pass \`query\`, optionally \`since\`, \`city\`/\`set_filter\`, or \`leadIds\`
11721
+ Prefer when: user wants to FILTER a known portfolio by a web-research signal in bulk \u2014 pass \`query\`, optionally \`since\`, \`city\`/\`set_filter\`, or \`leadIds\`; NEVER a country name in \`city\` \u2014 a whole-country ask means NO geo filter
10751
11722
 
10752
11723
  Examples that SHOULD invoke this tool:
10753
11724
  - "Which of my leads acquired a company since 2025?"
@@ -10784,7 +11755,27 @@ match". Qualify them with \`leadbay_bulk_qualify_leads\`, then re-scan.
10784
11755
 
10785
11756
  **Scope.** Pass \`leadIds\` for an explicit cohort, or omit it to scan the
10786
11757
  Monitor portfolio. Narrow the Monitor scope with \`city\` / \`set_filter\` exactly
10787
- as \`leadbay_pull_followups\` does (store-then-apply server-side filter). The
11758
+ as \`leadbay_pull_followups\` does (store-then-apply server-side filter).
11759
+
11760
+ **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
11761
+
11762
+ **On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
11763
+
11764
+ \`axis: "include"\`:
11765
+
11766
+ - \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
11767
+ - \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
11768
+ - \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
11769
+ - \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
11770
+
11771
+ \`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
11772
+
11773
+ On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
11774
+
11775
+ **Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
11776
+
11777
+ Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
11778
+ The
10788
11779
  scan is bounded by \`max_leads\` (default 200, hard cap 300); when the portfolio
10789
11780
  is larger, \`truncated_at\` is set and coverage is partial \u2014 say so.
10790
11781
 
@@ -11226,7 +12217,7 @@ Trigger phrases: "visiting <city> in <N> days", "I'm in <city> next week / Tuesd
11226
12217
 
11227
12218
  Do NOT use for: "follow-ups only, no new prospects" \u2192 \`leadbay_followups_map\`; "new leads only" \u2192 \`leadbay_pull_leads\`; "research one account" \u2192 \`leadbay_research_lead_by_id\`.
11228
12219
 
11229
- Prefer when: user wants known accounts plus new discoveries in one geographic itinerary
12220
+ Prefer when: user wants known accounts plus new discoveries in one geographic itinerary; NEVER a country name \u2014 unlike the Monitor tools, do NOT omit \`city\`; a city-less tour is arbitrary nationwide leads, so ask which city or region
11230
12221
 
11231
12222
  Examples that SHOULD invoke this tool:
11232
12223
  - "I'm flying to Limoges in 4 days \u2014 give me 3 customers, 3 qualified prospects, and 3 new high-potential."
@@ -11255,7 +12246,35 @@ prose paragraph. Full recipe below.
11255
12246
 
11256
12247
  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."*
11257
12248
 
11258
- **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\`.
12249
+ **Geo resolution** is identical to \`leadbay_followups_map\`: pass \`city\` (any level from state down to neighborhood \u2014 state, *r\xE9gion*, county, city \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\`.
12250
+
12251
+ **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
12252
+
12253
+ **On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
12254
+
12255
+ \`axis: "include"\`:
12256
+
12257
+ - \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
12258
+ - \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
12259
+ - \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
12260
+ - \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
12261
+
12262
+ \`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
12263
+
12264
+ On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
12265
+
12266
+ **Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
12267
+
12268
+ Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
12269
+
12270
+
12271
+ **Tour-specific override of the rule above.** For a tour, the home-country
12272
+ recovery ("omit the geo argument") does NOT apply: this tool accepts a missing
12273
+ \`city\` and then returns arbitrary leads from across the whole workspace, which is
12274
+ not an itinerary. So for ANY country-level \`city\` \u2014 this workspace's own included
12275
+ \u2014 do not drop the argument. Ask which city or region the user is actually
12276
+ visiting and re-call with that. \`status: "country_level_location"\` carries the
12277
+ same instruction in its \`hint\`.
11259
12278
 
11260
12279
  **Counts**: \`followups_count\` (default 6 \u2014 generous so the agent can split into "customers + qualified" client-side) and \`discover_count\` (default 6 after client-side geo filter). The composite over-pulls Discover (30 raw) because the wishlist endpoint has no server-side geo filter \u2014 it then filters by \`location.city/state/country/full\` substring match against the requested city. The \`discover_filter_note\` string in the response tells the agent the match ratio so it can be honest about coverage ("matched 3/30 by city/state" vs. "matched 12/30").
11261
12280
 
@@ -11434,7 +12453,27 @@ WHEN NOT TO USE: to change which leads the lens shows \u2014 that's a filter ope
11434
12453
 
11435
12454
  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\`.
11436
12455
  `;
11437
- var leadbay_update_lens_filter = `Replace the audience filter (sectors, sizes, locations) on a lens. Body is the full \`Filter\` object \u2014 this is a REPLACE, not a merge. Returns 400 \`default_lens\` if applied to the org default lens (clone it first). \`dry_run:true\` returns the call shape without contacting the backend.
12456
+ var leadbay_update_lens_filter = `Replace the audience filter (sectors, sizes, locations) on a lens. Body is the full \`Filter\` object \u2014 this is a REPLACE, not a merge. Returns 400 \`default_lens\` if applied to the org default lens (clone it first). \`dry_run:true\` returns the call shape without contacting the backend. A country name anywhere in the payload's \`location_ids\` criteria (or in the echoed \`locations.results[]\` block) is rejected with \`code: "COUNTRY_LEVEL_LOCATION"\` \u2014 including on a dry run, so a preview can never suggest such a body is valid.
12457
+
12458
+ **One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
12459
+
12460
+ **On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
12461
+
12462
+ \`axis: "include"\`:
12463
+
12464
+ - \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
12465
+ - \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
12466
+ - \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
12467
+ - \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
12468
+
12469
+ \`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
12470
+
12471
+ On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
12472
+
12473
+ **Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
12474
+
12475
+ Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
12476
+
11438
12477
 
11439
12478
  WHEN TO USE: low-level mutation when you've already prepared the merged filter.
11440
12479
 
@@ -12240,45 +13279,1093 @@ var getLensScoring = {
12240
13279
  execute: async (client, params) => {
12241
13280
  return await client.request("GET", `/lenses/${params.lensId}/scoring`);
12242
13281
  }
12243
- };
12244
-
12245
- // ../core/dist/tools/list-sectors.js
12246
- var listSectors = {
12247
- name: "leadbay_list_sectors",
12248
- annotations: {
12249
- title: "List sector taxonomy",
12250
- readOnlyHint: true,
12251
- destructiveHint: false,
12252
- idempotentHint: true,
12253
- openWorldHint: true
12254
- },
12255
- description: leadbay_list_sectors,
12256
- inputSchema: {
12257
- type: "object",
12258
- properties: {
12259
- lang: { type: "string", description: "BCP-47 language tag (default: en)" },
12260
- includeInvisible: {
12261
- type: "boolean",
12262
- description: "Include sectors hidden from the UI (default false; ~91k items if true)"
13282
+ };
13283
+
13284
+ // ../core/dist/tools/list-sectors.js
13285
+ var listSectors = {
13286
+ name: "leadbay_list_sectors",
13287
+ annotations: {
13288
+ title: "List sector taxonomy",
13289
+ readOnlyHint: true,
13290
+ destructiveHint: false,
13291
+ idempotentHint: true,
13292
+ openWorldHint: true
13293
+ },
13294
+ description: leadbay_list_sectors,
13295
+ inputSchema: {
13296
+ type: "object",
13297
+ properties: {
13298
+ lang: { type: "string", description: "BCP-47 language tag (default: en)" },
13299
+ includeInvisible: {
13300
+ type: "boolean",
13301
+ description: "Include sectors hidden from the UI (default false; ~91k items if true)"
13302
+ }
13303
+ },
13304
+ additionalProperties: false
13305
+ },
13306
+ execute: async (client, params) => {
13307
+ let lang = params.lang;
13308
+ if (!lang) {
13309
+ try {
13310
+ const me = await client.resolveMe();
13311
+ lang = me.language ?? "en";
13312
+ } catch {
13313
+ lang = "en";
13314
+ }
13315
+ }
13316
+ const includeInvisible = params.includeInvisible ? "true" : "false";
13317
+ const path = `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=${includeInvisible}`;
13318
+ return await client.request("GET", path);
13319
+ }
13320
+ };
13321
+
13322
+ // ../core/dist/composite/_country-names.js
13323
+ function countryKey(raw) {
13324
+ return raw.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().replace(/\./g, "").replace(/['’`]/g, " ").replace(/[^a-z0-9]+/g, " ").replace(/\s+/g, " ").trim().replace(/^(les|the|la|le|l|el|los)\s+/, "").trim();
13325
+ }
13326
+ var COUNTRIES = [
13327
+ { iso2: "AD", iso3: "AND", name: "Andorra", nameFr: "Andorre" },
13328
+ { iso2: "AE", iso3: "ARE", name: "United Arab Emirates", nameFr: "\xC9mirats arabes unis", aliases: ["UAE"] },
13329
+ { iso2: "AF", iso3: "AFG", name: "Afghanistan", nameFr: "Afghanistan" },
13330
+ { iso2: "AG", iso3: "ATG", name: "Antigua and Barbuda", nameFr: "Antigua-et-Barbuda", aliases: ["Antigua & Barbuda", "Antigua"] },
13331
+ { iso2: "AI", iso3: "AIA", name: "Anguilla", nameFr: "Anguilla", sovereign: "GB" },
13332
+ { iso2: "AL", iso3: "ALB", name: "Albania", nameFr: "Albanie" },
13333
+ { iso2: "AM", iso3: "ARM", name: "Armenia", nameFr: "Arm\xE9nie" },
13334
+ { iso2: "AO", iso3: "AGO", name: "Angola", nameFr: "Angola" },
13335
+ { iso2: "AQ", iso3: "ATA", name: "Antarctica", nameFr: "Antarctique" },
13336
+ { iso2: "AR", iso3: "ARG", name: "Argentina", nameFr: "Argentine" },
13337
+ { iso2: "AS", iso3: "ASM", name: "American Samoa", nameFr: "Samoa am\xE9ricaines", sovereign: "US" },
13338
+ { iso2: "AT", iso3: "AUT", name: "Austria", nameFr: "Autriche" },
13339
+ { iso2: "AU", iso3: "AUS", name: "Australia", nameFr: "Australie" },
13340
+ { iso2: "AW", iso3: "ABW", name: "Aruba", nameFr: "Aruba", sovereign: "NL" },
13341
+ { iso2: "AX", iso3: "ALA", name: "\xC5land Islands", nameFr: "\xCEles \xC5land", sovereign: "FI" },
13342
+ { iso2: "AZ", iso3: "AZE", name: "Azerbaijan", nameFr: "Azerba\xEFdjan" },
13343
+ { iso2: "BA", iso3: "BIH", name: "Bosnia and Herzegovina", nameFr: "Bosnie-Herz\xE9govine", aliases: ["Bosnia & Herzegovina", "Bosnia"] },
13344
+ { iso2: "BB", iso3: "BRB", name: "Barbados", nameFr: "Barbade" },
13345
+ { iso2: "BD", iso3: "BGD", name: "Bangladesh", nameFr: "Bangladesh" },
13346
+ { iso2: "BE", iso3: "BEL", name: "Belgium", nameFr: "Belgique" },
13347
+ { iso2: "BF", iso3: "BFA", name: "Burkina Faso", nameFr: "Burkina Faso" },
13348
+ { iso2: "BG", iso3: "BGR", name: "Bulgaria", nameFr: "Bulgarie" },
13349
+ { iso2: "BH", iso3: "BHR", name: "Bahrain", nameFr: "Bahre\xEFn" },
13350
+ { iso2: "BI", iso3: "BDI", name: "Burundi", nameFr: "Burundi" },
13351
+ { iso2: "BJ", iso3: "BEN", name: "Benin", nameFr: "B\xE9nin" },
13352
+ { iso2: "BL", iso3: "BLM", name: "Saint Barth\xE9lemy", nameFr: "Saint-Barth\xE9lemy", sovereign: "FR" },
13353
+ { iso2: "BM", iso3: "BMU", name: "Bermuda", nameFr: "Bermudes", sovereign: "GB" },
13354
+ { iso2: "BN", iso3: "BRN", name: "Brunei Darussalam", nameFr: "Brun\xE9i", aliases: ["Brunei"] },
13355
+ { iso2: "BO", iso3: "BOL", name: "Bolivia", nameFr: "Bolivie" },
13356
+ { iso2: "BQ", iso3: "BES", name: "Bonaire, Sint Eustatius and Saba", nameFr: "Pays-Bas carib\xE9ens", sovereign: "NL" },
13357
+ { iso2: "BR", iso3: "BRA", name: "Brazil", nameFr: "Br\xE9sil" },
13358
+ { iso2: "BS", iso3: "BHS", name: "Bahamas", nameFr: "Bahamas" },
13359
+ { iso2: "BT", iso3: "BTN", name: "Bhutan", nameFr: "Bhoutan" },
13360
+ { iso2: "BV", iso3: "BVT", name: "Bouvet Island", nameFr: "\xCEle Bouvet", sovereign: "NO" },
13361
+ { iso2: "BW", iso3: "BWA", name: "Botswana", nameFr: "Botswana" },
13362
+ { iso2: "BY", iso3: "BLR", name: "Belarus", nameFr: "Bi\xE9lorussie" },
13363
+ { iso2: "BZ", iso3: "BLZ", name: "Belize", nameFr: "Belize" },
13364
+ { iso2: "CA", iso3: "CAN", name: "Canada", nameFr: "Canada" },
13365
+ { iso2: "CC", iso3: "CCK", name: "Cocos (Keeling) Islands", nameFr: "\xCEles Cocos", sovereign: "AU" },
13366
+ { iso2: "CD", iso3: "COD", name: "Democratic Republic of the Congo", nameFr: "R\xE9publique d\xE9mocratique du Congo", aliases: ["DR Congo", "DRC", "Congo-Kinshasa"] },
13367
+ { iso2: "CF", iso3: "CAF", name: "Central African Republic", nameFr: "R\xE9publique centrafricaine" },
13368
+ { iso2: "CG", iso3: "COG", name: "Congo", nameFr: "Congo", aliases: ["Republic of the Congo", "Congo-Brazzaville"] },
13369
+ { iso2: "CH", iso3: "CHE", name: "Switzerland", nameFr: "Suisse" },
13370
+ { iso2: "CI", iso3: "CIV", name: "C\xF4te d'Ivoire", nameFr: "C\xF4te d'Ivoire", aliases: ["Ivory Coast"] },
13371
+ { iso2: "CK", iso3: "COK", name: "Cook Islands", nameFr: "\xCEles Cook", sovereign: "NZ" },
13372
+ { iso2: "CL", iso3: "CHL", name: "Chile", nameFr: "Chili" },
13373
+ { iso2: "CM", iso3: "CMR", name: "Cameroon", nameFr: "Cameroun" },
13374
+ { iso2: "CN", iso3: "CHN", name: "China", nameFr: "Chine" },
13375
+ { iso2: "CO", iso3: "COL", name: "Colombia", nameFr: "Colombie" },
13376
+ { iso2: "CR", iso3: "CRI", name: "Costa Rica", nameFr: "Costa Rica" },
13377
+ { iso2: "CU", iso3: "CUB", name: "Cuba", nameFr: "Cuba" },
13378
+ { iso2: "CV", iso3: "CPV", name: "Cabo Verde", nameFr: "Cap-Vert", aliases: ["Cape Verde"] },
13379
+ { iso2: "CW", iso3: "CUW", name: "Cura\xE7ao", nameFr: "Cura\xE7ao", sovereign: "NL" },
13380
+ { iso2: "CX", iso3: "CXR", name: "Christmas Island", nameFr: "\xCEle Christmas", sovereign: "AU" },
13381
+ { iso2: "CY", iso3: "CYP", name: "Cyprus", nameFr: "Chypre" },
13382
+ { iso2: "CZ", iso3: "CZE", name: "Czechia", nameFr: "Tch\xE9quie", aliases: ["Czech Republic"] },
13383
+ { iso2: "DE", iso3: "DEU", name: "Germany", nameFr: "Allemagne", aliases: ["Deutschland"] },
13384
+ { iso2: "DJ", iso3: "DJI", name: "Djibouti", nameFr: "Djibouti" },
13385
+ { iso2: "DK", iso3: "DNK", name: "Denmark", nameFr: "Danemark" },
13386
+ { iso2: "DM", iso3: "DMA", name: "Dominica", nameFr: "Dominique" },
13387
+ { iso2: "DO", iso3: "DOM", name: "Dominican Republic", nameFr: "R\xE9publique dominicaine" },
13388
+ { iso2: "DZ", iso3: "DZA", name: "Algeria", nameFr: "Alg\xE9rie" },
13389
+ { iso2: "EC", iso3: "ECU", name: "Ecuador", nameFr: "\xC9quateur" },
13390
+ { iso2: "EE", iso3: "EST", name: "Estonia", nameFr: "Estonie" },
13391
+ { iso2: "EG", iso3: "EGY", name: "Egypt", nameFr: "\xC9gypte" },
13392
+ { iso2: "EH", iso3: "ESH", name: "Western Sahara", nameFr: "Sahara occidental" },
13393
+ { iso2: "ER", iso3: "ERI", name: "Eritrea", nameFr: "\xC9rythr\xE9e" },
13394
+ { iso2: "ES", iso3: "ESP", name: "Spain", nameFr: "Espagne", aliases: ["Espa\xF1a"] },
13395
+ { iso2: "ET", iso3: "ETH", name: "Ethiopia", nameFr: "\xC9thiopie" },
13396
+ { iso2: "FI", iso3: "FIN", name: "Finland", nameFr: "Finlande" },
13397
+ { iso2: "FJ", iso3: "FJI", name: "Fiji", nameFr: "Fidji" },
13398
+ { iso2: "FK", iso3: "FLK", name: "Falkland Islands", nameFr: "\xCEles Malouines", sovereign: "GB" },
13399
+ { iso2: "FM", iso3: "FSM", name: "Micronesia", nameFr: "Micron\xE9sie" },
13400
+ { iso2: "FO", iso3: "FRO", name: "Faroe Islands", nameFr: "\xCEles F\xE9ro\xE9", sovereign: "DK" },
13401
+ { iso2: "FR", iso3: "FRA", name: "France", nameFr: "France", aliases: ["French Republic", "R\xE9publique fran\xE7aise"] },
13402
+ { iso2: "GA", iso3: "GAB", name: "Gabon", nameFr: "Gabon" },
13403
+ { iso2: "GB", iso3: "GBR", name: "United Kingdom", nameFr: "Royaume-Uni", aliases: ["UK", "Great Britain", "Britain", "United Kingdom of Great Britain and Northern Ireland"] },
13404
+ { iso2: "GD", iso3: "GRD", name: "Grenada", nameFr: "Grenade" },
13405
+ { iso2: "GE", iso3: "GEO", name: "Georgia", nameFr: "G\xE9orgie" },
13406
+ { iso2: "GF", iso3: "GUF", name: "French Guiana", nameFr: "Guyane fran\xE7aise", sovereign: "FR", aliases: ["Guyane"] },
13407
+ { iso2: "GG", iso3: "GGY", name: "Guernsey", nameFr: "Guernesey", sovereign: "GB" },
13408
+ { iso2: "GH", iso3: "GHA", name: "Ghana", nameFr: "Ghana" },
13409
+ { iso2: "GI", iso3: "GIB", name: "Gibraltar", nameFr: "Gibraltar", sovereign: "GB" },
13410
+ { iso2: "GL", iso3: "GRL", name: "Greenland", nameFr: "Groenland", sovereign: "DK" },
13411
+ { iso2: "GM", iso3: "GMB", name: "Gambia", nameFr: "Gambie" },
13412
+ { iso2: "GN", iso3: "GIN", name: "Guinea", nameFr: "Guin\xE9e" },
13413
+ { iso2: "GP", iso3: "GLP", name: "Guadeloupe", nameFr: "Guadeloupe", sovereign: "FR" },
13414
+ { iso2: "GQ", iso3: "GNQ", name: "Equatorial Guinea", nameFr: "Guin\xE9e \xE9quatoriale" },
13415
+ { iso2: "GR", iso3: "GRC", name: "Greece", nameFr: "Gr\xE8ce" },
13416
+ { iso2: "GS", iso3: "SGS", name: "South Georgia and the South Sandwich Islands", nameFr: "G\xE9orgie du Sud-et-les \xCEles Sandwich du Sud", sovereign: "GB" },
13417
+ { iso2: "GT", iso3: "GTM", name: "Guatemala", nameFr: "Guatemala" },
13418
+ { iso2: "GU", iso3: "GUM", name: "Guam", nameFr: "Guam", sovereign: "US" },
13419
+ { iso2: "GW", iso3: "GNB", name: "Guinea-Bissau", nameFr: "Guin\xE9e-Bissau" },
13420
+ { iso2: "GY", iso3: "GUY", name: "Guyana", nameFr: "Guyana" },
13421
+ { iso2: "HK", iso3: "HKG", name: "Hong Kong", nameFr: "Hong Kong", sovereign: "CN" },
13422
+ { iso2: "HM", iso3: "HMD", name: "Heard Island and McDonald Islands", nameFr: "\xCEles Heard-et-MacDonald", sovereign: "AU" },
13423
+ { iso2: "HN", iso3: "HND", name: "Honduras", nameFr: "Honduras" },
13424
+ { iso2: "HR", iso3: "HRV", name: "Croatia", nameFr: "Croatie" },
13425
+ { iso2: "HT", iso3: "HTI", name: "Haiti", nameFr: "Ha\xEFti" },
13426
+ { iso2: "HU", iso3: "HUN", name: "Hungary", nameFr: "Hongrie" },
13427
+ { iso2: "ID", iso3: "IDN", name: "Indonesia", nameFr: "Indon\xE9sie" },
13428
+ { iso2: "IE", iso3: "IRL", name: "Ireland", nameFr: "Irlande" },
13429
+ { iso2: "IL", iso3: "ISR", name: "Israel", nameFr: "Isra\xEBl" },
13430
+ { iso2: "IM", iso3: "IMN", name: "Isle of Man", nameFr: "\xCEle de Man", sovereign: "GB" },
13431
+ { iso2: "IN", iso3: "IND", name: "India", nameFr: "Inde" },
13432
+ { iso2: "IO", iso3: "IOT", name: "British Indian Ocean Territory", nameFr: "Territoire britannique de l'oc\xE9an Indien", sovereign: "GB" },
13433
+ { iso2: "IQ", iso3: "IRQ", name: "Iraq", nameFr: "Irak" },
13434
+ { iso2: "IR", iso3: "IRN", name: "Iran", nameFr: "Iran" },
13435
+ { iso2: "IS", iso3: "ISL", name: "Iceland", nameFr: "Islande" },
13436
+ { iso2: "IT", iso3: "ITA", name: "Italy", nameFr: "Italie" },
13437
+ { iso2: "JE", iso3: "JEY", name: "Jersey", nameFr: "Jersey", sovereign: "GB" },
13438
+ { iso2: "JM", iso3: "JAM", name: "Jamaica", nameFr: "Jama\xEFque" },
13439
+ { iso2: "JO", iso3: "JOR", name: "Jordan", nameFr: "Jordanie" },
13440
+ { iso2: "JP", iso3: "JPN", name: "Japan", nameFr: "Japon" },
13441
+ { iso2: "KE", iso3: "KEN", name: "Kenya", nameFr: "Kenya" },
13442
+ { iso2: "KG", iso3: "KGZ", name: "Kyrgyzstan", nameFr: "Kirghizistan" },
13443
+ { iso2: "KH", iso3: "KHM", name: "Cambodia", nameFr: "Cambodge" },
13444
+ { iso2: "KI", iso3: "KIR", name: "Kiribati", nameFr: "Kiribati" },
13445
+ { iso2: "KM", iso3: "COM", name: "Comoros", nameFr: "Comores" },
13446
+ { iso2: "KN", iso3: "KNA", name: "Saint Kitts and Nevis", nameFr: "Saint-Christophe-et-Ni\xE9v\xE8s" },
13447
+ { iso2: "KP", iso3: "PRK", name: "North Korea", nameFr: "Cor\xE9e du Nord" },
13448
+ { iso2: "KR", iso3: "KOR", name: "South Korea", nameFr: "Cor\xE9e du Sud" },
13449
+ { iso2: "KW", iso3: "KWT", name: "Kuwait", nameFr: "Kowe\xEFt" },
13450
+ { iso2: "KY", iso3: "CYM", name: "Cayman Islands", nameFr: "\xCEles Ca\xEFmans", sovereign: "GB" },
13451
+ { iso2: "KZ", iso3: "KAZ", name: "Kazakhstan", nameFr: "Kazakhstan" },
13452
+ { iso2: "LA", iso3: "LAO", name: "Laos", nameFr: "Laos" },
13453
+ { iso2: "LB", iso3: "LBN", name: "Lebanon", nameFr: "Liban" },
13454
+ { iso2: "LC", iso3: "LCA", name: "Saint Lucia", nameFr: "Sainte-Lucie" },
13455
+ { iso2: "LI", iso3: "LIE", name: "Liechtenstein", nameFr: "Liechtenstein" },
13456
+ { iso2: "LK", iso3: "LKA", name: "Sri Lanka", nameFr: "Sri Lanka" },
13457
+ { iso2: "LR", iso3: "LBR", name: "Liberia", nameFr: "Liberia" },
13458
+ { iso2: "LS", iso3: "LSO", name: "Lesotho", nameFr: "Lesotho" },
13459
+ { iso2: "LT", iso3: "LTU", name: "Lithuania", nameFr: "Lituanie" },
13460
+ { iso2: "LU", iso3: "LUX", name: "Luxembourg", nameFr: "Luxembourg" },
13461
+ { iso2: "LV", iso3: "LVA", name: "Latvia", nameFr: "Lettonie" },
13462
+ { iso2: "LY", iso3: "LBY", name: "Libya", nameFr: "Libye" },
13463
+ { iso2: "MA", iso3: "MAR", name: "Morocco", nameFr: "Maroc" },
13464
+ { iso2: "MC", iso3: "MCO", name: "Monaco", nameFr: "Monaco" },
13465
+ { iso2: "MD", iso3: "MDA", name: "Moldova", nameFr: "Moldavie" },
13466
+ { iso2: "ME", iso3: "MNE", name: "Montenegro", nameFr: "Mont\xE9n\xE9gro" },
13467
+ { iso2: "MF", iso3: "MAF", name: "Saint Martin", nameFr: "Saint-Martin", sovereign: "FR" },
13468
+ { iso2: "MG", iso3: "MDG", name: "Madagascar", nameFr: "Madagascar" },
13469
+ { iso2: "MH", iso3: "MHL", name: "Marshall Islands", nameFr: "\xCEles Marshall" },
13470
+ { iso2: "MK", iso3: "MKD", name: "North Macedonia", nameFr: "Mac\xE9doine du Nord" },
13471
+ { iso2: "ML", iso3: "MLI", name: "Mali", nameFr: "Mali" },
13472
+ { iso2: "MM", iso3: "MMR", name: "Myanmar", nameFr: "Birmanie", aliases: ["Burma"] },
13473
+ { iso2: "MN", iso3: "MNG", name: "Mongolia", nameFr: "Mongolie" },
13474
+ { iso2: "MO", iso3: "MAC", name: "Macao", nameFr: "Macao", sovereign: "CN" },
13475
+ { iso2: "MP", iso3: "MNP", name: "Northern Mariana Islands", nameFr: "\xCEles Mariannes du Nord", sovereign: "US" },
13476
+ { iso2: "MQ", iso3: "MTQ", name: "Martinique", nameFr: "Martinique", sovereign: "FR" },
13477
+ { iso2: "MR", iso3: "MRT", name: "Mauritania", nameFr: "Mauritanie" },
13478
+ { iso2: "MS", iso3: "MSR", name: "Montserrat", nameFr: "Montserrat", sovereign: "GB" },
13479
+ { iso2: "MT", iso3: "MLT", name: "Malta", nameFr: "Malte" },
13480
+ { iso2: "MU", iso3: "MUS", name: "Mauritius", nameFr: "Maurice" },
13481
+ { iso2: "MV", iso3: "MDV", name: "Maldives", nameFr: "Maldives" },
13482
+ { iso2: "MW", iso3: "MWI", name: "Malawi", nameFr: "Malawi" },
13483
+ { iso2: "MX", iso3: "MEX", name: "Mexico", nameFr: "Mexique" },
13484
+ { iso2: "MY", iso3: "MYS", name: "Malaysia", nameFr: "Malaisie" },
13485
+ { iso2: "MZ", iso3: "MOZ", name: "Mozambique", nameFr: "Mozambique" },
13486
+ { iso2: "NA", iso3: "NAM", name: "Namibia", nameFr: "Namibie" },
13487
+ { iso2: "NC", iso3: "NCL", name: "New Caledonia", nameFr: "Nouvelle-Cal\xE9donie", sovereign: "FR" },
13488
+ { iso2: "NE", iso3: "NER", name: "Niger", nameFr: "Niger" },
13489
+ { iso2: "NF", iso3: "NFK", name: "Norfolk Island", nameFr: "\xCEle Norfolk", sovereign: "AU" },
13490
+ { iso2: "NG", iso3: "NGA", name: "Nigeria", nameFr: "Nig\xE9ria" },
13491
+ { iso2: "NI", iso3: "NIC", name: "Nicaragua", nameFr: "Nicaragua" },
13492
+ { iso2: "NL", iso3: "NLD", name: "Netherlands", nameFr: "Pays-Bas", aliases: ["Holland"] },
13493
+ { iso2: "NO", iso3: "NOR", name: "Norway", nameFr: "Norv\xE8ge" },
13494
+ { iso2: "NP", iso3: "NPL", name: "Nepal", nameFr: "N\xE9pal" },
13495
+ { iso2: "NR", iso3: "NRU", name: "Nauru", nameFr: "Nauru" },
13496
+ { iso2: "NU", iso3: "NIU", name: "Niue", nameFr: "Niue", sovereign: "NZ" },
13497
+ { iso2: "NZ", iso3: "NZL", name: "New Zealand", nameFr: "Nouvelle-Z\xE9lande" },
13498
+ { iso2: "OM", iso3: "OMN", name: "Oman", nameFr: "Oman" },
13499
+ { iso2: "PA", iso3: "PAN", name: "Panama", nameFr: "Panama" },
13500
+ { iso2: "PE", iso3: "PER", name: "Peru", nameFr: "P\xE9rou" },
13501
+ { iso2: "PF", iso3: "PYF", name: "French Polynesia", nameFr: "Polyn\xE9sie fran\xE7aise", sovereign: "FR" },
13502
+ { iso2: "PG", iso3: "PNG", name: "Papua New Guinea", nameFr: "Papouasie-Nouvelle-Guin\xE9e" },
13503
+ { iso2: "PH", iso3: "PHL", name: "Philippines", nameFr: "Philippines" },
13504
+ { iso2: "PK", iso3: "PAK", name: "Pakistan", nameFr: "Pakistan" },
13505
+ { iso2: "PL", iso3: "POL", name: "Poland", nameFr: "Pologne" },
13506
+ { iso2: "PM", iso3: "SPM", name: "Saint Pierre and Miquelon", nameFr: "Saint-Pierre-et-Miquelon", sovereign: "FR" },
13507
+ { iso2: "PN", iso3: "PCN", name: "Pitcairn", nameFr: "Pitcairn", sovereign: "GB" },
13508
+ { iso2: "PR", iso3: "PRI", name: "Puerto Rico", nameFr: "Porto Rico", sovereign: "US" },
13509
+ { iso2: "PS", iso3: "PSE", name: "Palestine", nameFr: "Palestine" },
13510
+ { iso2: "PT", iso3: "PRT", name: "Portugal", nameFr: "Portugal" },
13511
+ { iso2: "PW", iso3: "PLW", name: "Palau", nameFr: "Palaos" },
13512
+ { iso2: "PY", iso3: "PRY", name: "Paraguay", nameFr: "Paraguay" },
13513
+ { iso2: "QA", iso3: "QAT", name: "Qatar", nameFr: "Qatar" },
13514
+ { iso2: "RE", iso3: "REU", name: "R\xE9union", nameFr: "La R\xE9union", sovereign: "FR" },
13515
+ { iso2: "RO", iso3: "ROU", name: "Romania", nameFr: "Roumanie" },
13516
+ { iso2: "RS", iso3: "SRB", name: "Serbia", nameFr: "Serbie" },
13517
+ { iso2: "RU", iso3: "RUS", name: "Russia", nameFr: "Russie", aliases: ["Russian Federation"] },
13518
+ { iso2: "RW", iso3: "RWA", name: "Rwanda", nameFr: "Rwanda" },
13519
+ { iso2: "SA", iso3: "SAU", name: "Saudi Arabia", nameFr: "Arabie saoudite" },
13520
+ { iso2: "SB", iso3: "SLB", name: "Solomon Islands", nameFr: "\xCEles Salomon" },
13521
+ { iso2: "SC", iso3: "SYC", name: "Seychelles", nameFr: "Seychelles" },
13522
+ { iso2: "SD", iso3: "SDN", name: "Sudan", nameFr: "Soudan" },
13523
+ { iso2: "SE", iso3: "SWE", name: "Sweden", nameFr: "Su\xE8de" },
13524
+ { iso2: "SG", iso3: "SGP", name: "Singapore", nameFr: "Singapour" },
13525
+ { iso2: "SH", iso3: "SHN", name: "Saint Helena", nameFr: "Sainte-H\xE9l\xE8ne", sovereign: "GB" },
13526
+ { iso2: "SI", iso3: "SVN", name: "Slovenia", nameFr: "Slov\xE9nie" },
13527
+ { iso2: "SJ", iso3: "SJM", name: "Svalbard and Jan Mayen", nameFr: "Svalbard et Jan Mayen", sovereign: "NO" },
13528
+ { iso2: "SK", iso3: "SVK", name: "Slovakia", nameFr: "Slovaquie" },
13529
+ { iso2: "SL", iso3: "SLE", name: "Sierra Leone", nameFr: "Sierra Leone" },
13530
+ { iso2: "SM", iso3: "SMR", name: "San Marino", nameFr: "Saint-Marin" },
13531
+ { iso2: "SN", iso3: "SEN", name: "Senegal", nameFr: "S\xE9n\xE9gal" },
13532
+ { iso2: "SO", iso3: "SOM", name: "Somalia", nameFr: "Somalie" },
13533
+ { iso2: "SR", iso3: "SUR", name: "Suriname", nameFr: "Suriname" },
13534
+ { iso2: "SS", iso3: "SSD", name: "South Sudan", nameFr: "Soudan du Sud" },
13535
+ { iso2: "ST", iso3: "STP", name: "Sao Tome and Principe", nameFr: "Sao Tom\xE9-et-Principe" },
13536
+ { iso2: "SV", iso3: "SLV", name: "El Salvador", nameFr: "Salvador" },
13537
+ { iso2: "SX", iso3: "SXM", name: "Sint Maarten", nameFr: "Saint-Martin (partie n\xE9erlandaise)", sovereign: "NL" },
13538
+ { iso2: "SY", iso3: "SYR", name: "Syria", nameFr: "Syrie" },
13539
+ { iso2: "SZ", iso3: "SWZ", name: "Eswatini", nameFr: "Eswatini", aliases: ["Swaziland"] },
13540
+ { iso2: "TC", iso3: "TCA", name: "Turks and Caicos Islands", nameFr: "\xCEles Turques-et-Ca\xEFques", sovereign: "GB" },
13541
+ { iso2: "TD", iso3: "TCD", name: "Chad", nameFr: "Tchad" },
13542
+ { iso2: "TF", iso3: "ATF", name: "French Southern Territories", nameFr: "Terres australes et antarctiques fran\xE7aises", sovereign: "FR" },
13543
+ { iso2: "TG", iso3: "TGO", name: "Togo", nameFr: "Togo" },
13544
+ { iso2: "TH", iso3: "THA", name: "Thailand", nameFr: "Tha\xEFlande" },
13545
+ { iso2: "TJ", iso3: "TJK", name: "Tajikistan", nameFr: "Tadjikistan" },
13546
+ { iso2: "TK", iso3: "TKL", name: "Tokelau", nameFr: "Tokelau", sovereign: "NZ" },
13547
+ { iso2: "TL", iso3: "TLS", name: "Timor-Leste", nameFr: "Timor oriental", aliases: ["East Timor"] },
13548
+ { iso2: "TM", iso3: "TKM", name: "Turkmenistan", nameFr: "Turkm\xE9nistan" },
13549
+ { iso2: "TN", iso3: "TUN", name: "Tunisia", nameFr: "Tunisie" },
13550
+ { iso2: "TO", iso3: "TON", name: "Tonga", nameFr: "Tonga" },
13551
+ { iso2: "TR", iso3: "TUR", name: "T\xFCrkiye", nameFr: "Turquie", aliases: ["Turkey"] },
13552
+ { iso2: "TT", iso3: "TTO", name: "Trinidad and Tobago", nameFr: "Trinit\xE9-et-Tobago", aliases: ["Trinidad & Tobago"] },
13553
+ { iso2: "TV", iso3: "TUV", name: "Tuvalu", nameFr: "Tuvalu" },
13554
+ { iso2: "TW", iso3: "TWN", name: "Taiwan", nameFr: "Ta\xEFwan" },
13555
+ { iso2: "TZ", iso3: "TZA", name: "Tanzania", nameFr: "Tanzanie" },
13556
+ { iso2: "UA", iso3: "UKR", name: "Ukraine", nameFr: "Ukraine" },
13557
+ { iso2: "UG", iso3: "UGA", name: "Uganda", nameFr: "Ouganda" },
13558
+ { iso2: "UM", iso3: "UMI", name: "United States Minor Outlying Islands", nameFr: "\xCEles mineures \xE9loign\xE9es des \xC9tats-Unis", sovereign: "US" },
13559
+ {
13560
+ iso2: "US",
13561
+ iso3: "USA",
13562
+ name: "United States",
13563
+ nameFr: "\xC9tats-Unis",
13564
+ aliases: [
13565
+ "United States of America",
13566
+ "America",
13567
+ "U.S.A.",
13568
+ "\xC9tats-Unis d'Am\xE9rique",
13569
+ "Etats-Unis"
13570
+ ]
13571
+ },
13572
+ { iso2: "UY", iso3: "URY", name: "Uruguay", nameFr: "Uruguay" },
13573
+ { iso2: "UZ", iso3: "UZB", name: "Uzbekistan", nameFr: "Ouzb\xE9kistan" },
13574
+ { iso2: "VA", iso3: "VAT", name: "Holy See", nameFr: "Saint-Si\xE8ge", aliases: ["Vatican", "Vatican City"] },
13575
+ { iso2: "VC", iso3: "VCT", name: "Saint Vincent and the Grenadines", nameFr: "Saint-Vincent-et-les-Grenadines" },
13576
+ { iso2: "VE", iso3: "VEN", name: "Venezuela", nameFr: "Venezuela" },
13577
+ { iso2: "VG", iso3: "VGB", name: "British Virgin Islands", nameFr: "\xCEles Vierges britanniques", sovereign: "GB" },
13578
+ { iso2: "VI", iso3: "VIR", name: "United States Virgin Islands", nameFr: "\xCEles Vierges des \xC9tats-Unis", sovereign: "US", aliases: ["US Virgin Islands"] },
13579
+ { iso2: "VN", iso3: "VNM", name: "Vietnam", nameFr: "Vi\xEAt Nam", aliases: ["Viet Nam"] },
13580
+ { iso2: "VU", iso3: "VUT", name: "Vanuatu", nameFr: "Vanuatu" },
13581
+ { iso2: "WF", iso3: "WLF", name: "Wallis and Futuna", nameFr: "Wallis-et-Futuna", sovereign: "FR" },
13582
+ { iso2: "WS", iso3: "WSM", name: "Samoa", nameFr: "Samoa" },
13583
+ { iso2: "YE", iso3: "YEM", name: "Yemen", nameFr: "Y\xE9men" },
13584
+ { iso2: "YT", iso3: "MYT", name: "Mayotte", nameFr: "Mayotte", sovereign: "FR" },
13585
+ { iso2: "ZA", iso3: "ZAF", name: "South Africa", nameFr: "Afrique du Sud" },
13586
+ { iso2: "ZM", iso3: "ZMB", name: "Zambia", nameFr: "Zambie" },
13587
+ { iso2: "ZW", iso3: "ZWE", name: "Zimbabwe", nameFr: "Zimbabwe" }
13588
+ ];
13589
+ var WHOLE_WORKSPACE_LABELS = [
13590
+ // The bare noun earns its place: it is what the wrapper strip REDUCES the
13591
+ // common phrasings to. "country-wide" normalizes to "country wide" and loses
13592
+ // its suffix to /\s+wide$/; "across the country" loses "across " and then
13593
+ // the article. Both land on "country", and without this entry both missed
13594
+ // every key and reached /geo/search — the exact fence this module prevents.
13595
+ "Country",
13596
+ "Nationwide",
13597
+ "Nation-wide",
13598
+ "Countrywide",
13599
+ "Whole country",
13600
+ "Entire country",
13601
+ "The whole country",
13602
+ "Everywhere",
13603
+ "Anywhere",
13604
+ "All regions",
13605
+ "Tout le pays",
13606
+ "Toute la France",
13607
+ "Partout",
13608
+ "Partout en France",
13609
+ "\xC9chelle nationale",
13610
+ "National",
13611
+ "Nationale"
13612
+ ];
13613
+ var SUPRANATIONAL_LABELS = [
13614
+ "EU",
13615
+ "European Union",
13616
+ // The FRENCH spellings, which shipped missing while their English twins were
13617
+ // here — on the one backend whose users type French. "des leads dans l'UE"
13618
+ // classified as nothing and went on to /geo/search, so the label the FR
13619
+ // workspace is most likely to receive was the one label not covered.
13620
+ "UE",
13621
+ "Union europ\xE9enne",
13622
+ "Europe",
13623
+ "EMEA",
13624
+ "DACH",
13625
+ "Benelux",
13626
+ "Scandinavia",
13627
+ "Nordics",
13628
+ "North America",
13629
+ "South America",
13630
+ "Latin America",
13631
+ "LATAM",
13632
+ "Am\xE9rique du Nord",
13633
+ "Am\xE9rique du Sud",
13634
+ "Am\xE9rique latine",
13635
+ "Zone euro",
13636
+ "APAC",
13637
+ "Asia",
13638
+ "Africa",
13639
+ "Middle East",
13640
+ "Worldwide",
13641
+ "Global",
13642
+ "Globally",
13643
+ "International",
13644
+ "All countries",
13645
+ "Monde",
13646
+ "Monde entier",
13647
+ "Le monde entier"
13648
+ ];
13649
+ var HOME_COUNTRY_BY_REGION = {
13650
+ us: "US",
13651
+ fr: "FR"
13652
+ };
13653
+ var REGION_EXEMPT_KEYS = {
13654
+ // "Georgia": a US rep prospecting the STATE writes exactly this, and would
13655
+ // never write "Georgia, US". "Jersey": colloquial New Jersey.
13656
+ us: /* @__PURE__ */ new Set(["georgia", "jersey"]),
13657
+ // Empty by design: no French région or département shares a bare country
13658
+ // name. Every FR homonym is a dependent territory (Guadeloupe, Martinique,
13659
+ // La Réunion, Mayotte, Guyane…), which the `sovereign` rule already exempts.
13660
+ fr: /* @__PURE__ */ new Set()
13661
+ };
13662
+ var US_STATE_POSTAL_CODES = /* @__PURE__ */ new Set([
13663
+ "al",
13664
+ "ak",
13665
+ "az",
13666
+ "ar",
13667
+ "ca",
13668
+ "co",
13669
+ "ct",
13670
+ "de",
13671
+ "dc",
13672
+ "fl",
13673
+ "ga",
13674
+ "hi",
13675
+ "id",
13676
+ "il",
13677
+ "in",
13678
+ "ia",
13679
+ "ks",
13680
+ "ky",
13681
+ "la",
13682
+ "me",
13683
+ "md",
13684
+ "ma",
13685
+ "mi",
13686
+ "mn",
13687
+ "ms",
13688
+ "mo",
13689
+ "mt",
13690
+ "ne",
13691
+ "nv",
13692
+ "nh",
13693
+ "nj",
13694
+ "nm",
13695
+ "ny",
13696
+ "nc",
13697
+ "nd",
13698
+ "oh",
13699
+ "ok",
13700
+ "or",
13701
+ "pa",
13702
+ "ri",
13703
+ "sc",
13704
+ "sd",
13705
+ "tn",
13706
+ "tx",
13707
+ "ut",
13708
+ "vt",
13709
+ "va",
13710
+ "wa",
13711
+ "wv",
13712
+ "wi",
13713
+ "wy"
13714
+ ]);
13715
+ function buildKeyIndex() {
13716
+ const byKey = /* @__PURE__ */ new Map();
13717
+ const collisions = [];
13718
+ for (const entry of COUNTRIES) {
13719
+ const labels = [
13720
+ entry.name,
13721
+ entry.nameFr,
13722
+ entry.iso2,
13723
+ entry.iso3,
13724
+ ...entry.aliases ?? []
13725
+ ];
13726
+ for (const label of labels) {
13727
+ const key = countryKey(label);
13728
+ if (!key)
13729
+ continue;
13730
+ const existing = byKey.get(key);
13731
+ if (existing && existing.iso2 !== entry.iso2) {
13732
+ collisions.push(`${key}: ${existing.iso2} vs ${entry.iso2}`);
13733
+ continue;
13734
+ }
13735
+ byKey.set(key, entry);
13736
+ }
13737
+ }
13738
+ return { byKey, collisions };
13739
+ }
13740
+ var KEY_INDEX = buildKeyIndex();
13741
+ var COUNTRY_BY_KEY = KEY_INDEX.byKey;
13742
+ var COUNTRY_KEY_COLLISIONS = KEY_INDEX.collisions;
13743
+ var SUPRANATIONAL_KEYS = new Set(SUPRANATIONAL_LABELS.map((label) => countryKey(label)).filter(Boolean));
13744
+ var SCOPE_WRAPPERS = [
13745
+ // ORDER MATTERS: the stripper takes the FIRST wrapper that matches, so every
13746
+ // longer form must precede the shorter one it contains. "the whole of France"
13747
+ // hit the bare /^whole\s+/ first and was left as "of france", which matches no
13748
+ // country — so the guard returned no hit and the caller went on to /geo/search
13749
+ // and the same-named-town fence this module exists to prevent. There is no
13750
+ // generic "of " strip: it belongs to this phrase, not to place names.
13751
+ /^whole\s+of\s+/,
13752
+ /^whole\s+/,
13753
+ /^all\s+of\s+/,
13754
+ /^all\s+/,
13755
+ /^across\s+/,
13756
+ /^entire\s+/,
13757
+ /^anywhere\s+in\s+/,
13758
+ /^everywhere\s+in\s+/,
13759
+ /^nationwide\s+in\s+/,
13760
+ /^throughout\s+/,
13761
+ /^partout\s+en\s+/,
13762
+ /^partout\s+dans\s+/,
13763
+ /^toute\s+la\s+/,
13764
+ /^tout\s+le\s+/,
13765
+ /^toute\s+l\s+/,
13766
+ /^dans\s+toute\s+la\s+/,
13767
+ /^dans\s+tout\s+le\s+/,
13768
+ // BARE PREPOSITIONS, last in the prefix group so every longer form above
13769
+ // still wins ("dans toute la France" must not be eaten by /^dans\s+/).
13770
+ //
13771
+ // These are the plainest way anyone names a country in a location argument —
13772
+ // "in the United States", "en France", "aux États-Unis" — and they were the
13773
+ // one shape the wrapper list missed, so those values reached /geo/search and
13774
+ // hit the same-named-town fence this module exists to prevent. Safe despite
13775
+ // how common the words are: a strip only counts when the REMAINDER is a
13776
+ // recognized country / supra-national / whole-workspace key, so "In Salah"
13777
+ // and "Aubervilliers" (no trailing space to match) are untouched.
13778
+ /^in\s+/,
13779
+ /^en\s+/,
13780
+ /^aux\s+/,
13781
+ /^au\s+/,
13782
+ /^dans\s+/,
13783
+ /\s+wide$/,
13784
+ /\s+entier$/,
13785
+ /\s+entiere$/
13786
+ ];
13787
+ var LEADING_ARTICLE = /^(les|the|la|le|l|el|los|du|de|d)\s+/;
13788
+ function embeddedKey(key, known) {
13789
+ let current = key;
13790
+ for (let pass = 0; pass < 4; pass += 1) {
13791
+ if (known.has(current))
13792
+ return current;
13793
+ let next = current;
13794
+ for (const wrapper of SCOPE_WRAPPERS) {
13795
+ const stripped = next.replace(wrapper, "").trim();
13796
+ if (stripped !== next && stripped.length > 0) {
13797
+ next = stripped;
13798
+ break;
13799
+ }
13800
+ }
13801
+ next = next.replace(LEADING_ARTICLE, "").trim();
13802
+ if (next === current || next.length === 0)
13803
+ return void 0;
13804
+ current = next;
13805
+ }
13806
+ return known.has(current) ? current : void 0;
13807
+ }
13808
+ function embeddedCountryKey(key) {
13809
+ return embeddedKey(key, COUNTRY_BY_KEY);
13810
+ }
13811
+ function embeddedSupranationalKey(key) {
13812
+ return embeddedKey(key, SUPRANATIONAL_KEYS);
13813
+ }
13814
+ var WHOLE_WORKSPACE_KEYS = new Set(WHOLE_WORKSPACE_LABELS.map((label) => countryKey(label)).filter(Boolean));
13815
+ function embeddedWholeWorkspaceKey(key) {
13816
+ return embeddedKey(key, WHOLE_WORKSPACE_KEYS);
13817
+ }
13818
+
13819
+ // ../core/dist/composite/_country-guard.js
13820
+ var COUNTRY_LEVEL_LOCATION = "COUNTRY_LEVEL_LOCATION";
13821
+ var COUNTRY_LEVEL_STATUS = "country_level_location";
13822
+ function exemptKeysFor(region) {
13823
+ if (region === "us")
13824
+ return REGION_EXEMPT_KEYS.us;
13825
+ if (region === "fr")
13826
+ return REGION_EXEMPT_KEYS.fr;
13827
+ return /* @__PURE__ */ new Set([...REGION_EXEMPT_KEYS.us, ...REGION_EXEMPT_KEYS.fr]);
13828
+ }
13829
+ function alpha2LooksLocal(region) {
13830
+ return region !== "fr";
13831
+ }
13832
+ function homeCountryIso2(region) {
13833
+ return region === "us" || region === "fr" ? HOME_COUNTRY_BY_REGION[region] : void 0;
13834
+ }
13835
+ function homeCountryName(region) {
13836
+ const iso2 = homeCountryIso2(region);
13837
+ return iso2 ? COUNTRY_BY_KEY.get(countryKey(iso2))?.name : void 0;
13838
+ }
13839
+ function classify(value, region) {
13840
+ const key = countryKey(value);
13841
+ if (!key)
13842
+ return null;
13843
+ if (SUPRANATIONAL_KEYS.has(key))
13844
+ return { kind: "supranational" };
13845
+ const namedKey = embeddedCountryKey(key);
13846
+ if (namedKey === void 0) {
13847
+ if (embeddedWholeWorkspaceKey(key) !== void 0) {
13848
+ const homeIso2 = homeCountryIso2(region);
13849
+ if (homeIso2 === void 0)
13850
+ return { kind: "country_indeterminate" };
13851
+ const homeEntry = COUNTRY_BY_KEY.get(countryKey(homeIso2));
13852
+ return { kind: "home_country", entry: homeEntry };
13853
+ }
13854
+ if (embeddedSupranationalKey(key) !== void 0)
13855
+ return { kind: "supranational" };
13856
+ }
13857
+ const entry = COUNTRY_BY_KEY.get(namedKey ?? key);
13858
+ if (!entry)
13859
+ return null;
13860
+ const bareKey = namedKey ?? key;
13861
+ if (exemptKeysFor(region).has(bareKey))
13862
+ return null;
13863
+ const home = homeCountryIso2(region);
13864
+ if (entry.sovereign !== void 0 && (home === void 0 || entry.sovereign === home)) {
13865
+ return null;
13866
+ }
13867
+ if (home !== void 0 && entry.iso2 === home) {
13868
+ return { kind: "home_country", entry };
13869
+ }
13870
+ if (bareKey.length <= 2 && alpha2LooksLocal(region) && US_STATE_POSTAL_CODES.has(bareKey)) {
13871
+ return null;
13872
+ }
13873
+ if (home === void 0)
13874
+ return { kind: "country_indeterminate", entry };
13875
+ return { kind: "foreign_country", entry };
13876
+ }
13877
+ function detectCountryLocations(input, param, region, axis = "include", selectedId) {
13878
+ if (input === void 0 || input === null)
13879
+ return [];
13880
+ const list = Array.isArray(input) ? input : [input];
13881
+ const flagged = [];
13882
+ const kept = [];
13883
+ for (const value of list) {
13884
+ if (typeof value !== "string") {
13885
+ if (value !== void 0 && value !== null)
13886
+ kept.push(String(value));
13887
+ continue;
13888
+ }
13889
+ const verdict = classify(value, region);
13890
+ if (!verdict) {
13891
+ kept.push(value);
13892
+ continue;
13893
+ }
13894
+ flagged.push({ value, verdict });
13895
+ }
13896
+ return flagged.map(({ value, verdict }) => ({
13897
+ value,
13898
+ param,
13899
+ kind: verdict.kind,
13900
+ country: verdict.entry?.name ?? null,
13901
+ axis,
13902
+ kept,
13903
+ ...selectedId === void 0 ? {} : { selectedId }
13904
+ }));
13905
+ }
13906
+ function detectCountryLocationsIn(params, region) {
13907
+ const hits = [];
13908
+ for (const { input, param, axis } of params) {
13909
+ hits.push(...detectCountryLocations(input, param, region, axis ?? "include"));
13910
+ }
13911
+ return hits;
13912
+ }
13913
+ function geoScopeSurvives(params, region) {
13914
+ for (const { input } of params) {
13915
+ if (input === void 0 || input === null)
13916
+ continue;
13917
+ for (const value of Array.isArray(input) ? input : [input]) {
13918
+ if (typeof value !== "string")
13919
+ return true;
13920
+ if (countryKey(value) && classify(value, region) === null)
13921
+ return true;
13922
+ }
13923
+ }
13924
+ return false;
13925
+ }
13926
+ var NARROW_EXAMPLES = {
13927
+ us: `a city / county / state name ("Dallas, TX", "Texas", "Bay Area")`,
13928
+ fr: `a city / d\xE9partement / r\xE9gion name ("Limoges", "Indre-et-Loire", "\xCEle-de-France")`,
13929
+ custom: `a city / county / state / r\xE9gion name`
13930
+ };
13931
+ function messageFor(hit, region) {
13932
+ const home = homeCountryName(region);
13933
+ if (hit.kind === "supranational") {
13934
+ return `${hit.param} value "${hit.value}" is a supra-national scope, which is never an admin area \u2014 it cannot resolve to anything.`;
13935
+ }
13936
+ if (hit.kind === "home_country") {
13937
+ const effect = hit.axis === "exclude" ? `so excluding it would remove every company in the workspace` : `so filtering by it removes nothing`;
13938
+ return `${hit.param} value "${hit.value}" names this whole workspace, not a place inside it \u2014 this backend serves ${hit.country} and nothing else, ${effect}. Country names are absent from the admin-area index (product#3885), so the value silently trigram-matches a same-named town instead ("France" \u2192 the commune of Francs, "United States" \u2192 Statesboro) and fences the search to one village.`;
13939
+ }
13940
+ if (hit.kind === "country_indeterminate" && hit.country === null) {
13941
+ return `${hit.param} value "${hit.value}" asks for this whole workspace, not a place inside it, so it is not a location filter \u2014 and no admin area is named "${hit.value}" either, so it would silently trigram-match a same-named town and fence the search to one village. This backend is custom-configured, so WHICH country the workspace covers is unknown.`;
13942
+ }
13943
+ if (hit.kind === "country_indeterminate") {
13944
+ return `${hit.param} value "${hit.value}" is a country name, which is never a usable location filter: country names are absent from the admin-area index (product#3885), so the value silently trigram-matches a same-named town and fences the search to one village. This backend is custom-configured, so which country it serves is unknown \u2014 ${hit.country} may or may not be it.`;
13945
+ }
13946
+ const foreignEffect = hit.axis === "exclude" ? `so excluding it removes nothing \u2014 there is nothing here to exclude` : `so it holds no ${hit.country} companies`;
13947
+ return `${hit.param} value "${hit.value}" is a country outside this workspace \u2014 this backend serves ${home} only, ${foreignEffect}. A country name is also absent from the admin-area index (product#3885), so it silently trigram-matches a same-named town and fences the search to one village.`;
13948
+ }
13949
+ function excludeBlocksWrite(hit) {
13950
+ return hit.axis === "exclude" && hit.kind !== "foreign_country";
13951
+ }
13952
+ function includeBlocksWrite(hit) {
13953
+ if (hit.axis !== "include")
13954
+ return false;
13955
+ if (hit.kind === "home_country")
13956
+ return false;
13957
+ if (hit.kind === "country_indeterminate" && hit.country === null)
13958
+ return false;
13959
+ return true;
13960
+ }
13961
+ function blocksWrite(hit) {
13962
+ return excludeBlocksWrite(hit) || includeBlocksWrite(hit);
13963
+ }
13964
+ function hintFor(hit, region, intent, otherScope) {
13965
+ const narrow = NARROW_EXAMPLES[region];
13966
+ const home = homeCountryName(region);
13967
+ const holds = home ? `holds ${home} companies only` : "covers a single country";
13968
+ const anonymousWhole = hit.kind === "country_indeterminate" && hit.country === null;
13969
+ const unnamed = "This backend is custom-configured, so do NOT name which country that is.";
13970
+ if (intent === "write" && hit.kept.length === 0 && otherScope) {
13971
+ const carry = `Drop ${hit.param} from the call and re-call ONCE with the rest of the request intact \u2014 the rest of the request carries real scope and must not be lost with it.`;
13972
+ if (hit.kind === "home_country") {
13973
+ return hit.axis === "exclude" ? `${carry} Excluding ${hit.country} would empty the audience, so that part cannot be honoured at all \u2014 say so rather than silently ignoring it.` : `${carry} The lens then carries no geo criterion, which is correct: the workspace already covers all of ${hit.country}.`;
13974
+ }
13975
+ if (hit.kind === "foreign_country") {
13976
+ return `${carry} And say this workspace ${holds}, so there is no ${hit.country} audience to add \u2014 the result is scoped by the other criteria only.`;
13977
+ }
13978
+ if (anonymousWhole) {
13979
+ return hit.axis === "exclude" ? `${carry} Excluding the workspace's own country would empty the audience, so that part cannot be honoured at all \u2014 say so rather than silently ignoring it. ${unnamed}` : `${carry} The lens then carries no geo criterion, which is correct: the workspace already covers its entire country. ${unnamed}`;
13980
+ }
13981
+ if (hit.kind === "country_indeterminate") {
13982
+ return `${carry} This backend is custom-configured, so claim nothing about whether ${hit.country} is inside it.`;
13983
+ }
13984
+ return `${carry} And say what the workspace covers rather than presenting the audience as "${hit.value}".`;
13985
+ }
13986
+ if (intent === "write" && hit.kept.length === 0) {
13987
+ const stop = `A country-level value was the ONLY scope passed, so do NOT re-call this tool with ${hit.param} omitted: that persists a lens or filter change carrying no scope at all, to express something this workspace already is. Write NOTHING here.`;
13988
+ if (hit.kind === "home_country") {
13989
+ return hit.axis === "exclude" ? `${stop} Excluding ${hit.country} would empty the entire audience, so it cannot be written either. Ask what should actually be carved out \u2014 ${narrow} \u2014 and only then write.` : `${stop} Say the audience already covers all of ${hit.country}, then offer the axes that DO narrow it: sector, size, or ${narrow}.`;
13990
+ }
13991
+ if (hit.kind === "foreign_country") {
13992
+ return `${stop} Say this workspace ${holds}, so there is no ${hit.country} audience to scope to and none can be created. Ask what to target inside it \u2014 ${narrow}.`;
13993
+ }
13994
+ if (anonymousWhole) {
13995
+ return hit.axis === "exclude" ? `${stop} Excluding the workspace's own country would empty the entire audience, so it cannot be written either. Ask what should actually be carved out \u2014 ${narrow} \u2014 and only then write. ${unnamed}` : `${stop} Say the audience already covers the workspace entirely, then offer the axes that DO narrow it: sector, size, or ${narrow}. ${unnamed}`;
13996
+ }
13997
+ if (hit.kind === "country_indeterminate") {
13998
+ return `${stop} This backend is custom-configured, so claim nothing about whether ${hit.country} is inside it. Ask what should be targeted \u2014 ${narrow} \u2014 before writing anything.`;
13999
+ }
14000
+ return `${stop} A supra-national scope is not an admin area and cannot be persisted. Say what the workspace covers, then ask which part of it to target \u2014 ${narrow}.`;
14001
+ }
14002
+ if (hit.kept.length > 0) {
14003
+ const rest = hit.kept.map((v) => `"${v}"`).join(", ");
14004
+ const plural = hit.kept.length > 1 ? "are" : "is";
14005
+ const surgical = `Do NOT omit ${hit.param} \u2014 ${rest} ${plural} valid and would be lost with it. Remove ONLY "${hit.value}" and re-call with the rest.`;
14006
+ if (hit.axis === "exclude" && hit.kind !== "foreign_country") {
14007
+ const empties = hit.kind === "home_country" ? `Excluding ${hit.country} excludes this ENTIRE workspace` : hit.kind === "country_indeterminate" && hit.country === null ? `Excluding the whole workspace` : hit.kind === "country_indeterminate" ? `This backend is custom-configured, so whether excluding ${hit.country} empties the workspace is unknown, and` : `A supra-national scope may well cover this whole workspace, so excluding it`;
14008
+ return `${empties} \u2014 so the request as written cannot be honoured, and there is no partial version of it to run. Do NOT re-call with only ${rest} excluded: that answers a much narrower question than the one asked, and nothing in the result would show the substitution. Ask what was actually meant to be carved out \u2014 ${narrow} \u2014 before re-calling at all.`;
14009
+ }
14010
+ if (hit.kind === "home_country") {
14011
+ return `${surgical} The result then covers ${rest} \u2014 describe it as those places, NOT as the whole workspace.`;
14012
+ }
14013
+ if (hit.kind === "foreign_country") {
14014
+ return `${surgical} And say this workspace ${holds}: there are no ${hit.country} leads in it either way, so the result speaks only for ${rest}.`;
14015
+ }
14016
+ if (anonymousWhole) {
14017
+ return `${surgical} The result then covers ${rest} \u2014 describe it as those places, NOT as the whole workspace.`;
14018
+ }
14019
+ if (hit.kind === "country_indeterminate") {
14020
+ return `${surgical} This backend is custom-configured, so claim nothing about whether ${hit.country} is inside it \u2014 report the result as covering ${rest}.`;
14021
+ }
14022
+ return `${surgical} And say what the workspace actually covers rather than presenting the result as "${hit.value}" \u2014 it speaks only for ${rest}.`;
14023
+ }
14024
+ if (hit.axis === "exclude") {
14025
+ if (hit.kind === "home_country") {
14026
+ return `Excluding ${hit.country} excludes this ENTIRE workspace, so the result would be empty \u2014 and dropping ${hit.param} does the reverse of what was asked, returning every company instead. Neither is what the user wants: ask what they actually meant to carve out, then exclude ${narrow} instead.`;
14027
+ }
14028
+ if (hit.kind === "foreign_country") {
14029
+ return `Nothing in this workspace is in ${hit.country}, so this exclusion changes nothing \u2014 it is a no-op, not an unsupported request. Drop ${hit.param} and say the result is unaffected. To carve something out for real, exclude ${narrow}.`;
14030
+ }
14031
+ if (anonymousWhole) {
14032
+ return `Excluding the whole workspace leaves nothing, and dropping ${hit.param} does the reverse of what was asked, returning every company instead. Neither is what the user wants: ask what they actually meant to carve out, then exclude ${narrow} instead.`;
14033
+ }
14034
+ if (hit.kind === "country_indeterminate") {
14035
+ return `This backend is custom-configured, so whether ${hit.country} is inside this workspace is unknown \u2014 the exclusion may remove everything or nothing. Do not guess: ask what should be carved out, then exclude ${narrow}.`;
14036
+ }
14037
+ return `A supra-national scope cannot be excluded as an admin area, and dropping ${hit.param} would instead include everything. Say what the workspace covers and ask what should be carved out, then exclude ${narrow}.`;
14038
+ }
14039
+ const coversAll = !otherScope;
14040
+ if (hit.kind === "home_country") {
14041
+ return coversAll ? `Whole-workspace intent = OMIT ${hit.param} entirely, then say the result covers everything. To narrow, pass ${narrow}. Do NOT retry with another spelling or a nearby city.` : `Whole-workspace intent = OMIT ${hit.param} entirely. The rest of the request still scopes the result, so describe it by those criteria \u2014 NOT as covering everything. To narrow further, pass ${narrow}. Do NOT retry with another spelling or a nearby city.`;
14042
+ }
14043
+ if (anonymousWhole) {
14044
+ return coversAll ? `Whole-workspace intent = OMIT ${hit.param} entirely, then say the result covers everything in this workspace. ${unnamed} To narrow, pass ${narrow}. Do NOT retry with another spelling or a nearby city.` : `Whole-workspace intent = OMIT ${hit.param} entirely. The rest of the request still scopes the result, so describe it by those criteria \u2014 NOT as covering this whole workspace. ${unnamed} To narrow further, pass ${narrow}. Do NOT retry with another spelling or a nearby city.`;
14045
+ }
14046
+ if (hit.kind === "country_indeterminate") {
14047
+ return `If you meant this entire workspace, OMIT ${hit.param} and say the result covers all of it. If you meant a place inside it, pass ${narrow}. Do NOT re-run unfiltered while presenting the result as an answer about ${hit.country} specifically, and do NOT retry another spelling.`;
14048
+ }
14049
+ if (hit.kind === "foreign_country") {
14050
+ return `Do NOT simply drop ${hit.param} and re-run \u2014 an unfiltered result is ${home} data, which does NOT answer a question about ${hit.country}. Tell the user this workspace ${holds}, so there are no ${hit.country} leads to return. If they actually meant a same-named town inside it, qualify the value ("Germany, OH") \u2014 a qualified place name is accepted.`;
14051
+ }
14052
+ return `Do NOT drop ${hit.param} and re-run as though the result answered this \u2014 a supra-national ask is not the same as the whole workspace. Say the workspace ${holds}, then offer the whole-workspace view as an explicit choice. To narrow instead, pass ${narrow}.`;
14053
+ }
14054
+ function reconciledHint(hits, region, intent, otherScope) {
14055
+ const { param, axis, kept } = hits[0];
14056
+ const narrow = NARROW_EXAMPLES[region];
14057
+ const home = homeCountryName(region);
14058
+ const holds = home ? `holds ${home} companies only` : "covers a single country";
14059
+ const quoted = (values) => values.map((v) => `"${v}"`).join(", ");
14060
+ const offending = quoted(hits.map((h) => h.value));
14061
+ const countriesOf = (kind) => [
14062
+ ...new Set(hits.filter((h) => h.kind === kind).map((h) => h.country).filter((c) => !!c))
14063
+ ];
14064
+ const homeCountry = countriesOf("home_country")[0];
14065
+ const foreign = countriesOf("foreign_country");
14066
+ const indeterminate = countriesOf("country_indeterminate");
14067
+ const supra = hits.filter((h) => h.kind === "supranational").map((h) => h.value);
14068
+ const because = new Set(hits.map((h) => h.kind)).size > 1 ? `they are country-level or wider, and mixing kinds makes none of them usable` : `not one of them is a usable location filter`;
14069
+ const surgical = kept.length > 0 ? `Do NOT omit ${param} \u2014 ${quoted(kept)} ${kept.length > 1 ? "are" : "is"} valid and would be lost with it. Remove ALL of ${offending} in ONE re-call and keep the rest.` : `Remove every one of ${offending} from ${param} \u2014 ${because}.`;
14070
+ if (intent === "write" && kept.length === 0 && otherScope) {
14071
+ return `${surgical} Then re-call ONCE with the rest of the request intact \u2014 the rest of the request carries real scope and must not be lost with this argument. Say what the audience actually covers: ${[
14072
+ homeCountry ? `it already spans all of ${homeCountry}` : void 0,
14073
+ foreign.length > 0 ? `this workspace ${holds}, so no ${foreign.join(", ")} audience can be added` : void 0,
14074
+ indeterminate.length > 0 ? `this backend is custom-configured, so claim nothing about ${indeterminate.join(", ")}` : void 0,
14075
+ supra.length > 0 ? `${quoted(supra)} is a supra-national scope, not a place` : void 0
14076
+ ].filter(Boolean).join("; ")}.`;
14077
+ }
14078
+ if (intent === "write" && kept.length === 0) {
14079
+ const cannot = [];
14080
+ if (homeCountry) {
14081
+ cannot.push(axis === "exclude" ? `excluding ${homeCountry} would empty the audience entirely` : `the audience already covers all of ${homeCountry}`);
14082
+ }
14083
+ if (foreign.length > 0) {
14084
+ cannot.push(`this workspace ${holds}, so there is no ${foreign.join(", ")} audience to scope to`);
14085
+ }
14086
+ if (indeterminate.length > 0) {
14087
+ cannot.push(`this backend is custom-configured, so whether ${indeterminate.join(", ")} is inside it is unknown`);
14088
+ }
14089
+ if (supra.length > 0) {
14090
+ cannot.push(`${quoted(supra)} is a supra-national scope, which cannot be persisted`);
14091
+ }
14092
+ return `${surgical} Then STOP: do NOT re-call this tool with ${param} omitted, which would persist a lens or filter change carrying no scope at all. Write NOTHING \u2014 ${cannot.join("; ")}. Say what the audience already covers, then offer the axes that DO narrow it: sector, size, or ${narrow}.`;
14093
+ }
14094
+ const say = [];
14095
+ if (axis === "exclude") {
14096
+ if (homeCountry) {
14097
+ say.push(`excluding ${homeCountry} would empty the ENTIRE workspace, so that part cannot be honoured at all`);
14098
+ }
14099
+ if (foreign.length > 0) {
14100
+ say.push(`excluding ${foreign.join(", ")} removes nothing \u2014 there is nothing here to exclude`);
14101
+ }
14102
+ if (indeterminate.length > 0) {
14103
+ say.push(`this backend is custom-configured, so whether ${indeterminate.join(", ")} is inside it is unknown and its exclusion may remove everything or nothing`);
14104
+ }
14105
+ if (supra.length > 0) {
14106
+ say.push(`${quoted(supra)} is a supra-national scope, which is not an admin area and cannot be excluded`);
14107
+ }
14108
+ const tail = kept.length > 0 ? `The other exclusions still apply.` : `Do NOT present the result as though any of these exclusions had been applied.`;
14109
+ return `${surgical} Then say why: ${say.join("; ")}. ${tail} Ask what should actually be carved out, then exclude ${narrow}.`;
14110
+ }
14111
+ const scope = kept.length > 0 ? `The result then covers ${quoted(kept)} \u2014 describe it as those places only.` : homeCountry ? `Omitting ${param} entirely then returns the whole workspace, which IS ${homeCountry}: that answers the ${homeCountry} part of the ask and nothing else \u2014 say so in those words.` : `Do NOT re-run with ${param} omitted as though the unfiltered result answered this.`;
14112
+ if (foreign.length > 0) {
14113
+ say.push(`this workspace ${holds}, so it holds no ${foreign.join(", ")} companies and the result says nothing about ${foreign.join(", ")}`);
14114
+ }
14115
+ if (indeterminate.length > 0) {
14116
+ say.push(`this backend is custom-configured, so claim nothing about whether ${indeterminate.join(", ")} is inside it`);
14117
+ }
14118
+ if (supra.length > 0) {
14119
+ say.push(`${quoted(supra)} is a supra-national scope, not a place \u2014 say what the workspace covers and offer the whole-workspace view as an explicit choice, rather than letting the result stand for it`);
14120
+ }
14121
+ return `${surgical} ${scope} And be explicit that ${say.join("; ")}. To narrow, pass ${narrow}. Do NOT retry with another spelling.`;
14122
+ }
14123
+ function blockedWriteHint(hits, region) {
14124
+ const narrow = NARROW_EXAMPLES[region];
14125
+ const blocked = hits.filter(blocksWrite);
14126
+ const quoted = (values) => values.map((v) => `"${v}"`).join(", ");
14127
+ const names = quoted([...new Set(blocked.map((h) => h.value))]);
14128
+ const inverts = blocked.some(excludeBlocksWrite);
14129
+ const unsupported = blocked.some(includeBlocksWrite);
14130
+ const why = [
14131
+ ...new Set(blocked.map((hit) => {
14132
+ if (hit.axis === "exclude") {
14133
+ return hit.kind === "home_country" ? `"${hit.value}" is this entire workspace, so excluding it asks for an empty audience` : hit.kind === "country_indeterminate" ? `this backend is custom-configured, so whether "${hit.value}" covers it is unknown` : `"${hit.value}" is a supra-national scope, which may well cover this whole workspace`;
14134
+ }
14135
+ return hit.kind === "foreign_country" ? `"${hit.value}" is outside this workspace, so there is no such audience to create` : hit.kind === "country_indeterminate" ? `this backend is custom-configured, so whether "${hit.value}" is inside it is unknown` : `"${hit.value}" is a supra-national scope, which no single workspace can be scoped to`;
14136
+ }))
14137
+ ].join("; ");
14138
+ const blockedValues = new Set(blocked.map((h) => h.value));
14139
+ const alsoBad = [
14140
+ ...new Set(hits.filter((h) => !blocksWrite(h) && !blockedValues.has(h.value)).map((h) => h.value))
14141
+ ];
14142
+ const also = alsoBad.length > 0 ? ` When a corrected call is eventually made, ${quoted(alsoBad)} must come off it too \u2014 country-level values are never usable.` : "";
14143
+ const consequence = inverts ? `Any call that leaves ${names} out persists the OPPOSITE of the exclusion: an audience holding exactly what was asked to be removed. The rest of the request cannot be written either, because it would be written under that inverted scope.` : `Any call that leaves ${names} out persists an audience for THIS workspace instead \u2014 a real, saved audience for a territory nobody asked about. The rest of the request does not survive on its own: sectors, sizes and keywords were qualifying ${names}, not a second request to be written without it.`;
14144
+ const bothNote = inverts && unsupported ? " Both failures are present in this one call, and neither is fixed by dropping the other." : "";
14145
+ const ask = inverts ? `Ask what should actually be carved out \u2014 ${narrow} \u2014 and write only once that is settled.` : `Ask what should actually be targeted \u2014 ${narrow} \u2014 and write only once that is settled.`;
14146
+ return `Write NOTHING, and do NOT re-call this tool in any form \u2014 not without ${names}, and not "with the rest of the request intact". ${why}. ${consequence}${bothNote}${also} ${ask}`;
14147
+ }
14148
+ function countryLocationEnvelope(hits, region, intent = "read", otherScope = false, omitCaveat) {
14149
+ const message = hits.map((hit) => messageFor(hit, region)).join(" ");
14150
+ const selectedIds = [
14151
+ ...new Set(hits.filter((hit) => hit.selectedId !== void 0).map((hit) => `"${hit.selectedId}" (echoed as "${hit.value}")`))
14152
+ ];
14153
+ const siblings = [
14154
+ ...new Set(hits.flatMap((hit) => hit.siblingCriteria ?? []))
14155
+ ];
14156
+ const emptiesCriterion = hits.filter((hit) => (hit.siblingCriteria?.length ?? 0) > 0).every((hit) => hit.kept.length === 0);
14157
+ const siblingNote = siblings.length === 0 ? "" : `${emptiesCriterion ? " Removing it leaves that `location_ids` criterion holding nothing, so remove the WHOLE criterion rather than just its `locations` property \u2014 an empty `location_ids` criterion is invalid, not neutral." : " Keep the `location_ids` criterion itself \u2014 it still selects a real place once the country comes off."} The other criteria in this filter (${siblings.map((type) => `\`${type}\``).join(", ")}) survive and keep scoping the result, so describe it by them and never as covering everything.`;
14158
+ const idNote = selectedIds.length === 0 ? "" : ` ${selectedIds.length > 1 ? "These are" : "This is"} selected by ID, not by name: remove ${selectedIds.join(", ")} from the \`location_ids\` criterion in \`lens_filter.items[].criteria[]\` itself. Deleting the echoed \`locations.results[].name\` row alone leaves the id selected and the country filter in force.`;
14159
+ if (intent === "write" && hits.some(blocksWrite)) {
14160
+ const blocked = blockedWriteHint(hits, region) + siblingNote + idNote;
14161
+ return { code: COUNTRY_LEVEL_LOCATION, message, hint: blocked };
14162
+ }
14163
+ const groups = /* @__PURE__ */ new Map();
14164
+ for (const hit of hits) {
14165
+ const key = `${hit.param}\0${hit.axis}`;
14166
+ const group = groups.get(key);
14167
+ if (group)
14168
+ group.push(hit);
14169
+ else
14170
+ groups.set(key, [hit]);
14171
+ }
14172
+ const scoped = otherScope || siblings.length > 0;
14173
+ const hints = [];
14174
+ const push = (hint2) => hints.push(hint2);
14175
+ for (const group of groups.values()) {
14176
+ if (group.length === 1)
14177
+ push(hintFor(group[0], region, intent, scoped));
14178
+ else
14179
+ push(reconciledHint(group, region, intent, scoped));
14180
+ }
14181
+ const joined = hints.join(" ");
14182
+ const caveat = omitCaveat !== void 0 && joined.includes("OMIT") ? ` ${omitCaveat}` : "";
14183
+ const hint = joined + caveat + siblingNote + idNote;
14184
+ return { code: COUNTRY_LEVEL_LOCATION, message, hint };
14185
+ }
14186
+ function countryLocationStatus(hits, region, intent = "read", otherScope = false, omitCaveat) {
14187
+ const envelope = countryLocationEnvelope(hits, region, intent, otherScope, omitCaveat);
14188
+ return {
14189
+ status: COUNTRY_LEVEL_STATUS,
14190
+ code: envelope.code,
14191
+ message: envelope.message,
14192
+ hint: envelope.hint,
14193
+ country_locations: [...hits]
14194
+ };
14195
+ }
14196
+ function criteriaHits(criteria, param, region) {
14197
+ if (!Array.isArray(criteria))
14198
+ return [];
14199
+ const hits = [];
14200
+ for (const criterion of criteria) {
14201
+ const record = criterion;
14202
+ if (!record || record.type !== "location_ids")
14203
+ continue;
14204
+ const axis = record.is_excluded === true ? "exclude" : "include";
14205
+ const siblings = [
14206
+ ...new Set(criteria.filter((other) => other !== criterion).map((other) => other?.type).filter((type) => typeof type === "string"))
14207
+ ];
14208
+ hits.push(...detectCountryLocations(record.locations, param, region, axis).map((hit) => siblings.length === 0 ? hit : { ...hit, siblingCriteria: siblings }));
14209
+ }
14210
+ return hits;
14211
+ }
14212
+ function detectCountryLocationsInSetFilter(setFilter, param, region) {
14213
+ if (!setFilter || typeof setFilter !== "object")
14214
+ return [];
14215
+ const criteria = setFilter.criteria;
14216
+ return criteriaHits(criteria, `${param}.criteria[].locations`, region);
14217
+ }
14218
+ function echoedCountryIds(filter, region) {
14219
+ const ids = /* @__PURE__ */ new Set();
14220
+ const locations = filter?.locations;
14221
+ for (const block of ["results", "parents"]) {
14222
+ const rows = locations?.[block];
14223
+ if (!Array.isArray(rows))
14224
+ continue;
14225
+ for (const row of rows) {
14226
+ const record = row;
14227
+ const name = record?.name;
14228
+ const id = record?.id;
14229
+ if (typeof name !== "string")
14230
+ continue;
14231
+ if (typeof id !== "string" && typeof id !== "number")
14232
+ continue;
14233
+ if (classify(name, region) !== null)
14234
+ ids.add(String(id));
14235
+ }
14236
+ }
14237
+ return ids;
14238
+ }
14239
+ function filterCarriesOtherScope(filter, region) {
14240
+ if (!filter || typeof filter !== "object")
14241
+ return false;
14242
+ const lensFilter = filter.lens_filter;
14243
+ const items = lensFilter?.items;
14244
+ if (!Array.isArray(items))
14245
+ return false;
14246
+ const countryIds = echoedCountryIds(filter, region);
14247
+ for (const item of items) {
14248
+ const criteria = item?.criteria;
14249
+ if (!Array.isArray(criteria))
14250
+ continue;
14251
+ for (const criterion of criteria) {
14252
+ const record = criterion;
14253
+ if (!record)
14254
+ continue;
14255
+ if (record.type !== "location_ids")
14256
+ return true;
14257
+ const values = (Array.isArray(record.locations) ? record.locations : []).filter((value) => !countryIds.has(String(value)));
14258
+ if (geoScopeSurvives([{ input: values, param: "locations" }], region)) {
14259
+ return true;
12263
14260
  }
12264
- },
12265
- additionalProperties: false
12266
- },
12267
- execute: async (client, params) => {
12268
- let lang = params.lang;
12269
- if (!lang) {
12270
- try {
12271
- const me = await client.resolveMe();
12272
- lang = me.language ?? "en";
12273
- } catch {
12274
- lang = "en";
14261
+ }
14262
+ }
14263
+ return false;
14264
+ }
14265
+ function setFilterCarriesOtherScope(setFilter, region) {
14266
+ if (!setFilter || typeof setFilter !== "object")
14267
+ return false;
14268
+ const criteria = setFilter.criteria;
14269
+ if (!Array.isArray(criteria))
14270
+ return false;
14271
+ for (const criterion of criteria) {
14272
+ const record = criterion;
14273
+ if (!record)
14274
+ continue;
14275
+ if (record.type !== "location_ids")
14276
+ return true;
14277
+ const values = Array.isArray(record.locations) ? record.locations : [];
14278
+ if (geoScopeSurvives([{ input: values, param: "locations" }], region))
14279
+ return true;
14280
+ }
14281
+ return false;
14282
+ }
14283
+ function detectCountryLocationsInFilter(filter, region) {
14284
+ if (!filter || typeof filter !== "object")
14285
+ return [];
14286
+ const hits = [];
14287
+ const asRecord = filter;
14288
+ const lensFilter = asRecord.lens_filter;
14289
+ const items = lensFilter?.items;
14290
+ const polarityById = /* @__PURE__ */ new Map();
14291
+ const siblingsById = /* @__PURE__ */ new Map();
14292
+ const criterionIdsById = /* @__PURE__ */ new Map();
14293
+ if (Array.isArray(items)) {
14294
+ for (const item of items) {
14295
+ const criteria = item?.criteria;
14296
+ hits.push(...criteriaHits(criteria, "filter.lens_filter.items[].criteria[].locations", region));
14297
+ if (!Array.isArray(criteria))
14298
+ continue;
14299
+ for (const criterion of criteria) {
14300
+ const record = criterion;
14301
+ if (!record || record.type !== "location_ids")
14302
+ continue;
14303
+ const axis = record.is_excluded === true ? "exclude" : "include";
14304
+ const siblings = [
14305
+ ...new Set(criteria.filter((other) => other !== criterion).map((other) => other?.type).filter((type) => typeof type === "string"))
14306
+ ];
14307
+ const ids = Array.isArray(record.locations) ? record.locations : [];
14308
+ for (const id of ids) {
14309
+ if (typeof id === "string" || typeof id === "number") {
14310
+ const key = String(id);
14311
+ if (axis === "exclude" || !polarityById.has(key)) {
14312
+ polarityById.set(key, axis);
14313
+ }
14314
+ if (siblings.length > 0) {
14315
+ siblingsById.set(key, [
14316
+ .../* @__PURE__ */ new Set([...siblingsById.get(key) ?? [], ...siblings])
14317
+ ]);
14318
+ }
14319
+ const others = ids.filter((other) => typeof other === "string" || typeof other === "number").map((other) => String(other)).filter((other) => other !== key);
14320
+ if (others.length > 0) {
14321
+ criterionIdsById.set(key, [
14322
+ .../* @__PURE__ */ new Set([...criterionIdsById.get(key) ?? [], ...others])
14323
+ ]);
14324
+ }
14325
+ }
14326
+ }
12275
14327
  }
12276
14328
  }
12277
- const includeInvisible = params.includeInvisible ? "true" : "false";
12278
- const path = `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=${includeInvisible}`;
12279
- return await client.request("GET", path);
12280
14329
  }
12281
- };
14330
+ const locations = asRecord.locations;
14331
+ const echoedRows = [];
14332
+ for (const block of ["results", "parents"]) {
14333
+ const rows = locations?.[block];
14334
+ if (!Array.isArray(rows))
14335
+ continue;
14336
+ for (const row of rows) {
14337
+ const record = row;
14338
+ const name = record?.name;
14339
+ if (typeof name !== "string")
14340
+ continue;
14341
+ const id = record?.id;
14342
+ if (typeof id !== "string" && typeof id !== "number")
14343
+ continue;
14344
+ echoedRows.push({ id: String(id), name });
14345
+ }
14346
+ }
14347
+ const countryIds = new Set(echoedRows.filter(({ id, name }) => {
14348
+ const axis = polarityById.get(id);
14349
+ return axis !== void 0 && detectCountryLocations(name, "probe", region, axis).length > 0;
14350
+ }).map(({ id }) => id));
14351
+ for (const { id, name } of echoedRows) {
14352
+ const axis = polarityById.get(id);
14353
+ if (axis === void 0)
14354
+ continue;
14355
+ const siblings = siblingsById.get(id);
14356
+ const nameById = new Map(echoedRows.map((row) => [row.id, row.name]));
14357
+ const kept = (criterionIdsById.get(id) ?? []).filter((other) => !countryIds.has(other)).map((other) => {
14358
+ const label = nameById.get(other);
14359
+ return label === void 0 ? other : `${other} (${label})`;
14360
+ });
14361
+ hits.push(...detectCountryLocations(name, `filter.lens_filter.items[].criteria[].locations`, region, axis, id).map((hit) => ({
14362
+ ...hit,
14363
+ ...siblings === void 0 ? {} : { siblingCriteria: siblings },
14364
+ ...kept.length === 0 ? {} : { kept }
14365
+ })));
14366
+ }
14367
+ return hits;
14368
+ }
12282
14369
 
12283
14370
  // ../core/dist/tools/list-locations.js
12284
14371
  var listLocations = {
@@ -12296,7 +14383,7 @@ var listLocations = {
12296
14383
  properties: {
12297
14384
  q: {
12298
14385
  type: "string",
12299
- description: "Free-text city / region name (e.g. 'Berlin', 'NYC', 'S\xE3o Paulo'). Returns top-10 prefix matches sorted by relevance, each with an admin_area id usable in FilterCriterion.location_ids."
14386
+ description: "Free-text city / region name (e.g. 'Berlin', 'NYC', 'S\xE3o Paulo'). Returns top-10 prefix matches sorted by relevance, each with an admin_area id usable in FilterCriterion.location_ids. A COUNTRY name is refused \u2014 the index holds no country nodes, so the lookup could only return a same-named town."
12300
14387
  }
12301
14388
  },
12302
14389
  required: ["q"],
@@ -12314,6 +14401,15 @@ var listLocations = {
12314
14401
  type: "array",
12315
14402
  description: "Parent admin areas referenced by `results[].parent_ids`, returned for breadcrumb / hover-disambiguation rendering.",
12316
14403
  items: { type: "object" }
14404
+ },
14405
+ status: {
14406
+ type: "string",
14407
+ description: "`country_level_location` when `q` was a country name \u2014 `results` is empty on purpose. This workspace serves exactly ONE country, so there is no country to look up and no id to pass on. Absent on the happy path."
14408
+ },
14409
+ country_locations: {
14410
+ type: "array",
14411
+ description: "Per offending value: {value, param, kind, country, axis, kept}. Only present when `status === 'country_level_location'`. Unlike the lead-reading tools, the recovery here is NOT to drop `q` and re-call: `q` is required and an empty lookup returns no results, not workspace-wide coverage. There is simply no country id to hand out \u2014 see `hint`.",
14412
+ items: { type: "object" }
12317
14413
  }
12318
14414
  },
12319
14415
  required: ["results", "parents"]
@@ -12322,6 +14418,25 @@ var listLocations = {
12322
14418
  const q = (params.q ?? "").trim();
12323
14419
  if (!q)
12324
14420
  return { results: [], parents: [] };
14421
+ const countryHits = detectCountryLocations(q, "q", client.region);
14422
+ if (countryHits.length > 0) {
14423
+ const envelope = countryLocationStatus(countryHits, client.region);
14424
+ return {
14425
+ results: [],
14426
+ parents: [],
14427
+ ...envelope,
14428
+ // The shared read recovery is "omit the geo argument and the result
14429
+ // covers the whole workspace". That is right for a tool that READS
14430
+ // leads and wrong here in both halves: `q` is required, so omitting it
14431
+ // fails schema validation, and the empty-`q` branch above returns an
14432
+ // empty envelope rather than workspace-wide data — so an agent that
14433
+ // followed the advice would report "covers everything" over a lookup
14434
+ // that found nothing. This tool hands out IDS; there is no country id
14435
+ // to hand out and no wider lookup to fall back to, so there is nothing
14436
+ // to retry. Overridden the same way tour_plan overrides it.
14437
+ hint: `There is no country to look up: country nodes are absent from the admin-area index (product#3885), so no id exists to return and no spelling of "${q}" will produce one. Do NOT re-call this tool with \`q\` omitted \u2014 \`q\` is required, and an empty lookup is not a whole-workspace result. If the caller wanted somewhere INSIDE this workspace, look up that place instead; if they meant the workspace as a whole, no location id is needed at all \u2014 the tools that consume these ids simply omit the geo argument.`
14438
+ };
14439
+ }
12325
14440
  const path = `/geo/search?q=${encodeURIComponent(q)}`;
12326
14441
  return await client.request("GET", path);
12327
14442
  }
@@ -14893,6 +17008,16 @@ var updateLensFilter = {
14893
17008
  additionalProperties: false
14894
17009
  },
14895
17010
  execute: async (client, params) => {
17011
+ const countryHits = detectCountryLocationsInFilter(params.filter, client.region);
17012
+ if (countryHits.length > 0) {
17013
+ const envelope = countryLocationEnvelope(countryHits, client.region, "write", filterCarriesOtherScope(params.filter, client.region));
17014
+ throw {
17015
+ error: true,
17016
+ code: envelope.code,
17017
+ message: envelope.message,
17018
+ hint: envelope.hint
17019
+ };
17020
+ }
14896
17021
  if (params.dry_run) {
14897
17022
  return {
14898
17023
  dry_run: true,
@@ -16650,14 +18775,14 @@ var pullFollowups = {
16650
18775
  properties: {
16651
18776
  criteria: {
16652
18777
  type: "array",
16653
- description: "Array of FilterCriterion objects per the backend FilterCriterion anyOf schema (location_ids, sector_ids, size, keywords, last_action, last_action_date, liked, yc, custom_field, custom_field_comparison).",
18778
+ description: "Array of FilterCriterion objects per the backend FilterCriterion anyOf schema (location_ids, sector_ids, size, keywords, last_action, last_action_date, liked, yc, custom_field, custom_field_comparison). A `location_ids` criterion must carry sub-country admin areas only \u2014 a country name here is rejected with COUNTRY_LEVEL_LOCATION before anything is persisted.",
16654
18779
  items: { type: "object" }
16655
18780
  }
16656
18781
  }
16657
18782
  },
16658
18783
  city: {
16659
18784
  type: "string",
16660
- description: "Free-text city / region (e.g. 'Berlin', 'NYC', 'S\xE3o Paulo'). The composite resolves it to an admin_area id via GET /geo/search and merges it into the active Monitor filter as a `location_ids` FilterCriterion. Ambiguous matches surface as `status: 'ambiguous_locations'` with `location_ambiguities[]` \u2014 the agent picks an id and re-calls via `city_id`."
18785
+ description: "Free-text city / region (e.g. 'Berlin', 'NYC', 'S\xE3o Paulo'). The composite resolves it to an admin_area id via GET /geo/search and merges it into the active Monitor filter as a `location_ids` FilterCriterion. Ambiguous matches surface as `status: 'ambiguous_locations'` with `location_ambiguities[]` \u2014 the agent picks an id and re-calls via `city_id`. NEVER a country name: this workspace serves exactly ONE country, so a whole-country ask means omitting `city` entirely."
16661
18786
  },
16662
18787
  city_id: {
16663
18788
  type: "string",
@@ -16688,13 +18813,18 @@ var pullFollowups = {
16688
18813
  },
16689
18814
  status: {
16690
18815
  type: "string",
16691
- description: "`ambiguous_locations` when a passed `city` matched multiple admin_areas; the agent picks an id from `location_ambiguities` and re-calls with `city_id`. Absent on the happy path."
18816
+ description: "`ambiguous_locations` when a passed `city` matched multiple admin_areas; the agent picks an id from `location_ambiguities` and re-calls with `city_id`. `country_level_location` when `city`, `city_id` or a `set_filter` `location_ids` criterion carried a country-level value \u2014 nothing was read and no filter was persisted; read `hint` for the recovery, which differs per case. Absent on the happy path."
16692
18817
  },
16693
18818
  location_ambiguities: {
16694
18819
  type: "array",
16695
18820
  description: "Per ambiguous city: {location_text, matches:[{id, name, country, level, score}]}. Only present when `status === 'ambiguous_locations'`.",
16696
18821
  items: { type: "object" }
16697
18822
  },
18823
+ country_locations: {
18824
+ type: "array",
18825
+ description: "Per offending value: {value, param, kind, country, axis, kept}. Only present when `status === 'country_level_location'`. The recovery BRANCHES on `country_locations[].axis` and `[].kind`; `hint` states the one for THIS call \u2014 follow it verbatim. Dropping the argument is NOT the general answer: on an `exclude` axis it returns the very companies the user asked to remove, and for a `foreign_country` an unfiltered result is this workspace's own leads, which answer a different question. Never retry with another spelling or a nearby city.",
18826
+ items: { type: "object" }
18827
+ },
16698
18828
  _meta: {
16699
18829
  type: "object",
16700
18830
  description: "Operator context: region + last-call latency.",
@@ -16713,6 +18843,30 @@ var pullFollowups = {
16713
18843
  const liked = params.liked ?? false;
16714
18844
  const page = params.page ?? 0;
16715
18845
  const count = Math.min(params.count ?? 20, 200);
18846
+ const countryHits = [
18847
+ ...detectCountryLocationsIn([
18848
+ { input: params.city, param: "city" },
18849
+ { input: params.city_id, param: "city_id" }
18850
+ ], client.region),
18851
+ ...detectCountryLocationsInSetFilter(params.set_filter, "set_filter", client.region)
18852
+ ];
18853
+ if (countryHits.length > 0) {
18854
+ const survivingCriteria = setFilterCarriesOtherScope(params.set_filter, client.region) || countryHits.some((hit) => hit.kept.length > 0);
18855
+ const omitCaveat = survivingCriteria ? "Do NOT pass `filtered:false`, and do NOT send `set_filter:{criteria:[]}`: either one discards the other criteria in this request, turning a scoped read into an unscoped one. Re-call with `set_filter` carrying the SURVIVING criteria and the country criterion removed \u2014 that overwrites the stored filter with the corrected one, so no stale filter can leak in. Then describe the result by the criteria that remain, never as covering everything." : "Omitting the geo argument is NOT enough here: `filtered` defaults to true, so the Monitor view is still read through the filter persisted from an earlier call. Nothing else was requested, so pass `filtered:false` as well (or clear the stored filter with `set_filter:{criteria:[]}`) \u2014 otherwise a stale cohort comes back looking like the whole workspace. `active_filters` in the response reports what was actually applied; check it before describing the scope.";
18856
+ return {
18857
+ // `survivingCriteria` is passed, not `false`: it already decided the
18858
+ // caveat above, and the hint has to agree with it. Hardcoding false let
18859
+ // the hint say "OMIT it, then say the result covers everything" while
18860
+ // the caveat it was concatenated with ended "never as covering
18861
+ // everything" — one recovery telling the agent both.
18862
+ ...countryLocationStatus(countryHits, client.region, "read", survivingCriteria, omitCaveat),
18863
+ leads: [],
18864
+ active_filters: null,
18865
+ pagination: null,
18866
+ total_excluded_by_pushback: 0,
18867
+ _meta: { region: client.region, latency_ms: null }
18868
+ };
18869
+ }
16716
18870
  let effectiveSetFilter = params.set_filter;
16717
18871
  const geoTexts = [];
16718
18872
  if (params.city)
@@ -16897,7 +19051,7 @@ var tourPlan = {
16897
19051
  properties: {
16898
19052
  city: {
16899
19053
  type: "string",
16900
- description: "Free-text city or region (e.g. 'Limoges', 'Bay Area'). Resolved via the same /geo/search the followups_map uses. Ambiguous matches surface as `status: ambiguous_locations` with location_ambiguities[]; pick a location id and re-call with city_id."
19054
+ description: "Free-text city or region (e.g. 'Limoges', 'Bay Area'). Resolved via the same /geo/search the followups_map uses. Ambiguous matches surface as `status: ambiguous_locations` with location_ambiguities[]; pick a location id and re-call with city_id. NEVER a country name \u2014 and unlike the Monitor tools the fix is NOT to omit this argument: a tour with no city returns arbitrary leads from the whole workspace, which is not an itinerary. Ask which city or region the user is visiting and pass that."
16901
19055
  },
16902
19056
  city_id: {
16903
19057
  type: "string",
@@ -16949,12 +19103,17 @@ var tourPlan = {
16949
19103
  },
16950
19104
  status: {
16951
19105
  type: "string",
16952
- description: "'ambiguous_locations' when the passed `city` matched multiple admin areas \u2014 pick an id from location_ambiguities and re-call with city_id."
19106
+ description: "'ambiguous_locations' when the passed `city` matched multiple admin areas \u2014 pick an id from location_ambiguities and re-call with city_id. 'country_level_location' when `city` was a country name \u2014 do NOT drop the argument (a city-less tour is arbitrary nationwide leads); ask which city or region to use. The itinerary arrays are empty and nothing was fetched."
16953
19107
  },
16954
19108
  location_ambiguities: {
16955
19109
  type: "array",
16956
19110
  items: { type: "object" }
16957
19111
  },
19112
+ country_locations: {
19113
+ type: "array",
19114
+ description: "Per offending value: {value, param, kind, country}. Only present when `status === 'country_level_location'`. Unlike the Monitor tools, the recovery here is NOT to drop `city`: a tour with no city returns arbitrary leads from the whole workspace, which is not an itinerary. Ask which city or region the user is visiting and re-call with that \u2014 see `hint`.",
19115
+ items: { type: "object" }
19116
+ },
16958
19117
  _meta: {
16959
19118
  type: "object",
16960
19119
  properties: {
@@ -16966,6 +19125,39 @@ var tourPlan = {
16966
19125
  required: ["monitor_leads", "discover_leads", "map_locations"]
16967
19126
  },
16968
19127
  execute: async (client, params, ctx) => {
19128
+ const countryHits = detectCountryLocationsIn([
19129
+ { input: params.city, param: "city" },
19130
+ { input: params.city_id, param: "city_id" }
19131
+ ], client.region);
19132
+ if (countryHits.length > 0) {
19133
+ const envelope = countryLocationStatus(countryHits, client.region);
19134
+ return {
19135
+ ...envelope,
19136
+ // The shared hint says "omit the geo argument and the result covers the
19137
+ // whole workspace" — right for a Monitor pull, WRONG here. tour_plan
19138
+ // accepts no city and then returns arbitrary nationwide leads, which is
19139
+ // not an itinerary; the prompt contract requires asking which city or
19140
+ // region the user is visiting (prompts/leadbay_plan_tour_in_city.md.tmpl).
19141
+ // So this tool overrides the recovery rather than forwarding advice that
19142
+ // would produce a confident, useless tour.
19143
+ hint: "A tour needs a place to walk around in, so there is nothing to omit here: do NOT re-call without `city`, which would return arbitrary leads from across the whole workspace as an itinerary. Ask which city or region the user is actually visiting, then re-call with that. Do NOT retry another spelling of the country.",
19144
+ monitor_leads: [],
19145
+ discover_leads: [],
19146
+ // A STRING, not null: the declared schema allows only a string, and a
19147
+ // client that validates structuredContent would reject the whole
19148
+ // rejection payload — hiding the very recovery hint it carries.
19149
+ discover_filter_note: "No Discover leads were fetched: the request named a country, which cannot scope an itinerary.",
19150
+ map_locations: [],
19151
+ map_summary: {
19152
+ total_leads: 0,
19153
+ leads_with_coords: 0,
19154
+ leads_without_coords: 0
19155
+ },
19156
+ city: params.city ?? null,
19157
+ city_id: params.city_id ?? null,
19158
+ _meta: { region: client.region }
19159
+ };
19160
+ }
16969
19161
  const followupsCount = params.followups_count ?? DEFAULT_FOLLOWUPS_COUNT;
16970
19162
  const discoverCount = params.discover_count ?? DEFAULT_DISCOVER_COUNT;
16971
19163
  const [followupsResult, leadsResult] = await Promise.allSettled([
@@ -18325,6 +20517,197 @@ var getQualificationQuestions = {
18325
20517
  }
18326
20518
  };
18327
20519
 
20520
+ // ../core/dist/composite/getting-started.js
20521
+ var ONE_OPTION_RULE = "Every gate presents exactly ONE way forward, plus a way out \u2014 two options, never more: the action, and 'I'm done for now'. A first-run user does not yet know enough to choose between PATHS; one forward move makes the next step obvious, and the click is what teaches them the tool. The exit keeps the tour from being a trap, and satisfies the host widget's 2-4 option requirement \u2014 a lone option is rejected or silently degrades to prose, which kills the feature. Never add a third option, and never turn the exit into an alternative route ('show me my lenses instead'), which reintroduces the choice this rule removes. The gate IS the widget: never render it as a prose question \u2014 'say the word and I'll check it' is a defect, not a gate. Typing also works: if the user types something off-script, abandon the walkthrough and serve what they asked.";
20522
+ var DOCS_QUICKSTART = "https://docs.leadbay.app/doc/leadbay-mcp/quickstart";
20523
+ var DOCS_NOTE = "Surface this link in exactly TWO moments and nowhere else. (1) BEFORE the tour, when the user's problem is SETUP rather than usage \u2014 the connector isn't installed, they can't sign in, their Leadbay tools aren't appearing, or they want to run this on another host. The walkthrough cannot fix any of that: it assumes a working connection, and gate 1 is what proves it. Point them at the page instead of guessing at install steps. (2) At the CLOSING, as one plain link beside the keep_going cheat-sheet, for what the four gates didn't cover \u2014 installing on another machine, adding a teammate, signing in again later. NEVER paste it between gates: a link mid-tour is an invitation to leave the thing they're in the middle of doing.";
20524
+ var ZOE_CALENDLY = "https://calendly.com/zoe-leadbay/demo-leadbay";
20525
+ var EXIT_OFFER = "Picking 'I'm done for now' is ENDING B, and it has THREE beats in order: (1) one short line acknowledging the stop, (2) the keep_going cheat-sheet and the docs_url link, (3) the 1:1 offer \u2014 LAST, and REQUIRED. Beat 3 is the one that gets dropped: an agent that renders the cheat-sheet feels finished and stops, so the user who just stepped out never hears about the help that would have brought them back. An exit close WITHOUT the offer is incomplete. ONE SENTENCE and calendly_url, e.g. 'If you want a hand tuning this to your own market, Zoe on our team runs 1:1 sessions: <url>'. That length is the rule, not a suggestion: anything longer reads as a pitch. Do NOT enumerate everything Zoe could help with \u2014 that turns an offer into promotional copy, which is exactly what someone who just said they were done does not want. Never re-open the walkthrough, never re-fire the declined gate, and never argue for finishing the tour. If they instead left by TYPING something off-script that is ENDING C, not B \u2014 serve what they asked and skip the cheat-sheet, the link AND the offer.";
20526
+ var EXIT_OPTION = {
20527
+ label: "I'm done for now",
20528
+ description: "Stop the walkthrough here.",
20529
+ kind: "walkthrough_exit"
20530
+ };
20531
+ var INTRO = "Open with a SHORT paragraph \u2014 3-4 sentences, then the widget, all in your first message. Cover, in the user's own language and without jargon: (1) what Leadbay is \u2014 it brings you a fresh batch of companies worth selling to every day, rather than you hunting for them; (2) how it knows what to send \u2014 you describe who you sell to (that description is your LENS) and it goes and finds companies matching it, learning from what you engage with; (3) what this walkthrough will do \u2014 four quick steps, each one a real action on their own account, ending with leads in hand, a first email already written, and the person to send it to; (4) one line handing off to the first step, e.g. 'First, let's see which account you're on.' Then fire gate 1's widget immediately and stop. Keep it to a paragraph \u2014 do NOT walk through the four steps one at a time here (each gate explains itself when its turn arrives), and call no tool in the opening.";
20532
+ var KEEP_GOING = [
20533
+ { want: "Today's fresh leads", say: "Show me today's leads" },
20534
+ { want: "Who to follow up with", say: "What should I follow up on" },
20535
+ { want: "The story on one company", say: "Research <Company>" },
20536
+ { want: "An email to a contact", say: "Draft outreach for <Contact>" },
20537
+ { want: "Change who you target", say: "Narrow the audience to <sector>" },
20538
+ { want: "Switch target audience", say: "Show me my lenses" }
20539
+ ];
20540
+ var STOP = "The walkthrough DRAFTS an email at gate 3 but never SENDS one. The draft stays in the chat for the user to read and judge; nothing leaves. Never send it, never offer to send it on their behalf, and never call leadbay_report_outreach \u2014 logging an outreach that never happened poisons the human team's pipeline. End by waiting for the user.";
20541
+ var GETTING_STARTED_MANIFEST = {
20542
+ version: 1,
20543
+ intro: INTRO,
20544
+ one_option_rule: ONE_OPTION_RULE,
20545
+ docs_url: DOCS_QUICKSTART,
20546
+ docs_note: DOCS_NOTE,
20547
+ calendly_url: ZOE_CALENDLY,
20548
+ exit_offer: EXIT_OFFER,
20549
+ steps: [
20550
+ {
20551
+ n: 1,
20552
+ gate_label: "Check my account",
20553
+ gate_description: "Check my Leadbay account status.",
20554
+ explain: "The opening paragraph IS this gate's explanation \u2014 do not add another one. Just hand off in a line ('First, let's see which account you're on') and fire the widget in the SAME message. WHY IT'S USEFUL, if you say anything at all: this is where they can see at a glance how much they've used this week and what's left, so a batch that comes back small later has a visible reason. On click, the ANSWER is the account itself: user + org, then the full quota windows (see branches).",
20555
+ next_steps: {
20556
+ question: "Let's start with your account status.",
20557
+ options: [
20558
+ {
20559
+ label: "Check my account",
20560
+ description: "Check my Leadbay account status.",
20561
+ kind: "walkthrough_account_status"
20562
+ },
20563
+ EXIT_OPTION
20564
+ ]
20565
+ },
20566
+ calls: "leadbay_account_status",
20567
+ args: {},
20568
+ branches: [
20569
+ {
20570
+ when: "quota is readable",
20571
+ then: "Show them their ACTUAL account \u2014 this is the payoff of the click. One line on who they're signed in as and their organization, then render the quota windows in full the way the web app does: Daily / Weekly / Monthly, each with a \u25B0\u25B1 gauge, % used, $ spent against the cap, and when it resets, plus the per-resource breakdown underneath. Follow the canonical quota-windows rendering (never raw 'credits'). A one-line 'you're connected as X' under-delivers on a button labelled 'check my account status'. THEN EXPLAIN IT in one or two plain lines \u2014 a first-run user has never seen these numbers and can't tell if they're good or bad: say what it counts (the AI work Leadbay does for them \u2014 researching companies and qualifying leads, not something they spend by clicking around) and why it matters (it paces how many fresh leads arrive; heavy use now means a bigger batch queued for next time, and it's where a smaller-than-expected batch would show its reason). Keep it to a sentence or two, don't walk through every resource row, and don't turn it into a pricing pitch."
20572
+ },
20573
+ {
20574
+ when: "quota is null, quota_error is set, or organization.unlimited_credits is true",
20575
+ then: "Say NOTHING about quota \u2014 no gauge, no 'unreadable', no 'unlimited', and skip the quota EXPLANATION too (there is nothing on screen to explain, and describing an absent gauge just confuses). A brand-new org often has no billing plan yet, so the quota read fails; that is not an error worth showing. Do not mention a 401, and above all do NOT tell the user to log in again or reconnect: their token is fine, the same response just read their account. Fall back to the short user + org line and move on. (WORKFLOWS #30.)"
20576
+ },
20577
+ {
20578
+ when: "always",
20579
+ then: "Do NOT volunteer the lens. The response deliberately withholds it unless the user asked, so there is nothing to report, and no other tool should be called to find it. The lens appears naturally at gate 2. (WORKFLOWS #31.)"
20580
+ }
20581
+ ]
20582
+ },
20583
+ {
20584
+ n: 2,
20585
+ gate_label: "Pull today's leads",
20586
+ gate_description: "Pull today's leads from your lens.",
20587
+ explain: "Explain the LENS before firing: Leadbay keeps a lens \u2014 the description of who they sell to \u2014 and every day it finds fresh companies matching it. This click pulls today's batch. WHY IT'S USEFUL: it replaces the hour spent digging through directories and LinkedIn for someone worth calling \u2014 the list is already waiting, scored, when they sit down. And it gets sharper: the leads they like, contact or skip teach the lens what a good fit looks like, so tomorrow's batch is closer than today's.",
20588
+ next_steps: {
20589
+ question: "Now let's see today's leads. Ready?",
20590
+ options: [
20591
+ {
20592
+ label: "Pull today's leads",
20593
+ description: "Pull today's leads from your lens.",
20594
+ kind: "walkthrough_pull_leads"
20595
+ },
20596
+ EXIT_OPTION
20597
+ ]
20598
+ },
20599
+ calls: "leadbay_pull_leads",
20600
+ args: {},
20601
+ pin: "lens.id \u2014 pass as an explicit lensId on every later step, so step 4 enriches the same lens the user just saw. Also pin the TOP-SCORING lead's id and name: gate 3 drafts to it, and gate 4 reveals its contact",
20602
+ branches: [
20603
+ {
20604
+ when: "leads.length > 0",
20605
+ then: "Render the canonical pull_leads table, then advance to gate 3."
20606
+ },
20607
+ {
20608
+ when: "leads.length === 0 && (computing_wishlist || computing_scores)",
20609
+ then: "The lens is still building \u2014 normal on a new account. Say so in the user's terms, then render the tool's own next_steps payload VERBATIM (it carries two options: 'Re-pull in ~30s' / 'Refine audience'). This is the ONE place a gate carries two options, because the server built the payload. On re-pull, wait ~30s and return to gate 2. NEVER say 'no leads found'."
20610
+ },
20611
+ {
20612
+ when: "leads.length === 0 && !computing_wishlist && !computing_scores",
20613
+ then: "The lens is genuinely empty or too narrow and next_steps is null. Say so honestly, offer to widen the audience, and end the walkthrough \u2014 there is nothing to enrich."
20614
+ }
20615
+ ]
20616
+ },
20617
+ {
20618
+ n: 3,
20619
+ gate_label: "Draft the first email",
20620
+ gate_description: "Write a first email to the best company in today's batch.",
20621
+ explain: "Name the TOP-SCORING lead from gate 2 out loud, so the offer is about a real company and not an abstraction. Explain what's about to happen: Leadbay already worked out WHY this company fits them, so it can write the first email instead of leaving them at a blank page. WHY IT'S USEFUL: finding companies was never the hard part \u2014 writing the twentieth opener of the day is where prospecting actually dies. This turns a row in a table into something they could send in a minute. Say plainly that it only DRAFTS: nothing is sent, and they see it first.",
20622
+ next_steps: {
20623
+ question: "Want me to draft the first email to your top lead?",
20624
+ options: [
20625
+ {
20626
+ label: "Draft the first email",
20627
+ description: "Write a first email to the best company in today's batch. Nothing is sent.",
20628
+ kind: "walkthrough_draft_outreach"
20629
+ },
20630
+ EXIT_OPTION
20631
+ ]
20632
+ },
20633
+ calls: "leadbay_prepare_outreach",
20634
+ args: {
20635
+ leadId: "<the highest-scoring lead id from step 2>"
20636
+ },
20637
+ forbidden_args: [
20638
+ "enrich \u2014 enrich:true launches a PAID contact reveal off the back of a DRAFT click. They agreed to see an email written, not to spend. Gate 4 is where the reveal gets asked for, explicitly and on its own terms."
20639
+ ],
20640
+ spend: "This gate spends NOTHING. Call leadbay_prepare_outreach with leadId and nothing else. `recommended_contact` comes back in its post-enrichment shape with email and phone still null \u2014 that is EXPECTED, not a failure, and it is precisely the hook for gate 4: an email written, and nobody to send it to yet. Do not apologise for the missing contact, and do not reach for another tool to fill it in.",
20641
+ branches: [
20642
+ {
20643
+ when: "always",
20644
+ then: "Render the draft through message_compose_v1 \u2014 kind:'email', a summary_title naming the company, and 2-3 variants whose labels name the STRATEGY ('Lead with the growth signal', 'Ask about their current setup'), never the tone. Do NOT also paste the body into chat prose; the composer IS the answer. Address it to the recommended contact's JOB TITLE ('the Head of Operations at <Company>') \u2014 you do not have a name yet, and inventing one is fabrication. Say in one line what made this company the pick: its score and the fit reason from the lead's summary, so the draft reads as reasoned rather than generated."
20645
+ },
20646
+ {
20647
+ when: "the host exposes no message_compose_v1",
20648
+ then: "Fall back to the canonical prepare-outreach rendering: one short context line, then the subject and body as a quoted block. Same content, same no-name rule."
20649
+ }
20650
+ ]
20651
+ },
20652
+ {
20653
+ n: 4,
20654
+ gate_label: "Find who to email",
20655
+ gate_description: "Reveal the person at that company to send the draft to.",
20656
+ explain: "Point straight at the gap the draft just opened: they have an email ready and nobody to send it to \u2014 it's addressed to a job title, not a person. That's what this step fixes. Explain what enrichment IS: Leadbay can find which roles exist at that company, then reveal the actual human and how to reach them. WHY IT'S USEFUL: they ask for the operations director by name instead of pitching whoever answers the switchboard \u2014 the difference between a conversation and a dead end. Say plainly that the first look is free, and that revealing the contact costs credits and needs their say-so.",
20657
+ next_steps: {
20658
+ question: "Want to find out who to send that email to?",
20659
+ options: [
20660
+ {
20661
+ label: "Find who to email",
20662
+ description: "See the roles at that company. Free \u2014 no contact details revealed yet.",
20663
+ kind: "walkthrough_enrich_titles"
20664
+ },
20665
+ EXIT_OPTION
20666
+ ]
20667
+ },
20668
+ calls: "leadbay_enrich_titles",
20669
+ branches: [
20670
+ {
20671
+ when: "leadbay_enrich_titles is NOT in your tool set",
20672
+ then: "This is a read-only deployment (LEADBAY_MCP_WRITE=0) \u2014 the reveal tool simply is not registered. Do NOT fire this gate's widget, and do not hunt for another way to get contact details. Close the tour after gate 3 instead: say plainly that revealing contacts isn't enabled on this connection, that the draft they just watched being written is still theirs, and go to the closing. A gate whose tool cannot run is a dead end, and offering the button anyway is worse than ending one step early."
20673
+ }
20674
+ ],
20675
+ args: {
20676
+ leadIds: "[<the ONE lead you drafted for at step 3>] \u2014 an ARRAY, always",
20677
+ lensId: "<the pinned lens id from step 2>"
20678
+ },
20679
+ spend: "TWO BEATS \u2014 free preview FIRST, the real reveal only after the user confirms. Beat 1: call leadbay_enrich_titles with the drafted lead's id + lensId and NO titles / NO confirm / NO email / NO phone. That returns mode:'discover' \u2014 the FREE list of job titles at that company. Say plainly that nothing has been spent yet. Beat 2: name the title the draft is addressed to, tell them BEFORE they decide what it costs (one credit per contact revealed \u2014 here that is ONE contact, one credit), and ask them to confirm. Only then call leadbay_enrich_titles AGAIN with leadIds: [<that lead id>] \u2014 ALWAYS the array, even for a single lead: `leadId` singular is not a key this tool reads, so it is dropped and the paid call falls back to the default wishlist selection, charging for the whole batch \u2014 plus the chosen title, confirm:true and email:true. Poll leadbay_bulk_enrich_status with the returned bulk_id until all_done (or the count plateaus), and report the contact that actually resolved. NEVER launch the reveal without an explicit confirm: silence is not consent, and neither is 'they clicked the gate'. If they decline, keep the draft and the title and move on \u2014 that is a normal outcome, not a failure.",
20680
+ quota_note: "After the reveal, close the loop on gate 1 in one line: one credit per contact revealed, so this cost one. Then say the thing that makes it land \u2014 the draft from gate 3 now has a real person and a real address to go to. Re-check leadbay_account_status if you want to show the moved windows. This is where gate 1's numbers stop being abstract: they just watched them move, and got something for it. Keep it to a line; no pricing pitch."
20681
+ }
20682
+ ],
20683
+ keep_going: KEEP_GOING,
20684
+ stop: STOP
20685
+ };
20686
+ var gettingStarted = {
20687
+ name: "leadbay_getting_started",
20688
+ annotations: {
20689
+ title: "Guided Leadbay walkthrough",
20690
+ readOnlyHint: true,
20691
+ destructiveHint: false,
20692
+ idempotentHint: true,
20693
+ openWorldHint: false
20694
+ },
20695
+ description: leadbay_getting_started2,
20696
+ write: false,
20697
+ inputSchema: {
20698
+ type: "object",
20699
+ properties: {},
20700
+ additionalProperties: false
20701
+ },
20702
+ // No outputSchema by design — same trade-off as leadbay_artifact_kit:
20703
+ // declaring one enrolls the tool in the output-schema-conformance
20704
+ // drift-catcher (an existing test file we don't modify). The server still
20705
+ // emits the plain-object return as structuredContent.
20706
+ execute: async (_client, _params, _ctx) => {
20707
+ return structuredClone(GETTING_STARTED_MANIFEST);
20708
+ }
20709
+ };
20710
+
18328
20711
  // ../core/dist/composite/set-qualification-questions.js
18329
20712
  var setQualificationQuestions = {
18330
20713
  name: "leadbay_set_qualification_questions",
@@ -18712,7 +21095,7 @@ var scanPortfolioSignals = {
18712
21095
  },
18713
21096
  city: {
18714
21097
  type: "string",
18715
- description: "Free-text city / region to scope the Monitor portfolio before scanning (resolved via /geo/search, same as leadbay_pull_followups). Ignored when `leadIds` is given."
21098
+ description: "Free-text city / region to scope the Monitor portfolio before scanning (resolved via /geo/search, same as leadbay_pull_followups). Ignored when `leadIds` is given. NEVER a country name: this workspace serves exactly ONE country, so a whole-country ask means omitting `city` entirely."
18716
21099
  },
18717
21100
  city_id: {
18718
21101
  type: "string",
@@ -18720,7 +21103,7 @@ var scanPortfolioSignals = {
18720
21103
  },
18721
21104
  set_filter: {
18722
21105
  type: "object",
18723
- description: "Optional Monitor FilterItem ({criteria: FilterCriterion[]}) to scope the portfolio before scanning. Persisted server-side then applied, mirroring leadbay_pull_followups. Ignored when `leadIds` is given.",
21106
+ description: "Optional Monitor FilterItem ({criteria: FilterCriterion[]}) to scope the portfolio before scanning. Persisted server-side then applied, mirroring leadbay_pull_followups. Ignored when `leadIds` is given. A `location_ids` criterion must carry sub-country admin areas only \u2014 a country name here is rejected with COUNTRY_LEVEL_LOCATION before anything is persisted.",
18724
21107
  properties: {
18725
21108
  criteria: { type: "array", items: { type: "object" } }
18726
21109
  }
@@ -18765,13 +21148,18 @@ var scanPortfolioSignals = {
18765
21148
  },
18766
21149
  status: {
18767
21150
  type: "string",
18768
- description: "`ambiguous_locations` when a passed `city` matched multiple admin_areas; pick an id from `location_ambiguities` and re-call with `city_id`. Absent on the happy path."
21151
+ description: "`ambiguous_locations` when a passed `city` matched multiple admin_areas; pick an id from `location_ambiguities` and re-call with `city_id`. `country_level_location` when `city`, `city_id` or a `set_filter` `location_ids` criterion carried a country name \u2014 nothing was scanned and no filter was persisted. Absent on the happy path."
18769
21152
  },
18770
21153
  location_ambiguities: {
18771
21154
  type: "array",
18772
21155
  description: "Only present when status === 'ambiguous_locations'.",
18773
21156
  items: { type: "object" }
18774
21157
  },
21158
+ country_locations: {
21159
+ type: "array",
21160
+ description: "Per offending value: {value, param, kind, country, axis, kept}. Only present when `status === 'country_level_location'`. The recovery BRANCHES on `country_locations[].axis` and `[].kind`; `hint` states the one for THIS call \u2014 follow it verbatim. Dropping the argument is NOT the general answer: on an `exclude` axis it returns the very companies the user asked to remove, and for a `foreign_country` an unfiltered result is this workspace's own leads, which answer a different question. Never retry with another spelling or a nearby city.",
21161
+ items: { type: "object" }
21162
+ },
18775
21163
  _meta: {
18776
21164
  type: "object",
18777
21165
  properties: {
@@ -18796,6 +21184,34 @@ var scanPortfolioSignals = {
18796
21184
  truncatedAt = maxLeads;
18797
21185
  portfolio = sliced.map((id) => ({ id, name: null, location: null }));
18798
21186
  } else {
21187
+ const countryHits = [
21188
+ ...detectCountryLocationsIn([
21189
+ { input: params.city, param: "city" },
21190
+ { input: params.city_id, param: "city_id" }
21191
+ ], client.region),
21192
+ ...detectCountryLocationsInSetFilter(params.set_filter, "set_filter", client.region)
21193
+ ];
21194
+ if (countryHits.length > 0) {
21195
+ const survivingCriteria = setFilterCarriesOtherScope(params.set_filter, client.region) || countryHits.some((hit) => hit.kept.length > 0);
21196
+ return {
21197
+ ...countryLocationStatus(
21198
+ countryHits,
21199
+ client.region,
21200
+ "read",
21201
+ // Same flag that picks the caveat below, so the hint cannot claim
21202
+ // the result "covers everything" while the caveat forbids saying
21203
+ // exactly that.
21204
+ survivingCriteria,
21205
+ survivingCriteria ? "Re-call with `set_filter` carrying the SURVIVING criteria and the country criterion removed \u2014 do NOT send an empty `criteria` array and do NOT drop the other criteria, which are part of the request. A `set_filter` that fails validation is not a no-op here: the failed POST makes this tool scan UNFILTERED, so the criteria you were asked to keep would silently vanish from the scan. Describe the result by the criteria that remain, never as covering everything." : void 0
21206
+ ),
21207
+ matched: [],
21208
+ not_researched: [],
21209
+ scanned_count: 0,
21210
+ matched_count: 0,
21211
+ quota_exceeded: false,
21212
+ _meta: { region: client.region }
21213
+ };
21214
+ }
18799
21215
  let effectiveSetFilter = params.set_filter;
18800
21216
  const geoTexts = [];
18801
21217
  if (params.city)
@@ -22051,17 +24467,17 @@ var adjustAudience = {
22051
24467
  locations: {
22052
24468
  type: "array",
22053
24469
  items: { type: "string" },
22054
- 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."
24470
+ description: "Geographic scope \u2014 free text (e.g. ['Indre-et-Loire', 'Texas', 'Austin']) or admin-area ids. Resolved via /geo/search at any level from state down to city (state / r\xE9gion / d\xE9partement / county / city). NEVER a country name \u2014 this workspace serves exactly ONE country, so a whole-country ask means passing NO location at all (rejected with COUNTRY_LEVEL_LOCATION). Place names go HERE, never in sectors/keywords."
22055
24471
  },
22056
24472
  location_ids: {
22057
24473
  type: "array",
22058
24474
  items: { type: "string" },
22059
- description: "Explicit admin-area ids (skips /geo/search resolution)"
24475
+ description: "Explicit admin-area ids (skips /geo/search resolution). Sub-country areas only \u2014 a country name here is rejected with COUNTRY_LEVEL_LOCATION."
22060
24476
  },
22061
24477
  exclude_locations: {
22062
24478
  type: "array",
22063
24479
  items: { type: "string" },
22064
- description: "Locations to exclude (free text or ids)"
24480
+ description: "Locations to exclude (free text or ids). Sub-country areas only \u2014 excluding a country is meaningless on a single-country workspace and is rejected."
22065
24481
  },
22066
24482
  lensId: { type: "number", description: "Lens id (escape hatch)" },
22067
24483
  lensName: {
@@ -22081,11 +24497,16 @@ var adjustAudience = {
22081
24497
  },
22082
24498
  outputSchema: {
22083
24499
  type: "object",
22084
- 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).",
24500
+ 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); 'country_level_location' when a country-level value was passed as a location (nothing was read or written; read `hint` \u2014 re-calling without the value is often itself wrong); 'lens_not_found' / 'ambiguous_lens' when a lensName didn't resolve to exactly one lens (re-call with lensId or an exact lensName).",
22085
24501
  properties: {
22086
24502
  status: {
22087
24503
  type: "string",
22088
- description: "'applied', 'ambiguous_sectors', 'ambiguous_locations', 'lens_not_found', or 'ambiguous_lens'."
24504
+ description: "'applied', 'ambiguous_sectors', 'ambiguous_locations', 'country_level_location', 'lens_not_found', or 'ambiguous_lens'."
24505
+ },
24506
+ country_locations: {
24507
+ type: "array",
24508
+ description: "On 'country_level_location': per offending value {value, param, kind, country, axis, kept}. A country name is never a location criterion \u2014 each workspace serves exactly ONE country. The lens was NOT modified. The recovery BRANCHES on `country_locations[].axis` and `[].kind`; `hint` states the one for THIS call \u2014 follow it verbatim. When the country was the ONLY scope, or on ANY non-foreign `exclude`, the answer is to write NOTHING at all \u2014 re-calling with the value merely dropped persists a scope that inverts the request. Never retry with another spelling or a nearby city.",
24509
+ items: { type: "object" }
22089
24510
  },
22090
24511
  sector_ambiguities: {
22091
24512
  type: "array",
@@ -22125,6 +24546,23 @@ var adjustAudience = {
22125
24546
  required: ["status"]
22126
24547
  },
22127
24548
  execute: async (client, params, ctx) => {
24549
+ const geoParams = [
24550
+ { input: params.locations, param: "locations" },
24551
+ { input: params.location_ids, param: "location_ids" },
24552
+ { input: params.exclude_locations, param: "exclude_locations", axis: "exclude" }
24553
+ ];
24554
+ const countryHits = detectCountryLocationsIn(geoParams, client.region);
24555
+ if (countryHits.length > 0) {
24556
+ const otherScope = (params.sectors?.length ?? 0) > 0 || (params.sector_ids?.length ?? 0) > 0 || (params.exclude_sectors?.length ?? 0) > 0 || (params.sizes?.length ?? 0) > 0 || geoScopeSurvives(geoParams, client.region);
24557
+ const envelope = countryLocationStatus(countryHits, client.region, "write", otherScope);
24558
+ if (!/re-call ONCE/.test(envelope.hint))
24559
+ return envelope;
24560
+ const lensRef = params.lensId !== void 0 ? String(params.lensId) : "<the lens being edited>";
24561
+ return {
24562
+ ...envelope,
24563
+ hint: `${envelope.hint} Before that re-call, read \`lens://${lensRef}/definition\` \u2014 location criteria MERGE here rather than replace, so any geography the lens already carries survives the re-call untouched. \`leadbay_pull_leads\` returns only \`lens: {id}\` and \`leadbay_my_lenses\` returns no filter, so neither can tell you what it is. If the lens is already scoped to a place, the edited audience stays scoped to it: say which places it actually covers, or clear those criteria first if whole-workspace is what was meant.`
24564
+ };
24565
+ }
22128
24566
  const me = await client.resolveMe();
22129
24567
  const isAdmin = me.admin === true;
22130
24568
  let namedLensId;
@@ -22945,12 +25383,12 @@ var newLens = {
22945
25383
  locations: {
22946
25384
  type: "array",
22947
25385
  items: { type: "string" },
22948
- 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."
25386
+ description: "Geographic scope \u2014 free text (e.g. ['Indre-et-Loire', 'Texas']) or admin-area ids. Resolved via /geo/search at any level from state down to city (state / r\xE9gion / d\xE9partement / county / city). NEVER a country name \u2014 this workspace serves exactly ONE country, so a whole-country ask means passing NO location at all (rejected with COUNTRY_LEVEL_LOCATION). Scopes the lens to a sales territory."
22949
25387
  },
22950
25388
  exclude_locations: {
22951
25389
  type: "array",
22952
25390
  items: { type: "string" },
22953
- description: "Locations to exclude \u2014 free text or ids."
25391
+ description: "Locations to exclude \u2014 free text or ids. Sub-country areas only \u2014 excluding a country is meaningless on a single-country workspace and is rejected."
22954
25392
  },
22955
25393
  base: {
22956
25394
  type: "number",
@@ -22967,9 +25405,9 @@ var newLens = {
22967
25405
  },
22968
25406
  outputSchema: {
22969
25407
  type: "object",
22970
- 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).",
25408
+ 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); 'country_level_location' when a country-level value was passed as a location (the lens was NOT created; read `hint` \u2014 re-calling without the value is often itself wrong).",
22971
25409
  properties: {
22972
- status: { type: "string", description: "'preview', 'created', 'ambiguous_sectors', 'ambiguous_locations', or 'orphan_created' (filter write failed + cleanup failed)." },
25410
+ status: { type: "string", description: "'preview', 'created', 'ambiguous_sectors', 'ambiguous_locations', 'country_level_location', or 'orphan_created' (filter write failed + cleanup failed)." },
22973
25411
  will_create: {
22974
25412
  type: "object",
22975
25413
  description: "On 'preview': what WILL be created \u2014 {name, description, sectors, exclude_sectors, sizes, locations, exclude_locations}. Nothing has been written yet."
@@ -22989,6 +25427,11 @@ var newLens = {
22989
25427
  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.",
22990
25428
  items: { type: "object" }
22991
25429
  },
25430
+ country_locations: {
25431
+ type: "array",
25432
+ description: "On 'country_level_location': per offending value {value, param, kind, country, axis, kept}. A country name is never a location criterion \u2014 each workspace serves exactly ONE country. The recovery BRANCHES on `country_locations[].axis` and `[].kind`; `hint` states the one for THIS call \u2014 follow it verbatim. When the country was the ONLY scope, or on ANY non-foreign `exclude`, the answer is to write NOTHING at all \u2014 re-calling with the value merely dropped persists a scope that inverts the request. Never retry with another spelling or a nearby city.",
25433
+ items: { type: "object" }
25434
+ },
22992
25435
  filter_applied: { type: "object", description: "On 'created': the FilterPayload POSTed to the new lens." },
22993
25436
  computing_wishlist: {
22994
25437
  type: "boolean",
@@ -23000,6 +25443,24 @@ var newLens = {
23000
25443
  required: ["status"]
23001
25444
  },
23002
25445
  execute: async (client, params, ctx) => {
25446
+ const geoParams = [
25447
+ { input: params.locations, param: "locations" },
25448
+ { input: params.exclude_locations, param: "exclude_locations", axis: "exclude" }
25449
+ ];
25450
+ const countryHits = detectCountryLocationsIn(geoParams, client.region);
25451
+ if (countryHits.length > 0) {
25452
+ const otherScope = (params.sectors?.length ?? 0) > 0 || (params.exclude_sectors?.length ?? 0) > 0 || (params.sizes?.length ?? 0) > 0 || // A real place on ANOTHER geo argument is scope too: `kept` only sees
25453
+ // the argument its own value came from.
25454
+ geoScopeSurvives(geoParams, client.region);
25455
+ const envelope = countryLocationStatus(countryHits, client.region, "write", otherScope);
25456
+ const authorizesReCall = /re-call ONCE/.test(envelope.hint);
25457
+ if (!authorizesReCall)
25458
+ return envelope;
25459
+ return {
25460
+ ...envelope,
25461
+ hint: `${envelope.hint} Before that re-call, read the geography of the lens being cloned \u2014 \`lens://${params.base ?? "<active lens id>"}/definition\`, which is the only place a lens's \`location_ids\` are visible (\`leadbay_pull_leads\` returns only \`lens: {id}\`, and \`leadbay_my_lenses\` returns no filter at all). A clone INHERITS that geography, so if the base carries any, the new lens is scoped to it no matter that no location was passed \u2014 and calling the result whole-workspace would be false. If it does carry geography, either clear it on the new lens or say plainly which places it actually covers.`
25462
+ };
25463
+ }
23003
25464
  const includeRes = await resolveSectors(client, params.sectors ?? [], ctx);
23004
25465
  const excludeRes = await resolveSectors(client, params.exclude_sectors ?? [], ctx);
23005
25466
  const ambiguities = [...includeRes.ambiguities, ...excludeRes.ambiguities];
@@ -23881,6 +26342,16 @@ var compositeReadTools = [
23881
26342
  // is a first-session question, and the underlying get_taste_profile is
23882
26343
  // ADVANCED-gated. Read-only; no MCP edit endpoint exists (issue #3768).
23883
26344
  getQualificationQuestions,
26345
+ // Guided first-run walkthrough (issue #3952). ALWAYS exposed, read-only:
26346
+ // returns the six-gate script a brand-new user clicks through to learn
26347
+ // Leadbay by doing (check account → pull leads → draft the first email →
26348
+ // reveal who to send it to → CRM → schedule it).
26349
+ // Makes no backend call. In compositeReadTools so the tour is reachable on a
26350
+ // read-only (LEADBAY_MCP_WRITE=0) deployment — where gate 4's
26351
+ // leadbay_enrich_titles is NOT registered (it is write-gated), so the
26352
+ // manifest's gate-4 branch ends the tour after gate 3 rather than offering a
26353
+ // button whose tool cannot run.
26354
+ gettingStarted,
23884
26355
  // Per-lead custom-field VALUES. ALWAYS exposed: complements the always-on
23885
26356
  // list_mappable_fields (which returns DEFINITIONS only). The lead payload
23886
26357
  // embeds each field's definition, so no catalog join is needed (issue #3768).
@@ -24855,6 +27326,7 @@ function buildScoringParagraph(has) {
24855
27326
  }
24856
27327
  return base;
24857
27328
  }
27329
+ var FIRST_RUN_ROUTING = 'FIRST RUN \u2014 when the user asks to be SHOWN how Leadbay works ("walk me through Leadbay", "I\'m new", "how do I use this", "getting started", "give me a tour", "I just installed this"), invoke the `leadbay_getting_started` prompt via `prompts/get` and follow it. Do NOT improvise your own overview, tour, or summary of the product \u2014 it ships a five-gate walkthrough where each gate is a single-option choice widget the user clicks, so they learn by doing. Writing your own prose tour instead replaces the thing they asked for with a lecture.';
24858
27330
  function buildStartHereParagraph(has) {
24859
27331
  const base = "Start with leadbay_account_status to see the user's state, then leadbay_pull_leads to surface fresh leads. Use leadbay_research_lead_by_id to dig into one lead deeply (qualification answers, signals, contacts).";
24860
27332
  const compositeNames = ["bulk_qualify_leads", "adjust_audience", "refine_prompt", "enrich_titles"].filter((n) => has(`leadbay_${n}`));
@@ -24950,6 +27422,7 @@ function buildServerInstructions(exposed) {
24950
27422
  parts.push(QUOTA_TOPUP);
24951
27423
  parts.push(TRANSIENT_401);
24952
27424
  parts.push(buildScoringParagraph(has));
27425
+ parts.push(FIRST_RUN_ROUTING);
24953
27426
  parts.push(buildStartHereParagraph(has));
24954
27427
  parts.push(buildRhythmParagraph(has));
24955
27428
  const updateParagraph = buildUpdateAvailableParagraph(has);
@@ -25678,8 +28151,22 @@ function makeBrokenClient(stubError, region) {
25678
28151
  }
25679
28152
 
25680
28153
  // src/auth-http.ts
28154
+ var PROBE_TIMEOUT_MS = 4e3;
28155
+ function regionFromToken(token) {
28156
+ const i = token.lastIndexOf("_");
28157
+ if (i < 0) return void 0;
28158
+ const tag = token.slice(i + 1).toLowerCase();
28159
+ return tag === "us" || tag === "fr" ? tag : void 0;
28160
+ }
25681
28161
  async function resolveClientFromToken(token, opts = {}) {
25682
- const { region, baseUrl, logger: logger2 } = opts;
28162
+ const {
28163
+ region,
28164
+ preferRegion,
28165
+ baseUrl,
28166
+ logger: logger2,
28167
+ validate = true,
28168
+ probeTimeoutMs = PROBE_TIMEOUT_MS
28169
+ } = opts;
25683
28170
  if (!token || token.length === 0) {
25684
28171
  const fallbackRegion = region === "fr" ? "fr" : "us";
25685
28172
  return {
@@ -25701,47 +28188,98 @@ async function resolveClientFromToken(token, opts = {}) {
25701
28188
  if (region) config.region = region;
25702
28189
  return { client: createClient(config), authState: "ok" };
25703
28190
  }
25704
- logger2?.info?.("hosted MCP: region unpinned, probing api-us + api-fr in parallel");
25705
- const probe = async (r) => {
25706
- const c = createClient({ token, region: r });
25707
- await c.resolveMe();
25708
- return c;
25709
- };
25710
- try {
25711
- const client = await Promise.any([probe("us"), probe("fr")]);
25712
- return { client, authState: "ok" };
25713
- } catch (err) {
25714
- const errors = err?.errors ?? [];
25715
- const firstAuth = errors.find(
25716
- (e) => e?.code === "AUTH_EXPIRED" || e?.code === "NOT_AUTHENTICATED"
25717
- );
25718
- if (firstAuth) {
25719
- return {
25720
- client: makeBrokenClient(
25721
- {
25722
- error: true,
25723
- code: firstAuth.code,
25724
- message: firstAuth.message,
25725
- hint: "Verify the bearer token is valid. Pin the region with an `X-Leadbay-Region: us|fr` header to skip auto-probing. Authenticate again with `npx -y @leadbay/mcp login --oauth`."
25726
- },
25727
- "us"
25728
- ),
25729
- authState: "expired"
25730
- };
28191
+ const suffixRegion = regionFromToken(token);
28192
+ const primaryRegion = suffixRegion ?? preferRegion ?? "us";
28193
+ if (!validate) {
28194
+ return { client: createClient({ token, region: primaryRegion }), authState: "ok" };
28195
+ }
28196
+ const candidates = primaryRegion === "us" ? ["us", "fr"] : ["fr", "us"];
28197
+ let sawAuthReject = false;
28198
+ let primaryAuthRejected = false;
28199
+ const authRejectedRegions = /* @__PURE__ */ new Set();
28200
+ let firstAuthError;
28201
+ let nonAuthFaultRegion;
28202
+ for (const r of candidates) {
28203
+ const client = createClient({ token, region: r });
28204
+ try {
28205
+ const me = await client.request(
28206
+ "GET",
28207
+ "/users/me",
28208
+ void 0,
28209
+ { retryOn401: false, timeoutMs: probeTimeoutMs }
28210
+ );
28211
+ client.seedMe(me);
28212
+ return { client, authState: "ok" };
28213
+ } catch (e) {
28214
+ const code = e?.code;
28215
+ if (code === "AUTH_EXPIRED" || code === "NOT_AUTHENTICATED") {
28216
+ sawAuthReject = true;
28217
+ if (r === primaryRegion) primaryAuthRejected = true;
28218
+ authRejectedRegions.add(r);
28219
+ firstAuthError ??= e;
28220
+ } else {
28221
+ if (code === "TIMEOUT") {
28222
+ logger2?.warn?.(
28223
+ `hosted MCP auth probe against ${r} exceeded ${probeTimeoutMs}ms \u2014 moving on to the next candidate region`
28224
+ );
28225
+ }
28226
+ nonAuthFaultRegion ??= r;
28227
+ }
28228
+ continue;
28229
+ }
28230
+ }
28231
+ const aboutToExpire = sawAuthReject && nonAuthFaultRegion === void 0;
28232
+ const retryPlan = aboutToExpire ? candidates.filter((r) => authRejectedRegions.has(r)).map((r) => ({ region: r, retryOn401: false })) : primaryAuthRejected && suffixRegion !== void 0 ? [{ region: primaryRegion, retryOn401: true }] : [];
28233
+ for (const step of retryPlan) {
28234
+ const client = createClient({ token, region: step.region });
28235
+ try {
28236
+ const me = await client.request(
28237
+ "GET",
28238
+ "/users/me",
28239
+ void 0,
28240
+ { retryOn401: step.retryOn401, timeoutMs: probeTimeoutMs }
28241
+ );
28242
+ logger2?.warn?.(
28243
+ `hosted MCP auth probe against ${step.region} recovered on retry \u2014 the first 401 was a transient blip, not an expired token`
28244
+ );
28245
+ client.seedMe(me);
28246
+ return { client, authState: "ok" };
28247
+ } catch (e) {
28248
+ const code = e?.code;
28249
+ if (code !== "AUTH_EXPIRED" && code !== "NOT_AUTHENTICATED") {
28250
+ if (aboutToExpire) {
28251
+ nonAuthFaultRegion = step.region;
28252
+ break;
28253
+ }
28254
+ nonAuthFaultRegion = step.region;
28255
+ } else {
28256
+ firstAuthError ??= e;
28257
+ }
25731
28258
  }
28259
+ }
28260
+ if (sawAuthReject && nonAuthFaultRegion === void 0) {
28261
+ logger2?.warn?.("hosted MCP bearer rejected by all candidate regions \u2014 emitting invalid_token challenge");
25732
28262
  return {
25733
- client: createClient({ token, region: "us" }),
25734
- authState: "probe_failed"
28263
+ client: makeBrokenClient(
28264
+ {
28265
+ error: true,
28266
+ code: firstAuthError?.code ?? "AUTH_EXPIRED",
28267
+ message: firstAuthError?.message ?? "The Leadbay access token was rejected.",
28268
+ hint: 'The token is invalid or expired. The 401 challenge carries `error="invalid_token"` so a spec-compliant host refreshes silently; otherwise authenticate again with `npx -y @leadbay/mcp login --oauth`.'
28269
+ },
28270
+ primaryRegion
28271
+ ),
28272
+ authState: "expired"
25735
28273
  };
25736
28274
  }
28275
+ const bindRegion = nonAuthFaultRegion ?? primaryRegion;
28276
+ return { client: createClient({ token, region: bindRegion }), authState: "ok" };
25737
28277
  }
25738
- function regionAuthServer(region) {
25739
- return region === "fr" ? REGIONS.fr : REGIONS.us;
25740
- }
28278
+ var STARGATE_AUTH_SERVER = process.env.LEADBAY_AUTH_SERVER ?? "https://stargate.leadbay.app";
25741
28279
  function protectedResourceMetadata(opts) {
25742
28280
  return {
25743
28281
  resource: opts.resourceUrl,
25744
- authorization_servers: [regionAuthServer(opts.region)],
28282
+ authorization_servers: [STARGATE_AUTH_SERVER],
25745
28283
  bearer_methods_supported: ["header"]
25746
28284
  };
25747
28285
  }
@@ -25769,7 +28307,7 @@ function parseWriteEnv(env = process.env) {
25769
28307
  }
25770
28308
 
25771
28309
  // src/http-server.ts
25772
- var VERSION = true ? "0.28.0" : "0.0.0-dev";
28310
+ var VERSION = true ? "0.30.0" : "0.0.0-dev";
25773
28311
  var PORT = Number(process.env.PORT ?? 8080);
25774
28312
  var HOST = process.env.HOST ?? "0.0.0.0";
25775
28313
  var logger = {
@@ -25789,7 +28327,7 @@ async function resolveTelemetryContext(client) {
25789
28327
  const region = client.region;
25790
28328
  try {
25791
28329
  const me = await Promise.race([
25792
- client.resolveMe(),
28330
+ client.resolveMe(false, { timeoutMs: IDENTITY_RESOLVE_TIMEOUT_MS }),
25793
28331
  new Promise(
25794
28332
  (resolve) => setTimeout(() => resolve(null), IDENTITY_RESOLVE_TIMEOUT_MS)
25795
28333
  )
@@ -25904,10 +28442,7 @@ function buildServerFromClient(client, requestTelemetry) {
25904
28442
  });
25905
28443
  }
25906
28444
  var PRM_PREFIX = "/.well-known/oauth-protected-resource";
25907
- var RESOURCE_PATHS = ["/mcp", "/fr/mcp", "/sse", "/fr/sse"];
25908
- function regionForResourcePath(resourcePath) {
25909
- return /^\/fr(\/|$)/.test(resourcePath) ? "fr" : "us";
25910
- }
28445
+ var RESOURCE_PATHS = ["/mcp", "/sse", "/fr/mcp", "/fr/sse"];
25911
28446
  function requestOrigin(c) {
25912
28447
  const url = new URL(c.req.url);
25913
28448
  const proto = c.req.header("x-forwarded-proto") ?? url.protocol.replace(/:$/, "");
@@ -25941,8 +28476,7 @@ function servePrm(c, resourcePath) {
25941
28476
  c.header("Cache-Control", "public, max-age=3600");
25942
28477
  return c.json(
25943
28478
  protectedResourceMetadata({
25944
- resourceUrl: `${requestOrigin(c)}${resourcePath}`,
25945
- region: regionForResourcePath(resourcePath)
28479
+ resourceUrl: `${requestOrigin(c)}${resourcePath}`
25946
28480
  })
25947
28481
  );
25948
28482
  }
@@ -25977,7 +28511,8 @@ async function handleStreamable(c, resourcePath) {
25977
28511
  const foreign = rejectForeignOrigin(c);
25978
28512
  if (foreign) return foreign;
25979
28513
  const token = extractBearer(c.req.header("authorization"));
25980
- const resolved = await resolveClientFromToken(token, { logger });
28514
+ const preferRegion = resourcePath === "/fr/mcp" ? "fr" : void 0;
28515
+ const resolved = await resolveClientFromToken(token, { preferRegion, logger });
25981
28516
  if (resolved.authState === "missing" || resolved.authState === "expired") {
25982
28517
  return sendChallenge(c, resourcePath, resolved.authState);
25983
28518
  }
@@ -26027,7 +28562,8 @@ async function handleSse(c, resourcePath) {
26027
28562
  const foreign = rejectForeignOrigin(c);
26028
28563
  if (foreign) return foreign;
26029
28564
  const token = extractBearer(c.req.header("authorization"));
26030
- const resolved = await resolveClientFromToken(token, { logger });
28565
+ const preferRegion = resourcePath === "/fr/sse" ? "fr" : void 0;
28566
+ const resolved = await resolveClientFromToken(token, { preferRegion, logger });
26031
28567
  if (resolved.authState === "missing" || resolved.authState === "expired") {
26032
28568
  return sendChallenge(c, resourcePath, resolved.authState);
26033
28569
  }
@@ -26110,7 +28646,7 @@ function scheduleSseTelemetryRefresh(session, stampSeqAtMessageStart, timeoutMs
26110
28646
  timedOut = true;
26111
28647
  applyIfCurrent(failClosed, false);
26112
28648
  }, timeoutMs);
26113
- void session.client.fetchTelemetryEnabled().then(
28649
+ void session.client.fetchTelemetryEnabled({ timeoutMs }).then(
26114
28650
  (enabled) => {
26115
28651
  readSettled = true;
26116
28652
  clearTimeout(timeout);