@leadbay/mcp 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +36 -1
- package/README.md +9 -0
- package/dist/bin.js +205 -27
- package/dist/{chunk-3WNCQ7MP.js → chunk-2EGNZRD7.js} +1195 -50
- package/dist/{dist-2RTYPHB3.js → dist-IIPWWDS5.js} +7 -1
- package/package.json +1 -1
|
@@ -571,6 +571,8 @@ WHEN NOT TO USE: as a substitute for leadbay_research_lead \u2014 that already i
|
|
|
571
571
|
`;
|
|
572
572
|
var leadbay_bulk_qualify_leads = `Pick the next N unqualified leads in the active lens and qualify them (run AI rescore + web fetch). Pass \`wait_for_completion:false\` to return quickly with \`{status:'running', qualify_id}\`; poll leadbay_qualify_status with that id. With \`wait_for_completion\` omitted/true, the legacy behavior polls until the answers are populated or a budget is exhausted. Already-qualified leads (those with a non-null \`ai_agent_lead_score\`) are silently no-ops on the backend, so this composite paginates past them to find fresh candidates. On 429 mid-fanout, stops launching but keeps polling already-launched leads.
|
|
573
573
|
|
|
574
|
+
**Default to \`wait_for_completion:false\`** for any \`count > 5\` or when chained inside a multi-phase workflow \u2014 the blocking default can hit the MCP per-call timeout and surface as \`"Request timed out"\` even when the server is still working fine. The async pattern (capture \`qualify_id\`, poll \`leadbay_qualify_status\` every ~10s) is timeout-proof. Reserve the blocking form for tiny single-digit counts in interactive use.
|
|
575
|
+
|
|
574
576
|
Context: Leadbay auto-qualifies roughly the top 10 of each daily batch. Leads below the top ~10 are NOT worse \u2014 the system is saving resources. This tool is how the agent spends more resources to go deeper on promising-looking leads the user hasn't had time to surface yet.
|
|
575
577
|
|
|
576
578
|
WHEN TO USE: when the user wants more qualified leads than what's currently shown, or when a lead looks promising in leadbay_pull_leads but has an empty \`qualification_summary\`.
|
|
@@ -578,6 +580,43 @@ WHEN TO USE: when the user wants more qualified leads than what's currently show
|
|
|
578
580
|
WHEN NOT TO USE: to qualify a single specific lead \u2014 that's leadbay_qualify_lead (granular, advanced).
|
|
579
581
|
|
|
580
582
|
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\`.
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
---
|
|
586
|
+
|
|
587
|
+
## Status / scalar \u2014 single-sentence shape
|
|
588
|
+
|
|
589
|
+
The response is a status confirmation or scalar \u2014 render exactly one sentence inline. Do NOT emit a card or a table. Do NOT enumerate the affected records (that's the next tool's job).
|
|
590
|
+
|
|
591
|
+
Template patterns to follow:
|
|
592
|
+
|
|
593
|
+
- Job kicked off \u2192 \`"\u2713 <Verb> N <noun(s)> \u2014 typically ~M minutes. I'll refresh when it's done."\`
|
|
594
|
+
- No work needed \u2192 \`"All N <noun(s)> already <state> \u2014 no work to do."\`
|
|
595
|
+
- Long-running \u2192 \`"\u23F3 <Verb> still running \u2014 N% complete; check back in ~M minutes."\`
|
|
596
|
+
- Failure \u2192 \`"\u26A0 <Verb> failed: <error>. <recovery hint>"\`
|
|
597
|
+
|
|
598
|
+
After the status line, propose the obvious refresh / progress-check / recovery action in the NEXT STEPS block. Never expand the status into a card.
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
Specifically for bulk qualify:
|
|
602
|
+
|
|
603
|
+
- Kicked off async \u2192 \`"\u2713 Qualifying N lead(s) (qualify_id <id>) \u2014 typically ~M minutes. I'll refresh your leads view when it's done."\`
|
|
604
|
+
- Blocking call returned with answers \u2192 \`"\u2713 Qualified N lead(s). Refresh your leads to see the new \u2756 caps."\`
|
|
605
|
+
- Already-qualified short-circuit \u2192 \`"All N leads are already qualified \u2014 no work to do."\`
|
|
606
|
+
- 429 mid-fanout \u2192 \`"\u26A0 Rate-limited after launching M of N \u2014 already-launched leads will complete; re-call later for the rest."\`
|
|
607
|
+
|
|
608
|
+
Do not enumerate the affected leads \u2014 that's the job of \`leadbay_pull_leads\`.
|
|
609
|
+
|
|
610
|
+
---
|
|
611
|
+
|
|
612
|
+
## NEXT STEPS \u2014 after kicking off bulk qualification
|
|
613
|
+
|
|
614
|
+
Exactly two offers \u2014 keep it terse, this is a status tool:
|
|
615
|
+
|
|
616
|
+
| Observation | Suggest | Calls |
|
|
617
|
+
|--------------------------------------|-----------------------------------------------|--------------------------------|
|
|
618
|
+
| Qualification kicked off (async) | "Check progress in ~30s" | leadbay_qualify_status |
|
|
619
|
+
| Job is done / blocking call returned | "Refresh leads view \u2014 the new qualifications should be on the top" | leadbay_pull_leads(lensId = pinned) |
|
|
581
620
|
`;
|
|
582
621
|
var leadbay_clear_selection = `Clear the user's transient selection.
|
|
583
622
|
|
|
@@ -759,6 +798,38 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
|
|
|
759
798
|
|
|
760
799
|
|
|
761
800
|
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role; active billing. Imported leads are NOT auto-promoted to the Monitor view; lens-scoring threshold decides.
|
|
801
|
+
|
|
802
|
+
---
|
|
803
|
+
|
|
804
|
+
## RENDERING \u2014 import result summary (single-record, terse)
|
|
805
|
+
|
|
806
|
+
The response carries either a completed result or an async handle. Render a brief summary; do NOT enumerate every imported lead.
|
|
807
|
+
|
|
808
|
+
**Header \u2014 single line, choose by status:**
|
|
809
|
+
|
|
810
|
+
- Completed: \`"\u2713 Import complete \u2014 N leads imported \xB7 M failed \xB7 P resolved-with-ambiguity"\`
|
|
811
|
+
- Running: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
|
|
812
|
+
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 qualify_id <id>"\`
|
|
813
|
+
|
|
814
|
+
**When failures or ambiguous rows are non-empty**, follow the header with a small bulleted list (\u2264 5 items): \`<row identifier or domain> \xB7 <reason>\`. Then \`"*+N more \u2014 leadbay_import_status for full detail*"\`.
|
|
815
|
+
|
|
816
|
+
**When the user's request implied a downstream use** ("import then prep outreach for them"), emit \`Imported leadIds: <up to 5 ids, then '+N more'>\` \u2014 just the ids. Let the next composite render the leads.
|
|
817
|
+
|
|
818
|
+
Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_research_lead\` in NEXT STEPS.
|
|
819
|
+
|
|
820
|
+
|
|
821
|
+
---
|
|
822
|
+
|
|
823
|
+
## NEXT STEPS \u2014 after an import
|
|
824
|
+
|
|
825
|
+
| Observation | Suggest | Calls |
|
|
826
|
+
|------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
827
|
+
| Status: running | "Check progress" | leadbay_import_status(handle_id) |
|
|
828
|
+
| Status: complete, imports succeeded | "Run AI qualification on the imported leads" | leadbay_bulk_qualify_leads([leadIds]) \u2014 or use leadbay_import_and_qualify next time |
|
|
829
|
+
| Ambiguous / unresolved rows present | "Resolve the ambiguous rows" | leadbay_resolve_import_rows(records, identity_mappings)|
|
|
830
|
+
| Failed rows from bad mappings | "Check the org's mappable fields and remap" | leadbay_list_mappable_fields |
|
|
831
|
+
| User wants to see the imported leads | "See the imported leads in your view" | leadbay_pull_leads |
|
|
832
|
+
| User had follow-up intent for the imports | "Prep outreach for [a specific imported lead]" | leadbay_prepare_outreach(leadId) |
|
|
762
833
|
`;
|
|
763
834
|
var leadbay_import_leads = `Import leads into Leadbay's CRM via the file-import wizard. Returns stable Leadbay leadIds for downstream chaining into leadbay_bulk_qualify_leads / leadbay_research_lead. For MCP clients with short transport timeouts, pass \`wait_for_completion:false\` to return quickly with \`{status:'running', handle_id}\`; poll leadbay_import_status with that handle. For end-to-end import+qualify in one call, prefer leadbay_import_and_qualify. For messy files, prefer the \`leadbay_import_file\` prompt which walks an agent through scan \u2192 resolve \u2192 preserve \u2192 commit phases.
|
|
764
835
|
|
|
@@ -774,12 +845,76 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
|
|
|
774
845
|
|
|
775
846
|
|
|
776
847
|
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role on the Leadbay account; active billing.
|
|
848
|
+
|
|
849
|
+
---
|
|
850
|
+
|
|
851
|
+
## RENDERING \u2014 import result summary (single-record, terse)
|
|
852
|
+
|
|
853
|
+
The response carries either a completed result or an async handle. Render a brief summary; do NOT enumerate every imported lead.
|
|
854
|
+
|
|
855
|
+
**Header \u2014 single line, choose by status:**
|
|
856
|
+
|
|
857
|
+
- Completed: \`"\u2713 Import complete \u2014 N leads imported \xB7 M failed \xB7 P resolved-with-ambiguity"\`
|
|
858
|
+
- Running: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
|
|
859
|
+
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 qualify_id <id>"\`
|
|
860
|
+
|
|
861
|
+
**When failures or ambiguous rows are non-empty**, follow the header with a small bulleted list (\u2264 5 items): \`<row identifier or domain> \xB7 <reason>\`. Then \`"*+N more \u2014 leadbay_import_status for full detail*"\`.
|
|
862
|
+
|
|
863
|
+
**When the user's request implied a downstream use** ("import then prep outreach for them"), emit \`Imported leadIds: <up to 5 ids, then '+N more'>\` \u2014 just the ids. Let the next composite render the leads.
|
|
864
|
+
|
|
865
|
+
Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_research_lead\` in NEXT STEPS.
|
|
866
|
+
|
|
867
|
+
|
|
868
|
+
---
|
|
869
|
+
|
|
870
|
+
## NEXT STEPS \u2014 after an import
|
|
871
|
+
|
|
872
|
+
| Observation | Suggest | Calls |
|
|
873
|
+
|------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
874
|
+
| Status: running | "Check progress" | leadbay_import_status(handle_id) |
|
|
875
|
+
| Status: complete, imports succeeded | "Run AI qualification on the imported leads" | leadbay_bulk_qualify_leads([leadIds]) \u2014 or use leadbay_import_and_qualify next time |
|
|
876
|
+
| Ambiguous / unresolved rows present | "Resolve the ambiguous rows" | leadbay_resolve_import_rows(records, identity_mappings)|
|
|
877
|
+
| Failed rows from bad mappings | "Check the org's mappable fields and remap" | leadbay_list_mappable_fields |
|
|
878
|
+
| User wants to see the imported leads | "See the imported leads in your view" | leadbay_pull_leads |
|
|
879
|
+
| User had follow-up intent for the imports | "Prep outreach for [a specific imported lead]" | leadbay_prepare_outreach(leadId) |
|
|
777
880
|
`;
|
|
778
881
|
var leadbay_import_status = `Retrieve the current state of an async lead import. Pass \`handle_id\` returned by \`leadbay_import_leads({wait_for_completion:false})\`, or pass legacy \`importIds[]\` to inspect backend wizard rows. This status call performs a single refresh pass and never polls in a loop.
|
|
779
882
|
|
|
780
883
|
WHEN TO USE: after leadbay_import_leads or leadbay_import_and_qualify returns \`{status:'running', handle_id}\` for the import phase, call this tool later to retrieve progress or the final import result without re-running the import.
|
|
781
884
|
|
|
782
885
|
WHEN NOT TO USE: for qualification handles returned as \`qualify_id\` \u2014 use leadbay_qualify_status for those; or when you still want the legacy blocking behavior from leadbay_import_leads with \`wait_for_completion=true\`.
|
|
886
|
+
|
|
887
|
+
---
|
|
888
|
+
|
|
889
|
+
## Status / scalar \u2014 single-sentence shape
|
|
890
|
+
|
|
891
|
+
The response is a status confirmation or scalar \u2014 render exactly one sentence inline. Do NOT emit a card or a table. Do NOT enumerate the affected records (that's the next tool's job).
|
|
892
|
+
|
|
893
|
+
Template patterns to follow:
|
|
894
|
+
|
|
895
|
+
- Job kicked off \u2192 \`"\u2713 <Verb> N <noun(s)> \u2014 typically ~M minutes. I'll refresh when it's done."\`
|
|
896
|
+
- No work needed \u2192 \`"All N <noun(s)> already <state> \u2014 no work to do."\`
|
|
897
|
+
- Long-running \u2192 \`"\u23F3 <Verb> still running \u2014 N% complete; check back in ~M minutes."\`
|
|
898
|
+
- Failure \u2192 \`"\u26A0 <Verb> failed: <error>. <recovery hint>"\`
|
|
899
|
+
|
|
900
|
+
After the status line, propose the obvious refresh / progress-check / recovery action in the NEXT STEPS block. Never expand the status into a card.
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
Specifically for import status:
|
|
904
|
+
|
|
905
|
+
- Running \u2192 \`"\u23F3 Import still running \u2014 N% complete; check back in ~M minutes."\`
|
|
906
|
+
- Complete \u2192 \`"\u2713 Import complete \u2014 N leads imported, M failed."\`
|
|
907
|
+
- Error / failed \u2192 \`"\u26A0 Import failed: <error>. See leadbay_resolve_import_rows for diagnosis."\`
|
|
908
|
+
|
|
909
|
+
---
|
|
910
|
+
|
|
911
|
+
## NEXT STEPS
|
|
912
|
+
|
|
913
|
+
| Observation | Suggest | Calls |
|
|
914
|
+
|-------------------------|----------------------------------------|--------------------------------|
|
|
915
|
+
| Status: complete | "See the imported leads" | leadbay_pull_leads |
|
|
916
|
+
| Status: running | "Check again in N minutes" | leadbay_import_status \u2014 re-call|
|
|
917
|
+
| Status: error / failed | "Diagnose the failure" | leadbay_resolve_import_rows |
|
|
783
918
|
`;
|
|
784
919
|
var leadbay_launch_bulk_enrichment = `Launch a bulk-enrichment job against the current selection. The backend requires \`email=true\` OR \`phone=true\` (both can be true). Returns 204 with no body \u2014 there is no bulk_id and no per-job status endpoint. Track results by polling individual leads via leadbay_get_contacts after ~60s; \`contact.enrichment.done\` flips to true. \`dry_run:true\` returns the call shape without contacting the backend.
|
|
785
920
|
|
|
@@ -803,7 +938,35 @@ Optional \`for_records\` param: pass a sample of CSV-shaped rows and the tool al
|
|
|
803
938
|
|
|
804
939
|
WHEN TO USE: before authoring an import mapping, especially when the CSV has columns that aren't obvious matches for standard fields.
|
|
805
940
|
|
|
806
|
-
WHEN NOT TO USE: when you already know the mapping \u2014 this call is cheap (~50ms without for_records, ~5\u201310s with) but unnecessary if the agent has already cached the catalog within the same conversation.
|
|
941
|
+
WHEN NOT TO USE: when you already know the mapping \u2014 this call is cheap (~50ms without for_records, ~5\u201310s with) but unnecessary if the agent has already cached the catalog within the same conversation. Cache the result per session; re-fetch only after a \`leadbay_create_custom_field\` call (which can change the catalog).
|
|
942
|
+
|
|
943
|
+
---
|
|
944
|
+
|
|
945
|
+
## RENDERING
|
|
946
|
+
|
|
947
|
+
When called mid-import (internal use), do NOT render \u2014 return the data only.
|
|
948
|
+
|
|
949
|
+
When the user asks to inspect the org's fields directly, render two sectioned bulleted lists:
|
|
950
|
+
|
|
951
|
+
##### Standard fields
|
|
952
|
+
|
|
953
|
+
Bullet each, grouped by domain (lead identity \u2192 location \u2192 sector \u2192 contact \u2192 status). Keep field names verbatim.
|
|
954
|
+
|
|
955
|
+
##### Custom fields (N defined)
|
|
956
|
+
|
|
957
|
+
Bullet each as \`<name> \xB7 <type> \xB7 \` \`\` \`<mapping_value>\` \`\` (mapping_value in backticks \u2014 agents need it verbatim for import calls).
|
|
958
|
+
|
|
959
|
+
When called with \`for_records\`, append a final section **"Mapping hints for your file"** with the per-column suggestions; flag low-confidence mappings explicitly.
|
|
960
|
+
|
|
961
|
+
---
|
|
962
|
+
|
|
963
|
+
## NEXT STEPS
|
|
964
|
+
|
|
965
|
+
| Observation | Suggest | Calls |
|
|
966
|
+
|--------------------------------------------|-------------------------------------------------------------|--------------------------------------------------------|
|
|
967
|
+
| User is preparing an import | "Map a file" | leadbay_resolve_import_rows / leadbay_import_leads |
|
|
968
|
+
| User needs a custom field that doesn't exist | "Create a new custom field" | leadbay_create_custom_field |
|
|
969
|
+
| for_records was passed; hints look good | "Run the import with these mappings" | leadbay_import_leads(records, mappings) |
|
|
807
970
|
`;
|
|
808
971
|
var leadbay_list_sectors = `List the sector taxonomy (id + display name in the requested language). Default: \`lang\` follows the caller's language; \`includeInvisible=false\` returns ~1,091 visible sectors.
|
|
809
972
|
|
|
@@ -827,11 +990,110 @@ WHEN NOT TO USE: from agent flow \u2014 use leadbay_answer_clarification.
|
|
|
827
990
|
|
|
828
991
|
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\`.
|
|
829
992
|
`;
|
|
830
|
-
var leadbay_prepare_outreach = `Prepare
|
|
993
|
+
var leadbay_prepare_outreach = `Prepare a single-lead outreach brief: the full \`lead\` block (score, \`split_ai_summary\`, \`location\`, \`size\`, \`phone_numbers\`, \`website\`, \`description\`, \`social_urls\`, \`social_presence\`), the \`recommended_contact\` (always in the post-enrichment shape \u2014 \`contact_id\`, \`first_name\`, \`last_name\`, \`job_title\`, \`email\`, \`phone_number\`, \`linkedin_page\`, \`is_org_contact\` \u2014 with nulls where data isn't yet enriched), \`additional_contacts_count\` (other contacts at this company), and an \`enrichment\` block describing async state.
|
|
994
|
+
|
|
995
|
+
Optionally trigger contact enrichment in-flight with \`enrich:true\`. Enrichment is async (~60s). **Self-polling pattern (no separate tool needed):** re-call \`leadbay_prepare_outreach(leadId)\` without \`enrich\`; check \`enrichment.complete\`. When \`complete: true\`, the recommended contact now carries \`email\` and/or \`phone_number\`.
|
|
831
996
|
|
|
832
|
-
|
|
997
|
+
The first call to this tool on a lead records a \`LEAD_VIEWED\`-style prospecting action server-side (one per lead per session \u2014 deduped) so the lead ages out of the Discover "new" view.
|
|
998
|
+
|
|
999
|
+
IRON LAW \u2014 OUTCOME AFTER OUTREACH. The moment the user reports outreach happened ("I sent it", "she didn't pick up", "left a voicemail", "they replied", a forwarded email thread, a calendar invite), you MUST (1) call leadbay_report_outreach with verification (gmail_message_id, calendar_event_id, or the user's literal one-sentence confirmation as user_confirmed.ref) AND (2) ask the user about the outcome and set epilogue_status to one of the 4 canonical values: EPILOGUE_INTEREST_VALIDATED_OR_MEETING_PLANED ("Meeting booked"), EPILOGUE_COULD_NOT_REACH_STILL_TRYING ("Trying to reach"), EPILOGUE_NOT_INTERESTED_LOST ("Not interested"), EPILOGUE_STILL_CHASING ("In progress"). Use the user-facing labels in dialogue ("What's the outcome \u2014 meeting booked, trying to reach, not interested, or in progress?"); never say "epilogue" out loud. Skipping this step silently de-ranks every future follow-up suggestion because pull_followups depends on honest, current outcomes.
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
WHEN TO USE: when the agent is about to draft outreach for ONE specific lead and needs everything to compose \u2014 channels + angles + history context.
|
|
833
1003
|
|
|
834
1004
|
WHEN NOT TO USE: across many leads \u2014 use leadbay_enrich_titles for bulk; for general lead detail use leadbay_research_lead (richer signals); to actually log the outreach action use leadbay_report_outreach (requires verification).
|
|
1005
|
+
|
|
1006
|
+
---
|
|
1007
|
+
|
|
1008
|
+
## RENDERING \u2014 outreach brief (single-record card)
|
|
1009
|
+
|
|
1010
|
+
Present as the richest single-record card the MCP emits. The user is seconds-to-minutes away from contacting someone \u2014 every section earns its place by either (a) telling them HOW to outreach, (b) showing what they've done before, or (c) surfacing what's missing and how to get it.
|
|
1011
|
+
|
|
1012
|
+
**Async enrichment.** When \`enrichment.triggered && !enrichment.complete\`, do NOT block the user. Render the brief with \`\u23F3\` on un-enriched channels and IMMEDIATELY draft a first version of the outreach using whatever data IS available (\`split_ai_summary.approach_angle\`, company-line phone, LinkedIn-search fallback). Tell the user: *"I'll refresh once enriched data lands."* On their next message (or after a clear pause), re-call \`leadbay_prepare_outreach(leadId)\` without \`enrich\`; if \`enrichment.complete: true\`, surface the now-resolved channels and offer to revise the draft.
|
|
1013
|
+
|
|
1014
|
+
### Structure
|
|
1015
|
+
|
|
1016
|
+
**Header** (H5): \`\u{1F4DE} Outreach prep \u2014 [Contact name](LinkedIn) \xB7 [Company](website)\`
|
|
1017
|
+
|
|
1018
|
+
- Sub-line: job title \xB7 \`+N more contacts\` when \`additional_contacts_count > 0\`.
|
|
1019
|
+
- Prefix \`https://\` to \`website\` if it's a bare hostname.
|
|
1020
|
+
|
|
1021
|
+
**Score line** (when \`lead.score\` is present): the 10-segment bar inline, no \`<br>\`. Same algorithm as \`pull_leads\`.
|
|
1022
|
+
|
|
1023
|
+
**Channel readiness** \u2014 a single line of pill chips, \` \xB7 \`-separated:
|
|
1024
|
+
|
|
1025
|
+
- \`\u{1F517} LinkedIn\` \u2014 \`profile\` (linked to real URL) if \`linkedin_page\` present; \`search\` (linked to people-search fallback) otherwise. \`\u23F3\` during enrichment.
|
|
1026
|
+
- \`\u{1F4E7} Email\` \u2014 show address if present; \`\u23F3 enriching\` when \`enrichment.triggered && !complete\`; \`\u26AA not enriched\` otherwise.
|
|
1027
|
+
- \`\u{1F4DE} Phone\` \u2014 contact-specific number if present; fall back to \`lead.phone_numbers[0]\` with \`(company line)\` annotation; \`\u23F3\` / \`\u26AA\` otherwise.
|
|
1028
|
+
|
|
1029
|
+
**H5: \u{1F3AF} Angles & approach**
|
|
1030
|
+
|
|
1031
|
+
- Render \`lead.split_ai_summary.approach_angle\` as the lead-in.
|
|
1032
|
+
- 3\u20134 bullets distilling \`split_ai_summary.next_step\` and any signals from a prior \`research_company\` call into salesperson-voice talking points. Cite \`[source](url)\` inline when known.
|
|
1033
|
+
- Final line: \`Recommended channel: <X> \u2014 <rationale>\`. Compute the recommendation from what data is available (email present \u2192 email; phone present \u2192 call; LinkedIn only \u2192 DM).
|
|
1034
|
+
|
|
1035
|
+
**H5: \u{1F4DC} History with [Contact name]**
|
|
1036
|
+
|
|
1037
|
+
When prior contact-level actions / notes are surfaced (or when \`prospecting_actions_count > 0\`), render a reverse-chronological timeline: \`<date> \xB7 <action_type> \xB7 <one-line summary>\`. Quote-block recent notes below. If empty: \`*No prior touchpoints with this contact.*\`
|
|
1038
|
+
|
|
1039
|
+
**H5: \u{1F3E2} History with [Company name]**
|
|
1040
|
+
|
|
1041
|
+
Same shape as the contact history, but only include items NOT duplicated from the contact section. If both empty: \`*No company-level history recorded.*\`
|
|
1042
|
+
|
|
1043
|
+
**H5: \u{1F465} Other contacts** (only if \`additional_contacts_count > 0\`)
|
|
1044
|
+
|
|
1045
|
+
One line: \`+N more contacts at this company \u2014 [see them all](leadbay_research_company)\`.
|
|
1046
|
+
|
|
1047
|
+
**Closing line** (when enrichment is in progress): \`*Enrichment running \u2014 I'll refresh once email/phone lands.*\`
|
|
1048
|
+
|
|
1049
|
+
**Hide:** \`id\`, \`lead.id\`, raw \`enrichment.hint\` when redundant with channel pills, history items without descriptions, any field whose value is the string \`"null"\`, deprecated \`other_contacts_count\` (use \`additional_contacts_count\`).
|
|
1050
|
+
|
|
1051
|
+
## Linking a contact's name
|
|
1052
|
+
|
|
1053
|
+
Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
|
|
1054
|
+
|
|
1055
|
+
When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
|
|
1056
|
+
|
|
1057
|
+
Otherwise fall back to a LinkedIn people-search URL:
|
|
1058
|
+
|
|
1059
|
+
\`\`\`
|
|
1060
|
+
https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
|
|
1061
|
+
\`\`\`
|
|
1062
|
+
|
|
1063
|
+
URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
|
|
1064
|
+
|
|
1065
|
+
Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
|
|
1066
|
+
|
|
1067
|
+
## Linking the company
|
|
1068
|
+
|
|
1069
|
+
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.)
|
|
1070
|
+
|
|
1071
|
+
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.
|
|
1072
|
+
|
|
1073
|
+
\`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).
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
|
|
1077
|
+
---
|
|
1078
|
+
|
|
1079
|
+
## NEXT STEPS \u2014 after the outreach brief
|
|
1080
|
+
|
|
1081
|
+
Offer 2\u20133 follow-ups. Choose based on enrichment state + available channels + history. Always offer the "log outreach" option once the user has clearly contacted someone.
|
|
1082
|
+
|
|
1083
|
+
| Observation | Suggest | Calls |
|
|
1084
|
+
|-------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
1085
|
+
| \`enrichment.triggered && !enrichment.complete\` | "Refresh now to check enrichment progress" | leadbay_prepare_outreach(leadId) \u2014 re-call |
|
|
1086
|
+
| Email available | "Draft the outreach email" | (agent self-drafts inline, using split_ai_summary) |
|
|
1087
|
+
| Direct phone available | "Draft the 60-second call opener" | (agent self-drafts inline) |
|
|
1088
|
+
| LinkedIn URL available | "Draft the LinkedIn DM" | (agent self-drafts inline) |
|
|
1089
|
+
| Only company line, no direct phone | "Draft a switchboard script targeting [Contact]" | (agent self-drafts; flag uncertainty) |
|
|
1090
|
+
| \`additional_contacts_count > 0\` | "Show me the other N contacts at this company" | leadbay_get_contacts(leadId) |
|
|
1091
|
+
| History is empty | "Pull the strategic overview before drafting" | leadbay_research_company(leadId) |
|
|
1092
|
+
| User reports they reached out | "Log this outreach \u2014 creates prospecting action + outcome" | leadbay_report_outreach(leadId, contact_id, ...) |
|
|
1093
|
+
| User adds context for next time | "Save a note on the contact or company" | leadbay_add_note |
|
|
1094
|
+
| After a successful exchange | "Update qualification answers based on what you learned" | leadbay_answer_clarification |
|
|
1095
|
+
|
|
1096
|
+
The "log outreach" step is the most-important follow-up \u2014 it closes the loop and populates history for the next \`leadbay_prepare_outreach\` call. Detect intent from natural language: "I sent the email", "she didn't pick up", "left a voicemail", "they responded yes/no", etc.
|
|
835
1097
|
`;
|
|
836
1098
|
var leadbay_preview_bulk_enrichment = `Preview a bulk-enrichment cost given a set of job titles applied to the current selection. Returns \`{selected_leads, enriched_contacts, enrichable_contacts, title_suggestions, auto_included_titles, previously_enriched_titles}\`. \`previously_enriched_titles\` is a newer field (in prod soon) \u2014 when present, the agent can recommend repeating those titles for new leads.
|
|
837
1099
|
|
|
@@ -847,13 +1109,252 @@ WHEN NOT TO USE: as a non-admin (will fail with 403); for personal lens changes
|
|
|
847
1109
|
|
|
848
1110
|
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\`.
|
|
849
1111
|
`;
|
|
850
|
-
var
|
|
1112
|
+
var leadbay_pull_followups = `Pull KNOWN leads from the user's Monitor view \u2014 the re-engagement entry point of the two-entry-point workflow. Use when the user asks "what should I follow up on", "leads I haven't contacted", "leads in [city]", "before my trip", "this month", "what's overdue", or any phrasing that implies pre-existing pipeline context. For NEW leads from the Discover wishlist, use \`leadbay_pull_leads\` instead.
|
|
1113
|
+
|
|
1114
|
+
Backend: wraps \`GET /1.5/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.5/monitor/filter\` to persist the filter server-side. The Monitor filter is a single \`FilterItem\` per user \u2014 refreshing the page restores it.
|
|
1115
|
+
|
|
1116
|
+
**Filter mechanism \u2014 store-then-apply.** Pass \`set_filter: { criteria: FilterCriterion[] }\` to overwrite the server-stored filter, then the composite re-fetches with \`filtered:true\`. \`FilterCriterion\` is the backend's \`anyOf\` over 10 typed criteria: \`size\`, \`keywords\`, \`sector_ids\`, \`location_ids\`, \`custom_field\`, \`custom_field_comparison\`, \`yc\`, \`liked\`, \`last_action\` (filters by MonitorActionType enum), \`last_action_date\` (with \`last_days\` for "last N days").
|
|
1117
|
+
|
|
1118
|
+
Practical mapping from user phrasing to criterion:
|
|
1119
|
+
|
|
1120
|
+
| User phrase | Criterion |
|
|
1121
|
+
|--------------------------------------|---------------------------------------------------------------------------|
|
|
1122
|
+
| "leads in Lyon" | \`{type: "location_ids", locations: [<admin_area_id>]}\` |
|
|
1123
|
+
| "healthcare staffing" | \`{type: "keywords", keywords: ["healthcare", "staffing"]}\` |
|
|
1124
|
+
| "leads I haven't touched in 30 days" | \`{type: "last_action_date", last_days: 30}\` |
|
|
1125
|
+
| "leads I liked" | \`{type: "liked"}\` |
|
|
1126
|
+
| "leads where I purchased contacts" | \`{type: "last_action", types: ["PURCHASE_LEAD_CONTACT"]}\` |
|
|
1127
|
+
| "leads 50\u2013200 employees" | \`{type: "size", sizes: [{min: 50, max: 200}]}\` |
|
|
1128
|
+
| "Y Combinator companies" | \`{type: "yc"}\` |
|
|
1129
|
+
|
|
1130
|
+
Geo filtering requires \`admin_area_id\` resolution (the backend doesn't accept free-text city names in \`location_ids\`). The MCP doesn't expose an admin-area lookup yet \u2014 for now, ask the user to pick the geo from the Leadbay app's filter UI, or skip the geo filter and rely on agent post-filtering of the response.
|
|
1131
|
+
|
|
1132
|
+
**Pushback exclusion.** Leads with active pushback (\`pushback_status\` set and \`pushback_until > today\`) are excluded from the response. The composite enforces this client-side; \`total_excluded_by_pushback\` in the output reports how many rows were dropped.
|
|
1133
|
+
|
|
1134
|
+
WHEN TO USE: when the user is re-engaging pipeline ("what should I follow up on", "leads in [city]", "stale leads"), or wants to filter their monitored leads by city / sector / recency / action type / liked.
|
|
1135
|
+
|
|
1136
|
+
WHEN NOT TO USE: for NEW leads from the wishlist \u2014 that's \`leadbay_pull_leads\` (discovery). The two surfaces overlap but rank differently; pulling the wrong one wastes the user's time.
|
|
1137
|
+
|
|
1138
|
+
---
|
|
1139
|
+
|
|
1140
|
+
## RENDERING \u2014 follow-ups table, status-badge driven
|
|
1141
|
+
|
|
1142
|
+
Markdown table with FOUR columns, sorted by \`last_monitor_action_at\` desc. **NO score bar in this view** \u2014 discovery owns the \`\u25B0\u2756\u25B1\` visual identity; follow-up uses status badges. Active-pushback leads are already excluded server-side.
|
|
1143
|
+
|
|
1144
|
+
**Active-filters line** ABOVE the table, \` \xB7 \`-separated chips from \`active_filters.criteria\`:
|
|
1145
|
+
|
|
1146
|
+
| Criterion type | Chip |
|
|
1147
|
+
|-----------------------|----------------------------|
|
|
1148
|
+
| \`location_ids\` | \u{1F4CD} \\<resolved name\\> |
|
|
1149
|
+
| \`sector_ids\` | \u{1F3F7} \\<sector name\\> |
|
|
1150
|
+
| \`keywords\` | \u{1F50D} \\<keyword\\> |
|
|
1151
|
+
| \`size\` | \u{1F465} \\<min\\>\u2013\\<max\\> |
|
|
1152
|
+
| \`last_action_date\` | \u{1F4C5} \\<window\\> |
|
|
1153
|
+
| \`last_action\` | \u{1F3AF} \\<action types\\> |
|
|
1154
|
+
| \`liked\` / \`yc\` | \u2B50 liked / \u{1F3C5} YC |
|
|
1155
|
+
| \`custom_field*\` | \u2699 \\<field name\\> |
|
|
1156
|
+
|
|
1157
|
+
Render \`*No filters applied.*\` when empty.
|
|
1158
|
+
|
|
1159
|
+
**Column 1 \u2014 Status** (DERIVED from existing fields, priority order):
|
|
851
1160
|
|
|
852
|
-
|
|
1161
|
+
1. \`epilogue_status == "EPILOGUE_INTEREST_VALIDATED_OR_MEETING_PLANED"\` \u2192 \u{1F3AF} Meeting booked
|
|
1162
|
+
2. \`epilogue_status == "EPILOGUE_COULD_NOT_REACH_STILL_TRYING"\` \u2192 \u26A1 Trying to reach
|
|
1163
|
+
3. \`epilogue_status == "EPILOGUE_STILL_CHASING"\` + last_prospecting_action_at within 14d \u2192 \u{1F7E2} In progress
|
|
1164
|
+
4. \`epilogue_status == "EPILOGUE_NOT_INTERESTED_LOST"\` \u2192 \u2744 Not interested (usually filtered out)
|
|
1165
|
+
5. \`epilogue_status == null\` + \`last_prospecting_action_at == null\` \u2192 \u2728 New
|
|
1166
|
+
6. \`epilogue_status == null\` + \`last_prospecting_action_at > 60d\` \u2192 \u{1F4A4} Dormant
|
|
1167
|
+
7. Otherwise \u2192 \u{1F525} Hot
|
|
1168
|
+
|
|
1169
|
+
Append \`<relative time>\` of \`last_monitor_action_at\` (\`today\`/\`Nd\`/\`Nw\`/\`Nmo\`). For \u2728, show \`new\`. Line 2: **[Company](website)** bold. Line 3: short location \xB7 compact size.
|
|
1170
|
+
|
|
1171
|
+
**Column 2 \u2014 AI take** (3 lines from \`split_ai_summary\` verbatim, \`<br>\`-separated):
|
|
1172
|
+
|
|
1173
|
+
- Line 1: emoji + **bold split_ai_summary.worth_pursuing**. Emoji from leading word: \`Yes\u2026\`\u2192\u2705, \`No\u2026\`\u2192\u274C, \`Maybe\u2026\`\u2192\u{1F914}, else\u2192\u{1F4A1}.
|
|
1174
|
+
- Line 2: *italic split_ai_summary.approach_angle* (\u2264 18 words).
|
|
1175
|
+
- Line 3: **Next:** split_ai_summary.next_step.
|
|
1176
|
+
- Fallback when null: render \`ai_summary\` italic, no emoji.
|
|
1177
|
+
|
|
1178
|
+
**Column 3 \u2014 History & notes**
|
|
1179
|
+
|
|
1180
|
+
- Line 1: \`<relative time> ago \xB7 <last_prospecting_action>\` (humanize: snake_case \u2192 Title Case; drop \`LEAD_\` prefix). When null: \`*Never touched.*\`
|
|
1181
|
+
- Line 2 (when \`epilogue_status\` set): \`\u{1F4CC} Outcome: <user-facing label> \xB7 <relative time>\` \u2014 NEVER show the wire-format \`EPILOGUE_*\` value.
|
|
1182
|
+
- Line 3 (when a recent note surfaces): \`\u{1F4DD} *<note clipped \u2264 14 words>* (<rel time>)\`.
|
|
1183
|
+
- If neither: \`*0 prior touches \xB7 0 notes*\`.
|
|
1184
|
+
|
|
1185
|
+
**Column 4 \u2014 Contacts** (max 3 lines, recommended_contact first):
|
|
1186
|
+
|
|
1187
|
+
\`\`\`
|
|
1188
|
+
\u2605 [Name](linkedin_page or people-search) \xB7 \u260E phone \xB7 \u{1F4E7} email
|
|
1189
|
+
\`\`\`
|
|
1190
|
+
|
|
1191
|
+
Markers: \`\u2605\` recommended, \`\u{1F48E}\` hot in web_insights key_people. Channel pills: \`\u260E phone\` (rec.phone_number \u2192 lead.phone_numbers[0] \`(co)\` \u2192 \`\u26AA phone\`); \`\u{1F4E7} email\` (rec.email \u2192 \`\u26AA email\`). When no visible contact has email/direct phone: append \`*enrich first*\` in italic.
|
|
1192
|
+
|
|
1193
|
+
**Hide:** \`id\`, \`location.pos\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`stale_at\`, \`sector_id\`, zero counters, \`social_presence\` booleans (except \xB0-flag), \`score\`, string \`"null"\`.
|
|
1194
|
+
|
|
1195
|
+
**Legend** (user-facing \u2014 NEVER say "epilogue"):
|
|
1196
|
+
|
|
1197
|
+
\u{1F3AF} Meeting booked \xB7 \u26A1 Trying to reach \xB7 \u{1F7E2} In progress \xB7 \u{1F4A4} Dormant \xB7 \u2728 New \xB7 \u{1F525} Hot \xB7 \u2744 Not interested \xB7 \u2605 recommended \xB7 \u{1F48E} hot in web_insights \xB7 \u260E (co) = company line \xB7 \u26AA not enriched
|
|
1198
|
+
|
|
1199
|
+
## Linking a contact's name
|
|
1200
|
+
|
|
1201
|
+
Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
|
|
1202
|
+
|
|
1203
|
+
When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
|
|
1204
|
+
|
|
1205
|
+
Otherwise fall back to a LinkedIn people-search URL:
|
|
1206
|
+
|
|
1207
|
+
\`\`\`
|
|
1208
|
+
https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
|
|
1209
|
+
\`\`\`
|
|
1210
|
+
|
|
1211
|
+
URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
|
|
1212
|
+
|
|
1213
|
+
Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
|
|
1214
|
+
|
|
1215
|
+
## Linking the company
|
|
1216
|
+
|
|
1217
|
+
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.)
|
|
1218
|
+
|
|
1219
|
+
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.
|
|
1220
|
+
|
|
1221
|
+
\`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).
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
---
|
|
1226
|
+
|
|
1227
|
+
## NEXT STEPS \u2014 after the follow-ups table
|
|
1228
|
+
|
|
1229
|
+
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.
|
|
1230
|
+
|
|
1231
|
+
| Observation | Suggest | Calls |
|
|
1232
|
+
|-----------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------------------------------------------|
|
|
1233
|
+
| Always (top of menu) | "Prep outreach for [top row's contact]" | leadbay_prepare_outreach(leadId) |
|
|
1234
|
+
| User named a city / sector / timeframe | "Refilter by [their phrase]" | leadbay_pull_followups(set_filter: { criteria: [...] }) |
|
|
1235
|
+
| \`pagination.has_more == true\` | "Pull the next page" | leadbay_pull_followups(page = current + 1) |
|
|
1236
|
+
| \u22653 rows \u2728 (never-touched) | "Surface only never-touched leads" | set_filter with \`last_action_date.last_days = 0\` |
|
|
1237
|
+
| \u22653 rows \u26A1 (Trying to reach) | "Focus on overdue commitments" | set_filter with \`last_action.types = ["EPILOGUE_COULD_NOT_REACH_STILL_TRYING"]\` |
|
|
1238
|
+
| User planning a trip / in a city | "Group by city for trip planning" | set_filter with \`location_ids\` (requires admin_area_id) |
|
|
1239
|
+
| All rows last action > 60d | "Re-qualify \u2014 context may have changed" | leadbay_bulk_qualify_leads([leadId, ...]) |
|
|
1240
|
+
| One obvious priority row | "Take me to that lead's full brief" | leadbay_prepare_outreach(leadId) / leadbay_research_lead(leadId) |
|
|
1241
|
+
| User wants to defer a lead | "Snooze [Company] for 3 / 6 / 12 months" | leadbay_set_pushback({ lead_ids:[leadId], status:"3" }) |
|
|
1242
|
+
| User completed outreach mid-flow | "Log the outreach + record the outcome" | leadbay_report_outreach |
|
|
1243
|
+
| Discovery mode might fit better | "Looking for NEW leads instead? Switch to discovery." | leadbay_pull_leads |
|
|
1244
|
+
|
|
1245
|
+
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.
|
|
1246
|
+
`;
|
|
1247
|
+
var leadbay_pull_leads = `Pull up new leads from the user's last-active lens \u2014 the canonical "show me today's prospects" tool. Leadbay works like an inbox: each time the user logs back in, a fresh batch is delivered, paced by how many leads they've actually acted on recently. Pulling more won't produce more; user outreach/skips/saves does. Each returned lead carries a one-line \`qualification_summary\` built from leadbay_ai_agent_responses, plus the rich tags / scores / engagement counters / in-flight flags from the lead summary.
|
|
1248
|
+
|
|
1249
|
+
Roughly the top 10 of the batch come pre-qualified (populated qualification_summary + ai_agent_lead_score); leads below the top ~10 carry only the basic firmographic \`score\` \u2014 not worse, just resource-saved by the system. Call leadbay_bulk_qualify_leads to deepen any of them on demand \u2014 a healthy daily rhythm is to bulk-qualify the rows without \u2756 caps so tomorrow's top-10 list is richer.
|
|
1250
|
+
|
|
1251
|
+
Every lead carries \`recommended_contact\` (with \`linkedin_page\` when the backend has it), \`phone_numbers\` (when available), \`split_ai_summary.{worth_pursuing, approach_angle, next_step}\` (when AI-qualified), and \`social_urls\` per-platform (linkedin, instagram, tiktok, facebook, twitter, crunchbase). Use them \u2014 they're already in the response, you don't need a second call.
|
|
853
1252
|
|
|
854
1253
|
WHEN TO USE: as the agent's default opening move when the user wants to see leads, or as a daily check-in for what's new today.
|
|
855
1254
|
|
|
856
1255
|
WHEN NOT TO USE: when the user has named a specific lens \u2014 pass \`lensId\` to override the auto-resolution. Replaces the older leadbay_find_prospects (removed in v0.2.0).
|
|
1256
|
+
|
|
1257
|
+
The active lens can change between calls (5-min cache + backend \`last_requested_lens\`). If a multi-step workflow depends on staying on one lens, **capture \`response.lens.id\` from the first response and pass it as the \`lensId\` argument on every subsequent Leadbay call** \u2014 including re-pulls, bulk qualifies, and research. (Field-name caveat: response nests it as \`lens.id\`; the parameter is \`lensId\`.) Re-pulling without \`lensId\` after a long-running tool may silently switch to a different lens and discard prior work.
|
|
1258
|
+
|
|
1259
|
+
---
|
|
1260
|
+
|
|
1261
|
+
## RENDERING \u2014 markdown table, three columns, score-bar driven
|
|
1262
|
+
|
|
1263
|
+
Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
|
|
1264
|
+
|
|
1265
|
+
## Score-bar (10-segment, inline-code wrapped)
|
|
1266
|
+
|
|
1267
|
+
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.
|
|
1268
|
+
|
|
1269
|
+
Glyphs (use these exact characters; do not substitute):
|
|
1270
|
+
|
|
1271
|
+
- \`\u25B0\` \u2014 firmographic-only fill
|
|
1272
|
+
- \`\u2756\` \u2014 AI-booster cap (placed at the RIGHT END of the filled run, never the front)
|
|
1273
|
+
- \`\u25B1\` \u2014 empty
|
|
1274
|
+
|
|
1275
|
+
Computation:
|
|
1276
|
+
|
|
1277
|
+
\`\`\`
|
|
1278
|
+
total_filled = round(score / 10), clamped to 0..10
|
|
1279
|
+
ai_segments = round(qualification_summary.avg_qualification_boost / 3.3),
|
|
1280
|
+
clamped to [0, total_filled]
|
|
1281
|
+
normal_filled = total_filled \u2212 ai_segments
|
|
1282
|
+
bar = "\u25B0" \xD7 normal_filled
|
|
1283
|
+
+ "\u2756" \xD7 ai_segments
|
|
1284
|
+
+ "\u25B1" \xD7 (10 \u2212 total_filled)
|
|
1285
|
+
\`\`\`
|
|
1286
|
+
|
|
1287
|
+
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.
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
**Column 1 \u2014 Company**
|
|
1291
|
+
|
|
1292
|
+
- Line 1: the 10-segment score bar in inline-code backticks (see the score-bar snippet above for the algorithm).
|
|
1293
|
+
- Insert \`<br>\` between lines.
|
|
1294
|
+
- Line 2: linked company name + \` \xB7 \` + short location + \` \xB7 \` + compact size.
|
|
1295
|
+
- Link target: \`website\` (prefix \`https://\` if it's a bare hostname). Don't synthesize an app deep-link.
|
|
1296
|
+
- Location: shorten "City of New York" \u2192 "NYC"; otherwise "City ST"; state alone only when city missing.
|
|
1297
|
+
- Size: \`"Xk+"\` when \`size.min >= 1000\`, \`"min\u2013max"\` otherwise.
|
|
1298
|
+
|
|
1299
|
+
**Column 2 \u2014 Why it fits**
|
|
1300
|
+
|
|
1301
|
+
- One sentence, \u2264 20 words.
|
|
1302
|
+
- 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\` or \`leadbay_research_company\`.
|
|
1303
|
+
- Do NOT append \`(boost N)\` \u2014 the \u2756 cap in column 1 already carries that signal.
|
|
1304
|
+
- No bullet lists, no line breaks inside the cell.
|
|
1305
|
+
|
|
1306
|
+
**Column 3 \u2014 Contact**
|
|
1307
|
+
|
|
1308
|
+
\`[Contact name](LINK) \xB7 short job title\`. See linking/contact-linkedin for LINK priority and the \xB0-flag fallback.
|
|
1309
|
+
|
|
1310
|
+
**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"\`.
|
|
1311
|
+
|
|
1312
|
+
## Linking a contact's name
|
|
1313
|
+
|
|
1314
|
+
Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
|
|
1315
|
+
|
|
1316
|
+
When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
|
|
1317
|
+
|
|
1318
|
+
Otherwise fall back to a LinkedIn people-search URL:
|
|
1319
|
+
|
|
1320
|
+
\`\`\`
|
|
1321
|
+
https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
|
|
1322
|
+
\`\`\`
|
|
1323
|
+
|
|
1324
|
+
URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
|
|
1325
|
+
|
|
1326
|
+
Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
|
|
1327
|
+
|
|
1328
|
+
## Linking the company
|
|
1329
|
+
|
|
1330
|
+
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.)
|
|
1331
|
+
|
|
1332
|
+
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.
|
|
1333
|
+
|
|
1334
|
+
\`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).
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
|
|
1338
|
+
---
|
|
1339
|
+
|
|
1340
|
+
## NEXT STEPS \u2014 after rendering the pull_leads table
|
|
1341
|
+
|
|
1342
|
+
Pick 2\u20133 items below based on what was actually observed in the response. Surface them as a short bulleted list \u2014 do NOT recite the whole table.
|
|
1343
|
+
|
|
1344
|
+
| Observation | Suggest | Calls |
|
|
1345
|
+
|------------------------------------------------------------|--------------------------------------------------------------|--------------------------------------------------------|
|
|
1346
|
+
| \`has_more == true\` | "Pull the next page (page N+1 of M)" | leadbay_pull_leads(page = current + 1, lensId = pinned)|
|
|
1347
|
+
| \u2265 3 rows have \`qualification_summary.answered == 0\` | "Deepen AI qualification on the rows without \u2756 caps" | leadbay_bulk_qualify_leads(leadIds=[\u2026]) |
|
|
1348
|
+
| User points at a single row | "Research [Company] in depth" | leadbay_research_lead(leadId) |
|
|
1349
|
+
| User wants the company-level web view | "Pull the company-level research for [Company]" | leadbay_research_company({leadId} or {companyName}) |
|
|
1350
|
+
| Top row has phone AND email | "Prepare an outreach for [Contact] \u2014 call + email" | leadbay_prepare_outreach(leadId) |
|
|
1351
|
+
| Top row has email but no phone | "Draft an outreach email for [Contact]" | leadbay_prepare_outreach(leadId) |
|
|
1352
|
+
| Top row has phone but no email | "Show [Contact]'s call details + a 60-second opener" | leadbay_prepare_outreach(leadId) |
|
|
1353
|
+
| Top row has contacts but no phone/email | "Order contact enrichment to surface email/phone first" | leadbay_enrich_titles(...) or leadbay_prepare_outreach(leadId, enrich:true) |
|
|
1354
|
+
| \`computing_scores == true\` or \`computing_wishlist == true\` | "Scores are still being computed \u2014 re-pull in ~30s" | leadbay_pull_leads (retry with same lensId) |
|
|
1355
|
+
| User wants a narrower / wider audience | "Adjust the lens filters (sector / size)" | leadbay_adjust_audience(...) |
|
|
1356
|
+
|
|
1357
|
+
If nothing in the menu applies cleanly, suggest only "pull next page" and "research a specific lead in depth" \u2014 never invent a tool that doesn't exist.
|
|
857
1358
|
`;
|
|
858
1359
|
var leadbay_qualify_lead = `Trigger AI qualification for a single lead (web fetch + AI rescore). The operation is asynchronous \u2014 results take ~60s. \`forceFetch:true\` re-runs even if recent data exists.
|
|
859
1360
|
|
|
@@ -889,6 +1390,16 @@ WHEN TO USE: when an outreach action was logged in error and needs to be undone.
|
|
|
889
1390
|
|
|
890
1391
|
WHEN NOT TO USE: to change status \u2014 call leadbay_set_epilogue_status with the new status (it overwrites).
|
|
891
1392
|
|
|
1393
|
+
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\`.
|
|
1394
|
+
`;
|
|
1395
|
+
var leadbay_remove_pushback = `Clear an active pushback on one or more leads. They re-enter \`leadbay_pull_followups\` immediately. Use when the user changes their mind ("actually let's reach out now"), or when external context shifts (the company announces hiring / funding / a new product that makes them ripe sooner than the deferral window expected).
|
|
1396
|
+
|
|
1397
|
+
Bulk-native: pass up to 1000 lead UUIDs per call. No status enum \u2014 pushback is binary (either an active window exists, or it doesn't).
|
|
1398
|
+
|
|
1399
|
+
WHEN TO USE: the user wants to revive a snoozed lead now, ahead of the pushback window expiry.
|
|
1400
|
+
|
|
1401
|
+
WHEN NOT TO USE: the pushback expired naturally (no API call needed; the lead reappears in \`pull_followups\` automatically).
|
|
1402
|
+
|
|
892
1403
|
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\`.
|
|
893
1404
|
`;
|
|
894
1405
|
var leadbay_report_outreach = `Log an outreach action (email, call, message, meeting) on a lead so the human team using Leadbay sees the progress in their UI. Writes a NOTE on the lead and (optionally) sets an EPILOGUE status (still chasing, meeting booked, etc.). Bulk variant: pass \`lead_ids=[uuid,...]\` instead of \`lead_id\` (epilogue is bulk-native; notes fan out per-lead).
|
|
@@ -903,9 +1414,120 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
|
|
|
903
1414
|
`;
|
|
904
1415
|
var leadbay_research_company = `Deep-dive research on a specific company by NAME (fuzzy match against the active lens's wishlist). Pass \`companyName\` (matches the top-scoring lead with that name) or \`leadId\` (takes precedence when both supplied).
|
|
905
1416
|
|
|
906
|
-
|
|
1417
|
+
The response carries the rich-lead block (firmographics, \`phone_numbers\`, \`split_ai_summary\`, \`social_urls\`), \`qualification[]\` (Q&A pairs from the AI agent \u2014 empty until the lead is qualified), \`contacts[]\` (paid + org, each with a normalized \`linkedin_page\`), \`web_insights\` (keyed by emoji-prefixed section labels \u2014 see RENDERING for handling), \`web_insights_fetched_at\` (staleness), and \`recent_activities\` (engagement history).
|
|
1418
|
+
|
|
1419
|
+
WHEN TO USE: when the user references a company by name and you don't yet have its \`lead_id\`, OR when you want a single-record card with web-research signals (business signals, prospecting clues, strategic positioning) rather than a raw lead profile.
|
|
1420
|
+
|
|
1421
|
+
WHEN NOT TO USE: when you already have the lead_id and need the bundled deeper lens-scoped data \u2014 use leadbay_research_lead.
|
|
1422
|
+
|
|
1423
|
+
---
|
|
1424
|
+
|
|
1425
|
+
## RENDERING \u2014 single-record research card, mode-adaptive
|
|
1426
|
+
|
|
1427
|
+
Present as a single-record card, not a table. This tool gets invoked in two distinct user contexts \u2014 detect which and adapt the body density accordingly.
|
|
1428
|
+
|
|
1429
|
+
**MODE A \u2014 Discovery.** The user is evaluating whether to pursue this company as a target. Signals: "tell me about", "what do they do", "is this a fit", "research [company]", arrival via a click-through from \`leadbay_pull_leads\`, no prior outreach context in the conversation. Next step is usually qualify, deep-dive via \`leadbay_research_lead\`, or decide whether to start outreach.
|
|
1430
|
+
|
|
1431
|
+
**MODE B \u2014 Contact preparation.** The user is about to call or email someone at this company and needs the talking points. Signals: "I'm calling them", "draft an email", "before my call", "outreach prep", "what should I say", or the conversation has already touched on a specific contact. Next step is usually \`leadbay_prepare_outreach\`.
|
|
1432
|
+
|
|
1433
|
+
Default to MODE A when uncertain. Always offer the cross-mode pivot at the end so the user can redirect if you guessed wrong.
|
|
1434
|
+
|
|
1435
|
+
### Common structure (both modes)
|
|
1436
|
+
|
|
1437
|
+
- **Header** (H4 or H5): \`<10-segment score bar>\` \`[Company name](website)\`. Use the score-bar algorithm; the bar lives in a single inline-code span. Prefix \`https://\` to website if it's a bare hostname.
|
|
1438
|
+
- **Pill row** (immediately below the header): short location \xB7 compact size \xB7 social pill chips iterated over \`social_urls\` (each non-null platform becomes \`[<platform-label>](<url>)\`) \xB7 \`[website-domain](website)\` \xB7 \`\u260E phone\` when \`phone_numbers[]\` is non-empty (use the first number). All \` \xB7 \`-separated.
|
|
1439
|
+
- **Blurb**: render \`description\` (preferred) or \`short_description\` as a single blockquoted paragraph.
|
|
1440
|
+
- **Staleness line**: italic, \`"Researched <relative time>"\` from \`web_insights_fetched_at\`. Use \`"today"\` / \`"yesterday"\` / \`"N days ago"\` up to 30 days, then absolute date. Prefix with \`\u26A0\` if older than 30 days.
|
|
1441
|
+
- **Contacts table** (always at the bottom):
|
|
1442
|
+
\`\`\`
|
|
1443
|
+
| | Name | Title | LinkedIn |
|
|
1444
|
+
\`\`\`
|
|
1445
|
+
Markers in column 1:
|
|
1446
|
+
- \`\u2605\` \u2014 \`recommended_contact\` match.
|
|
1447
|
+
- \`\u{1F48E}\` \u2014 name fuzzy-matches a \`hot: true\` entry in \`web_insights\` key_people. (Use \`\u{1F48E}\`, not \`\u{1F525}\`, to avoid glyph collision with the follow-up status badge.)
|
|
1448
|
+
Sort \`\u2605\` first, then \`\u{1F48E}\`-only rows, then API order. Link the name via \`linkedin_page\` first; fall back to LinkedIn people-search with \`<First>+<Last>+<Company>\`. Append \`\xB0\` only when the fallback is in use AND \`social_presence.linkedin == false\`. Cap to 6 rows; if \`contacts_count > shown\`, end with \`"+N more \u2014 ask to see the full list"\`.
|
|
1449
|
+
|
|
1450
|
+
### MODE A body (Discovery, fuller, scannable)
|
|
1451
|
+
|
|
1452
|
+
Render each non-empty \`web_insights\` section as H5 with the emoji + label intact. Section order: \`\u{1F3E2} company profile\` \u2192 \`\u{1F4C8} business signals\` \u2192 \`\u{1F4A1} prospecting clues\` \u2192 \`\u{1F9E9} strategic positioning\` \u2192 \`\u{1F50E} technologies & innovation\`. Inside each, bullet 3\u20135 items. Sort \`hot: true\` items first. **Bold** the description text of hot items; leave cold items plain. Render \`source\` as \`[source](url)\` at the end; include \`date\` when present. Omit empty sections. Skip \`\u{1F517} social links\` (already in the pill row) and \`\u{1F464} key people\` (already in the contacts table).
|
|
1453
|
+
|
|
1454
|
+
### MODE B body (Contact preparation, tighter)
|
|
1455
|
+
|
|
1456
|
+
Render exactly two H5 sections:
|
|
1457
|
+
|
|
1458
|
+
##### \u{1F3AF} Conversation hooks
|
|
907
1459
|
|
|
908
|
-
|
|
1460
|
+
Distill the 3 most recent / most hot signals from \`\u{1F4C8} business signals\` and \`\u{1F4A1} prospecting clues\` into one-sentence talking points in salesperson voice. Strip the academic framing. Cite the source inline.
|
|
1461
|
+
|
|
1462
|
+
##### \u{1F464} About the person *(only when recommended_contact is non-empty)*
|
|
1463
|
+
|
|
1464
|
+
2-line summary: their title + any context from \`web_insights\` key_people. If they appear in a hot signal ("X appointed CEO"), surface that prominently.
|
|
1465
|
+
|
|
1466
|
+
Skip \u{1F3E2} profile, \u{1F9E9} strategic positioning, \u{1F50E} technologies in MODE B \u2014 context the user doesn't need for the next 30 seconds.
|
|
1467
|
+
|
|
1468
|
+
If \`qualification[]\` is non-empty, append one collapsed line: \`"Qualification: N questions answered, avg boost X"\` and offer to expand in NEXT STEPS.
|
|
1469
|
+
|
|
1470
|
+
**Hide:** \`id\`, \`lead.id\`, \`contact.id\`, \`lead.location.pos\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`recommended_contact_title\` (duplicates \`recommended_contact.job_title\`), empty arrays, fields whose value is the string \`"null"\`, \`contact.source\` (internal), insights whose \`source\` is empty.
|
|
1471
|
+
|
|
1472
|
+
**Legend (print once below the card):** \`\` \`\u25B0\` firmographic \xB7 \`\u2756\` AI booster \xB7 \`\u25B1\` unfilled \xB7 \u2605 recommended \xB7 \u{1F48E} hot in web_insights \xB7 \xB0 = no company LinkedIn (fallback link only) \`\`
|
|
1473
|
+
|
|
1474
|
+
## Linking a contact's name
|
|
1475
|
+
|
|
1476
|
+
Two LinkedIn URLs exist and must never be conflated: the **company's** LinkedIn page and an **individual person's** profile.
|
|
1477
|
+
|
|
1478
|
+
When the response carries a real contact LinkedIn URL \u2014 \`contact.linkedin_page\` is a string that starts with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it) \u2014 link the contact's name to that URL.
|
|
1479
|
+
|
|
1480
|
+
Otherwise fall back to a LinkedIn people-search URL:
|
|
1481
|
+
|
|
1482
|
+
\`\`\`
|
|
1483
|
+
https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>
|
|
1484
|
+
\`\`\`
|
|
1485
|
+
|
|
1486
|
+
URL-encode the params. Strip Inc / LLC / Corp / Ltd / GmbH suffixes from the company name. Append a trailing \` \xB0\` to the rendered name ONLY when the fallback is in use AND \`social_presence.linkedin == false\` (no company LinkedIn \u2192 search may not resolve). Never append \`\xB0\` when a real \`linkedin_page\` was used.
|
|
1487
|
+
|
|
1488
|
+
Never link a person's name to the company's LinkedIn page (and vice versa). The two surfaces are different \u2014 conflating them quietly degrades the workflow.
|
|
1489
|
+
|
|
1490
|
+
## Linking the company
|
|
1491
|
+
|
|
1492
|
+
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.)
|
|
1493
|
+
|
|
1494
|
+
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.
|
|
1495
|
+
|
|
1496
|
+
\`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).
|
|
1497
|
+
|
|
1498
|
+
|
|
1499
|
+
|
|
1500
|
+
---
|
|
1501
|
+
|
|
1502
|
+
## NEXT STEPS \u2014 after the research card
|
|
1503
|
+
|
|
1504
|
+
Offer 2\u20133 follow-ups that match the detected mode. Always offer a cross-mode pivot at the end so the user can redirect if you guessed wrong.
|
|
1505
|
+
|
|
1506
|
+
### MODE A (Discovery)
|
|
1507
|
+
|
|
1508
|
+
| Observation | Suggest | Calls |
|
|
1509
|
+
|--------------------------------------------------------|----------------------------------------------------|----------------------------------------------------|
|
|
1510
|
+
| \`qualification[]\` is empty | "Run AI qualification on this lead" | leadbay_bulk_qualify_leads([leadId]) |
|
|
1511
|
+
| \u22651 hot recent item in \u{1F4C8} business signals | "Prepare outreach referencing [signal headline]" | leadbay_prepare_outreach(leadId) |
|
|
1512
|
+
| \`contacts_count > len(contacts)\` shown | "Pull the full contact list (N more)" | leadbay_get_contacts(leadId) |
|
|
1513
|
+
| \`web_insights_fetched_at\` > 30 days | "Re-run the web research \u2014 this is stale" | leadbay_research_company(leadId) \u2014 refresh |
|
|
1514
|
+
| User wants the deeper lens-scoped bundle | "Pull the full lead profile (research_lead)" | leadbay_research_lead(leadId) |
|
|
1515
|
+
| User is exploring multiple companies | "Back to the lead list" | leadbay_pull_leads |
|
|
1516
|
+
| \`qualification[]\` non-empty | "Expand the AI qualification answers" | (render qualification[] as a sub-card) |
|
|
1517
|
+
|
|
1518
|
+
End MODE A with the pivot offer: \`"Want the contact-prep view for [recommended contact name]?"\`
|
|
1519
|
+
|
|
1520
|
+
### MODE B (Contact preparation)
|
|
1521
|
+
|
|
1522
|
+
| Observation | Suggest | Calls |
|
|
1523
|
+
|--------------------------------------------------------|----------------------------------------------------|----------------------------------------------------|
|
|
1524
|
+
| \`phone_numbers[]\` non-empty | "Show full call notes + a 60-second opener" | leadbay_prepare_outreach(leadId) |
|
|
1525
|
+
| Recommended contact has an email | "Draft the outreach email" | leadbay_prepare_outreach(leadId) |
|
|
1526
|
+
| Neither phone nor email for recommended contact | "Order contact enrichment first" | leadbay_prepare_outreach(leadId, enrich:true) or leadbay_enrich_titles |
|
|
1527
|
+
| After the user reports a touchpoint | "Log the call/email outcome" | leadbay_report_outreach |
|
|
1528
|
+
| Adding pre-call context | "Add a note to this lead" | leadbay_add_note |
|
|
1529
|
+
|
|
1530
|
+
End MODE B with the pivot offer: \`"Want the full strategic overview instead?"\`
|
|
909
1531
|
`;
|
|
910
1532
|
var leadbay_research_lead = `Tell me everything decision-relevant about a single lead. Bundles the lens-scoped lead profile, the AI qualification answers (the agent's knowledge-base food), the structured web-research signals (with hot flags + sources), the enriched contacts, and the recent notes/epilogue/prospecting activity in one call. Order is deliberate: qualification first, then signals, then firmographics, then contacts, then engagement.
|
|
911
1533
|
|
|
@@ -914,12 +1536,46 @@ Scoring has two layers: the basic \`score\` (firmographic, always present, alrea
|
|
|
914
1536
|
WHEN TO USE: when picking up a single lead from leadbay_pull_leads to decide whether to act on it.
|
|
915
1537
|
|
|
916
1538
|
WHEN NOT TO USE: across many leads at once \u2014 that's leadbay_pull_leads' job. (This composite supersedes the lower-level leadbay_get_lead_profile in agent flow; the granular tool stays available for fine-grained access.)
|
|
1539
|
+
|
|
1540
|
+
**Concurrency note**: this is a composite that reads many sub-resources per call. Call it **sequentially** or in small batches (\u22643 parallel) when researching multiple leads. Firing 10+ in parallel can saturate the transport and produce misleading \`"Tool permission stream closed"\` errors that look like permission failures but are really backpressure. On a transient stream/timeout failure, retry the same lead once before moving on.
|
|
917
1541
|
`;
|
|
918
1542
|
var leadbay_resolve_import_rows = `Resolve messy CSV-shaped lead rows against Leadbay before file import. The tool sends each row's available identity signals to \`POST /leads/resolve\`, returns matched lead IDs or ambiguous candidate IDs, and produces \`records_for_import\` plus a SAFE identity-only \`mappings_for_import\` starting point for leadbay_import_leads / leadbay_import_and_qualify. This tool deliberately does not try to understand every CSV dialect; the agent should inspect the file, derive clean helper columns when useful, pass explicit \`identity_mappings\`, and build the final CRM mapping from \`mapping_guidance\`.
|
|
919
1543
|
|
|
920
1544
|
WHEN TO USE: before importing user-supplied files when domains, names, CRM IDs, registry numbers, or Leadbay IDs may be inconsistently formatted; when the agent needs to pre-resolve messy rows, inspect ambiguous candidates, or prepare LEADBAY_ID values for the import composites. For contact-only files, first derive company website/domain from business contact emails where possible, while ignoring consumer mailbox domains. Deterministic matches get a LEADBAY_ID column inserted so the standard import commits immediately. Ambiguous rows are deliberately left without LEADBAY_ID; inspect candidates and choose one only when the evidence is good. Rows with websites but no match can still be imported; Leadbay may crawl and match them later, and leadbay_import_status can surface late matches.
|
|
921
1545
|
|
|
922
1546
|
WHEN NOT TO USE: for prospect discovery from scratch (use leadbay_pull_leads); for one known company profile (use leadbay_research_company / leadbay_research_lead); or when the file already has clean, final LEADBAY_ID/CRM_ID/SIREN mappings and no row-level identity disambiguation is needed.
|
|
1547
|
+
|
|
1548
|
+
---
|
|
1549
|
+
|
|
1550
|
+
## RENDERING
|
|
1551
|
+
|
|
1552
|
+
Present as a markdown table of resolution outcomes per row.
|
|
1553
|
+
|
|
1554
|
+
\`\`\`
|
|
1555
|
+
| Source row | Match status | Resolved leadId / candidates |
|
|
1556
|
+
\`\`\`
|
|
1557
|
+
|
|
1558
|
+
Status emoji map:
|
|
1559
|
+
|
|
1560
|
+
- \`\u2713\` unambiguous match \u2014 one Leadbay lead, high confidence
|
|
1561
|
+
- \`\u26A0\` ambiguous \u2014 multiple candidates returned
|
|
1562
|
+
- \`\u2717\` no match \u2014 backend has no candidates; can still import if \`website\` is present
|
|
1563
|
+
- \`\u23F3\` resolving \u2014 async resolve still running
|
|
1564
|
+
|
|
1565
|
+
For ambiguous rows, list up to 3 candidate names + leadIds inline. Truncate the source row to a short identifier (first non-empty column or domain).
|
|
1566
|
+
|
|
1567
|
+
Below the table, a one-liner: \`"Ready: K rows \xB7 Ambiguous: A rows \xB7 Unmatched: U rows"\`
|
|
1568
|
+
|
|
1569
|
+
---
|
|
1570
|
+
|
|
1571
|
+
## NEXT STEPS
|
|
1572
|
+
|
|
1573
|
+
| Observation | Suggest | Calls |
|
|
1574
|
+
|----------------------------------------|-------------------------------------------------------------|--------------------------------------------------------|
|
|
1575
|
+
| All rows resolved cleanly | "Import these rows now" | leadbay_import_leads(records_for_import, mappings_for_import) |
|
|
1576
|
+
| Ambiguous rows present | "Inspect candidates for each ambiguous row" | (re-call with include_candidate_profiles=true) |
|
|
1577
|
+
| Unmatched rows but websites present | "Import anyway \u2014 Leadbay will crawl and match later" | leadbay_import_leads (status check after) |
|
|
1578
|
+
| User wants to skip rows they can't ID | "Drop unmatched rows and import the rest" | leadbay_import_leads (with filtered records) |
|
|
923
1579
|
`;
|
|
924
1580
|
var leadbay_select_leads = `Add leads to the user's transient selection (used by selection-scoped bulk operations). Accepts 1-1000 \`leadIds\` per call.
|
|
925
1581
|
|
|
@@ -943,6 +1599,18 @@ WHEN TO USE: low-level.
|
|
|
943
1599
|
|
|
944
1600
|
WHEN NOT TO USE: from agent flow \u2014 leadbay_report_outreach pairs this with a note + verification, which is what humans actually need to see in Leadbay.
|
|
945
1601
|
|
|
1602
|
+
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\`.
|
|
1603
|
+
`;
|
|
1604
|
+
var leadbay_set_pushback = `Snooze (pushback) one or more leads for 3, 6, or 12 months. The leads remain in the user's pipeline but are excluded from \`leadbay_pull_followups\` until the pushback window expires. Use this when the user says "not now", "next quarter", "follow up in 3 months", "6 months out", "next year", or any equivalent deferral.
|
|
1605
|
+
|
|
1606
|
+
Accepts short labels (\`3\`, \`6\`, \`12\` \u2014 months) or the wire-format enum (\`PUSHBACK_3\`, \`PUSHBACK_6\`, \`PUSHBACK_12\`). Bulk-native: pass up to 1000 lead UUIDs per call.
|
|
1607
|
+
|
|
1608
|
+
User-facing language: "snooze for 3/6/12 months" (or "pushback" if the user uses the term themselves). Never say "pushback status" in dialogue \u2014 say "snooze" or "follow up in N months".
|
|
1609
|
+
|
|
1610
|
+
WHEN TO USE: the user explicitly says they want to defer a lead (or a small set of leads) for a known window. Pair with \`leadbay_add_note\` if the user's reason for deferring is worth preserving.
|
|
1611
|
+
|
|
1612
|
+
WHEN NOT TO USE: the user is just skipping ONE outreach attempt \u2014 that's a normal \`EPILOGUE_COULD_NOT_REACH_STILL_TRYING\` outcome via \`leadbay_report_outreach\`, not a snooze.
|
|
1613
|
+
|
|
946
1614
|
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\`.
|
|
947
1615
|
`;
|
|
948
1616
|
var leadbay_set_user_prompt = `Set the org's intelligence-refinement prompt \u2014 free-text instruction that steers Leadbay's lead recommendations beyond firmographics. Admin-only. Setting this clears any pending clarification and triggers a full intelligence regeneration (web search + high-reasoning). \`dry_run:true\` returns the call shape without contacting the backend.
|
|
@@ -1134,6 +1802,16 @@ var discoverLeads = {
|
|
|
1134
1802
|
};
|
|
1135
1803
|
|
|
1136
1804
|
// ../core/dist/tools/get-lead-profile.js
|
|
1805
|
+
function normalizeLinkedinPage(v) {
|
|
1806
|
+
if (v == null)
|
|
1807
|
+
return null;
|
|
1808
|
+
if (typeof v !== "string")
|
|
1809
|
+
return null;
|
|
1810
|
+
const trimmed = v.trim();
|
|
1811
|
+
if (!trimmed || trimmed.toLowerCase() === "null")
|
|
1812
|
+
return null;
|
|
1813
|
+
return trimmed;
|
|
1814
|
+
}
|
|
1137
1815
|
var getLeadProfile = {
|
|
1138
1816
|
name: "leadbay_get_lead_profile",
|
|
1139
1817
|
annotations: {
|
|
@@ -1210,7 +1888,7 @@ var getLeadProfile = {
|
|
|
1210
1888
|
last_name: c.last_name,
|
|
1211
1889
|
email: c.email,
|
|
1212
1890
|
phone_number: c.phone_number,
|
|
1213
|
-
linkedin_page: c.linkedin_page,
|
|
1891
|
+
linkedin_page: normalizeLinkedinPage(c.linkedin_page),
|
|
1214
1892
|
job_title: c.job_title,
|
|
1215
1893
|
recommended: c.recommended,
|
|
1216
1894
|
enrichment: c.enrichment,
|
|
@@ -1222,7 +1900,7 @@ var getLeadProfile = {
|
|
|
1222
1900
|
last_name: c.last_name,
|
|
1223
1901
|
email: c.email,
|
|
1224
1902
|
phone_number: c.phone_number,
|
|
1225
|
-
linkedin_page: c.linkedin_page,
|
|
1903
|
+
linkedin_page: normalizeLinkedinPage(c.linkedin_page),
|
|
1226
1904
|
job_title: c.job_title,
|
|
1227
1905
|
recommended: c.recommended,
|
|
1228
1906
|
enrichment: c.enrichment,
|
|
@@ -1247,8 +1925,14 @@ var getLeadProfile = {
|
|
|
1247
1925
|
phone_numbers: lead.phone_numbers,
|
|
1248
1926
|
keywords: lead.keywords,
|
|
1249
1927
|
contacts_count: lead.contacts_count,
|
|
1250
|
-
|
|
1251
|
-
recommended_contact:
|
|
1928
|
+
// B8: recommended_contact_title dropped — duplicates
|
|
1929
|
+
// recommended_contact.job_title. B1+B7: propagate linkedin_page.
|
|
1930
|
+
recommended_contact: lead.recommended_contact ? {
|
|
1931
|
+
...lead.recommended_contact,
|
|
1932
|
+
linkedin_page: normalizeLinkedinPage(lead.recommended_contact.linkedin_page ?? null)
|
|
1933
|
+
} : null,
|
|
1934
|
+
social_presence: lead.social_presence ?? null,
|
|
1935
|
+
social_urls: lead.social_urls ?? null,
|
|
1252
1936
|
web_fetch_in_progress: lead.web_fetch_in_progress ?? false
|
|
1253
1937
|
},
|
|
1254
1938
|
qualification: qualification?.map((q) => ({
|
|
@@ -4030,6 +4714,100 @@ var removeEpilogue = {
|
|
|
4030
4714
|
}
|
|
4031
4715
|
};
|
|
4032
4716
|
|
|
4717
|
+
// ../core/dist/tools/set-pushback.js
|
|
4718
|
+
var PUSHBACK_LABEL_MAP = {
|
|
4719
|
+
"3": "PUSHBACK_3",
|
|
4720
|
+
"6": "PUSHBACK_6",
|
|
4721
|
+
"12": "PUSHBACK_12",
|
|
4722
|
+
"3m": "PUSHBACK_3",
|
|
4723
|
+
"6m": "PUSHBACK_6",
|
|
4724
|
+
"12m": "PUSHBACK_12",
|
|
4725
|
+
"3_months": "PUSHBACK_3",
|
|
4726
|
+
"6_months": "PUSHBACK_6",
|
|
4727
|
+
"12_months": "PUSHBACK_12",
|
|
4728
|
+
PUSHBACK_3: "PUSHBACK_3",
|
|
4729
|
+
PUSHBACK_6: "PUSHBACK_6",
|
|
4730
|
+
PUSHBACK_12: "PUSHBACK_12"
|
|
4731
|
+
};
|
|
4732
|
+
var setPushback = {
|
|
4733
|
+
name: "leadbay_set_pushback",
|
|
4734
|
+
annotations: {
|
|
4735
|
+
title: "Pushback (snooze) leads for 3 / 6 / 12 months",
|
|
4736
|
+
readOnlyHint: false,
|
|
4737
|
+
destructiveHint: true,
|
|
4738
|
+
idempotentHint: true,
|
|
4739
|
+
openWorldHint: true
|
|
4740
|
+
},
|
|
4741
|
+
description: leadbay_set_pushback,
|
|
4742
|
+
optional: true,
|
|
4743
|
+
write: true,
|
|
4744
|
+
inputSchema: {
|
|
4745
|
+
type: "object",
|
|
4746
|
+
properties: {
|
|
4747
|
+
lead_ids: {
|
|
4748
|
+
type: "array",
|
|
4749
|
+
items: { type: "string" },
|
|
4750
|
+
description: "Lead UUIDs (1-1000)"
|
|
4751
|
+
},
|
|
4752
|
+
status: {
|
|
4753
|
+
type: "string",
|
|
4754
|
+
description: "One of: 3, 6, 12 (months) \u2014 or the long form PUSHBACK_3 / PUSHBACK_6 / PUSHBACK_12."
|
|
4755
|
+
}
|
|
4756
|
+
},
|
|
4757
|
+
required: ["lead_ids", "status"],
|
|
4758
|
+
additionalProperties: false
|
|
4759
|
+
},
|
|
4760
|
+
execute: async (client, params) => {
|
|
4761
|
+
const wire = PUSHBACK_LABEL_MAP[String(params.status)];
|
|
4762
|
+
if (!wire) {
|
|
4763
|
+
return {
|
|
4764
|
+
error: true,
|
|
4765
|
+
code: "BAD_INPUT",
|
|
4766
|
+
message: `Unknown pushback status: ${params.status}`,
|
|
4767
|
+
hint: "Use one of: 3, 6, 12 (months) \u2014 or PUSHBACK_3 / PUSHBACK_6 / PUSHBACK_12."
|
|
4768
|
+
};
|
|
4769
|
+
}
|
|
4770
|
+
await client.requestVoid("POST", "/leads/pushback", {
|
|
4771
|
+
lead_ids: params.lead_ids,
|
|
4772
|
+
status: wire
|
|
4773
|
+
});
|
|
4774
|
+
return { applied: true, count: params.lead_ids.length, status: wire };
|
|
4775
|
+
}
|
|
4776
|
+
};
|
|
4777
|
+
|
|
4778
|
+
// ../core/dist/tools/remove-pushback.js
|
|
4779
|
+
var removePushback = {
|
|
4780
|
+
name: "leadbay_remove_pushback",
|
|
4781
|
+
annotations: {
|
|
4782
|
+
title: "Remove pushback (un-snooze) leads",
|
|
4783
|
+
readOnlyHint: false,
|
|
4784
|
+
destructiveHint: true,
|
|
4785
|
+
idempotentHint: true,
|
|
4786
|
+
openWorldHint: true
|
|
4787
|
+
},
|
|
4788
|
+
description: leadbay_remove_pushback,
|
|
4789
|
+
optional: true,
|
|
4790
|
+
write: true,
|
|
4791
|
+
inputSchema: {
|
|
4792
|
+
type: "object",
|
|
4793
|
+
properties: {
|
|
4794
|
+
lead_ids: {
|
|
4795
|
+
type: "array",
|
|
4796
|
+
items: { type: "string" },
|
|
4797
|
+
description: "Lead UUIDs"
|
|
4798
|
+
}
|
|
4799
|
+
},
|
|
4800
|
+
required: ["lead_ids"],
|
|
4801
|
+
additionalProperties: false
|
|
4802
|
+
},
|
|
4803
|
+
execute: async (client, params) => {
|
|
4804
|
+
await client.requestVoid("POST", "/leads/remove_pushback", {
|
|
4805
|
+
lead_ids: params.lead_ids
|
|
4806
|
+
});
|
|
4807
|
+
return { cleared: true, count: params.lead_ids.length };
|
|
4808
|
+
}
|
|
4809
|
+
};
|
|
4810
|
+
|
|
4033
4811
|
// ../core/dist/tools/preview-bulk-enrichment.js
|
|
4034
4812
|
var previewBulkEnrichment = {
|
|
4035
4813
|
name: "leadbay_preview_bulk_enrichment",
|
|
@@ -4300,6 +5078,16 @@ var researchCompany = {
|
|
|
4300
5078
|
};
|
|
4301
5079
|
|
|
4302
5080
|
// ../core/dist/composite/prepare-outreach.js
|
|
5081
|
+
function normalizeLinkedinPage2(v) {
|
|
5082
|
+
if (v == null)
|
|
5083
|
+
return null;
|
|
5084
|
+
if (typeof v !== "string")
|
|
5085
|
+
return null;
|
|
5086
|
+
const trimmed = v.trim();
|
|
5087
|
+
if (!trimmed || trimmed.toLowerCase() === "null")
|
|
5088
|
+
return null;
|
|
5089
|
+
return trimmed;
|
|
5090
|
+
}
|
|
4303
5091
|
var prepareOutreach = {
|
|
4304
5092
|
name: "leadbay_prepare_outreach",
|
|
4305
5093
|
annotations: {
|
|
@@ -4320,7 +5108,7 @@ var prepareOutreach = {
|
|
|
4320
5108
|
},
|
|
4321
5109
|
enrich: {
|
|
4322
5110
|
type: "boolean",
|
|
4323
|
-
description: "If true and credits available, trigger enrichment on the recommended contact (default: false). Enrichment is async
|
|
5111
|
+
description: "If true and credits available, trigger enrichment on the recommended contact (default: false). Enrichment is async; re-call this tool (no enrich) after ~60s and check enrichment.complete to see if email/phone landed (B13: self-polling)."
|
|
4324
5112
|
}
|
|
4325
5113
|
},
|
|
4326
5114
|
required: ["leadId"],
|
|
@@ -4331,32 +5119,67 @@ var prepareOutreach = {
|
|
|
4331
5119
|
properties: {
|
|
4332
5120
|
lead: {
|
|
4333
5121
|
type: ["object", "null"],
|
|
4334
|
-
description: "
|
|
5122
|
+
description: "Lead context for the brief. Expanded per B15: score, ai_summary, split_ai_summary, location, size, phone_numbers, website, description.",
|
|
4335
5123
|
properties: {
|
|
5124
|
+
id: { type: "string" },
|
|
4336
5125
|
name: { type: "string" },
|
|
5126
|
+
score: { type: ["number", "null"] },
|
|
5127
|
+
ai_agent_lead_score: { type: ["number", "null"] },
|
|
4337
5128
|
ai_summary: { type: ["string", "null"] },
|
|
4338
|
-
|
|
5129
|
+
split_ai_summary: { type: ["object", "null"] },
|
|
5130
|
+
location: { type: ["object", "null"] },
|
|
5131
|
+
size: { type: ["object", "null"] },
|
|
5132
|
+
phone_numbers: { type: ["array", "null"], items: { type: "string" } },
|
|
5133
|
+
website: { type: ["string", "null"] },
|
|
5134
|
+
description: { type: ["string", "null"] },
|
|
5135
|
+
short_description: { type: ["string", "null"] },
|
|
5136
|
+
social_presence: { type: ["object", "null"] },
|
|
5137
|
+
social_urls: { type: ["object", "null"] }
|
|
4339
5138
|
}
|
|
4340
5139
|
},
|
|
4341
5140
|
recommended_contact: {
|
|
4342
5141
|
type: ["object", "null"],
|
|
4343
|
-
description: "Best contact to outreach to (
|
|
5142
|
+
description: "Best contact to outreach to. Always returned in the post-enrichment shape (B21) \u2014 first_name/last_name/contact_id/email/phone_number/linkedin_page/job_title \u2014 with nulls in fields that aren't yet enriched.",
|
|
5143
|
+
properties: {
|
|
5144
|
+
contact_id: { type: ["string", "null"] },
|
|
5145
|
+
first_name: { type: ["string", "null"] },
|
|
5146
|
+
last_name: { type: ["string", "null"] },
|
|
5147
|
+
job_title: { type: ["string", "null"] },
|
|
5148
|
+
email: { type: ["string", "null"] },
|
|
5149
|
+
phone_number: { type: ["string", "null"] },
|
|
5150
|
+
linkedin_page: { type: ["string", "null"] },
|
|
5151
|
+
is_org_contact: { type: ["boolean", "null"] }
|
|
5152
|
+
}
|
|
5153
|
+
},
|
|
5154
|
+
additional_contacts_count: {
|
|
5155
|
+
type: "number",
|
|
5156
|
+
description: "How many other contacts exist beyond the recommended one (renamed from other_contacts_count per B16; both shipped for one release)."
|
|
5157
|
+
},
|
|
5158
|
+
total_contacts_count: {
|
|
5159
|
+
type: "number",
|
|
5160
|
+
description: "Total contacts on this lead (recommended + others)."
|
|
4344
5161
|
},
|
|
4345
5162
|
other_contacts_count: {
|
|
4346
5163
|
type: "number",
|
|
4347
|
-
description: "
|
|
5164
|
+
description: "DEPRECATED: alias for additional_contacts_count. Will be removed in 0.10.0."
|
|
4348
5165
|
},
|
|
4349
5166
|
enrichment: {
|
|
4350
5167
|
type: "object",
|
|
4351
|
-
description: "
|
|
5168
|
+
description: "Self-polling status (B13): triggered = whether this call kicked off enrichment; complete = whether the recommended contact now has email OR phone.",
|
|
4352
5169
|
properties: {
|
|
4353
5170
|
triggered: { type: "boolean" },
|
|
5171
|
+
complete: { type: "boolean" },
|
|
4354
5172
|
error: { type: ["string", "null"] },
|
|
4355
5173
|
hint: { type: ["string", "null"] }
|
|
4356
5174
|
}
|
|
4357
5175
|
}
|
|
4358
5176
|
},
|
|
4359
|
-
required: [
|
|
5177
|
+
required: [
|
|
5178
|
+
"recommended_contact",
|
|
5179
|
+
"additional_contacts_count",
|
|
5180
|
+
"total_contacts_count",
|
|
5181
|
+
"enrichment"
|
|
5182
|
+
]
|
|
4360
5183
|
},
|
|
4361
5184
|
execute: async (client, params, ctx) => {
|
|
4362
5185
|
const contactsResult = await getContacts.execute(client, { leadId: params.leadId }, ctx);
|
|
@@ -4372,37 +5195,83 @@ var prepareOutreach = {
|
|
|
4372
5195
|
enrichmentError = e?.message ?? String(e);
|
|
4373
5196
|
}
|
|
4374
5197
|
}
|
|
4375
|
-
let
|
|
5198
|
+
let refreshed = contacts;
|
|
5199
|
+
if (enrichmentTriggered) {
|
|
5200
|
+
try {
|
|
5201
|
+
const again = await getContacts.execute(client, { leadId: params.leadId }, ctx);
|
|
5202
|
+
refreshed = again.contacts ?? contacts;
|
|
5203
|
+
} catch {
|
|
5204
|
+
}
|
|
5205
|
+
}
|
|
5206
|
+
const recommendedFresh = refreshed.find((c) => c.recommended) ?? recommended;
|
|
5207
|
+
let leadBlock = null;
|
|
4376
5208
|
try {
|
|
4377
5209
|
const profile = await getLeadProfile.execute(client, { leadId: params.leadId }, ctx);
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
5210
|
+
const p = profile.lead;
|
|
5211
|
+
leadBlock = {
|
|
5212
|
+
id: p.id ?? params.leadId,
|
|
5213
|
+
name: p.name ?? null,
|
|
5214
|
+
score: p.score ?? null,
|
|
5215
|
+
ai_agent_lead_score: p.ai_agent_lead_score ?? null,
|
|
5216
|
+
ai_summary: p.ai_summary ?? null,
|
|
5217
|
+
split_ai_summary: p.split_ai_summary ?? null,
|
|
5218
|
+
location: p.location ?? null,
|
|
5219
|
+
size: p.size ?? null,
|
|
5220
|
+
phone_numbers: p.phone_numbers ?? null,
|
|
5221
|
+
website: p.website ?? null,
|
|
5222
|
+
description: p.description ?? null,
|
|
5223
|
+
short_description: p.short_description ?? null,
|
|
5224
|
+
social_presence: p.social_presence ?? null,
|
|
5225
|
+
social_urls: p.social_urls ?? null
|
|
4382
5226
|
};
|
|
4383
5227
|
} catch {
|
|
5228
|
+
leadBlock = {
|
|
5229
|
+
id: params.leadId,
|
|
5230
|
+
name: null,
|
|
5231
|
+
ai_summary: null
|
|
5232
|
+
};
|
|
4384
5233
|
}
|
|
5234
|
+
const recommendedContact = recommendedFresh ? {
|
|
5235
|
+
contact_id: recommendedFresh.id ?? null,
|
|
5236
|
+
first_name: recommendedFresh.first_name,
|
|
5237
|
+
last_name: recommendedFresh.last_name,
|
|
5238
|
+
job_title: recommendedFresh.job_title,
|
|
5239
|
+
email: recommendedFresh.email,
|
|
5240
|
+
phone_number: recommendedFresh.phone_number,
|
|
5241
|
+
linkedin_page: normalizeLinkedinPage2(recommendedFresh.linkedin_page),
|
|
5242
|
+
is_org_contact: recommendedFresh.source === "org"
|
|
5243
|
+
} : null;
|
|
5244
|
+
const total = refreshed.length;
|
|
5245
|
+
const additional = recommendedFresh ? Math.max(0, total - 1) : total;
|
|
5246
|
+
const enrichmentComplete = Boolean(recommendedContact && (recommendedContact.email || recommendedContact.phone_number));
|
|
4385
5247
|
return {
|
|
4386
|
-
lead:
|
|
4387
|
-
recommended_contact:
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
phone_number: recommended.phone_number,
|
|
4393
|
-
linkedin_page: recommended.linkedin_page
|
|
4394
|
-
} : null,
|
|
4395
|
-
other_contacts_count: Math.max(0, contacts.length - 1),
|
|
5248
|
+
lead: leadBlock,
|
|
5249
|
+
recommended_contact: recommendedContact,
|
|
5250
|
+
additional_contacts_count: additional,
|
|
5251
|
+
total_contacts_count: total,
|
|
5252
|
+
// Deprecated alias kept for one release.
|
|
5253
|
+
other_contacts_count: additional,
|
|
4396
5254
|
enrichment: {
|
|
4397
5255
|
triggered: enrichmentTriggered,
|
|
5256
|
+
complete: enrichmentComplete,
|
|
4398
5257
|
error: enrichmentError,
|
|
4399
|
-
hint: enrichmentTriggered ? "Enrichment
|
|
5258
|
+
hint: enrichmentTriggered && !enrichmentComplete ? "Enrichment running (~60s). Re-call leadbay_prepare_outreach with the same leadId (no enrich) and check enrichment.complete." : null
|
|
4400
5259
|
}
|
|
4401
5260
|
};
|
|
4402
5261
|
}
|
|
4403
5262
|
};
|
|
4404
5263
|
|
|
4405
5264
|
// ../core/dist/composite/pull-leads.js
|
|
5265
|
+
function normalizeLinkedinPage3(v) {
|
|
5266
|
+
if (v == null)
|
|
5267
|
+
return null;
|
|
5268
|
+
if (typeof v !== "string")
|
|
5269
|
+
return null;
|
|
5270
|
+
const trimmed = v.trim();
|
|
5271
|
+
if (!trimmed || trimmed.toLowerCase() === "null")
|
|
5272
|
+
return null;
|
|
5273
|
+
return trimmed;
|
|
5274
|
+
}
|
|
4406
5275
|
function summarise(responses) {
|
|
4407
5276
|
const answered = responses.filter((r) => r.score != null).length;
|
|
4408
5277
|
const total = responses.length;
|
|
@@ -4514,18 +5383,29 @@ var pullLeads = {
|
|
|
4514
5383
|
}
|
|
4515
5384
|
}));
|
|
4516
5385
|
const summaryMap = new Map(summaries.map((s) => [s.leadId, s.summary]));
|
|
4517
|
-
const
|
|
5386
|
+
const augmentContact2 = (c) => c ? {
|
|
5387
|
+
...c,
|
|
5388
|
+
linkedin_page: normalizeLinkedinPage3(c.linkedin_page ?? null)
|
|
5389
|
+
} : null;
|
|
5390
|
+
const trimmed = (lead) => verbose ? {
|
|
5391
|
+
...lead,
|
|
5392
|
+
recommended_contact: augmentContact2(lead.recommended_contact)
|
|
5393
|
+
} : {
|
|
4518
5394
|
id: lead.id,
|
|
4519
5395
|
name: lead.name,
|
|
4520
5396
|
score: lead.score,
|
|
4521
5397
|
ai_agent_lead_score: lead.ai_agent_lead_score,
|
|
5398
|
+
ai_summary: lead.ai_summary ?? null,
|
|
5399
|
+
split_ai_summary: lead.split_ai_summary ?? null,
|
|
4522
5400
|
location: lead.location,
|
|
4523
5401
|
short_description: lead.short_description ?? lead.description,
|
|
4524
5402
|
size: lead.size,
|
|
4525
5403
|
website: lead.website,
|
|
5404
|
+
phone_numbers: lead.phone_numbers ?? null,
|
|
4526
5405
|
tags: lead.tags,
|
|
4527
|
-
|
|
4528
|
-
|
|
5406
|
+
social_presence: lead.social_presence ?? null,
|
|
5407
|
+
social_urls: lead.social_urls ?? null,
|
|
5408
|
+
recommended_contact: augmentContact2(lead.recommended_contact),
|
|
4529
5409
|
web_fetch_in_progress: lead.web_fetch_in_progress ?? false,
|
|
4530
5410
|
enrichment_in_progress: lead.enrichment_in_progress ?? false,
|
|
4531
5411
|
liked: lead.liked,
|
|
@@ -4560,7 +5440,182 @@ var pullLeads = {
|
|
|
4560
5440
|
}
|
|
4561
5441
|
};
|
|
4562
5442
|
|
|
5443
|
+
// ../core/dist/composite/pull-followups.js
|
|
5444
|
+
function normalizeLinkedinPage4(v) {
|
|
5445
|
+
if (v == null)
|
|
5446
|
+
return null;
|
|
5447
|
+
if (typeof v !== "string")
|
|
5448
|
+
return null;
|
|
5449
|
+
const trimmed = v.trim();
|
|
5450
|
+
if (!trimmed || trimmed.toLowerCase() === "null")
|
|
5451
|
+
return null;
|
|
5452
|
+
return trimmed;
|
|
5453
|
+
}
|
|
5454
|
+
function augmentContact(c) {
|
|
5455
|
+
if (!c)
|
|
5456
|
+
return null;
|
|
5457
|
+
return {
|
|
5458
|
+
...c,
|
|
5459
|
+
linkedin_page: normalizeLinkedinPage4(c.linkedin_page ?? null)
|
|
5460
|
+
};
|
|
5461
|
+
}
|
|
5462
|
+
var pullFollowups = {
|
|
5463
|
+
name: "leadbay_pull_followups",
|
|
5464
|
+
annotations: {
|
|
5465
|
+
title: "Pull known leads to follow up on (Monitor view)",
|
|
5466
|
+
readOnlyHint: true,
|
|
5467
|
+
destructiveHint: false,
|
|
5468
|
+
idempotentHint: true,
|
|
5469
|
+
openWorldHint: true
|
|
5470
|
+
},
|
|
5471
|
+
description: leadbay_pull_followups,
|
|
5472
|
+
inputSchema: {
|
|
5473
|
+
type: "object",
|
|
5474
|
+
properties: {
|
|
5475
|
+
filtered: {
|
|
5476
|
+
type: "boolean",
|
|
5477
|
+
description: "Apply the user's stored Monitor filter (server-persisted via POST /monitor/filter). Default true."
|
|
5478
|
+
},
|
|
5479
|
+
personal: {
|
|
5480
|
+
type: "boolean",
|
|
5481
|
+
description: "When true, restrict to leads this user has personally monitored (not org-wide). Default false."
|
|
5482
|
+
},
|
|
5483
|
+
liked: {
|
|
5484
|
+
type: "boolean",
|
|
5485
|
+
description: "When true, restrict to leads the user has explicitly liked. Default false."
|
|
5486
|
+
},
|
|
5487
|
+
count: {
|
|
5488
|
+
type: "number",
|
|
5489
|
+
description: "Leads per page, max 200 (default 20)."
|
|
5490
|
+
},
|
|
5491
|
+
page: {
|
|
5492
|
+
type: "number",
|
|
5493
|
+
description: "Page number, 0-indexed (default 0)."
|
|
5494
|
+
},
|
|
5495
|
+
set_filter: {
|
|
5496
|
+
type: "object",
|
|
5497
|
+
description: "Optional FilterItem ({criteria: FilterCriterion[]}). When provided, the composite POSTs it to /monitor/filter (server-persists across sessions) BEFORE fetching the filtered Monitor view. Use to refine 'leads to follow up' by city, sector, recency, action type, etc.",
|
|
5498
|
+
properties: {
|
|
5499
|
+
criteria: {
|
|
5500
|
+
type: "array",
|
|
5501
|
+
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).",
|
|
5502
|
+
items: { type: "object" }
|
|
5503
|
+
}
|
|
5504
|
+
}
|
|
5505
|
+
}
|
|
5506
|
+
},
|
|
5507
|
+
additionalProperties: false
|
|
5508
|
+
},
|
|
5509
|
+
outputSchema: {
|
|
5510
|
+
type: "object",
|
|
5511
|
+
properties: {
|
|
5512
|
+
active_filters: {
|
|
5513
|
+
type: ["object", "null"],
|
|
5514
|
+
description: "The FilterItem currently stored server-side for this user (via GET /monitor/filter). null when no filter is set or when filtered:false was passed."
|
|
5515
|
+
},
|
|
5516
|
+
leads: {
|
|
5517
|
+
type: "array",
|
|
5518
|
+
description: "The page of monitored leads. Each lead carries the FullLead shape augmented with normalized linkedin_page on contacts and `recommended_contact`.",
|
|
5519
|
+
items: { type: "object" }
|
|
5520
|
+
},
|
|
5521
|
+
pagination: {
|
|
5522
|
+
type: ["object", "null"],
|
|
5523
|
+
description: "page / pages / total \u2014 the backend's pagination envelope when present."
|
|
5524
|
+
},
|
|
5525
|
+
total_excluded_by_pushback: {
|
|
5526
|
+
type: "number",
|
|
5527
|
+
description: "Composite-derived count of leads in the page that were excluded because their `pushback_status` is active. The backend may or may not pre-filter; this exposes the count when the composite has to drop them itself."
|
|
5528
|
+
},
|
|
5529
|
+
_meta: {
|
|
5530
|
+
type: "object",
|
|
5531
|
+
description: "Operator context: region + last-call latency.",
|
|
5532
|
+
properties: {
|
|
5533
|
+
region: { type: "string" },
|
|
5534
|
+
latency_ms: { type: ["number", "null"] }
|
|
5535
|
+
}
|
|
5536
|
+
}
|
|
5537
|
+
},
|
|
5538
|
+
required: ["leads"]
|
|
5539
|
+
},
|
|
5540
|
+
execute: async (client, params, ctx) => {
|
|
5541
|
+
const filtered = params.filtered ?? true;
|
|
5542
|
+
const personal = params.personal ?? false;
|
|
5543
|
+
const liked = params.liked ?? false;
|
|
5544
|
+
const page = params.page ?? 0;
|
|
5545
|
+
const count = Math.min(params.count ?? 20, 200);
|
|
5546
|
+
if (params.set_filter) {
|
|
5547
|
+
try {
|
|
5548
|
+
await client.requestVoid("POST", "/monitor/filter", params.set_filter);
|
|
5549
|
+
} catch (err) {
|
|
5550
|
+
ctx?.logger?.warn?.(`pull_followups: POST /monitor/filter failed: ${err?.message ?? err?.code ?? err}`);
|
|
5551
|
+
}
|
|
5552
|
+
}
|
|
5553
|
+
const qs = new URLSearchParams({
|
|
5554
|
+
personal: String(personal),
|
|
5555
|
+
liked: String(liked),
|
|
5556
|
+
filtered: String(filtered),
|
|
5557
|
+
count: String(count),
|
|
5558
|
+
page: String(page)
|
|
5559
|
+
}).toString();
|
|
5560
|
+
const [filterR, monitorR] = await Promise.allSettled([
|
|
5561
|
+
filtered ? client.request("GET", "/monitor/filter") : Promise.resolve(null),
|
|
5562
|
+
client.request("GET", `/monitor?${qs}`)
|
|
5563
|
+
]);
|
|
5564
|
+
const activeFilter = filterR.status === "fulfilled" ? filterR.value ?? null : null;
|
|
5565
|
+
if (monitorR.status === "rejected") {
|
|
5566
|
+
throw monitorR.reason;
|
|
5567
|
+
}
|
|
5568
|
+
const monitor = monitorR.value ?? {};
|
|
5569
|
+
const rawLeads = Array.isArray(monitor.items) ? monitor.items : Array.isArray(monitor.leads) ? monitor.leads : Array.isArray(monitor) ? monitor : [];
|
|
5570
|
+
const now = Date.now();
|
|
5571
|
+
const isActivePushback = (lead) => {
|
|
5572
|
+
const status = lead?.pushback_status;
|
|
5573
|
+
if (!status)
|
|
5574
|
+
return false;
|
|
5575
|
+
const until = lead?.pushback_until ?? lead?.pushback_status_set_at;
|
|
5576
|
+
if (!until)
|
|
5577
|
+
return true;
|
|
5578
|
+
const ts = Date.parse(until);
|
|
5579
|
+
if (Number.isNaN(ts))
|
|
5580
|
+
return true;
|
|
5581
|
+
return ts > now;
|
|
5582
|
+
};
|
|
5583
|
+
let excluded = 0;
|
|
5584
|
+
const leads = rawLeads.filter((lead) => {
|
|
5585
|
+
if (isActivePushback(lead)) {
|
|
5586
|
+
excluded += 1;
|
|
5587
|
+
return false;
|
|
5588
|
+
}
|
|
5589
|
+
return true;
|
|
5590
|
+
}).map((lead) => ({
|
|
5591
|
+
...lead,
|
|
5592
|
+
recommended_contact: augmentContact(lead.recommended_contact),
|
|
5593
|
+
org_contacts: Array.isArray(lead.org_contacts) ? lead.org_contacts.map(augmentContact) : lead.org_contacts ?? null
|
|
5594
|
+
}));
|
|
5595
|
+
return {
|
|
5596
|
+
active_filters: activeFilter,
|
|
5597
|
+
leads,
|
|
5598
|
+
pagination: monitor.pagination ?? null,
|
|
5599
|
+
total_excluded_by_pushback: excluded,
|
|
5600
|
+
_meta: {
|
|
5601
|
+
region: client.region,
|
|
5602
|
+
latency_ms: client.lastMeta?.latency_ms ?? null
|
|
5603
|
+
}
|
|
5604
|
+
};
|
|
5605
|
+
}
|
|
5606
|
+
};
|
|
5607
|
+
|
|
4563
5608
|
// ../core/dist/composite/research-lead.js
|
|
5609
|
+
function normalizeLinkedinPage5(v) {
|
|
5610
|
+
if (v == null)
|
|
5611
|
+
return null;
|
|
5612
|
+
if (typeof v !== "string")
|
|
5613
|
+
return null;
|
|
5614
|
+
const trimmed = v.trim();
|
|
5615
|
+
if (!trimmed || trimmed.toLowerCase() === "null")
|
|
5616
|
+
return null;
|
|
5617
|
+
return trimmed;
|
|
5618
|
+
}
|
|
4564
5619
|
function renderResearchLeadMarkdown(shape) {
|
|
4565
5620
|
const out = [];
|
|
4566
5621
|
const firm = shape.firmographics ?? {};
|
|
@@ -4737,22 +5792,96 @@ var researchLead = {
|
|
|
4737
5792
|
},
|
|
4738
5793
|
firmographics: {
|
|
4739
5794
|
type: "object",
|
|
4740
|
-
description: "Lead profile basics.
|
|
5795
|
+
description: "Lead profile basics. The shapes here match the backend `LeadSimplified` schema verbatim \u2014 `size` is `{min,max,...}`, `location` is `{city,state,country,full,pos}`, `tags` are typed objects.",
|
|
4741
5796
|
properties: {
|
|
4742
5797
|
id: { type: "string" },
|
|
4743
5798
|
name: { type: "string" },
|
|
4744
5799
|
sector_id: { type: ["number", "string", "null"] },
|
|
4745
|
-
size: {
|
|
4746
|
-
|
|
5800
|
+
size: {
|
|
5801
|
+
type: ["object", "null"],
|
|
5802
|
+
description: "LeadSimplified.size \u2014 employee-count band.",
|
|
5803
|
+
properties: {
|
|
5804
|
+
low: { type: ["number", "null"] },
|
|
5805
|
+
high: { type: ["number", "null"] },
|
|
5806
|
+
min: { type: ["number", "null"] },
|
|
5807
|
+
max: { type: ["number", "null"] },
|
|
5808
|
+
label: { type: ["string", "null"] }
|
|
5809
|
+
}
|
|
5810
|
+
},
|
|
5811
|
+
location: {
|
|
5812
|
+
type: ["object", "null"],
|
|
5813
|
+
description: "LeadFullLocation \u2014 city/state/country/full/pos.",
|
|
5814
|
+
properties: {
|
|
5815
|
+
city: { type: ["string", "null"] },
|
|
5816
|
+
state: { type: ["string", "null"] },
|
|
5817
|
+
country: { type: ["string", "null"] },
|
|
5818
|
+
full: { type: ["string", "null"] },
|
|
5819
|
+
pos: {
|
|
5820
|
+
type: ["array", "null"],
|
|
5821
|
+
items: { type: "number" }
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5824
|
+
},
|
|
4747
5825
|
website: { type: ["string", "null"] },
|
|
4748
5826
|
description: { type: ["string", "null"] },
|
|
4749
5827
|
short_description: { type: ["string", "null"] },
|
|
4750
|
-
keywords: {
|
|
4751
|
-
|
|
5828
|
+
keywords: {
|
|
5829
|
+
type: "array",
|
|
5830
|
+
description: "Either bare strings (legacy) or {keyword,score} objects depending on the backend payload version.",
|
|
5831
|
+
items: {}
|
|
5832
|
+
},
|
|
5833
|
+
tags: {
|
|
5834
|
+
type: "array",
|
|
5835
|
+
description: "LeadTag[] \u2014 {id, display_name, tag, score}.",
|
|
5836
|
+
items: {
|
|
5837
|
+
type: "object",
|
|
5838
|
+
properties: {
|
|
5839
|
+
id: { type: ["number", "string", "null"] },
|
|
5840
|
+
display_name: { type: ["string", "null"] },
|
|
5841
|
+
tag: { type: "string" },
|
|
5842
|
+
score: { type: ["number", "null"] }
|
|
5843
|
+
}
|
|
5844
|
+
}
|
|
5845
|
+
},
|
|
4752
5846
|
score: { type: ["number", "null"] },
|
|
4753
5847
|
ai_agent_lead_score: { type: ["number", "null"] },
|
|
4754
|
-
|
|
4755
|
-
|
|
5848
|
+
ai_summary: { type: ["string", "null"] },
|
|
5849
|
+
split_ai_summary: {
|
|
5850
|
+
type: ["object", "null"],
|
|
5851
|
+
properties: {
|
|
5852
|
+
worth_pursuing: { type: ["string", "null"] },
|
|
5853
|
+
approach_angle: { type: ["string", "null"] },
|
|
5854
|
+
next_step: { type: ["string", "null"] }
|
|
5855
|
+
}
|
|
5856
|
+
},
|
|
5857
|
+
phone_numbers: {
|
|
5858
|
+
type: ["array", "null"],
|
|
5859
|
+
items: { type: "string" }
|
|
5860
|
+
},
|
|
5861
|
+
social_presence: {
|
|
5862
|
+
type: ["object", "null"],
|
|
5863
|
+
description: "LeadSocialPresence \u2014 6 booleans per platform. Use `social_urls` for URLs.",
|
|
5864
|
+
properties: {
|
|
5865
|
+
crunchbase: { type: "boolean" },
|
|
5866
|
+
facebook: { type: "boolean" },
|
|
5867
|
+
instagram: { type: "boolean" },
|
|
5868
|
+
linkedin: { type: "boolean" },
|
|
5869
|
+
tiktok: { type: "boolean" },
|
|
5870
|
+
twitter: { type: "boolean" }
|
|
5871
|
+
}
|
|
5872
|
+
},
|
|
5873
|
+
social_urls: {
|
|
5874
|
+
type: ["object", "null"],
|
|
5875
|
+
description: "LeadSocialUrls \u2014 URL strings per platform; null when the company has no profile.",
|
|
5876
|
+
properties: {
|
|
5877
|
+
crunchbase: { type: ["string", "null"] },
|
|
5878
|
+
facebook: { type: ["string", "null"] },
|
|
5879
|
+
instagram: { type: ["string", "null"] },
|
|
5880
|
+
linkedin: { type: ["string", "null"] },
|
|
5881
|
+
tiktok: { type: ["string", "null"] },
|
|
5882
|
+
twitter: { type: ["string", "null"] }
|
|
5883
|
+
}
|
|
5884
|
+
},
|
|
4756
5885
|
registry_ids: { type: ["object", "array", "null"] }
|
|
4757
5886
|
},
|
|
4758
5887
|
additionalProperties: false
|
|
@@ -4773,7 +5902,6 @@ var researchLead = {
|
|
|
4773
5902
|
liked: { type: "boolean" },
|
|
4774
5903
|
disliked: { type: "boolean" },
|
|
4775
5904
|
new: { type: "boolean" },
|
|
4776
|
-
recommended_contact_title: { type: ["string", "null"] },
|
|
4777
5905
|
recommended_contact: { type: ["object", "null"] },
|
|
4778
5906
|
notes_count: { type: "number" },
|
|
4779
5907
|
epilogue_actions_count: { type: "number" },
|
|
@@ -4894,11 +6022,16 @@ var researchLead = {
|
|
|
4894
6022
|
tags: lead.tags,
|
|
4895
6023
|
score: lead.score,
|
|
4896
6024
|
ai_agent_lead_score: lead.ai_agent_lead_score,
|
|
6025
|
+
ai_summary: lead.ai_summary ?? null,
|
|
6026
|
+
split_ai_summary: lead.split_ai_summary ?? null,
|
|
6027
|
+
phone_numbers: lead.phone_numbers ?? null,
|
|
4897
6028
|
social_presence: lead.social_presence ?? null,
|
|
4898
6029
|
social_urls: lead.social_urls ?? null,
|
|
4899
6030
|
registry_ids: lead.registry_ids ?? null
|
|
4900
6031
|
},
|
|
4901
6032
|
// 4) contacts (paid/enriched, plus org contacts if present)
|
|
6033
|
+
// B6: defensively coerce the literal string "null" to a real null —
|
|
6034
|
+
// some backend serializers emit it for un-enriched contacts.
|
|
4902
6035
|
contacts: {
|
|
4903
6036
|
enriched: paidContacts.map((c) => ({
|
|
4904
6037
|
id: c.id,
|
|
@@ -4907,7 +6040,7 @@ var researchLead = {
|
|
|
4907
6040
|
job_title: c.job_title,
|
|
4908
6041
|
email: c.email,
|
|
4909
6042
|
phone_number: c.phone_number,
|
|
4910
|
-
linkedin_page: c.linkedin_page,
|
|
6043
|
+
linkedin_page: normalizeLinkedinPage5(c.linkedin_page),
|
|
4911
6044
|
recommended: c.recommended,
|
|
4912
6045
|
enrichment_done: c.enrichment?.done ?? false
|
|
4913
6046
|
})),
|
|
@@ -4916,16 +6049,22 @@ var researchLead = {
|
|
|
4916
6049
|
first_name: c.first_name,
|
|
4917
6050
|
last_name: c.last_name,
|
|
4918
6051
|
job_title: c.job_title,
|
|
4919
|
-
email: c.email
|
|
6052
|
+
email: c.email,
|
|
6053
|
+
linkedin_page: normalizeLinkedinPage5(c.linkedin_page ?? null)
|
|
4920
6054
|
}))
|
|
4921
6055
|
},
|
|
4922
|
-
// 5) engagement — what humans/prior agent runs already did
|
|
6056
|
+
// 5) engagement — what humans/prior agent runs already did.
|
|
6057
|
+
// B8: `recommended_contact_title` dropped — it duplicates
|
|
6058
|
+
// `recommended_contact.job_title` and just confused agents.
|
|
4923
6059
|
engagement: {
|
|
4924
6060
|
liked: lead.liked,
|
|
4925
6061
|
disliked: lead.disliked,
|
|
4926
6062
|
new: lead.new ?? false,
|
|
4927
|
-
|
|
4928
|
-
|
|
6063
|
+
recommended_contact: lead.recommended_contact ? {
|
|
6064
|
+
...lead.recommended_contact,
|
|
6065
|
+
// B1+B7: propagate linkedin_page when the backend includes it.
|
|
6066
|
+
linkedin_page: normalizeLinkedinPage5(lead.recommended_contact.linkedin_page ?? null)
|
|
6067
|
+
} : null,
|
|
4929
6068
|
notes_count: lead.notes_count ?? 0,
|
|
4930
6069
|
epilogue_actions_count: lead.epilogue_actions_count ?? 0,
|
|
4931
6070
|
prospecting_actions_count: lead.prospecting_actions_count ?? 0,
|
|
@@ -8921,6 +10060,8 @@ var granularWriteTools = [
|
|
|
8921
10060
|
dismissClarification,
|
|
8922
10061
|
setEpilogueStatus,
|
|
8923
10062
|
removeEpilogue,
|
|
10063
|
+
setPushback,
|
|
10064
|
+
removePushback,
|
|
8924
10065
|
previewBulkEnrichment,
|
|
8925
10066
|
launchBulkEnrichment,
|
|
8926
10067
|
createCustomField
|
|
@@ -8935,6 +10076,7 @@ granularTools.forEach((t) => {
|
|
|
8935
10076
|
});
|
|
8936
10077
|
var compositeReadTools = [
|
|
8937
10078
|
pullLeads,
|
|
10079
|
+
pullFollowups,
|
|
8938
10080
|
researchLead,
|
|
8939
10081
|
recallOrderedTitles,
|
|
8940
10082
|
accountStatus,
|
|
@@ -9019,12 +10161,15 @@ export {
|
|
|
9019
10161
|
dismissClarification,
|
|
9020
10162
|
setEpilogueStatus,
|
|
9021
10163
|
removeEpilogue,
|
|
10164
|
+
setPushback,
|
|
10165
|
+
removePushback,
|
|
9022
10166
|
previewBulkEnrichment,
|
|
9023
10167
|
launchBulkEnrichment,
|
|
9024
10168
|
createCustomField,
|
|
9025
10169
|
researchCompany,
|
|
9026
10170
|
prepareOutreach,
|
|
9027
10171
|
pullLeads,
|
|
10172
|
+
pullFollowups,
|
|
9028
10173
|
researchLead,
|
|
9029
10174
|
recallOrderedTitles,
|
|
9030
10175
|
accountStatus,
|