@leadbay/mcp 0.8.0 → 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 +27 -0
- package/dist/bin.js +145 -8
- package/dist/{chunk-NVGZ432E.js → chunk-2EGNZRD7.js} +1190 -51
- package/dist/{dist-HS5N4SIS.js → dist-IIPWWDS5.js} +7 -1
- package/package.json +1 -1
|
@@ -580,6 +580,43 @@ WHEN TO USE: when the user wants more qualified leads than what's currently show
|
|
|
580
580
|
WHEN NOT TO USE: to qualify a single specific lead \u2014 that's leadbay_qualify_lead (granular, advanced).
|
|
581
581
|
|
|
582
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) |
|
|
583
620
|
`;
|
|
584
621
|
var leadbay_clear_selection = `Clear the user's transient selection.
|
|
585
622
|
|
|
@@ -761,6 +798,38 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
|
|
|
761
798
|
|
|
762
799
|
|
|
763
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) |
|
|
764
833
|
`;
|
|
765
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.
|
|
766
835
|
|
|
@@ -776,12 +845,76 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
|
|
|
776
845
|
|
|
777
846
|
|
|
778
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) |
|
|
779
880
|
`;
|
|
780
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.
|
|
781
882
|
|
|
782
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.
|
|
783
884
|
|
|
784
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 |
|
|
785
918
|
`;
|
|
786
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.
|
|
787
920
|
|
|
@@ -805,7 +938,35 @@ Optional \`for_records\` param: pass a sample of CSV-shaped rows and the tool al
|
|
|
805
938
|
|
|
806
939
|
WHEN TO USE: before authoring an import mapping, especially when the CSV has columns that aren't obvious matches for standard fields.
|
|
807
940
|
|
|
808
|
-
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) |
|
|
809
970
|
`;
|
|
810
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.
|
|
811
972
|
|
|
@@ -829,11 +990,110 @@ WHEN NOT TO USE: from agent flow \u2014 use leadbay_answer_clarification.
|
|
|
829
990
|
|
|
830
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\`.
|
|
831
992
|
`;
|
|
832
|
-
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\`.
|
|
833
996
|
|
|
834
|
-
|
|
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.
|
|
835
1003
|
|
|
836
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.
|
|
837
1097
|
`;
|
|
838
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.
|
|
839
1099
|
|
|
@@ -849,15 +1109,252 @@ WHEN NOT TO USE: as a non-admin (will fail with 403); for personal lens changes
|
|
|
849
1109
|
|
|
850
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\`.
|
|
851
1111
|
`;
|
|
852
|
-
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):
|
|
1160
|
+
|
|
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.
|
|
853
1250
|
|
|
854
|
-
|
|
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.
|
|
855
1252
|
|
|
856
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.
|
|
857
1254
|
|
|
858
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).
|
|
859
1256
|
|
|
860
|
-
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 \`
|
|
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.
|
|
861
1358
|
`;
|
|
862
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.
|
|
863
1360
|
|
|
@@ -893,6 +1390,16 @@ WHEN TO USE: when an outreach action was logged in error and needs to be undone.
|
|
|
893
1390
|
|
|
894
1391
|
WHEN NOT TO USE: to change status \u2014 call leadbay_set_epilogue_status with the new status (it overwrites).
|
|
895
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
|
+
|
|
896
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\`.
|
|
897
1404
|
`;
|
|
898
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).
|
|
@@ -907,9 +1414,120 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
|
|
|
907
1414
|
`;
|
|
908
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).
|
|
909
1416
|
|
|
910
|
-
|
|
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
|
|
1459
|
+
|
|
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.
|
|
911
1461
|
|
|
912
|
-
|
|
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?"\`
|
|
913
1531
|
`;
|
|
914
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.
|
|
915
1533
|
|
|
@@ -926,6 +1544,38 @@ var leadbay_resolve_import_rows = `Resolve messy CSV-shaped lead rows against Le
|
|
|
926
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.
|
|
927
1545
|
|
|
928
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) |
|
|
929
1579
|
`;
|
|
930
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.
|
|
931
1581
|
|
|
@@ -949,6 +1599,18 @@ WHEN TO USE: low-level.
|
|
|
949
1599
|
|
|
950
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.
|
|
951
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
|
+
|
|
952
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\`.
|
|
953
1615
|
`;
|
|
954
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.
|
|
@@ -1140,6 +1802,16 @@ var discoverLeads = {
|
|
|
1140
1802
|
};
|
|
1141
1803
|
|
|
1142
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
|
+
}
|
|
1143
1815
|
var getLeadProfile = {
|
|
1144
1816
|
name: "leadbay_get_lead_profile",
|
|
1145
1817
|
annotations: {
|
|
@@ -1216,7 +1888,7 @@ var getLeadProfile = {
|
|
|
1216
1888
|
last_name: c.last_name,
|
|
1217
1889
|
email: c.email,
|
|
1218
1890
|
phone_number: c.phone_number,
|
|
1219
|
-
linkedin_page: c.linkedin_page,
|
|
1891
|
+
linkedin_page: normalizeLinkedinPage(c.linkedin_page),
|
|
1220
1892
|
job_title: c.job_title,
|
|
1221
1893
|
recommended: c.recommended,
|
|
1222
1894
|
enrichment: c.enrichment,
|
|
@@ -1228,7 +1900,7 @@ var getLeadProfile = {
|
|
|
1228
1900
|
last_name: c.last_name,
|
|
1229
1901
|
email: c.email,
|
|
1230
1902
|
phone_number: c.phone_number,
|
|
1231
|
-
linkedin_page: c.linkedin_page,
|
|
1903
|
+
linkedin_page: normalizeLinkedinPage(c.linkedin_page),
|
|
1232
1904
|
job_title: c.job_title,
|
|
1233
1905
|
recommended: c.recommended,
|
|
1234
1906
|
enrichment: c.enrichment,
|
|
@@ -1253,8 +1925,14 @@ var getLeadProfile = {
|
|
|
1253
1925
|
phone_numbers: lead.phone_numbers,
|
|
1254
1926
|
keywords: lead.keywords,
|
|
1255
1927
|
contacts_count: lead.contacts_count,
|
|
1256
|
-
|
|
1257
|
-
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,
|
|
1258
1936
|
web_fetch_in_progress: lead.web_fetch_in_progress ?? false
|
|
1259
1937
|
},
|
|
1260
1938
|
qualification: qualification?.map((q) => ({
|
|
@@ -4036,6 +4714,100 @@ var removeEpilogue = {
|
|
|
4036
4714
|
}
|
|
4037
4715
|
};
|
|
4038
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
|
+
|
|
4039
4811
|
// ../core/dist/tools/preview-bulk-enrichment.js
|
|
4040
4812
|
var previewBulkEnrichment = {
|
|
4041
4813
|
name: "leadbay_preview_bulk_enrichment",
|
|
@@ -4306,6 +5078,16 @@ var researchCompany = {
|
|
|
4306
5078
|
};
|
|
4307
5079
|
|
|
4308
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
|
+
}
|
|
4309
5091
|
var prepareOutreach = {
|
|
4310
5092
|
name: "leadbay_prepare_outreach",
|
|
4311
5093
|
annotations: {
|
|
@@ -4326,7 +5108,7 @@ var prepareOutreach = {
|
|
|
4326
5108
|
},
|
|
4327
5109
|
enrich: {
|
|
4328
5110
|
type: "boolean",
|
|
4329
|
-
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)."
|
|
4330
5112
|
}
|
|
4331
5113
|
},
|
|
4332
5114
|
required: ["leadId"],
|
|
@@ -4337,32 +5119,67 @@ var prepareOutreach = {
|
|
|
4337
5119
|
properties: {
|
|
4338
5120
|
lead: {
|
|
4339
5121
|
type: ["object", "null"],
|
|
4340
|
-
description: "
|
|
5122
|
+
description: "Lead context for the brief. Expanded per B15: score, ai_summary, split_ai_summary, location, size, phone_numbers, website, description.",
|
|
4341
5123
|
properties: {
|
|
5124
|
+
id: { type: "string" },
|
|
4342
5125
|
name: { type: "string" },
|
|
5126
|
+
score: { type: ["number", "null"] },
|
|
5127
|
+
ai_agent_lead_score: { type: ["number", "null"] },
|
|
4343
5128
|
ai_summary: { type: ["string", "null"] },
|
|
4344
|
-
|
|
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"] }
|
|
4345
5138
|
}
|
|
4346
5139
|
},
|
|
4347
5140
|
recommended_contact: {
|
|
4348
5141
|
type: ["object", "null"],
|
|
4349
|
-
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)."
|
|
4350
5161
|
},
|
|
4351
5162
|
other_contacts_count: {
|
|
4352
5163
|
type: "number",
|
|
4353
|
-
description: "
|
|
5164
|
+
description: "DEPRECATED: alias for additional_contacts_count. Will be removed in 0.10.0."
|
|
4354
5165
|
},
|
|
4355
5166
|
enrichment: {
|
|
4356
5167
|
type: "object",
|
|
4357
|
-
description: "
|
|
5168
|
+
description: "Self-polling status (B13): triggered = whether this call kicked off enrichment; complete = whether the recommended contact now has email OR phone.",
|
|
4358
5169
|
properties: {
|
|
4359
5170
|
triggered: { type: "boolean" },
|
|
5171
|
+
complete: { type: "boolean" },
|
|
4360
5172
|
error: { type: ["string", "null"] },
|
|
4361
5173
|
hint: { type: ["string", "null"] }
|
|
4362
5174
|
}
|
|
4363
5175
|
}
|
|
4364
5176
|
},
|
|
4365
|
-
required: [
|
|
5177
|
+
required: [
|
|
5178
|
+
"recommended_contact",
|
|
5179
|
+
"additional_contacts_count",
|
|
5180
|
+
"total_contacts_count",
|
|
5181
|
+
"enrichment"
|
|
5182
|
+
]
|
|
4366
5183
|
},
|
|
4367
5184
|
execute: async (client, params, ctx) => {
|
|
4368
5185
|
const contactsResult = await getContacts.execute(client, { leadId: params.leadId }, ctx);
|
|
@@ -4378,37 +5195,83 @@ var prepareOutreach = {
|
|
|
4378
5195
|
enrichmentError = e?.message ?? String(e);
|
|
4379
5196
|
}
|
|
4380
5197
|
}
|
|
4381
|
-
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;
|
|
4382
5208
|
try {
|
|
4383
5209
|
const profile = await getLeadProfile.execute(client, { leadId: params.leadId }, ctx);
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
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
|
|
4388
5226
|
};
|
|
4389
5227
|
} catch {
|
|
5228
|
+
leadBlock = {
|
|
5229
|
+
id: params.leadId,
|
|
5230
|
+
name: null,
|
|
5231
|
+
ai_summary: null
|
|
5232
|
+
};
|
|
4390
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));
|
|
4391
5247
|
return {
|
|
4392
|
-
lead:
|
|
4393
|
-
recommended_contact:
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
phone_number: recommended.phone_number,
|
|
4399
|
-
linkedin_page: recommended.linkedin_page
|
|
4400
|
-
} : null,
|
|
4401
|
-
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,
|
|
4402
5254
|
enrichment: {
|
|
4403
5255
|
triggered: enrichmentTriggered,
|
|
5256
|
+
complete: enrichmentComplete,
|
|
4404
5257
|
error: enrichmentError,
|
|
4405
|
-
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
|
|
4406
5259
|
}
|
|
4407
5260
|
};
|
|
4408
5261
|
}
|
|
4409
5262
|
};
|
|
4410
5263
|
|
|
4411
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
|
+
}
|
|
4412
5275
|
function summarise(responses) {
|
|
4413
5276
|
const answered = responses.filter((r) => r.score != null).length;
|
|
4414
5277
|
const total = responses.length;
|
|
@@ -4520,18 +5383,29 @@ var pullLeads = {
|
|
|
4520
5383
|
}
|
|
4521
5384
|
}));
|
|
4522
5385
|
const summaryMap = new Map(summaries.map((s) => [s.leadId, s.summary]));
|
|
4523
|
-
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
|
+
} : {
|
|
4524
5394
|
id: lead.id,
|
|
4525
5395
|
name: lead.name,
|
|
4526
5396
|
score: lead.score,
|
|
4527
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,
|
|
4528
5400
|
location: lead.location,
|
|
4529
5401
|
short_description: lead.short_description ?? lead.description,
|
|
4530
5402
|
size: lead.size,
|
|
4531
5403
|
website: lead.website,
|
|
5404
|
+
phone_numbers: lead.phone_numbers ?? null,
|
|
4532
5405
|
tags: lead.tags,
|
|
4533
|
-
|
|
4534
|
-
|
|
5406
|
+
social_presence: lead.social_presence ?? null,
|
|
5407
|
+
social_urls: lead.social_urls ?? null,
|
|
5408
|
+
recommended_contact: augmentContact2(lead.recommended_contact),
|
|
4535
5409
|
web_fetch_in_progress: lead.web_fetch_in_progress ?? false,
|
|
4536
5410
|
enrichment_in_progress: lead.enrichment_in_progress ?? false,
|
|
4537
5411
|
liked: lead.liked,
|
|
@@ -4566,7 +5440,182 @@ var pullLeads = {
|
|
|
4566
5440
|
}
|
|
4567
5441
|
};
|
|
4568
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
|
+
|
|
4569
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
|
+
}
|
|
4570
5619
|
function renderResearchLeadMarkdown(shape) {
|
|
4571
5620
|
const out = [];
|
|
4572
5621
|
const firm = shape.firmographics ?? {};
|
|
@@ -4743,22 +5792,96 @@ var researchLead = {
|
|
|
4743
5792
|
},
|
|
4744
5793
|
firmographics: {
|
|
4745
5794
|
type: "object",
|
|
4746
|
-
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.",
|
|
4747
5796
|
properties: {
|
|
4748
5797
|
id: { type: "string" },
|
|
4749
5798
|
name: { type: "string" },
|
|
4750
5799
|
sector_id: { type: ["number", "string", "null"] },
|
|
4751
|
-
size: {
|
|
4752
|
-
|
|
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
|
+
},
|
|
4753
5825
|
website: { type: ["string", "null"] },
|
|
4754
5826
|
description: { type: ["string", "null"] },
|
|
4755
5827
|
short_description: { type: ["string", "null"] },
|
|
4756
|
-
keywords: {
|
|
4757
|
-
|
|
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
|
+
},
|
|
4758
5846
|
score: { type: ["number", "null"] },
|
|
4759
5847
|
ai_agent_lead_score: { type: ["number", "null"] },
|
|
4760
|
-
|
|
4761
|
-
|
|
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
|
+
},
|
|
4762
5885
|
registry_ids: { type: ["object", "array", "null"] }
|
|
4763
5886
|
},
|
|
4764
5887
|
additionalProperties: false
|
|
@@ -4779,7 +5902,6 @@ var researchLead = {
|
|
|
4779
5902
|
liked: { type: "boolean" },
|
|
4780
5903
|
disliked: { type: "boolean" },
|
|
4781
5904
|
new: { type: "boolean" },
|
|
4782
|
-
recommended_contact_title: { type: ["string", "null"] },
|
|
4783
5905
|
recommended_contact: { type: ["object", "null"] },
|
|
4784
5906
|
notes_count: { type: "number" },
|
|
4785
5907
|
epilogue_actions_count: { type: "number" },
|
|
@@ -4900,11 +6022,16 @@ var researchLead = {
|
|
|
4900
6022
|
tags: lead.tags,
|
|
4901
6023
|
score: lead.score,
|
|
4902
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,
|
|
4903
6028
|
social_presence: lead.social_presence ?? null,
|
|
4904
6029
|
social_urls: lead.social_urls ?? null,
|
|
4905
6030
|
registry_ids: lead.registry_ids ?? null
|
|
4906
6031
|
},
|
|
4907
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.
|
|
4908
6035
|
contacts: {
|
|
4909
6036
|
enriched: paidContacts.map((c) => ({
|
|
4910
6037
|
id: c.id,
|
|
@@ -4913,7 +6040,7 @@ var researchLead = {
|
|
|
4913
6040
|
job_title: c.job_title,
|
|
4914
6041
|
email: c.email,
|
|
4915
6042
|
phone_number: c.phone_number,
|
|
4916
|
-
linkedin_page: c.linkedin_page,
|
|
6043
|
+
linkedin_page: normalizeLinkedinPage5(c.linkedin_page),
|
|
4917
6044
|
recommended: c.recommended,
|
|
4918
6045
|
enrichment_done: c.enrichment?.done ?? false
|
|
4919
6046
|
})),
|
|
@@ -4922,16 +6049,22 @@ var researchLead = {
|
|
|
4922
6049
|
first_name: c.first_name,
|
|
4923
6050
|
last_name: c.last_name,
|
|
4924
6051
|
job_title: c.job_title,
|
|
4925
|
-
email: c.email
|
|
6052
|
+
email: c.email,
|
|
6053
|
+
linkedin_page: normalizeLinkedinPage5(c.linkedin_page ?? null)
|
|
4926
6054
|
}))
|
|
4927
6055
|
},
|
|
4928
|
-
// 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.
|
|
4929
6059
|
engagement: {
|
|
4930
6060
|
liked: lead.liked,
|
|
4931
6061
|
disliked: lead.disliked,
|
|
4932
6062
|
new: lead.new ?? false,
|
|
4933
|
-
|
|
4934
|
-
|
|
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,
|
|
4935
6068
|
notes_count: lead.notes_count ?? 0,
|
|
4936
6069
|
epilogue_actions_count: lead.epilogue_actions_count ?? 0,
|
|
4937
6070
|
prospecting_actions_count: lead.prospecting_actions_count ?? 0,
|
|
@@ -8927,6 +10060,8 @@ var granularWriteTools = [
|
|
|
8927
10060
|
dismissClarification,
|
|
8928
10061
|
setEpilogueStatus,
|
|
8929
10062
|
removeEpilogue,
|
|
10063
|
+
setPushback,
|
|
10064
|
+
removePushback,
|
|
8930
10065
|
previewBulkEnrichment,
|
|
8931
10066
|
launchBulkEnrichment,
|
|
8932
10067
|
createCustomField
|
|
@@ -8941,6 +10076,7 @@ granularTools.forEach((t) => {
|
|
|
8941
10076
|
});
|
|
8942
10077
|
var compositeReadTools = [
|
|
8943
10078
|
pullLeads,
|
|
10079
|
+
pullFollowups,
|
|
8944
10080
|
researchLead,
|
|
8945
10081
|
recallOrderedTitles,
|
|
8946
10082
|
accountStatus,
|
|
@@ -9025,12 +10161,15 @@ export {
|
|
|
9025
10161
|
dismissClarification,
|
|
9026
10162
|
setEpilogueStatus,
|
|
9027
10163
|
removeEpilogue,
|
|
10164
|
+
setPushback,
|
|
10165
|
+
removePushback,
|
|
9028
10166
|
previewBulkEnrichment,
|
|
9029
10167
|
launchBulkEnrichment,
|
|
9030
10168
|
createCustomField,
|
|
9031
10169
|
researchCompany,
|
|
9032
10170
|
prepareOutreach,
|
|
9033
10171
|
pullLeads,
|
|
10172
|
+
pullFollowups,
|
|
9034
10173
|
researchLead,
|
|
9035
10174
|
recallOrderedTitles,
|
|
9036
10175
|
accountStatus,
|