@leadbay/mcp 0.5.0 → 0.6.1
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 +18 -0
- package/MIGRATION.md +96 -0
- package/README.md +219 -10
- package/dist/bin.js +444 -15
- package/dist/{chunk-MCTWP5F2.js → chunk-NLG7GUZ3.js} +1976 -72
- package/dist/{dist-GUZQWQOO.js → dist-JUTSXWBL.js} +1 -1
- package/package.json +2 -1
|
@@ -535,6 +535,13 @@ function parseRetryAfter(value) {
|
|
|
535
535
|
// ../core/dist/tools/login.js
|
|
536
536
|
var login = {
|
|
537
537
|
name: "leadbay_login",
|
|
538
|
+
annotations: {
|
|
539
|
+
title: "Mint a Leadbay bearer token",
|
|
540
|
+
readOnlyHint: false,
|
|
541
|
+
destructiveHint: true,
|
|
542
|
+
idempotentHint: false,
|
|
543
|
+
openWorldHint: true
|
|
544
|
+
},
|
|
538
545
|
description: "Log in to Leadbay with email and password. Auto-detects region (us|fr) \u2014 the user does not need to know which backend their account lives on. When to use: at the start of a session if no token is preconfigured (cfg.token / LEADBAY_TOKEN). When NOT to use: if a token is already preconfigured (you'll just overwrite it). The user needs a Leadbay account \u2014 they can register at https://wow.leadbay.ai/?register=true",
|
|
539
546
|
inputSchema: {
|
|
540
547
|
type: "object",
|
|
@@ -542,7 +549,8 @@ var login = {
|
|
|
542
549
|
email: { type: "string", description: "Leadbay account email address" },
|
|
543
550
|
password: { type: "string", description: "Leadbay account password" }
|
|
544
551
|
},
|
|
545
|
-
required: ["email", "password"]
|
|
552
|
+
required: ["email", "password"],
|
|
553
|
+
additionalProperties: false
|
|
546
554
|
},
|
|
547
555
|
execute: async (client, params, ctx) => {
|
|
548
556
|
const cleanPassword = params.password.replace(/\\(.)/g, "$1");
|
|
@@ -578,10 +586,37 @@ var login = {
|
|
|
578
586
|
// ../core/dist/tools/list-lenses.js
|
|
579
587
|
var listLenses = {
|
|
580
588
|
name: "leadbay_list_lenses",
|
|
589
|
+
annotations: {
|
|
590
|
+
title: "List active lenses",
|
|
591
|
+
readOnlyHint: true,
|
|
592
|
+
destructiveHint: false,
|
|
593
|
+
idempotentHint: true,
|
|
594
|
+
openWorldHint: true
|
|
595
|
+
},
|
|
581
596
|
description: "List all available Leadbay lenses (saved lead search configurations). Each lens defines a different target market or buyer segment. The lens with is_last_active=true is used by default for lead discovery. When to use: when the user wants to switch lens or asks 'what lenses do I have'. When NOT to use: in normal flow \u2014 composites auto-resolve the active lens via /me.last_requested_lens.",
|
|
582
597
|
inputSchema: {
|
|
583
598
|
type: "object",
|
|
584
|
-
properties: {}
|
|
599
|
+
properties: {},
|
|
600
|
+
additionalProperties: false
|
|
601
|
+
},
|
|
602
|
+
outputSchema: {
|
|
603
|
+
type: "object",
|
|
604
|
+
properties: {
|
|
605
|
+
lenses: {
|
|
606
|
+
type: "array",
|
|
607
|
+
description: "Available lenses. Each: {id, name, is_last_active, description}.",
|
|
608
|
+
items: {
|
|
609
|
+
type: "object",
|
|
610
|
+
properties: {
|
|
611
|
+
id: { type: "number" },
|
|
612
|
+
name: { type: "string" },
|
|
613
|
+
is_last_active: { type: "boolean" },
|
|
614
|
+
description: { type: ["string", "null"] }
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
},
|
|
619
|
+
required: ["lenses"]
|
|
585
620
|
},
|
|
586
621
|
execute: async (client) => {
|
|
587
622
|
const lenses = await client.request("GET", "/lenses");
|
|
@@ -599,6 +634,13 @@ var listLenses = {
|
|
|
599
634
|
// ../core/dist/tools/discover-leads.js
|
|
600
635
|
var discoverLeads = {
|
|
601
636
|
name: "leadbay_discover_leads",
|
|
637
|
+
annotations: {
|
|
638
|
+
title: "Discover leads in a lens",
|
|
639
|
+
readOnlyHint: true,
|
|
640
|
+
destructiveHint: false,
|
|
641
|
+
idempotentHint: true,
|
|
642
|
+
openWorldHint: true
|
|
643
|
+
},
|
|
602
644
|
description: "Get AI-recommended leads from Leadbay. Returns paginated lead summaries with scores, AI summaries, tags, and recommended contacts. When to use: low-level when you need raw paginated wishlist access without the qualification_summary attached by leadbay_pull_leads. When NOT to use: as the agent's default lead-discovery entry point \u2014 use leadbay_pull_leads, which adds a one-line qualification summary per lead.",
|
|
603
645
|
inputSchema: {
|
|
604
646
|
type: "object",
|
|
@@ -615,13 +657,18 @@ var discoverLeads = {
|
|
|
615
657
|
type: "number",
|
|
616
658
|
description: "Results per page, max 50 (default: 20)"
|
|
617
659
|
}
|
|
618
|
-
}
|
|
660
|
+
},
|
|
661
|
+
additionalProperties: false
|
|
619
662
|
},
|
|
620
663
|
execute: async (client, params) => {
|
|
621
664
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
622
665
|
const page = params.page ?? 0;
|
|
623
666
|
const count = Math.min(params.count ?? 20, 50);
|
|
624
667
|
const res = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${count}&page=${page}&contacts=true`);
|
|
668
|
+
const totalPages = res.pagination?.pages ?? 0;
|
|
669
|
+
const currentPage = res.pagination?.page ?? page;
|
|
670
|
+
const hasMore = currentPage < totalPages - 1;
|
|
671
|
+
const nextPage = hasMore ? currentPage + 1 : null;
|
|
625
672
|
return {
|
|
626
673
|
leads: res.items.map((lead) => ({
|
|
627
674
|
id: lead.id,
|
|
@@ -641,7 +688,9 @@ var discoverLeads = {
|
|
|
641
688
|
recommended_contact_title: lead.recommended_contact_title ?? null,
|
|
642
689
|
recommended_contact: lead.recommended_contact ?? null
|
|
643
690
|
})),
|
|
644
|
-
pagination: res.pagination
|
|
691
|
+
pagination: res.pagination,
|
|
692
|
+
has_more: hasMore,
|
|
693
|
+
next_page: nextPage
|
|
645
694
|
};
|
|
646
695
|
}
|
|
647
696
|
};
|
|
@@ -649,6 +698,13 @@ var discoverLeads = {
|
|
|
649
698
|
// ../core/dist/tools/get-lead-profile.js
|
|
650
699
|
var getLeadProfile = {
|
|
651
700
|
name: "leadbay_get_lead_profile",
|
|
701
|
+
annotations: {
|
|
702
|
+
title: "Read a lead profile",
|
|
703
|
+
readOnlyHint: true,
|
|
704
|
+
destructiveHint: false,
|
|
705
|
+
idempotentHint: true,
|
|
706
|
+
openWorldHint: true
|
|
707
|
+
},
|
|
652
708
|
description: "Get a full lead profile including company details, AI qualification scores, web insights, and contacts. When to use: low-level \u2014 for fine-grained access to the raw shape of the lead profile. When NOT to use: as the agent's default lead-detail tool \u2014 use leadbay_research_lead, which structures the data top-down (qualification first, then signals, then firmographics, then contacts, then engagement) and reshapes web_fetch.content into a stable array form.",
|
|
653
709
|
inputSchema: {
|
|
654
710
|
type: "object",
|
|
@@ -662,7 +718,30 @@ var getLeadProfile = {
|
|
|
662
718
|
description: "Lens ID (optional, auto-resolves to active lens)"
|
|
663
719
|
}
|
|
664
720
|
},
|
|
665
|
-
required: ["leadId"]
|
|
721
|
+
required: ["leadId"],
|
|
722
|
+
additionalProperties: false
|
|
723
|
+
},
|
|
724
|
+
outputSchema: {
|
|
725
|
+
type: "object",
|
|
726
|
+
properties: {
|
|
727
|
+
lead: {
|
|
728
|
+
type: "object",
|
|
729
|
+
description: "Lead basics: id, name, score, ai_agent_lead_score, location, description, short_description, size, website, logo, ai_summary, split_ai_summary, tags, phone_numbers, keywords, contacts_count, recommended_contact_title, recommended_contact, web_fetch_in_progress."
|
|
730
|
+
},
|
|
731
|
+
qualification: {
|
|
732
|
+
type: ["array", "null"],
|
|
733
|
+
description: "Per-question AI qualification answers ({question, score, response, computed_at, outdated_at}), or null if none.",
|
|
734
|
+
items: { type: "object" }
|
|
735
|
+
},
|
|
736
|
+
contacts: {
|
|
737
|
+
type: "array",
|
|
738
|
+
description: "Merged org+paid contacts. Each: {id, first_name, last_name, email, phone_number, linkedin_page, job_title, recommended, enrichment, source:'org'|'paid'}.",
|
|
739
|
+
items: { type: "object" }
|
|
740
|
+
},
|
|
741
|
+
web_insights: { description: "Latest /web_fetch content (string) or null." },
|
|
742
|
+
web_insights_fetched_at: { description: "ISO timestamp of /web_fetch (string) or null." }
|
|
743
|
+
},
|
|
744
|
+
required: ["lead", "contacts"]
|
|
666
745
|
},
|
|
667
746
|
execute: async (client, params) => {
|
|
668
747
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
@@ -751,6 +830,13 @@ var getLeadProfile = {
|
|
|
751
830
|
// ../core/dist/tools/get-contacts.js
|
|
752
831
|
var getContacts = {
|
|
753
832
|
name: "leadbay_get_contacts",
|
|
833
|
+
annotations: {
|
|
834
|
+
title: "Read enriched contacts",
|
|
835
|
+
readOnlyHint: true,
|
|
836
|
+
destructiveHint: false,
|
|
837
|
+
idempotentHint: true,
|
|
838
|
+
openWorldHint: true
|
|
839
|
+
},
|
|
754
840
|
description: "Get contacts for a lead, including enriched email and phone data. Returns both organization contacts and enrichable contacts with IDs. When to use: to check enrichment status (contact.enrichment.done) on individual leads after a bulk enrichment was launched, or to find the contact_id needed by leadbay_enrich_contacts. When NOT to use: as a substitute for leadbay_research_lead, which already includes enriched contacts in its return.",
|
|
755
841
|
inputSchema: {
|
|
756
842
|
type: "object",
|
|
@@ -760,7 +846,8 @@ var getContacts = {
|
|
|
760
846
|
description: "Lead UUID (required)"
|
|
761
847
|
}
|
|
762
848
|
},
|
|
763
|
-
required: ["leadId"]
|
|
849
|
+
required: ["leadId"],
|
|
850
|
+
additionalProperties: false
|
|
764
851
|
},
|
|
765
852
|
execute: async (client, params) => {
|
|
766
853
|
const [orgResult, paidResult] = await Promise.allSettled([
|
|
@@ -803,8 +890,26 @@ var getContacts = {
|
|
|
803
890
|
// ../core/dist/tools/get-quota.js
|
|
804
891
|
var getQuota = {
|
|
805
892
|
name: "leadbay_get_quota",
|
|
893
|
+
annotations: {
|
|
894
|
+
title: "Read quota status",
|
|
895
|
+
readOnlyHint: true,
|
|
896
|
+
destructiveHint: false,
|
|
897
|
+
idempotentHint: true,
|
|
898
|
+
openWorldHint: true
|
|
899
|
+
},
|
|
806
900
|
description: "Read remaining quota / spend across daily, weekly, monthly windows for the org's resources (llm_completion, ai_rescore, web_fetch). Each entry shows current_units vs max_units and resets_at. When to use: after a 429 error, to explain to the user which window was hit and when it resets. When NOT to use: as a pre-flight gate before bulk operations \u2014 operations themselves return 429 with hints; this tool is for diagnostics, not gating.",
|
|
807
|
-
inputSchema: { type: "object", properties: {} },
|
|
901
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
902
|
+
outputSchema: {
|
|
903
|
+
type: "object",
|
|
904
|
+
properties: {
|
|
905
|
+
plan: { type: ["string", "null"], description: "Org plan tier (e.g., FREE, PRO)." },
|
|
906
|
+
windows: {
|
|
907
|
+
type: "array",
|
|
908
|
+
description: "Per-resource per-window limits. Each: {resource, window, current_units, max_units, resets_at}.",
|
|
909
|
+
items: { type: "object" }
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
},
|
|
808
913
|
execute: async (client) => {
|
|
809
914
|
const orgId = await client.resolveOrgId();
|
|
810
915
|
return await client.request("GET", `/organizations/${orgId}/quota_status`);
|
|
@@ -814,10 +919,41 @@ var getQuota = {
|
|
|
814
919
|
// ../core/dist/tools/get-taste-profile.js
|
|
815
920
|
var getTasteProfile = {
|
|
816
921
|
name: "leadbay_get_taste_profile",
|
|
922
|
+
annotations: {
|
|
923
|
+
title: "Read the org's taste profile",
|
|
924
|
+
readOnlyHint: true,
|
|
925
|
+
destructiveHint: false,
|
|
926
|
+
idempotentHint: true,
|
|
927
|
+
openWorldHint: true
|
|
928
|
+
},
|
|
817
929
|
description: "Get the user's Ideal Buyer Profile, purchase intent tags, and qualification questions. When to use: at the very start of a session to understand what kind of leads the user is looking for. Data is cached. When NOT to use: per-lead \u2014 leadbay_research_lead already includes the per-lead qualification answers (which are scored against these org-level questions).",
|
|
818
930
|
inputSchema: {
|
|
819
931
|
type: "object",
|
|
820
|
-
properties: {}
|
|
932
|
+
properties: {},
|
|
933
|
+
additionalProperties: false
|
|
934
|
+
},
|
|
935
|
+
outputSchema: {
|
|
936
|
+
type: "object",
|
|
937
|
+
properties: {
|
|
938
|
+
ideal_buyer_profile: {
|
|
939
|
+
description: "Ideal Buyer Profile {summary, key_characteristics, anti_patterns} or null when none."
|
|
940
|
+
},
|
|
941
|
+
purchase_intent_tags: {
|
|
942
|
+
type: "array",
|
|
943
|
+
description: "Tags describing buying signals. Each: {display_name, description, score, reasoning}.",
|
|
944
|
+
items: { type: "object" }
|
|
945
|
+
},
|
|
946
|
+
qualification_questions: {
|
|
947
|
+
type: "array",
|
|
948
|
+
description: "Questions Leadbay asks for each lead. Each: {question}.",
|
|
949
|
+
items: { type: "object" }
|
|
950
|
+
},
|
|
951
|
+
hint: {
|
|
952
|
+
type: "string",
|
|
953
|
+
description: "Operator note when the taste profile is empty (no_profile state)."
|
|
954
|
+
}
|
|
955
|
+
},
|
|
956
|
+
required: ["purchase_intent_tags", "qualification_questions"]
|
|
821
957
|
},
|
|
822
958
|
execute: async (client) => {
|
|
823
959
|
const profile = await client.resolveTasteProfile();
|
|
@@ -847,6 +983,13 @@ var getTasteProfile = {
|
|
|
847
983
|
// ../core/dist/tools/qualify-lead.js
|
|
848
984
|
var qualifyLead = {
|
|
849
985
|
name: "leadbay_qualify_lead",
|
|
986
|
+
annotations: {
|
|
987
|
+
title: "Qualify a single lead",
|
|
988
|
+
readOnlyHint: false,
|
|
989
|
+
destructiveHint: true,
|
|
990
|
+
idempotentHint: true,
|
|
991
|
+
openWorldHint: true
|
|
992
|
+
},
|
|
850
993
|
description: "Trigger AI qualification for a single lead (web fetch + AI rescore). The operation is asynchronous \u2014 results take ~60s. When to use: low-level. When NOT to use: as the agent's bulk-qualify path \u2014 use leadbay_bulk_qualify_leads, which paginates past already-qualified leads, fan-outs, polls, and bails out cleanly on 429.",
|
|
851
994
|
optional: true,
|
|
852
995
|
inputSchema: {
|
|
@@ -861,7 +1004,8 @@ var qualifyLead = {
|
|
|
861
1004
|
description: "Force re-fetch even if recent data exists (default: false)"
|
|
862
1005
|
}
|
|
863
1006
|
},
|
|
864
|
-
required: ["leadId"]
|
|
1007
|
+
required: ["leadId"],
|
|
1008
|
+
additionalProperties: false
|
|
865
1009
|
},
|
|
866
1010
|
execute: async (client, params) => {
|
|
867
1011
|
const force = params.forceFetch ?? false;
|
|
@@ -876,6 +1020,13 @@ var qualifyLead = {
|
|
|
876
1020
|
// ../core/dist/tools/enrich-contacts.js
|
|
877
1021
|
var enrichContacts = {
|
|
878
1022
|
name: "leadbay_enrich_contacts",
|
|
1023
|
+
annotations: {
|
|
1024
|
+
title: "Enrich contacts for a lead",
|
|
1025
|
+
readOnlyHint: false,
|
|
1026
|
+
destructiveHint: true,
|
|
1027
|
+
idempotentHint: true,
|
|
1028
|
+
openWorldHint: true
|
|
1029
|
+
},
|
|
879
1030
|
description: "Order email and/or phone enrichment for a specific contact. When to use: when you have a specific contact_id (from leadbay_get_contacts) and want to enrich just that one. When NOT to use: for bulk enrichment by job title across many leads \u2014 use leadbay_enrich_titles, which handles the selection lifecycle and returns a clean preview/launch flow.",
|
|
880
1031
|
optional: true,
|
|
881
1032
|
inputSchema: {
|
|
@@ -898,7 +1049,8 @@ var enrichContacts = {
|
|
|
898
1049
|
description: "Enrich phone number (default: true)"
|
|
899
1050
|
}
|
|
900
1051
|
},
|
|
901
|
-
required: ["leadId", "contactId"]
|
|
1052
|
+
required: ["leadId", "contactId"],
|
|
1053
|
+
additionalProperties: false
|
|
902
1054
|
},
|
|
903
1055
|
execute: async (client, params) => {
|
|
904
1056
|
const email = params.email ?? true;
|
|
@@ -942,6 +1094,13 @@ var enrichContacts = {
|
|
|
942
1094
|
// ../core/dist/tools/add-note.js
|
|
943
1095
|
var addNote = {
|
|
944
1096
|
name: "leadbay_add_note",
|
|
1097
|
+
annotations: {
|
|
1098
|
+
title: "Add a note on a lead",
|
|
1099
|
+
readOnlyHint: false,
|
|
1100
|
+
destructiveHint: true,
|
|
1101
|
+
idempotentHint: false,
|
|
1102
|
+
openWorldHint: true
|
|
1103
|
+
},
|
|
945
1104
|
description: "Add a note to a lead. Notes are visible to the whole organization in Leadbay. When to use: low-level \u2014 for free-form notes not tied to outreach actions. When NOT to use: to log an outreach action \u2014 use leadbay_report_outreach, which requires verification (gmail/calendar/user_confirmed) to prevent hallucinated outreach poisoning the SDR pipeline.",
|
|
946
1105
|
optional: true,
|
|
947
1106
|
inputSchema: {
|
|
@@ -956,7 +1115,17 @@ var addNote = {
|
|
|
956
1115
|
description: "Note text (max 4095 characters)"
|
|
957
1116
|
}
|
|
958
1117
|
},
|
|
959
|
-
required: ["leadId", "note"]
|
|
1118
|
+
required: ["leadId", "note"],
|
|
1119
|
+
additionalProperties: false
|
|
1120
|
+
},
|
|
1121
|
+
outputSchema: {
|
|
1122
|
+
type: "object",
|
|
1123
|
+
properties: {
|
|
1124
|
+
id: { type: "string", description: "Note id assigned by the backend." },
|
|
1125
|
+
note: { type: "string", description: "Echoed note text (truncated to 4095 chars)." },
|
|
1126
|
+
created_at: { type: "string", description: "ISO timestamp of creation." }
|
|
1127
|
+
},
|
|
1128
|
+
required: ["id", "note", "created_at"]
|
|
960
1129
|
},
|
|
961
1130
|
execute: async (client, params) => {
|
|
962
1131
|
if (!params.note || params.note.trim().length === 0) {
|
|
@@ -975,6 +1144,13 @@ var addNote = {
|
|
|
975
1144
|
// ../core/dist/tools/get-lead-activities.js
|
|
976
1145
|
var getLeadActivities = {
|
|
977
1146
|
name: "leadbay_get_lead_activities",
|
|
1147
|
+
annotations: {
|
|
1148
|
+
title: "Read a lead's activity feed",
|
|
1149
|
+
readOnlyHint: true,
|
|
1150
|
+
destructiveHint: false,
|
|
1151
|
+
idempotentHint: true,
|
|
1152
|
+
openWorldHint: true
|
|
1153
|
+
},
|
|
978
1154
|
description: "Get prospecting activity history for a lead (emails sent, calls made, status changes, notes). When to use: to avoid redundant outreach and understand where this lead is in the sales process. When NOT to use: when leadbay_research_lead has already been called \u2014 it includes recent prospecting actions in its engagement block.",
|
|
979
1155
|
inputSchema: {
|
|
980
1156
|
type: "object",
|
|
@@ -988,7 +1164,29 @@ var getLeadActivities = {
|
|
|
988
1164
|
description: "Number of activities to return, max 100 (default: 50)"
|
|
989
1165
|
}
|
|
990
1166
|
},
|
|
991
|
-
required: ["leadId"]
|
|
1167
|
+
required: ["leadId"],
|
|
1168
|
+
additionalProperties: false
|
|
1169
|
+
},
|
|
1170
|
+
outputSchema: {
|
|
1171
|
+
type: "object",
|
|
1172
|
+
properties: {
|
|
1173
|
+
activities: {
|
|
1174
|
+
type: "array",
|
|
1175
|
+
description: "Activity entries. Each: {type, date}. Older activities trimmed by `count`.",
|
|
1176
|
+
items: {
|
|
1177
|
+
type: "object",
|
|
1178
|
+
properties: {
|
|
1179
|
+
type: { type: "string" },
|
|
1180
|
+
date: { type: "string" }
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
},
|
|
1184
|
+
total: {
|
|
1185
|
+
type: "number",
|
|
1186
|
+
description: "Total activity count for this lead (across all pages)."
|
|
1187
|
+
}
|
|
1188
|
+
},
|
|
1189
|
+
required: ["activities", "total"]
|
|
992
1190
|
},
|
|
993
1191
|
execute: async (client, params) => {
|
|
994
1192
|
const count = Math.min(params.count ?? 50, 100);
|
|
@@ -1006,13 +1204,21 @@ var getLeadActivities = {
|
|
|
1006
1204
|
// ../core/dist/tools/get-lens-filter.js
|
|
1007
1205
|
var getLensFilter = {
|
|
1008
1206
|
name: "leadbay_get_lens_filter",
|
|
1207
|
+
annotations: {
|
|
1208
|
+
title: "Read lens filter",
|
|
1209
|
+
readOnlyHint: true,
|
|
1210
|
+
destructiveHint: false,
|
|
1211
|
+
idempotentHint: true,
|
|
1212
|
+
openWorldHint: true
|
|
1213
|
+
},
|
|
1009
1214
|
description: "Read the firmographic filter (sectors, sizes, locations) currently applied to a lens. When to use: before adjusting an audience \u2014 see what's already restricted so changes are diffs, not full replacements. When NOT to use: to actually apply changes \u2014 use the leadbay_adjust_audience composite, which handles permissions transparently.",
|
|
1010
1215
|
inputSchema: {
|
|
1011
1216
|
type: "object",
|
|
1012
1217
|
properties: {
|
|
1013
1218
|
lensId: { type: "number", description: "Lens id (required)" }
|
|
1014
1219
|
},
|
|
1015
|
-
required: ["lensId"]
|
|
1220
|
+
required: ["lensId"],
|
|
1221
|
+
additionalProperties: false
|
|
1016
1222
|
},
|
|
1017
1223
|
execute: async (client, params) => {
|
|
1018
1224
|
return await client.request("GET", `/lenses/${params.lensId}/filter`);
|
|
@@ -1022,11 +1228,19 @@ var getLensFilter = {
|
|
|
1022
1228
|
// ../core/dist/tools/get-lens-scoring.js
|
|
1023
1229
|
var getLensScoring = {
|
|
1024
1230
|
name: "leadbay_get_lens_scoring",
|
|
1231
|
+
annotations: {
|
|
1232
|
+
title: "Read lens scoring",
|
|
1233
|
+
readOnlyHint: true,
|
|
1234
|
+
destructiveHint: false,
|
|
1235
|
+
idempotentHint: true,
|
|
1236
|
+
openWorldHint: true
|
|
1237
|
+
},
|
|
1025
1238
|
description: "Read the AI-scoring criteria configured on a lens (what makes a lead score 100 vs 30). When to use: when explaining why a lead got the score it did. When NOT to use: to mutate scoring \u2014 that's an admin/setup operation, not part of the agent loop.",
|
|
1026
1239
|
inputSchema: {
|
|
1027
1240
|
type: "object",
|
|
1028
1241
|
properties: { lensId: { type: "number", description: "Lens id (required)" } },
|
|
1029
|
-
required: ["lensId"]
|
|
1242
|
+
required: ["lensId"],
|
|
1243
|
+
additionalProperties: false
|
|
1030
1244
|
},
|
|
1031
1245
|
execute: async (client, params) => {
|
|
1032
1246
|
return await client.request("GET", `/lenses/${params.lensId}/scoring`);
|
|
@@ -1036,6 +1250,13 @@ var getLensScoring = {
|
|
|
1036
1250
|
// ../core/dist/tools/list-sectors.js
|
|
1037
1251
|
var listSectors = {
|
|
1038
1252
|
name: "leadbay_list_sectors",
|
|
1253
|
+
annotations: {
|
|
1254
|
+
title: "List sector taxonomy",
|
|
1255
|
+
readOnlyHint: true,
|
|
1256
|
+
destructiveHint: false,
|
|
1257
|
+
idempotentHint: true,
|
|
1258
|
+
openWorldHint: true
|
|
1259
|
+
},
|
|
1039
1260
|
description: "List the sector taxonomy (id + display name in the requested language). When to use: to resolve a free-text sector name (e.g. 'Healthcare') into the sector ids that leadbay_adjust_audience needs. Default: lang follows the caller's language; includeInvisible=false returns ~1,091 visible sectors. When NOT to use: when you already have sector ids \u2014 pass them directly.",
|
|
1040
1261
|
inputSchema: {
|
|
1041
1262
|
type: "object",
|
|
@@ -1045,7 +1266,8 @@ var listSectors = {
|
|
|
1045
1266
|
type: "boolean",
|
|
1046
1267
|
description: "Include sectors hidden from the UI (default false; ~91k items if true)"
|
|
1047
1268
|
}
|
|
1048
|
-
}
|
|
1269
|
+
},
|
|
1270
|
+
additionalProperties: false
|
|
1049
1271
|
},
|
|
1050
1272
|
execute: async (client, params) => {
|
|
1051
1273
|
let lang = params.lang;
|
|
@@ -1066,8 +1288,36 @@ var listSectors = {
|
|
|
1066
1288
|
// ../core/dist/tools/get-user-prompt.js
|
|
1067
1289
|
var getUserPrompt = {
|
|
1068
1290
|
name: "leadbay_get_user_prompt",
|
|
1291
|
+
annotations: {
|
|
1292
|
+
title: "Read user prompt",
|
|
1293
|
+
readOnlyHint: true,
|
|
1294
|
+
destructiveHint: false,
|
|
1295
|
+
idempotentHint: true,
|
|
1296
|
+
openWorldHint: true
|
|
1297
|
+
},
|
|
1069
1298
|
description: "Read the org's intelligence-refinement prompt (free-text instruction that steers lead recommendations beyond firmographics). Returns null if none is set (the backend returns 204 in that case). When to use: to know what's currently steering the agent's recommendations before suggesting a refine. When NOT to use: to set/change the prompt \u2014 use leadbay_refine_prompt.",
|
|
1070
|
-
inputSchema: { type: "object", properties: {} },
|
|
1299
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1300
|
+
outputSchema: {
|
|
1301
|
+
type: "object",
|
|
1302
|
+
properties: {
|
|
1303
|
+
prompt: {
|
|
1304
|
+
description: "Free-text instruction (string) or null when unset."
|
|
1305
|
+
},
|
|
1306
|
+
set: {
|
|
1307
|
+
type: "boolean",
|
|
1308
|
+
description: "True when a prompt is set; false when nothing has been configured."
|
|
1309
|
+
},
|
|
1310
|
+
// When the backend returns a populated UserPromptPayload, additional
|
|
1311
|
+
// fields may be spread into the response. The asserter is permissive
|
|
1312
|
+
// — declare common fields here so the conformance check accepts the
|
|
1313
|
+
// backend's full shape.
|
|
1314
|
+
user_prompt: {
|
|
1315
|
+
description: "Backend-form copy of the prompt text (when set)."
|
|
1316
|
+
},
|
|
1317
|
+
created_at: { type: ["string", "null"] },
|
|
1318
|
+
updated_at: { type: ["string", "null"] }
|
|
1319
|
+
}
|
|
1320
|
+
},
|
|
1071
1321
|
execute: async (client) => {
|
|
1072
1322
|
const orgId = await client.resolveOrgId();
|
|
1073
1323
|
const prompt = await client.request("GET", `/organizations/${orgId}/user_prompt`);
|
|
@@ -1078,8 +1328,38 @@ var getUserPrompt = {
|
|
|
1078
1328
|
// ../core/dist/tools/get-clarification.js
|
|
1079
1329
|
var getClarification = {
|
|
1080
1330
|
name: "leadbay_get_clarification",
|
|
1331
|
+
annotations: {
|
|
1332
|
+
title: "Read pending clarification",
|
|
1333
|
+
readOnlyHint: true,
|
|
1334
|
+
destructiveHint: false,
|
|
1335
|
+
idempotentHint: true,
|
|
1336
|
+
openWorldHint: true
|
|
1337
|
+
},
|
|
1081
1338
|
description: "Check whether Leadbay has a pending clarification question \u2014 a question raised when refining the intelligence prompt produced contradictory or ambiguous criteria. Returns null when nothing is pending (the backend returns 204). When to use: after leadbay_refine_prompt, to see if Leadbay needs the user to disambiguate. When NOT to use: to answer the question \u2014 use leadbay_answer_clarification.",
|
|
1082
|
-
inputSchema: { type: "object", properties: {} },
|
|
1339
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1340
|
+
outputSchema: {
|
|
1341
|
+
type: "object",
|
|
1342
|
+
properties: {
|
|
1343
|
+
pending: {
|
|
1344
|
+
type: "boolean",
|
|
1345
|
+
description: "False when no clarification is pending (and `clarification` is null)."
|
|
1346
|
+
},
|
|
1347
|
+
clarification: {
|
|
1348
|
+
description: "ClarificationPayload (object) when pending, otherwise null."
|
|
1349
|
+
},
|
|
1350
|
+
// When the backend returns a populated ClarificationPayload, the keys
|
|
1351
|
+
// are spread directly into the response. The asserter is permissive —
|
|
1352
|
+
// declare the union of keys here so the conformance check doesn't
|
|
1353
|
+
// flag drift.
|
|
1354
|
+
id: { type: "string", description: "Clarification id (when pending)." },
|
|
1355
|
+
question: { type: "string", description: "Question text (when pending)." },
|
|
1356
|
+
options: {
|
|
1357
|
+
type: "array",
|
|
1358
|
+
description: "Picker options (when pending). Each: {id, label}.",
|
|
1359
|
+
items: { type: "object" }
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
},
|
|
1083
1363
|
execute: async (client) => {
|
|
1084
1364
|
const orgId = await client.resolveOrgId();
|
|
1085
1365
|
const c = await client.request("GET", `/organizations/${orgId}/clarifications`);
|
|
@@ -1090,11 +1370,19 @@ var getClarification = {
|
|
|
1090
1370
|
// ../core/dist/tools/get-lead-notes.js
|
|
1091
1371
|
var getLeadNotes = {
|
|
1092
1372
|
name: "leadbay_get_lead_notes",
|
|
1373
|
+
annotations: {
|
|
1374
|
+
title: "Read lead notes",
|
|
1375
|
+
readOnlyHint: true,
|
|
1376
|
+
destructiveHint: false,
|
|
1377
|
+
idempotentHint: true,
|
|
1378
|
+
openWorldHint: true
|
|
1379
|
+
},
|
|
1093
1380
|
description: "Read existing notes on a lead \u2014 context the human team or prior agent runs have already captured. When to use: before adding a note via leadbay_report_outreach, to avoid duplicating or overwriting context the SDR already wrote. When NOT to use: when the lead summary's notes_count is 0 \u2014 there's nothing to fetch.",
|
|
1094
1381
|
inputSchema: {
|
|
1095
1382
|
type: "object",
|
|
1096
1383
|
properties: { leadId: { type: "string", description: "Lead UUID (required)" } },
|
|
1097
|
-
required: ["leadId"]
|
|
1384
|
+
required: ["leadId"],
|
|
1385
|
+
additionalProperties: false
|
|
1098
1386
|
},
|
|
1099
1387
|
execute: async (client, params) => {
|
|
1100
1388
|
return await client.request("GET", `/leads/${params.leadId}/notes`);
|
|
@@ -1104,6 +1392,13 @@ var getLeadNotes = {
|
|
|
1104
1392
|
// ../core/dist/tools/get-epilogue-responses.js
|
|
1105
1393
|
var getEpilogueResponses = {
|
|
1106
1394
|
name: "leadbay_get_epilogue_responses",
|
|
1395
|
+
annotations: {
|
|
1396
|
+
title: "Read epilogue responses",
|
|
1397
|
+
readOnlyHint: true,
|
|
1398
|
+
destructiveHint: false,
|
|
1399
|
+
idempotentHint: true,
|
|
1400
|
+
openWorldHint: true
|
|
1401
|
+
},
|
|
1107
1402
|
description: "Read the lead's epilogue history \u2014 what status (still chasing, meeting booked, etc.) was set when, and by whom. When to use: to see the lead's outreach progression before deciding the next step. When NOT to use: when the lead summary's epilogue_actions_count is 0.",
|
|
1108
1403
|
inputSchema: {
|
|
1109
1404
|
type: "object",
|
|
@@ -1112,7 +1407,8 @@ var getEpilogueResponses = {
|
|
|
1112
1407
|
count: { type: "number", description: "Items per page (1-200, default 20)" },
|
|
1113
1408
|
page: { type: "number", description: "Page number, 0-indexed (default 0)" }
|
|
1114
1409
|
},
|
|
1115
|
-
required: ["leadId"]
|
|
1410
|
+
required: ["leadId"],
|
|
1411
|
+
additionalProperties: false
|
|
1116
1412
|
},
|
|
1117
1413
|
execute: async (client, params) => {
|
|
1118
1414
|
const count = params.count ?? 20;
|
|
@@ -1124,6 +1420,13 @@ var getEpilogueResponses = {
|
|
|
1124
1420
|
// ../core/dist/tools/get-prospecting-actions.js
|
|
1125
1421
|
var getProspectingActions = {
|
|
1126
1422
|
name: "leadbay_get_prospecting_actions",
|
|
1423
|
+
annotations: {
|
|
1424
|
+
title: "Read prospecting actions",
|
|
1425
|
+
readOnlyHint: true,
|
|
1426
|
+
destructiveHint: false,
|
|
1427
|
+
idempotentHint: true,
|
|
1428
|
+
openWorldHint: true
|
|
1429
|
+
},
|
|
1127
1430
|
description: "Read the CRM-style activity log for a lead (calls, emails, meetings \u2014 actions performed by humans or prior agent runs). When to use: before contacting the lead, to avoid duplicating outreach the team already did. When NOT to use: when the lead summary's prospecting_actions_count is 0.",
|
|
1128
1431
|
inputSchema: {
|
|
1129
1432
|
type: "object",
|
|
@@ -1132,7 +1435,8 @@ var getProspectingActions = {
|
|
|
1132
1435
|
count: { type: "number", description: "Items per page (1-200, default 20)" },
|
|
1133
1436
|
page: { type: "number", description: "Page number, 0-indexed (default 0)" }
|
|
1134
1437
|
},
|
|
1135
|
-
required: ["leadId"]
|
|
1438
|
+
required: ["leadId"],
|
|
1439
|
+
additionalProperties: false
|
|
1136
1440
|
},
|
|
1137
1441
|
execute: async (client, params) => {
|
|
1138
1442
|
const count = params.count ?? 20;
|
|
@@ -1144,11 +1448,40 @@ var getProspectingActions = {
|
|
|
1144
1448
|
// ../core/dist/tools/get-web-fetch.js
|
|
1145
1449
|
var getWebFetch = {
|
|
1146
1450
|
name: "leadbay_get_web_fetch",
|
|
1451
|
+
annotations: {
|
|
1452
|
+
title: "Read web-fetch result",
|
|
1453
|
+
readOnlyHint: true,
|
|
1454
|
+
destructiveHint: false,
|
|
1455
|
+
idempotentHint: true,
|
|
1456
|
+
openWorldHint: true
|
|
1457
|
+
},
|
|
1147
1458
|
description: "Read the AI-generated web-research summary for a lead \u2014 company profile, business signals, prospecting clues, each with sources and 'hot' flags marking high-signal recent items. The content is dictioned by emoji-prefixed section labels in the raw API. When to use: when the agent already qualified this lead and wants the underlying research to reason from. When NOT to use: as the first read on a lead \u2014 the leadbay_research_lead composite bundles this with qualification answers and reshapes the dict into a stable array form.",
|
|
1148
1459
|
inputSchema: {
|
|
1149
1460
|
type: "object",
|
|
1150
1461
|
properties: { leadId: { type: "string", description: "Lead UUID (required)" } },
|
|
1151
|
-
required: ["leadId"]
|
|
1462
|
+
required: ["leadId"],
|
|
1463
|
+
additionalProperties: false
|
|
1464
|
+
},
|
|
1465
|
+
outputSchema: {
|
|
1466
|
+
type: "object",
|
|
1467
|
+
description: "Raw LeadWebFetchPayload as returned by /leads/{id}/web_fetch. Permissive shape \u2014 backend dict structure is documented in detail by leadbay_research_lead which reshapes it.",
|
|
1468
|
+
properties: {
|
|
1469
|
+
content: {
|
|
1470
|
+
description: "Backend dict content (object or string), or null when no fetch yet."
|
|
1471
|
+
},
|
|
1472
|
+
fetch_at: {
|
|
1473
|
+
description: "ISO timestamp of the most recent fetch (string or null)."
|
|
1474
|
+
},
|
|
1475
|
+
status: {
|
|
1476
|
+
type: "string",
|
|
1477
|
+
description: "'pending' | 'complete' | 'failed' (when present)."
|
|
1478
|
+
},
|
|
1479
|
+
signals: {
|
|
1480
|
+
type: "array",
|
|
1481
|
+
description: "Optional reshaped signals when the backend returns them.",
|
|
1482
|
+
items: { type: "object" }
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1152
1485
|
},
|
|
1153
1486
|
execute: async (client, params) => {
|
|
1154
1487
|
return await client.request("GET", `/leads/${params.leadId}/web_fetch`);
|
|
@@ -1158,8 +1491,15 @@ var getWebFetch = {
|
|
|
1158
1491
|
// ../core/dist/tools/get-selection-ids.js
|
|
1159
1492
|
var getSelectionIds = {
|
|
1160
1493
|
name: "leadbay_get_selection_ids",
|
|
1494
|
+
annotations: {
|
|
1495
|
+
title: "Read selection ids",
|
|
1496
|
+
readOnlyHint: true,
|
|
1497
|
+
destructiveHint: false,
|
|
1498
|
+
idempotentHint: true,
|
|
1499
|
+
openWorldHint: true
|
|
1500
|
+
},
|
|
1161
1501
|
description: "List the lead ids currently in the user's selection (the transient set that bulk operations like enrichment act on). When to use: to verify the selection state before/after bulk ops if a composite call has misbehaved. When NOT to use: in the normal flow \u2014 leadbay_enrich_titles manages selection lifecycle automatically (select \u2192 action \u2192 clear).",
|
|
1162
|
-
inputSchema: { type: "object", properties: {} },
|
|
1502
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1163
1503
|
execute: async (client) => {
|
|
1164
1504
|
return await client.request("GET", "/leads/selection/ids");
|
|
1165
1505
|
}
|
|
@@ -1168,8 +1508,15 @@ var getSelectionIds = {
|
|
|
1168
1508
|
// ../core/dist/tools/get-enrichment-job-titles.js
|
|
1169
1509
|
var getEnrichmentJobTitles = {
|
|
1170
1510
|
name: "leadbay_get_enrichment_job_titles",
|
|
1511
|
+
annotations: {
|
|
1512
|
+
title: "Read enrichment job titles",
|
|
1513
|
+
readOnlyHint: true,
|
|
1514
|
+
destructiveHint: false,
|
|
1515
|
+
idempotentHint: true,
|
|
1516
|
+
openWorldHint: true
|
|
1517
|
+
},
|
|
1171
1518
|
description: "List the actual job titles present across the leads currently in the user's selection \u2014 the candidate set the user can ask to enrich. When to use: after leadbay_select_leads, to know which titles are even available before launching a bulk enrichment. When NOT to use: standalone \u2014 the selection must already be populated, otherwise the result is an empty array. leadbay_enrich_titles wraps this whole flow when you don't need to inspect the title list manually.",
|
|
1172
|
-
inputSchema: { type: "object", properties: {} },
|
|
1519
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1173
1520
|
execute: async (client) => {
|
|
1174
1521
|
return await client.request("GET", "/leads/selection/enrichment/job_titles");
|
|
1175
1522
|
}
|
|
@@ -2071,6 +2418,16 @@ function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
|
2071
2418
|
}
|
|
2072
2419
|
var importLeads = {
|
|
2073
2420
|
name: "leadbay_import_leads",
|
|
2421
|
+
annotations: {
|
|
2422
|
+
title: "Import leads from list/file",
|
|
2423
|
+
readOnlyHint: false,
|
|
2424
|
+
destructiveHint: true,
|
|
2425
|
+
// Backend dedupes by domain/registry id; same input set ⇒ same lead set
|
|
2426
|
+
// (no duplicate leads are created). bulk-store also keys on the
|
|
2427
|
+
// input-hash → returns the same importId on retry.
|
|
2428
|
+
idempotentHint: true,
|
|
2429
|
+
openWorldHint: true
|
|
2430
|
+
},
|
|
2074
2431
|
description: "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.\n\nTWO MODES:\n A) Domain-list shortcut \u2014 pass `domains: [{domain, name?}]`. The tool builds a 2-column CSV (LEAD_NAME, LEAD_WEBSITE) and imports with the default mapping. Output: { leads: [{domain, leadId, name}], not_imported: [{domain, reason}], importIds, _meta }.\n B) Custom records + mapping \u2014 pass `records: [{Col1, Col2, ...}]` plus `mappings.fields: {Col1: 'LEAD_NAME', Col2: 'LEAD_WEBSITE', ...}`. The tool synthesizes a CSV from the union of record keys (deterministic order) and POSTs the caller-supplied mapping to the wizard. mappings.fields must include LEAD_NAME or LEAD_WEBSITE (the resolver needs at least one). Output: { leads: [{rowId, domain?, leadId, name}], not_imported: [{rowId, domain?, reason}], importIds, _meta }. `rowId` round-trips your input order.\n\nPass exactly one of `domains` / `records`. Reserved column MCP_ROW_ID (any case) cannot appear in records or mappings \u2014 the tool injects it for stable reconciliation.\n\n\u26A0\uFE0F MUTATES USER STATE. Each call:\n - creates a row in the user's CRM-imports list (visible in the web UI)\n - touches onboarding state (startFileless, onboarding step \u2192 PROCESSING)\nSuitable for occasional automation. NOT suitable for high-cadence (>5 calls/day) \u2014 wait for the backend programmatic endpoint (issue: leadbay/backend prolonged-import-with-crawl).\n\n\u2139\uFE0F Monitor-tab membership: imported leads are NOT auto-promoted to the user's Monitor view. Lens-scoring decides \u2014 only above-threshold leads get `in_monitor: true` server-side.\n\nWhen to use: you have a list of company domains from another system (CRM, analytics, email correspondents) and need stable Leadbay leadIds; or you have CRM-shaped rows with custom columns (sector, location, status, etc.) and want to drive the wizard with explicit field mappings.\nWhen NOT to use: for prospect discovery (use leadbay_pull_leads); for one specific company's profile (use leadbay_research_company); when you can't tolerate the side effects above.\n\nCustom fields: pass org-defined custom field mappings as 'CUSTOM.<id>' (raw wire format) in `mappings.fields`, OR use the ergonomic `mappings.custom_fields` shorthand: `{ColName: 8}` (numeric id) or `{ColName: 'priority_test'}` (field name). Discover available custom fields via leadbay_list_mappable_fields.\n\nRequires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role on the Leadbay account; active billing.",
|
|
2075
2432
|
write: true,
|
|
2076
2433
|
version: "0.3.0",
|
|
@@ -2092,7 +2449,10 @@ var importLeads = {
|
|
|
2092
2449
|
description: "Optional display name override; defaults to the domain."
|
|
2093
2450
|
}
|
|
2094
2451
|
},
|
|
2095
|
-
required: ["domain"]
|
|
2452
|
+
required: ["domain"],
|
|
2453
|
+
// Domain entries are a closed shape — agents passing extra keys
|
|
2454
|
+
// (e.g., `leadId: "..."`) would silently no-op. Reject explicitly.
|
|
2455
|
+
additionalProperties: false
|
|
2096
2456
|
}
|
|
2097
2457
|
},
|
|
2098
2458
|
records: {
|
|
@@ -2135,9 +2495,41 @@ var importLeads = {
|
|
|
2135
2495
|
type: "number",
|
|
2136
2496
|
description: `Overall cap across all phases (default ${DEFAULT_TOTAL_BUDGET_MS}ms).`
|
|
2137
2497
|
}
|
|
2138
|
-
}
|
|
2498
|
+
},
|
|
2139
2499
|
// Neither field is "required" at the schema level; xor + presence is
|
|
2140
2500
|
// enforced in execute() so we can produce specific error codes.
|
|
2501
|
+
additionalProperties: false
|
|
2502
|
+
},
|
|
2503
|
+
outputSchema: {
|
|
2504
|
+
type: "object",
|
|
2505
|
+
properties: {
|
|
2506
|
+
leads: {
|
|
2507
|
+
type: "array",
|
|
2508
|
+
description: "Imported leads. Domains mode: [{domain, leadId, name}]. Records mode: [{rowId, domain?, leadId, name}].",
|
|
2509
|
+
items: { type: "object" }
|
|
2510
|
+
},
|
|
2511
|
+
not_imported: {
|
|
2512
|
+
type: "array",
|
|
2513
|
+
description: "Inputs that did NOT yield a leadId. Each entry has a `reason` ('malformed', 'NO_MATCH', 'TIMEOUT', etc.) plus the input echo.",
|
|
2514
|
+
items: { type: "object" }
|
|
2515
|
+
},
|
|
2516
|
+
importIds: {
|
|
2517
|
+
type: "array",
|
|
2518
|
+
description: "Backend file-import handles (one per chunk of \u2264100 rows).",
|
|
2519
|
+
items: { type: "string" }
|
|
2520
|
+
},
|
|
2521
|
+
region: { type: "string" },
|
|
2522
|
+
cancelled: {
|
|
2523
|
+
type: "boolean",
|
|
2524
|
+
description: "True when ctx.signal aborted the call mid-flight."
|
|
2525
|
+
},
|
|
2526
|
+
dry_run: {
|
|
2527
|
+
type: "boolean",
|
|
2528
|
+
description: "True when dry_run:true was passed (preprocess only, no CRM commit)."
|
|
2529
|
+
},
|
|
2530
|
+
_meta: { type: "object" }
|
|
2531
|
+
},
|
|
2532
|
+
required: ["leads", "not_imported", "importIds", "region", "_meta"]
|
|
2141
2533
|
},
|
|
2142
2534
|
execute: async (client, params, ctx) => {
|
|
2143
2535
|
const signal = ctx?.signal;
|
|
@@ -2349,6 +2741,13 @@ var PREVIEW_SAMPLE_CAP = 50;
|
|
|
2349
2741
|
var PREPROCESS_BUDGET_MS = 6e4;
|
|
2350
2742
|
var listMappableFields = {
|
|
2351
2743
|
name: "leadbay_list_mappable_fields",
|
|
2744
|
+
annotations: {
|
|
2745
|
+
title: "List CRM-import mappable fields",
|
|
2746
|
+
readOnlyHint: true,
|
|
2747
|
+
destructiveHint: false,
|
|
2748
|
+
idempotentHint: true,
|
|
2749
|
+
openWorldHint: true
|
|
2750
|
+
},
|
|
2352
2751
|
description: "List every CRM field the agent can target when calling leadbay_import_leads or leadbay_import_and_qualify. Returns two arrays: `standard_fields` (Leadbay's built-in StandardCrmFieldType enum \u2014 LEAD_NAME, LEAD_WEBSITE, LEAD_STATUS, contact + location + sector fields) and `custom_fields` (this org's user-defined fields \u2014 id, name, type, and the literal `mapping_value` you pass in `mappings.fields`). For custom fields, `mapping_value` is the wire-format string `CUSTOM.<id>` \u2014 pass it verbatim.\n\nOptional `for_records` param: pass a sample of CSV-shaped rows and the tool also runs the wizard's preprocess on them, attaching `mapping_hints` (per-column AI-confidence suggestions) and `custom_field_candidates` (custom fields that match unmapped columns by exact / case-insensitive / fuzzy name) to the response. Saves a separate preview round-trip when the agent already has data in hand.\n\nWhen to use: before authoring an import mapping, especially when the CSV has columns that aren't obvious matches for standard fields. When NOT to use: when you already know the mapping \u2014 this call is cheap (~50ms with no for_records, ~5\u201310s with) but unnecessary if the agent has already cached the catalog within the same conversation.",
|
|
2353
2752
|
inputSchema: {
|
|
2354
2753
|
type: "object",
|
|
@@ -2361,6 +2760,44 @@ var listMappableFields = {
|
|
|
2361
2760
|
},
|
|
2362
2761
|
additionalProperties: false
|
|
2363
2762
|
},
|
|
2763
|
+
outputSchema: {
|
|
2764
|
+
type: "object",
|
|
2765
|
+
properties: {
|
|
2766
|
+
standard_fields: {
|
|
2767
|
+
type: "array",
|
|
2768
|
+
description: "Built-in StandardCrmFieldType entries (LEAD_NAME, LEAD_WEBSITE, contact + location + sector). Each: {name, description, mapping_value}.",
|
|
2769
|
+
items: { type: "object" }
|
|
2770
|
+
},
|
|
2771
|
+
custom_fields: {
|
|
2772
|
+
type: "array",
|
|
2773
|
+
description: "Org-defined custom fields. Each: {id, name, type, description, mapping_value:'CUSTOM.<id>'}.",
|
|
2774
|
+
items: { type: "object" }
|
|
2775
|
+
},
|
|
2776
|
+
mapping_hints: {
|
|
2777
|
+
type: "array",
|
|
2778
|
+
description: "Per-column AI-confidence suggestions (only when for_records was passed). Each: {column, target, confidence, reason}.",
|
|
2779
|
+
items: { type: "object" }
|
|
2780
|
+
},
|
|
2781
|
+
custom_field_candidates: {
|
|
2782
|
+
type: "array",
|
|
2783
|
+
description: "Custom fields matching unmapped columns by exact / case-insensitive / fuzzy name (only when for_records was passed).",
|
|
2784
|
+
items: { type: "object" }
|
|
2785
|
+
},
|
|
2786
|
+
sample_rows: {
|
|
2787
|
+
type: "array",
|
|
2788
|
+
description: "First few rows of the preprocessed sample (only when for_records was passed).",
|
|
2789
|
+
items: { type: "object" }
|
|
2790
|
+
},
|
|
2791
|
+
notes: {
|
|
2792
|
+
type: "array",
|
|
2793
|
+
description: "Operator notes (e.g., preprocess timeout, sample-size truncation).",
|
|
2794
|
+
items: { type: "string" }
|
|
2795
|
+
},
|
|
2796
|
+
region: { type: "string" },
|
|
2797
|
+
_meta: { type: "object" }
|
|
2798
|
+
},
|
|
2799
|
+
required: ["standard_fields", "custom_fields", "region", "_meta"]
|
|
2800
|
+
},
|
|
2364
2801
|
execute: async (client, params, ctx) => {
|
|
2365
2802
|
const signal = ctx?.signal;
|
|
2366
2803
|
const customs = await client.request("GET", "/crm/custom_fields");
|
|
@@ -2466,6 +2903,13 @@ function coerceCsvValue(v) {
|
|
|
2466
2903
|
// ../core/dist/tools/select-leads.js
|
|
2467
2904
|
var selectLeads = {
|
|
2468
2905
|
name: "leadbay_select_leads",
|
|
2906
|
+
annotations: {
|
|
2907
|
+
title: "Select leads",
|
|
2908
|
+
readOnlyHint: false,
|
|
2909
|
+
destructiveHint: true,
|
|
2910
|
+
idempotentHint: true,
|
|
2911
|
+
openWorldHint: true
|
|
2912
|
+
},
|
|
2469
2913
|
description: "Add leads to the user's transient selection (used by selection-scoped bulk operations). When to use: low-level. The user's selection is a per-token global state \u2014 be careful when invoking directly. When NOT to use: in normal flow \u2014 leadbay_enrich_titles wraps select \u2192 action \u2192 clear in one call with proper Mutex protection. Calling this directly without acquiring the selection lock can clobber concurrent composite calls.",
|
|
2470
2914
|
optional: true,
|
|
2471
2915
|
write: true,
|
|
@@ -2480,7 +2924,18 @@ var selectLeads = {
|
|
|
2480
2924
|
maxItems: 1e3
|
|
2481
2925
|
}
|
|
2482
2926
|
},
|
|
2483
|
-
required: ["leadIds"]
|
|
2927
|
+
required: ["leadIds"],
|
|
2928
|
+
additionalProperties: false
|
|
2929
|
+
},
|
|
2930
|
+
outputSchema: {
|
|
2931
|
+
type: "object",
|
|
2932
|
+
properties: {
|
|
2933
|
+
selected: {
|
|
2934
|
+
type: "number",
|
|
2935
|
+
description: "How many leadIds the call added to the selection (echoes input length)."
|
|
2936
|
+
}
|
|
2937
|
+
},
|
|
2938
|
+
required: ["selected"]
|
|
2484
2939
|
},
|
|
2485
2940
|
execute: async (client, params) => {
|
|
2486
2941
|
const qs = params.leadIds.map((id) => `leadIds=${encodeURIComponent(id)}`).join("&");
|
|
@@ -2492,6 +2947,13 @@ var selectLeads = {
|
|
|
2492
2947
|
// ../core/dist/tools/deselect-leads.js
|
|
2493
2948
|
var deselectLeads = {
|
|
2494
2949
|
name: "leadbay_deselect_leads",
|
|
2950
|
+
annotations: {
|
|
2951
|
+
title: "Deselect leads",
|
|
2952
|
+
readOnlyHint: false,
|
|
2953
|
+
destructiveHint: true,
|
|
2954
|
+
idempotentHint: true,
|
|
2955
|
+
openWorldHint: true
|
|
2956
|
+
},
|
|
2495
2957
|
description: "Remove leads from the user's transient selection. When to use: when narrowing a previously-built selection without clearing it entirely. When NOT to use: in normal flow \u2014 leadbay_enrich_titles handles selection lifecycle.",
|
|
2496
2958
|
optional: true,
|
|
2497
2959
|
write: true,
|
|
@@ -2505,7 +2967,8 @@ var deselectLeads = {
|
|
|
2505
2967
|
minItems: 1
|
|
2506
2968
|
}
|
|
2507
2969
|
},
|
|
2508
|
-
required: ["leadIds"]
|
|
2970
|
+
required: ["leadIds"],
|
|
2971
|
+
additionalProperties: false
|
|
2509
2972
|
},
|
|
2510
2973
|
execute: async (client, params) => {
|
|
2511
2974
|
const qs = params.leadIds.map((id) => `leadIds=${encodeURIComponent(id)}`).join("&");
|
|
@@ -2517,10 +2980,17 @@ var deselectLeads = {
|
|
|
2517
2980
|
// ../core/dist/tools/clear-selection.js
|
|
2518
2981
|
var clearSelection = {
|
|
2519
2982
|
name: "leadbay_clear_selection",
|
|
2983
|
+
annotations: {
|
|
2984
|
+
title: "Clear selection",
|
|
2985
|
+
readOnlyHint: false,
|
|
2986
|
+
destructiveHint: true,
|
|
2987
|
+
idempotentHint: true,
|
|
2988
|
+
openWorldHint: true
|
|
2989
|
+
},
|
|
2520
2990
|
description: "Clear the user's transient selection. When to use: cleanup after manual selection work, or recovery from a stuck composite. When NOT to use: in normal flow \u2014 composites clear in their own finally blocks.",
|
|
2521
2991
|
optional: true,
|
|
2522
2992
|
write: true,
|
|
2523
|
-
inputSchema: { type: "object", properties: {} },
|
|
2993
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
2524
2994
|
execute: async (client) => {
|
|
2525
2995
|
await client.requestVoid("POST", "/leads/selection/clear");
|
|
2526
2996
|
return { cleared: true };
|
|
@@ -2530,13 +3000,21 @@ var clearSelection = {
|
|
|
2530
3000
|
// ../core/dist/tools/set-active-lens.js
|
|
2531
3001
|
var setActiveLens = {
|
|
2532
3002
|
name: "leadbay_set_active_lens",
|
|
3003
|
+
annotations: {
|
|
3004
|
+
title: "Set active lens",
|
|
3005
|
+
readOnlyHint: false,
|
|
3006
|
+
destructiveHint: true,
|
|
3007
|
+
idempotentHint: true,
|
|
3008
|
+
openWorldHint: true
|
|
3009
|
+
},
|
|
2533
3010
|
description: "Mark a lens as last-used. Subsequent /me reads return it as last_requested_lens, so all composite tools default to it. When to use: after the user explicitly switched contexts (e.g. created a new lens via leadbay_create_lens). When NOT to use: in normal flow \u2014 leadbay_pull_leads and leadbay_adjust_audience auto-set the right lens.",
|
|
2534
3011
|
optional: true,
|
|
2535
3012
|
write: true,
|
|
2536
3013
|
inputSchema: {
|
|
2537
3014
|
type: "object",
|
|
2538
3015
|
properties: { lensId: { type: "number", description: "Lens id (required)" } },
|
|
2539
|
-
required: ["lensId"]
|
|
3016
|
+
required: ["lensId"],
|
|
3017
|
+
additionalProperties: false
|
|
2540
3018
|
},
|
|
2541
3019
|
execute: async (client, params) => {
|
|
2542
3020
|
await client.requestVoid("POST", `/lenses/${params.lensId}/update_last_requested`);
|
|
@@ -2549,6 +3027,13 @@ var setActiveLens = {
|
|
|
2549
3027
|
// ../core/dist/tools/create-lens.js
|
|
2550
3028
|
var createLens = {
|
|
2551
3029
|
name: "leadbay_create_lens",
|
|
3030
|
+
annotations: {
|
|
3031
|
+
title: "Create a new lens",
|
|
3032
|
+
readOnlyHint: false,
|
|
3033
|
+
destructiveHint: true,
|
|
3034
|
+
idempotentHint: false,
|
|
3035
|
+
openWorldHint: true
|
|
3036
|
+
},
|
|
2552
3037
|
description: "Create a new user-level lens by cloning an existing lens's filter/scoring as the starting point. When to use: when adjust_audience determined the current lens cannot be edited (e.g. it's the org default). When NOT to use: to update an existing lens \u2014 use leadbay_update_lens or leadbay_update_lens_filter.",
|
|
2553
3038
|
optional: true,
|
|
2554
3039
|
write: true,
|
|
@@ -2559,7 +3044,21 @@ var createLens = {
|
|
|
2559
3044
|
name: { type: "string", description: "Display name for the new lens" },
|
|
2560
3045
|
description: { type: "string" }
|
|
2561
3046
|
},
|
|
2562
|
-
required: ["base", "name"]
|
|
3047
|
+
required: ["base", "name"],
|
|
3048
|
+
additionalProperties: false
|
|
3049
|
+
},
|
|
3050
|
+
outputSchema: {
|
|
3051
|
+
type: "object",
|
|
3052
|
+
description: "Full LensPayload as returned by the backend. Permissive shape \u2014 backend may add fields over time.",
|
|
3053
|
+
properties: {
|
|
3054
|
+
id: { type: "number", description: "New lens id." },
|
|
3055
|
+
name: { type: "string" },
|
|
3056
|
+
description: { type: ["string", "null"] },
|
|
3057
|
+
is_default: { type: "boolean" },
|
|
3058
|
+
is_last_active: { type: "boolean" },
|
|
3059
|
+
user_id: { type: ["string", "number", "null"] }
|
|
3060
|
+
},
|
|
3061
|
+
required: ["id", "name"]
|
|
2563
3062
|
},
|
|
2564
3063
|
execute: async (client, params) => {
|
|
2565
3064
|
const lens = await client.request("POST", "/lenses", {
|
|
@@ -2575,6 +3074,13 @@ var createLens = {
|
|
|
2575
3074
|
// ../core/dist/tools/update-lens.js
|
|
2576
3075
|
var updateLens = {
|
|
2577
3076
|
name: "leadbay_update_lens",
|
|
3077
|
+
annotations: {
|
|
3078
|
+
title: "Update a lens",
|
|
3079
|
+
readOnlyHint: false,
|
|
3080
|
+
destructiveHint: true,
|
|
3081
|
+
idempotentHint: true,
|
|
3082
|
+
openWorldHint: true
|
|
3083
|
+
},
|
|
2578
3084
|
description: "Update lens metadata (name, description, mode flags). Does NOT change the audience filter \u2014 use leadbay_update_lens_filter for that. When to use: rename a lens or toggle multi_product_mode/use_hq_only. When NOT to use: to change which leads the lens shows \u2014 that's a filter operation.",
|
|
2579
3085
|
optional: true,
|
|
2580
3086
|
write: true,
|
|
@@ -2587,7 +3093,8 @@ var updateLens = {
|
|
|
2587
3093
|
multi_product_mode: { type: "boolean" },
|
|
2588
3094
|
use_hq_only: { type: "boolean" }
|
|
2589
3095
|
},
|
|
2590
|
-
required: ["lensId"]
|
|
3096
|
+
required: ["lensId"],
|
|
3097
|
+
additionalProperties: false
|
|
2591
3098
|
},
|
|
2592
3099
|
execute: async (client, params) => {
|
|
2593
3100
|
const { lensId, ...body } = params;
|
|
@@ -2600,6 +3107,13 @@ var updateLens = {
|
|
|
2600
3107
|
// ../core/dist/tools/update-lens-filter.js
|
|
2601
3108
|
var updateLensFilter = {
|
|
2602
3109
|
name: "leadbay_update_lens_filter",
|
|
3110
|
+
annotations: {
|
|
3111
|
+
title: "Update lens filter",
|
|
3112
|
+
readOnlyHint: false,
|
|
3113
|
+
destructiveHint: true,
|
|
3114
|
+
idempotentHint: true,
|
|
3115
|
+
openWorldHint: true
|
|
3116
|
+
},
|
|
2603
3117
|
description: "Replace the audience filter (sectors, sizes, locations) on a lens. Body is the full Filter object \u2014 this is a REPLACE, not a merge. Returns 400 'default_lens' if applied to the org default lens (clone it first). When to use: low-level mutation when you've already prepared the merged filter. When NOT to use: from agent flow \u2014 use leadbay_adjust_audience, which handles draft-vs-direct routing, permission fallback, and the merge logic so unrelated criteria aren't dropped.",
|
|
2604
3118
|
optional: true,
|
|
2605
3119
|
write: true,
|
|
@@ -2616,7 +3130,8 @@ var updateLensFilter = {
|
|
|
2616
3130
|
description: "If true, return the call shape that WOULD be sent without contacting the backend"
|
|
2617
3131
|
}
|
|
2618
3132
|
},
|
|
2619
|
-
required: ["lensId", "filter"]
|
|
3133
|
+
required: ["lensId", "filter"],
|
|
3134
|
+
additionalProperties: false
|
|
2620
3135
|
},
|
|
2621
3136
|
execute: async (client, params) => {
|
|
2622
3137
|
if (params.dry_run) {
|
|
@@ -2638,13 +3153,21 @@ var updateLensFilter = {
|
|
|
2638
3153
|
// ../core/dist/tools/create-lens-draft.js
|
|
2639
3154
|
var createLensDraft = {
|
|
2640
3155
|
name: "leadbay_create_lens_draft",
|
|
3156
|
+
annotations: {
|
|
3157
|
+
title: "Create a lens draft",
|
|
3158
|
+
readOnlyHint: false,
|
|
3159
|
+
destructiveHint: true,
|
|
3160
|
+
idempotentHint: false,
|
|
3161
|
+
openWorldHint: true
|
|
3162
|
+
},
|
|
2641
3163
|
description: "Create (or fetch existing) draft of an org-level lens. Idempotent \u2014 same user calling twice returns the same draft. The returned lens has draft_of set to the original lens id. When to use: when a non-admin needs to modify an org-level lens \u2014 make a draft, edit the draft. When NOT to use: from agent flow \u2014 leadbay_adjust_audience handles the draft-routing transparently.",
|
|
2642
3164
|
optional: true,
|
|
2643
3165
|
write: true,
|
|
2644
3166
|
inputSchema: {
|
|
2645
3167
|
type: "object",
|
|
2646
3168
|
properties: { lensId: { type: "number", description: "Lens id of the org-level lens to draft" } },
|
|
2647
|
-
required: ["lensId"]
|
|
3169
|
+
required: ["lensId"],
|
|
3170
|
+
additionalProperties: false
|
|
2648
3171
|
},
|
|
2649
3172
|
execute: async (client, params) => {
|
|
2650
3173
|
return await client.request("POST", `/lenses/${params.lensId}/draft`);
|
|
@@ -2654,13 +3177,21 @@ var createLensDraft = {
|
|
|
2654
3177
|
// ../core/dist/tools/promote-lens.js
|
|
2655
3178
|
var promoteLens = {
|
|
2656
3179
|
name: "leadbay_promote_lens",
|
|
3180
|
+
annotations: {
|
|
3181
|
+
title: "Promote a lens draft to active",
|
|
3182
|
+
readOnlyHint: false,
|
|
3183
|
+
destructiveHint: true,
|
|
3184
|
+
idempotentHint: false,
|
|
3185
|
+
openWorldHint: true
|
|
3186
|
+
},
|
|
2657
3187
|
description: "Promote a user-level lens (or draft) to org-level so all teammates see it. Admin-only. When to use: rare \u2014 when an admin user has built a lens (or refined a draft) and wants to share it org-wide. When NOT to use: as a non-admin (will fail with 403); for personal lens changes (those stay user-scoped).",
|
|
2658
3188
|
optional: true,
|
|
2659
3189
|
write: true,
|
|
2660
3190
|
inputSchema: {
|
|
2661
3191
|
type: "object",
|
|
2662
3192
|
properties: { lensId: { type: "number" } },
|
|
2663
|
-
required: ["lensId"]
|
|
3193
|
+
required: ["lensId"],
|
|
3194
|
+
additionalProperties: false
|
|
2664
3195
|
},
|
|
2665
3196
|
execute: async (client, params) => {
|
|
2666
3197
|
await client.requestVoid("POST", `/lenses/${params.lensId}/promote`);
|
|
@@ -2672,6 +3203,13 @@ var promoteLens = {
|
|
|
2672
3203
|
// ../core/dist/tools/set-user-prompt.js
|
|
2673
3204
|
var setUserPrompt = {
|
|
2674
3205
|
name: "leadbay_set_user_prompt",
|
|
3206
|
+
annotations: {
|
|
3207
|
+
title: "Set the user prompt",
|
|
3208
|
+
readOnlyHint: false,
|
|
3209
|
+
destructiveHint: true,
|
|
3210
|
+
idempotentHint: true,
|
|
3211
|
+
openWorldHint: true
|
|
3212
|
+
},
|
|
2675
3213
|
description: "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). When to use: low-level. When NOT to use: from agent flow \u2014 use leadbay_refine_prompt, which polls for follow-up clarifications.",
|
|
2676
3214
|
optional: true,
|
|
2677
3215
|
write: true,
|
|
@@ -2684,7 +3222,8 @@ var setUserPrompt = {
|
|
|
2684
3222
|
description: "If true, return the call shape that WOULD be sent without contacting the backend"
|
|
2685
3223
|
}
|
|
2686
3224
|
},
|
|
2687
|
-
required: ["prompt"]
|
|
3225
|
+
required: ["prompt"],
|
|
3226
|
+
additionalProperties: false
|
|
2688
3227
|
},
|
|
2689
3228
|
execute: async (client, params) => {
|
|
2690
3229
|
const orgId = await client.resolveOrgId();
|
|
@@ -2709,10 +3248,17 @@ var setUserPrompt = {
|
|
|
2709
3248
|
// ../core/dist/tools/clear-user-prompt.js
|
|
2710
3249
|
var clearUserPrompt = {
|
|
2711
3250
|
name: "leadbay_clear_user_prompt",
|
|
3251
|
+
annotations: {
|
|
3252
|
+
title: "Clear the user prompt",
|
|
3253
|
+
readOnlyHint: false,
|
|
3254
|
+
destructiveHint: true,
|
|
3255
|
+
idempotentHint: true,
|
|
3256
|
+
openWorldHint: true
|
|
3257
|
+
},
|
|
2712
3258
|
description: "Remove the org's intelligence-refinement prompt (revert to AI-only generation). Admin-only. Triggers full intelligence regeneration. When to use: when a refinement turned out to be the wrong direction. When NOT to use: to replace with a different prompt \u2014 just call leadbay_refine_prompt; that overwrites.",
|
|
2713
3259
|
optional: true,
|
|
2714
3260
|
write: true,
|
|
2715
|
-
inputSchema: { type: "object", properties: {} },
|
|
3261
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
2716
3262
|
execute: async (client) => {
|
|
2717
3263
|
const orgId = await client.resolveOrgId();
|
|
2718
3264
|
await client.requestVoid("DELETE", `/organizations/${orgId}/user_prompt`);
|
|
@@ -2724,6 +3270,13 @@ var clearUserPrompt = {
|
|
|
2724
3270
|
// ../core/dist/tools/pick-clarification.js
|
|
2725
3271
|
var pickClarification = {
|
|
2726
3272
|
name: "leadbay_pick_clarification",
|
|
3273
|
+
annotations: {
|
|
3274
|
+
title: "Pick a clarification answer",
|
|
3275
|
+
readOnlyHint: false,
|
|
3276
|
+
destructiveHint: true,
|
|
3277
|
+
idempotentHint: false,
|
|
3278
|
+
openWorldHint: true
|
|
3279
|
+
},
|
|
2727
3280
|
description: "Answer the pending clarification question \u2014 either by picking one of the offered options (option_id) or by typing a free-text answer. The answer is stored as the new user_prompt and triggers regeneration. Admin-only. When to use: low-level. When NOT to use: from agent flow \u2014 use leadbay_answer_clarification.",
|
|
2728
3281
|
optional: true,
|
|
2729
3282
|
write: true,
|
|
@@ -2732,7 +3285,18 @@ var pickClarification = {
|
|
|
2732
3285
|
properties: {
|
|
2733
3286
|
option_id: { type: "string", description: "Id of one of the clarification's options" },
|
|
2734
3287
|
text_answer: { type: "string", description: "Free-text answer (overrides option_id if both are set)" }
|
|
2735
|
-
}
|
|
3288
|
+
},
|
|
3289
|
+
additionalProperties: false
|
|
3290
|
+
},
|
|
3291
|
+
outputSchema: {
|
|
3292
|
+
type: "object",
|
|
3293
|
+
properties: {
|
|
3294
|
+
answered: {
|
|
3295
|
+
type: "boolean",
|
|
3296
|
+
description: "True when the answer was recorded; intelligence regeneration begins."
|
|
3297
|
+
}
|
|
3298
|
+
},
|
|
3299
|
+
required: ["answered"]
|
|
2736
3300
|
},
|
|
2737
3301
|
execute: async (client, params) => {
|
|
2738
3302
|
if (!params.option_id && !params.text_answer) {
|
|
@@ -2753,10 +3317,17 @@ var pickClarification = {
|
|
|
2753
3317
|
// ../core/dist/tools/dismiss-clarification.js
|
|
2754
3318
|
var dismissClarification = {
|
|
2755
3319
|
name: "leadbay_dismiss_clarification",
|
|
3320
|
+
annotations: {
|
|
3321
|
+
title: "Dismiss a clarification",
|
|
3322
|
+
readOnlyHint: false,
|
|
3323
|
+
destructiveHint: true,
|
|
3324
|
+
idempotentHint: false,
|
|
3325
|
+
openWorldHint: true
|
|
3326
|
+
},
|
|
2756
3327
|
description: "Dismiss the pending clarification without answering. Leadbay proceeds with its best guess. Admin-only. When to use: when the user explicitly doesn't want to answer the disambiguation. When NOT to use: as a default \u2014 answering with even a free-text reason gives Leadbay better signal.",
|
|
2757
3328
|
optional: true,
|
|
2758
3329
|
write: true,
|
|
2759
|
-
inputSchema: { type: "object", properties: {} },
|
|
3330
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
2760
3331
|
execute: async (client) => {
|
|
2761
3332
|
const orgId = await client.resolveOrgId();
|
|
2762
3333
|
await client.requestVoid("POST", `/organizations/${orgId}/dismiss_clarification`);
|
|
@@ -2779,6 +3350,13 @@ var EPILOGUE_LABEL_MAP = {
|
|
|
2779
3350
|
};
|
|
2780
3351
|
var setEpilogueStatus = {
|
|
2781
3352
|
name: "leadbay_set_epilogue_status",
|
|
3353
|
+
annotations: {
|
|
3354
|
+
title: "Set lead epilogue status",
|
|
3355
|
+
readOnlyHint: false,
|
|
3356
|
+
destructiveHint: true,
|
|
3357
|
+
idempotentHint: true,
|
|
3358
|
+
openWorldHint: true
|
|
3359
|
+
},
|
|
2782
3360
|
description: "Bulk-set the outreach progress (epilogue) status across a set of leads. Status values: STILL_CHASING, COULD_NOT_REACH_STILL_TRYING, INTEREST_VALIDATED_OR_MEETING_PLANED ('meeting booked'), NOT_INTERESTED_LOST (short labels accepted; mapped to the EPILOGUE_* enum). Up to 1000 leads per call. When to use: low-level. 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.",
|
|
2783
3361
|
optional: true,
|
|
2784
3362
|
write: true,
|
|
@@ -2795,7 +3373,8 @@ var setEpilogueStatus = {
|
|
|
2795
3373
|
description: "One of: STILL_CHASING, COULD_NOT_REACH_STILL_TRYING, INTEREST_VALIDATED_OR_MEETING_PLANED, NOT_INTERESTED_LOST"
|
|
2796
3374
|
}
|
|
2797
3375
|
},
|
|
2798
|
-
required: ["lead_ids", "status"]
|
|
3376
|
+
required: ["lead_ids", "status"],
|
|
3377
|
+
additionalProperties: false
|
|
2799
3378
|
},
|
|
2800
3379
|
execute: async (client, params) => {
|
|
2801
3380
|
const wire = EPILOGUE_LABEL_MAP[params.status];
|
|
@@ -2818,6 +3397,13 @@ var setEpilogueStatus = {
|
|
|
2818
3397
|
// ../core/dist/tools/remove-epilogue.js
|
|
2819
3398
|
var removeEpilogue = {
|
|
2820
3399
|
name: "leadbay_remove_epilogue",
|
|
3400
|
+
annotations: {
|
|
3401
|
+
title: "Remove lead epilogue",
|
|
3402
|
+
readOnlyHint: false,
|
|
3403
|
+
destructiveHint: true,
|
|
3404
|
+
idempotentHint: true,
|
|
3405
|
+
openWorldHint: true
|
|
3406
|
+
},
|
|
2821
3407
|
description: "Bulk-clear the epilogue status from a set of leads. When to use: when an outreach action was logged in error and needs to be undone. When NOT to use: to change status \u2014 call leadbay_set_epilogue_status with the new status (it overwrites).",
|
|
2822
3408
|
optional: true,
|
|
2823
3409
|
write: true,
|
|
@@ -2830,7 +3416,8 @@ var removeEpilogue = {
|
|
|
2830
3416
|
description: "Lead UUIDs"
|
|
2831
3417
|
}
|
|
2832
3418
|
},
|
|
2833
|
-
required: ["lead_ids"]
|
|
3419
|
+
required: ["lead_ids"],
|
|
3420
|
+
additionalProperties: false
|
|
2834
3421
|
},
|
|
2835
3422
|
execute: async (client, params) => {
|
|
2836
3423
|
await client.requestVoid("POST", "/leads/remove_epilogue", {
|
|
@@ -2843,6 +3430,13 @@ var removeEpilogue = {
|
|
|
2843
3430
|
// ../core/dist/tools/preview-bulk-enrichment.js
|
|
2844
3431
|
var previewBulkEnrichment = {
|
|
2845
3432
|
name: "leadbay_preview_bulk_enrichment",
|
|
3433
|
+
annotations: {
|
|
3434
|
+
title: "Preview bulk enrichment cost",
|
|
3435
|
+
readOnlyHint: true,
|
|
3436
|
+
destructiveHint: false,
|
|
3437
|
+
idempotentHint: true,
|
|
3438
|
+
openWorldHint: true
|
|
3439
|
+
},
|
|
2846
3440
|
description: "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. When to use: between selecting leads and launching, to know what the enrichment will cost. When NOT to use: from agent flow \u2014 leadbay_enrich_titles wraps preview + launch with the right safety checks.",
|
|
2847
3441
|
optional: true,
|
|
2848
3442
|
write: true,
|
|
@@ -2855,7 +3449,8 @@ var previewBulkEnrichment = {
|
|
|
2855
3449
|
description: "Job titles to enrich (matched against contacts in selected leads)"
|
|
2856
3450
|
}
|
|
2857
3451
|
},
|
|
2858
|
-
required: ["titles"]
|
|
3452
|
+
required: ["titles"],
|
|
3453
|
+
additionalProperties: false
|
|
2859
3454
|
},
|
|
2860
3455
|
execute: async (client, params) => {
|
|
2861
3456
|
return await client.request("POST", "/leads/selection/enrichment/preview", { titles: params.titles });
|
|
@@ -2865,6 +3460,13 @@ var previewBulkEnrichment = {
|
|
|
2865
3460
|
// ../core/dist/tools/launch-bulk-enrichment.js
|
|
2866
3461
|
var launchBulkEnrichment = {
|
|
2867
3462
|
name: "leadbay_launch_bulk_enrichment",
|
|
3463
|
+
annotations: {
|
|
3464
|
+
title: "Launch bulk enrichment",
|
|
3465
|
+
readOnlyHint: false,
|
|
3466
|
+
destructiveHint: true,
|
|
3467
|
+
idempotentHint: true,
|
|
3468
|
+
openWorldHint: true
|
|
3469
|
+
},
|
|
2868
3470
|
description: "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. When to use: low-level. When NOT to use: from agent flow \u2014 leadbay_enrich_titles handles selection lifecycle, preview, launch, and cleanup.",
|
|
2869
3471
|
optional: true,
|
|
2870
3472
|
write: true,
|
|
@@ -2879,7 +3481,8 @@ var launchBulkEnrichment = {
|
|
|
2879
3481
|
description: "If true, return the call shape WITHOUT contacting the backend"
|
|
2880
3482
|
}
|
|
2881
3483
|
},
|
|
2882
|
-
required: ["titles"]
|
|
3484
|
+
required: ["titles"],
|
|
3485
|
+
additionalProperties: false
|
|
2883
3486
|
},
|
|
2884
3487
|
execute: async (client, params) => {
|
|
2885
3488
|
const email = params.email ?? true;
|
|
@@ -2920,6 +3523,13 @@ var launchBulkEnrichment = {
|
|
|
2920
3523
|
// ../core/dist/composite/research-company.js
|
|
2921
3524
|
var researchCompany = {
|
|
2922
3525
|
name: "leadbay_research_company",
|
|
3526
|
+
annotations: {
|
|
3527
|
+
title: "Research a company by name",
|
|
3528
|
+
readOnlyHint: true,
|
|
3529
|
+
destructiveHint: false,
|
|
3530
|
+
idempotentHint: true,
|
|
3531
|
+
openWorldHint: true
|
|
3532
|
+
},
|
|
2923
3533
|
description: "Deep-dive research on a specific company by NAME (fuzzy match against the active lens's wishlist). When to use: when the user references a company by name and you don't yet have its lead_id. When NOT to use: when you already have the lead_id \u2014 use leadbay_research_lead directly (it bundles richer signals + better top-down ordering for the agent).",
|
|
2924
3534
|
inputSchema: {
|
|
2925
3535
|
type: "object",
|
|
@@ -2932,7 +3542,39 @@ var researchCompany = {
|
|
|
2932
3542
|
type: "string",
|
|
2933
3543
|
description: "Lead UUID if already known (one of companyName or leadId required). Takes precedence over companyName."
|
|
2934
3544
|
}
|
|
2935
|
-
}
|
|
3545
|
+
},
|
|
3546
|
+
additionalProperties: false
|
|
3547
|
+
},
|
|
3548
|
+
outputSchema: {
|
|
3549
|
+
type: "object",
|
|
3550
|
+
properties: {
|
|
3551
|
+
lead: {
|
|
3552
|
+
type: "object",
|
|
3553
|
+
description: "Lead profile basics (id, name, score, ai_agent_lead_score, location, description, short_description, size, website, logo, ai_summary, tags, phone_numbers, keywords, contacts_count, recommended_contact_title, recommended_contact, web_fetch_in_progress)."
|
|
3554
|
+
},
|
|
3555
|
+
qualification: {
|
|
3556
|
+
type: ["array", "null"],
|
|
3557
|
+
description: "Per-question AI qualification answers ({question, score, response, computed_at, outdated_at}), or null if none.",
|
|
3558
|
+
items: { type: "object" }
|
|
3559
|
+
},
|
|
3560
|
+
contacts: {
|
|
3561
|
+
type: "array",
|
|
3562
|
+
description: "Merged org + paid contacts. Each: {id, first_name, last_name, email, phone_number, linkedin_page, job_title, recommended, enrichment, source:'org'|'paid'}.",
|
|
3563
|
+
items: { type: "object" }
|
|
3564
|
+
},
|
|
3565
|
+
web_insights: {
|
|
3566
|
+
description: "Latest /web_fetch content (string) or null when no fetch is available."
|
|
3567
|
+
},
|
|
3568
|
+
web_insights_fetched_at: {
|
|
3569
|
+
description: "ISO timestamp of the latest /web_fetch (string) or null."
|
|
3570
|
+
},
|
|
3571
|
+
recent_activities: {
|
|
3572
|
+
type: "array",
|
|
3573
|
+
description: "Recent activities for this lead (top 20). Each is the activity payload as returned by /leads/{id}/activities.",
|
|
3574
|
+
items: { type: "object" }
|
|
3575
|
+
}
|
|
3576
|
+
},
|
|
3577
|
+
required: ["lead", "contacts", "recent_activities"]
|
|
2936
3578
|
},
|
|
2937
3579
|
execute: async (client, params, ctx) => {
|
|
2938
3580
|
if (!params.leadId && !params.companyName) {
|
|
@@ -2965,6 +3607,13 @@ var researchCompany = {
|
|
|
2965
3607
|
// ../core/dist/composite/prepare-outreach.js
|
|
2966
3608
|
var prepareOutreach = {
|
|
2967
3609
|
name: "leadbay_prepare_outreach",
|
|
3610
|
+
annotations: {
|
|
3611
|
+
title: "Prepare outreach package for a lead",
|
|
3612
|
+
readOnlyHint: true,
|
|
3613
|
+
destructiveHint: false,
|
|
3614
|
+
idempotentHint: true,
|
|
3615
|
+
openWorldHint: true
|
|
3616
|
+
},
|
|
2968
3617
|
description: "Prepare an outreach package for a single lead: recommended contact + enriched contact details + AI summary. When to use: when the agent is about to draft outreach for ONE specific lead and needs the contact's email/phone. 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).",
|
|
2969
3618
|
optional: true,
|
|
2970
3619
|
inputSchema: {
|
|
@@ -2979,11 +3628,44 @@ var prepareOutreach = {
|
|
|
2979
3628
|
description: "If true and credits available, trigger enrichment on the recommended contact (default: false). Enrichment is async \u2014 poll leadbay_get_contacts after ~60s."
|
|
2980
3629
|
}
|
|
2981
3630
|
},
|
|
2982
|
-
required: ["leadId"]
|
|
3631
|
+
required: ["leadId"],
|
|
3632
|
+
additionalProperties: false
|
|
2983
3633
|
},
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
3634
|
+
outputSchema: {
|
|
3635
|
+
type: "object",
|
|
3636
|
+
properties: {
|
|
3637
|
+
lead: {
|
|
3638
|
+
type: ["object", "null"],
|
|
3639
|
+
description: "Short lead summary for outreach context: {name, ai_summary, website}. Null if /lead profile fetch failed.",
|
|
3640
|
+
properties: {
|
|
3641
|
+
name: { type: "string" },
|
|
3642
|
+
ai_summary: { type: ["string", "null"] },
|
|
3643
|
+
website: { type: ["string", "null"] }
|
|
3644
|
+
}
|
|
3645
|
+
},
|
|
3646
|
+
recommended_contact: {
|
|
3647
|
+
type: ["object", "null"],
|
|
3648
|
+
description: "Best contact to outreach to ({id, name, job_title, email, phone_number, linkedin_page}). Null when no contacts known."
|
|
3649
|
+
},
|
|
3650
|
+
other_contacts_count: {
|
|
3651
|
+
type: "number",
|
|
3652
|
+
description: "How many other contacts exist beyond the recommended one (so the agent knows there's more to discover via leadbay_get_contacts)."
|
|
3653
|
+
},
|
|
3654
|
+
enrichment: {
|
|
3655
|
+
type: "object",
|
|
3656
|
+
description: "Status of opt-in enrichment (only set when enrich:true was passed): {triggered, error, hint}.",
|
|
3657
|
+
properties: {
|
|
3658
|
+
triggered: { type: "boolean" },
|
|
3659
|
+
error: { type: ["string", "null"] },
|
|
3660
|
+
hint: { type: ["string", "null"] }
|
|
3661
|
+
}
|
|
3662
|
+
}
|
|
3663
|
+
},
|
|
3664
|
+
required: ["recommended_contact", "other_contacts_count", "enrichment"]
|
|
3665
|
+
},
|
|
3666
|
+
execute: async (client, params, ctx) => {
|
|
3667
|
+
const contactsResult = await getContacts.execute(client, { leadId: params.leadId }, ctx);
|
|
3668
|
+
const contacts = contactsResult.contacts;
|
|
2987
3669
|
const recommended = contacts.find((c) => c.recommended) ?? contacts[0];
|
|
2988
3670
|
let enrichmentTriggered = false;
|
|
2989
3671
|
let enrichmentError = null;
|
|
@@ -3040,6 +3722,13 @@ function summarise(responses) {
|
|
|
3040
3722
|
}
|
|
3041
3723
|
var pullLeads = {
|
|
3042
3724
|
name: "leadbay_pull_leads",
|
|
3725
|
+
annotations: {
|
|
3726
|
+
title: "Pull fresh Leadbay leads",
|
|
3727
|
+
readOnlyHint: true,
|
|
3728
|
+
destructiveHint: false,
|
|
3729
|
+
idempotentHint: true,
|
|
3730
|
+
openWorldHint: true
|
|
3731
|
+
},
|
|
3043
3732
|
description: "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 / recommended_contact_title / engagement counters / in-flight flags from the lead summary. 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. 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. 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 (which is removed in v0.2.0).",
|
|
3044
3733
|
inputSchema: {
|
|
3045
3734
|
type: "object",
|
|
@@ -3054,7 +3743,57 @@ var pullLeads = {
|
|
|
3054
3743
|
type: "boolean",
|
|
3055
3744
|
description: "If true, include the full set of lead-summary fields. Default false: returns the trimmed agent-friendly form."
|
|
3056
3745
|
}
|
|
3057
|
-
}
|
|
3746
|
+
},
|
|
3747
|
+
additionalProperties: false
|
|
3748
|
+
},
|
|
3749
|
+
outputSchema: {
|
|
3750
|
+
type: "object",
|
|
3751
|
+
properties: {
|
|
3752
|
+
lens: {
|
|
3753
|
+
type: "object",
|
|
3754
|
+
description: "Lens metadata (id of the lens that was queried).",
|
|
3755
|
+
properties: { id: { type: "number" } }
|
|
3756
|
+
},
|
|
3757
|
+
leads: {
|
|
3758
|
+
type: "array",
|
|
3759
|
+
description: "The page of leads. In default mode (verbose:false) each lead is the trimmed agent-friendly shape; in verbose:true the full LeadPayload.",
|
|
3760
|
+
items: { type: "object" }
|
|
3761
|
+
},
|
|
3762
|
+
pagination: {
|
|
3763
|
+
type: "object",
|
|
3764
|
+
description: "page (0-indexed), pages (total), total (item count).",
|
|
3765
|
+
properties: {
|
|
3766
|
+
page: { type: "number" },
|
|
3767
|
+
pages: { type: "number" },
|
|
3768
|
+
total: { type: "number" }
|
|
3769
|
+
}
|
|
3770
|
+
},
|
|
3771
|
+
has_more: {
|
|
3772
|
+
type: "boolean",
|
|
3773
|
+
description: "True if at least one more page exists. Spec-aligned pagination metadata."
|
|
3774
|
+
},
|
|
3775
|
+
next_page: {
|
|
3776
|
+
type: ["number", "null"],
|
|
3777
|
+
description: "0-indexed next page number, or null on the last page."
|
|
3778
|
+
},
|
|
3779
|
+
computing_wishlist: {
|
|
3780
|
+
type: "boolean",
|
|
3781
|
+
description: "True if Leadbay is still rebuilding this lens's wishlist."
|
|
3782
|
+
},
|
|
3783
|
+
computing_scores: {
|
|
3784
|
+
type: "boolean",
|
|
3785
|
+
description: "True if scoring is still running."
|
|
3786
|
+
},
|
|
3787
|
+
_meta: {
|
|
3788
|
+
type: "object",
|
|
3789
|
+
description: "Operator context: region + last-call latency.",
|
|
3790
|
+
properties: {
|
|
3791
|
+
region: { type: "string" },
|
|
3792
|
+
latency_ms: { type: ["number", "null"] }
|
|
3793
|
+
}
|
|
3794
|
+
}
|
|
3795
|
+
},
|
|
3796
|
+
required: ["lens", "leads", "pagination"]
|
|
3058
3797
|
},
|
|
3059
3798
|
execute: async (client, params, ctx) => {
|
|
3060
3799
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
@@ -3103,6 +3842,10 @@ var pullLeads = {
|
|
|
3103
3842
|
epilogue_actions_count: lead.epilogue_actions_count ?? 0,
|
|
3104
3843
|
prospecting_actions_count: lead.prospecting_actions_count ?? 0
|
|
3105
3844
|
};
|
|
3845
|
+
const totalPages = res.pagination?.pages ?? 0;
|
|
3846
|
+
const currentPage = res.pagination?.page ?? page;
|
|
3847
|
+
const hasMore = currentPage < totalPages - 1;
|
|
3848
|
+
const nextPage = hasMore ? currentPage + 1 : null;
|
|
3106
3849
|
return {
|
|
3107
3850
|
lens: { id: lensId },
|
|
3108
3851
|
leads: res.items.map((lead) => ({
|
|
@@ -3110,6 +3853,8 @@ var pullLeads = {
|
|
|
3110
3853
|
qualification_summary: summaryMap.get(lead.id) ?? null
|
|
3111
3854
|
})),
|
|
3112
3855
|
pagination: res.pagination,
|
|
3856
|
+
has_more: hasMore,
|
|
3857
|
+
next_page: nextPage,
|
|
3113
3858
|
computing_wishlist: res.computing_wishlist,
|
|
3114
3859
|
computing_scores: res.computing_scores,
|
|
3115
3860
|
_meta: {
|
|
@@ -3121,6 +3866,86 @@ var pullLeads = {
|
|
|
3121
3866
|
};
|
|
3122
3867
|
|
|
3123
3868
|
// ../core/dist/composite/research-lead.js
|
|
3869
|
+
function renderResearchLeadMarkdown(shape) {
|
|
3870
|
+
const out = [];
|
|
3871
|
+
const firm = shape.firmographics ?? {};
|
|
3872
|
+
const name = firm.name ?? "(unnamed lead)";
|
|
3873
|
+
out.push(`# ${name}`);
|
|
3874
|
+
if (firm.website)
|
|
3875
|
+
out.push(`Website: ${firm.website}`);
|
|
3876
|
+
if (firm.location)
|
|
3877
|
+
out.push(`Location: ${firm.location}`);
|
|
3878
|
+
if (typeof firm.score === "number" || firm.score === null) {
|
|
3879
|
+
const aiScore = firm.ai_agent_lead_score;
|
|
3880
|
+
out.push(`Score: ${firm.score ?? "\u2014"}` + (aiScore != null ? ` \xB7 AI: ${aiScore}` : ""));
|
|
3881
|
+
}
|
|
3882
|
+
if (firm.short_description)
|
|
3883
|
+
out.push(`
|
|
3884
|
+
${firm.short_description}`);
|
|
3885
|
+
const qualification = Array.isArray(shape.qualification) ? shape.qualification : [];
|
|
3886
|
+
if (qualification.length > 0) {
|
|
3887
|
+
out.push(`
|
|
3888
|
+
## Qualification`);
|
|
3889
|
+
for (const q of qualification) {
|
|
3890
|
+
const score = q.boost_score != null ? `${q.boost_score}` : "\u2014";
|
|
3891
|
+
const resp = q.response ? String(q.response).slice(0, 200) : "\u2014";
|
|
3892
|
+
out.push(`- **${q.question}** (boost ${score}): ${resp}`);
|
|
3893
|
+
}
|
|
3894
|
+
}
|
|
3895
|
+
const signals = Array.isArray(shape.signals) ? shape.signals : [];
|
|
3896
|
+
if (signals.length > 0) {
|
|
3897
|
+
out.push(`
|
|
3898
|
+
## Signals`);
|
|
3899
|
+
for (const sec of signals) {
|
|
3900
|
+
const label = sec.section_label ?? "section";
|
|
3901
|
+
out.push(`### ${sec.section_emoji ?? ""} ${label}`.trim());
|
|
3902
|
+
const entries = Array.isArray(sec.entries) ? sec.entries : [];
|
|
3903
|
+
for (const e of entries.slice(0, 5)) {
|
|
3904
|
+
const text = e.text ?? e.summary ?? JSON.stringify(e).slice(0, 200);
|
|
3905
|
+
const hot = e.hot === true ? " \u{1F525}" : "";
|
|
3906
|
+
out.push(`- ${text}${hot}`);
|
|
3907
|
+
}
|
|
3908
|
+
if (entries.length > 5)
|
|
3909
|
+
out.push(`- _${entries.length - 5} more \u2026_`);
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
const contacts = shape.contacts ?? {};
|
|
3913
|
+
const enriched = Array.isArray(contacts.enriched) ? contacts.enriched : [];
|
|
3914
|
+
if (enriched.length > 0) {
|
|
3915
|
+
out.push(`
|
|
3916
|
+
## Contacts (enriched)`);
|
|
3917
|
+
for (const c of enriched.slice(0, 10)) {
|
|
3918
|
+
const fn = c.first_name ?? "";
|
|
3919
|
+
const ln = c.last_name ?? "";
|
|
3920
|
+
const title = c.job_title ?? "\u2014";
|
|
3921
|
+
const email = c.email ?? "no email";
|
|
3922
|
+
out.push(`- **${(fn + " " + ln).trim() || "(unknown)"}** \u2014 ${title} \xB7 ${email}`);
|
|
3923
|
+
}
|
|
3924
|
+
}
|
|
3925
|
+
const engagement = shape.engagement ?? {};
|
|
3926
|
+
const counts = [
|
|
3927
|
+
["notes", engagement.notes_count],
|
|
3928
|
+
["epilogue", engagement.epilogue_actions_count],
|
|
3929
|
+
["prospecting", engagement.prospecting_actions_count]
|
|
3930
|
+
];
|
|
3931
|
+
const activeCounts = counts.filter(([, v]) => typeof v === "number" && v > 0);
|
|
3932
|
+
if (activeCounts.length > 0 || engagement.liked || engagement.disliked) {
|
|
3933
|
+
out.push(`
|
|
3934
|
+
## Engagement`);
|
|
3935
|
+
if (engagement.liked)
|
|
3936
|
+
out.push(`- liked \u2705`);
|
|
3937
|
+
if (engagement.disliked)
|
|
3938
|
+
out.push(`- disliked \u274C`);
|
|
3939
|
+
for (const [k, v] of activeCounts) {
|
|
3940
|
+
out.push(`- ${k}: ${v}`);
|
|
3941
|
+
}
|
|
3942
|
+
}
|
|
3943
|
+
if (shape.truncated) {
|
|
3944
|
+
out.push(`
|
|
3945
|
+
_Truncated_: ${shape.truncation_hint ?? "response trimmed"}_`);
|
|
3946
|
+
}
|
|
3947
|
+
return out.join("\n");
|
|
3948
|
+
}
|
|
3124
3949
|
var SECTION_PRIORITY = ["profile", "signals", "clues"];
|
|
3125
3950
|
function splitEmojiSection(key) {
|
|
3126
3951
|
const m = key.match(/^([^\p{L}\p{N}\s]+)\s+(.+)$/u);
|
|
@@ -3155,6 +3980,13 @@ function reshapeWebFetchContent(content) {
|
|
|
3155
3980
|
}
|
|
3156
3981
|
var researchLead = {
|
|
3157
3982
|
name: "leadbay_research_lead",
|
|
3983
|
+
annotations: {
|
|
3984
|
+
title: "Research a Leadbay lead in depth",
|
|
3985
|
+
readOnlyHint: true,
|
|
3986
|
+
destructiveHint: false,
|
|
3987
|
+
idempotentHint: true,
|
|
3988
|
+
openWorldHint: true
|
|
3989
|
+
},
|
|
3158
3990
|
description: "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. Scoring has two layers: the basic `score` (firmographic, always present, already decent) and the AI qualification layer (`ai_agent_lead_score` + per-question answers + web_fetch signals). The AI layer is pre-populated for roughly the top 10 of each daily batch, and on-demand (via leadbay_bulk_qualify_leads) for anything below that. Combine both layers when judging a lead. When to use: when picking up a single lead from leadbay_pull_leads to decide whether to act on it. When NOT to use: across many leads at once \u2014 that's leadbay_pull_leads' job. (This composite supersedes the lower-level leadbay_get_lead_profile in agent flow; the granular tool stays available for fine-grained access.)",
|
|
3159
3991
|
inputSchema: {
|
|
3160
3992
|
type: "object",
|
|
@@ -3167,9 +3999,108 @@ var researchLead = {
|
|
|
3167
3999
|
concise: {
|
|
3168
4000
|
type: "boolean",
|
|
3169
4001
|
description: "If true, trim signals to hot=true items only (smaller payload). Default false."
|
|
4002
|
+
},
|
|
4003
|
+
response_format: {
|
|
4004
|
+
type: "string",
|
|
4005
|
+
enum: ["json", "markdown"],
|
|
4006
|
+
description: "How the agent wants the result rendered. 'json' (default): the structured payload as text. 'markdown': a compact human-readable rendering (sections + bullets) \u2014 useful for chat-rendering clients (Cursor, Claude Desktop) where the user sees the response directly. structuredContent is emitted in both modes so capable clients still get typed access."
|
|
3170
4007
|
}
|
|
3171
4008
|
},
|
|
3172
|
-
required: ["leadId"]
|
|
4009
|
+
required: ["leadId"],
|
|
4010
|
+
additionalProperties: false
|
|
4011
|
+
},
|
|
4012
|
+
outputSchema: {
|
|
4013
|
+
type: "object",
|
|
4014
|
+
properties: {
|
|
4015
|
+
qualification: {
|
|
4016
|
+
type: "array",
|
|
4017
|
+
description: "Per-question AI qualification answers, ordered by mission importance. Each entry: question, boost_score (canonical -10|0|10|20), score_scale, response, computed_at. score_0_to_10 is a deprecated alias of boost_score (removed in 0.7.0).",
|
|
4018
|
+
items: {
|
|
4019
|
+
type: "object",
|
|
4020
|
+
properties: {
|
|
4021
|
+
question: { type: "string" },
|
|
4022
|
+
boost_score: { type: ["number", "null"] },
|
|
4023
|
+
score_scale: { type: "string" },
|
|
4024
|
+
score_0_to_10: { type: ["number", "null"] },
|
|
4025
|
+
response: { type: ["string", "null"] },
|
|
4026
|
+
computed_at: { type: ["string", "null"] }
|
|
4027
|
+
}
|
|
4028
|
+
}
|
|
4029
|
+
},
|
|
4030
|
+
signals: {
|
|
4031
|
+
type: "array",
|
|
4032
|
+
description: "Web-research signals reshaped into priority-ordered sections (profile \u2192 signals \u2192 clues \u2192 other). Each entry: section_label, section_emoji, entries[]. With concise:true, only hot=true entries kept. May be auto-trimmed when truncated:true (see below).",
|
|
4033
|
+
items: { type: "object" }
|
|
4034
|
+
},
|
|
4035
|
+
truncated: {
|
|
4036
|
+
type: "boolean",
|
|
4037
|
+
description: "True when the response was auto-trimmed to stay under the ~25k-char budget. Always false when concise:true is passed."
|
|
4038
|
+
},
|
|
4039
|
+
truncation_hint: {
|
|
4040
|
+
type: ["string", "null"],
|
|
4041
|
+
description: "When truncated:true, names the specific argument that would reduce the payload (typically 'concise:true')."
|
|
4042
|
+
},
|
|
4043
|
+
firmographics: {
|
|
4044
|
+
type: "object",
|
|
4045
|
+
description: "Lead profile basics. iter-30: nested additionalProperties:false closes the output-side strictness frontier \u2014 runtime returns must match exactly these keys.",
|
|
4046
|
+
properties: {
|
|
4047
|
+
id: { type: "string" },
|
|
4048
|
+
name: { type: "string" },
|
|
4049
|
+
sector_id: { type: ["number", "string", "null"] },
|
|
4050
|
+
size: { type: ["string", "null"] },
|
|
4051
|
+
location: { type: ["string", "null"] },
|
|
4052
|
+
website: { type: ["string", "null"] },
|
|
4053
|
+
description: { type: ["string", "null"] },
|
|
4054
|
+
short_description: { type: ["string", "null"] },
|
|
4055
|
+
keywords: { type: "array", items: { type: "string" } },
|
|
4056
|
+
tags: { type: "array", items: { type: "string" } },
|
|
4057
|
+
score: { type: ["number", "null"] },
|
|
4058
|
+
ai_agent_lead_score: { type: ["number", "null"] },
|
|
4059
|
+
social_presence: { type: ["object", "string", "null"] },
|
|
4060
|
+
social_urls: { type: ["object", "array", "null"] },
|
|
4061
|
+
registry_ids: { type: ["object", "array", "null"] }
|
|
4062
|
+
},
|
|
4063
|
+
additionalProperties: false
|
|
4064
|
+
},
|
|
4065
|
+
contacts: {
|
|
4066
|
+
type: "object",
|
|
4067
|
+
description: "Two-tier contact set: `enriched` (paid contacts known on this lens for this lead) and `org` (org-level contacts visible beyond the lens).",
|
|
4068
|
+
properties: {
|
|
4069
|
+
enriched: { type: "array", items: { type: "object" } },
|
|
4070
|
+
org: { type: "array", items: { type: "object" } }
|
|
4071
|
+
},
|
|
4072
|
+
additionalProperties: false
|
|
4073
|
+
},
|
|
4074
|
+
engagement: {
|
|
4075
|
+
type: "object",
|
|
4076
|
+
description: "What humans/prior agent runs already did: liked/disliked flags, recommended_contact, counts (notes/epilogue/prospecting), and the most-recent items (recent_notes, recent_epilogue, recent_prospecting). Counts > 0 trigger conditional fan-out for the recent_* fields.",
|
|
4077
|
+
properties: {
|
|
4078
|
+
liked: { type: "boolean" },
|
|
4079
|
+
disliked: { type: "boolean" },
|
|
4080
|
+
new: { type: "boolean" },
|
|
4081
|
+
recommended_contact_title: { type: ["string", "null"] },
|
|
4082
|
+
recommended_contact: { type: ["object", "null"] },
|
|
4083
|
+
notes_count: { type: "number" },
|
|
4084
|
+
epilogue_actions_count: { type: "number" },
|
|
4085
|
+
prospecting_actions_count: { type: "number" },
|
|
4086
|
+
recent_notes: { type: "array", items: { type: "object" } },
|
|
4087
|
+
recent_epilogue: { type: "array", items: { type: "object" } },
|
|
4088
|
+
recent_prospecting: { type: "array", items: { type: "object" } }
|
|
4089
|
+
},
|
|
4090
|
+
additionalProperties: false
|
|
4091
|
+
},
|
|
4092
|
+
_meta: {
|
|
4093
|
+
type: "object",
|
|
4094
|
+
description: "Operator context: region (us/fr/custom), lens_id (the lens used for the lead-by-id fetch), web_fetch_in_progress (true if the backend is still hydrating signals).",
|
|
4095
|
+
properties: {
|
|
4096
|
+
region: { type: "string" },
|
|
4097
|
+
lens_id: { type: "number" },
|
|
4098
|
+
web_fetch_in_progress: { type: "boolean" }
|
|
4099
|
+
},
|
|
4100
|
+
additionalProperties: false
|
|
4101
|
+
}
|
|
4102
|
+
},
|
|
4103
|
+
required: ["qualification", "signals", "firmographics", "contacts", "engagement"]
|
|
3173
4104
|
},
|
|
3174
4105
|
execute: async (client, params, ctx) => {
|
|
3175
4106
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
@@ -3210,16 +4141,50 @@ var researchLead = {
|
|
|
3210
4141
|
}
|
|
3211
4142
|
const paidContacts = contactsR.status === "fulfilled" ? contactsR.value : [];
|
|
3212
4143
|
const orgContacts = valOrNull(orgContactsR) ?? [];
|
|
4144
|
+
const TRUNCATE_CHAR_BUDGET = 25e3;
|
|
4145
|
+
let truncated = false;
|
|
4146
|
+
let truncationHint = null;
|
|
4147
|
+
const probeSize = (obj) => {
|
|
4148
|
+
try {
|
|
4149
|
+
return JSON.stringify(obj).length;
|
|
4150
|
+
} catch {
|
|
4151
|
+
return 0;
|
|
4152
|
+
}
|
|
4153
|
+
};
|
|
4154
|
+
let signalsForReturn = signals;
|
|
4155
|
+
if (!params.concise) {
|
|
4156
|
+
const signalsSize = probeSize(signals);
|
|
4157
|
+
if (signalsSize > TRUNCATE_CHAR_BUDGET) {
|
|
4158
|
+
truncated = true;
|
|
4159
|
+
truncationHint = "Response truncated to fit context. Pass concise:true to filter to hot signals only.";
|
|
4160
|
+
signalsForReturn = signals.map((s) => ({
|
|
4161
|
+
...s,
|
|
4162
|
+
entries: s.entries.slice(0, 2)
|
|
4163
|
+
}));
|
|
4164
|
+
}
|
|
4165
|
+
}
|
|
3213
4166
|
return {
|
|
3214
4167
|
// 1) qualification — single most important block for "is this lead worth pursuing"
|
|
4168
|
+
// boost_score is the canonical field (per AiAgentResponse.score). The valid
|
|
4169
|
+
// set is the discrete -10/0/10/20 boost (see types.ts comment), NOT a 0-10
|
|
4170
|
+
// average — the eval doc flagged the old `score_0_to_10` field name as
|
|
4171
|
+
// misleading. We now ship `boost_score` as canonical alongside an explicit
|
|
4172
|
+
// `score_scale` annotation; `score_0_to_10` is kept as a deprecated alias
|
|
4173
|
+
// for one minor version (0.6.x) and removed in 0.7.0. See MIGRATION.md.
|
|
3215
4174
|
qualification: qualR.status === "fulfilled" ? qualR.value.map((r) => ({
|
|
3216
4175
|
question: r.question,
|
|
4176
|
+
boost_score: r.score,
|
|
4177
|
+
score_scale: "-10|0|10|20",
|
|
4178
|
+
// Deprecated alias — same value as boost_score. Will be removed
|
|
4179
|
+
// in 0.7.0; consumers should switch to boost_score.
|
|
3217
4180
|
score_0_to_10: r.score,
|
|
3218
4181
|
response: r.response,
|
|
3219
4182
|
computed_at: r.computed_at
|
|
3220
4183
|
})) : [],
|
|
3221
|
-
// 2) signals — knowledge-base food
|
|
3222
|
-
signals,
|
|
4184
|
+
// 2) signals — knowledge-base food (may be trimmed when truncated:true)
|
|
4185
|
+
signals: signalsForReturn,
|
|
4186
|
+
truncated,
|
|
4187
|
+
truncation_hint: truncationHint,
|
|
3223
4188
|
// 3) firmographics
|
|
3224
4189
|
firmographics: {
|
|
3225
4190
|
id: lead.id,
|
|
@@ -3281,10 +4246,32 @@ var researchLead = {
|
|
|
3281
4246
|
};
|
|
3282
4247
|
}
|
|
3283
4248
|
};
|
|
4249
|
+
var _innerExecute = researchLead.execute;
|
|
4250
|
+
researchLead.execute = async (client, params, ctx) => {
|
|
4251
|
+
const result = await _innerExecute(client, params, ctx);
|
|
4252
|
+
if (params.response_format !== "markdown")
|
|
4253
|
+
return result;
|
|
4254
|
+
if (result && typeof result === "object" && result.error === true) {
|
|
4255
|
+
return result;
|
|
4256
|
+
}
|
|
4257
|
+
const envelope = {
|
|
4258
|
+
__markdown_envelope: true,
|
|
4259
|
+
markdown: renderResearchLeadMarkdown(result),
|
|
4260
|
+
structured: result
|
|
4261
|
+
};
|
|
4262
|
+
return envelope;
|
|
4263
|
+
};
|
|
3284
4264
|
|
|
3285
4265
|
// ../core/dist/composite/recall-ordered-titles.js
|
|
3286
4266
|
var recallOrderedTitles = {
|
|
3287
4267
|
name: "leadbay_recall_ordered_titles",
|
|
4268
|
+
annotations: {
|
|
4269
|
+
title: "Recall titles previously enriched",
|
|
4270
|
+
readOnlyHint: true,
|
|
4271
|
+
destructiveHint: false,
|
|
4272
|
+
idempotentHint: true,
|
|
4273
|
+
openWorldHint: true
|
|
4274
|
+
},
|
|
3288
4275
|
description: "Show job titles the org has previously enriched, so the agent can repeat the same titles for new leads (or skip already-saturated ones). Two implementation paths: (1) PREFERRED: a selection-scoped preview call that reads previously_enriched_titles from the backend (newer prod field). (2) FALLBACK: live aggregation across each lead's enriched contacts. The composite picks transparently. When to use: before leadbay_enrich_titles, to plan which titles to order. When NOT to use: when you already know the exact titles you want to enrich.",
|
|
3289
4276
|
inputSchema: {
|
|
3290
4277
|
type: "object",
|
|
@@ -3298,7 +4285,32 @@ var recallOrderedTitles = {
|
|
|
3298
4285
|
type: "number",
|
|
3299
4286
|
description: "Override the auto-resolved last-active lens when omitting leadIds (escape hatch)"
|
|
3300
4287
|
}
|
|
3301
|
-
}
|
|
4288
|
+
},
|
|
4289
|
+
additionalProperties: false
|
|
4290
|
+
},
|
|
4291
|
+
outputSchema: {
|
|
4292
|
+
type: "object",
|
|
4293
|
+
properties: {
|
|
4294
|
+
source: {
|
|
4295
|
+
type: "string",
|
|
4296
|
+
description: "'preview_field' (preferred backend path) or 'live_aggregate' (fallback aggregation across each lead's contacts)."
|
|
4297
|
+
},
|
|
4298
|
+
titles: {
|
|
4299
|
+
type: "array",
|
|
4300
|
+
description: "Titles previously enriched. preview_field path: [{title}]. live_aggregate path: [{title, leads_with_enriched, total_enriched_contacts, leads_still_having_unenriched}].",
|
|
4301
|
+
items: { type: "object" }
|
|
4302
|
+
},
|
|
4303
|
+
available_in_selection: {
|
|
4304
|
+
type: "array",
|
|
4305
|
+
description: "Backend-suggested title candidates available for ordering (preview_field path only).",
|
|
4306
|
+
items: { type: "object" }
|
|
4307
|
+
},
|
|
4308
|
+
note: {
|
|
4309
|
+
type: "string",
|
|
4310
|
+
description: "Operator note explaining the chosen path (e.g., empty input set, fallback in use)."
|
|
4311
|
+
}
|
|
4312
|
+
},
|
|
4313
|
+
required: ["source", "titles"]
|
|
3302
4314
|
},
|
|
3303
4315
|
execute: async (client, params, ctx) => {
|
|
3304
4316
|
let leadIds = params.leadIds;
|
|
@@ -3374,8 +4386,58 @@ var recallOrderedTitles = {
|
|
|
3374
4386
|
// ../core/dist/composite/account-status.js
|
|
3375
4387
|
var accountStatus = {
|
|
3376
4388
|
name: "leadbay_account_status",
|
|
4389
|
+
annotations: {
|
|
4390
|
+
title: "Show Leadbay account + quota state",
|
|
4391
|
+
readOnlyHint: true,
|
|
4392
|
+
destructiveHint: false,
|
|
4393
|
+
idempotentHint: true,
|
|
4394
|
+
openWorldHint: true
|
|
4395
|
+
},
|
|
3377
4396
|
description: "Show the user's account state \u2014 admin rights, language, last-active lens, current quota usage across daily/weekly/monthly windows for llm_completion / ai_rescore / web_fetch resources, and whether the org's intelligence is mid-regeneration. Quota windows also hint at the user's consumption pace: heavy recent activity (ai_rescore / web_fetch near their window limits) is a signal that Leadbay will deliver a larger fresh batch next time the user logs back in, since batch size is paced by real consumption. When to use: at the start of a session to know what the agent can/can't do, or after a 429 to explain to the user which resource window was exhausted and when it resets. When NOT to use: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating.",
|
|
3378
|
-
inputSchema: { type: "object", properties: {} },
|
|
4397
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
4398
|
+
outputSchema: {
|
|
4399
|
+
type: "object",
|
|
4400
|
+
properties: {
|
|
4401
|
+
user: {
|
|
4402
|
+
type: "object",
|
|
4403
|
+
description: "Identity & roles for the current bearer-token holder.",
|
|
4404
|
+
properties: {
|
|
4405
|
+
email: { type: ["string", "null"] },
|
|
4406
|
+
name: { type: ["string", "null"] },
|
|
4407
|
+
admin: { type: "boolean" },
|
|
4408
|
+
manager: { type: "boolean" },
|
|
4409
|
+
language: { type: "string" }
|
|
4410
|
+
}
|
|
4411
|
+
},
|
|
4412
|
+
organization: {
|
|
4413
|
+
type: "object",
|
|
4414
|
+
description: "Org-level state and feature flags.",
|
|
4415
|
+
properties: {
|
|
4416
|
+
id: { type: "string" },
|
|
4417
|
+
name: { type: "string" },
|
|
4418
|
+
ai_agent_enabled: { type: "boolean" },
|
|
4419
|
+
computing_intelligence: {
|
|
4420
|
+
type: "boolean",
|
|
4421
|
+
description: "True if Leadbay is mid-regenerating intelligence after a refine_prompt; new leads will reflect it shortly."
|
|
4422
|
+
},
|
|
4423
|
+
plan: { type: ["string", "null"] }
|
|
4424
|
+
}
|
|
4425
|
+
},
|
|
4426
|
+
last_requested_lens: {
|
|
4427
|
+
type: ["number", "null"],
|
|
4428
|
+
description: "Most recent lens id the user pulled leads from."
|
|
4429
|
+
},
|
|
4430
|
+
quota: {
|
|
4431
|
+
type: ["object", "null"],
|
|
4432
|
+
description: "Per-resource quota state (llm_completion, ai_rescore, web_fetch) across daily/weekly/monthly windows. Null if /quota_status failed (logged in stderr)."
|
|
4433
|
+
},
|
|
4434
|
+
_meta: {
|
|
4435
|
+
type: "object",
|
|
4436
|
+
properties: { region: { type: "string" } }
|
|
4437
|
+
}
|
|
4438
|
+
},
|
|
4439
|
+
required: ["user", "organization"]
|
|
4440
|
+
},
|
|
3379
4441
|
execute: async (client, _params, ctx) => {
|
|
3380
4442
|
const me = await client.resolveMe();
|
|
3381
4443
|
let quota = null;
|
|
@@ -3419,6 +4481,15 @@ var DEFAULT_PER_LEAD_BUDGET_MS = 9e4;
|
|
|
3419
4481
|
var DEFAULT_TOTAL_BUDGET_MS2 = 5 * 6e4;
|
|
3420
4482
|
var bulkQualifyLeads = {
|
|
3421
4483
|
name: "leadbay_bulk_qualify_leads",
|
|
4484
|
+
annotations: {
|
|
4485
|
+
title: "Bulk-qualify next N leads",
|
|
4486
|
+
readOnlyHint: false,
|
|
4487
|
+
destructiveHint: true,
|
|
4488
|
+
// Same set of leads + same options ⇒ same backend job (idempotency
|
|
4489
|
+
// hash); already-qualified leads are silent no-ops. Re-call is safe.
|
|
4490
|
+
idempotentHint: true,
|
|
4491
|
+
openWorldHint: true
|
|
4492
|
+
},
|
|
3422
4493
|
description: "Pick the next N unqualified leads in the active lens and qualify them (run AI rescore + web fetch), polling until the answers are populated or a budget is exhausted. Already-qualified leads (those with a non-null ai_agent_lead_score) are silently no-ops on the backend, so this composite paginates past them to find fresh candidates. On 429 mid-fanout, stops launching but keeps polling already-launched leads. Context: Leadbay auto-qualifies roughly the top 10 of each daily batch. Leads below the top ~10 are NOT worse \u2014 the system is saving resources. This tool is how the agent spends more resources to go deeper on promising-looking leads the user hasn't had time to surface yet. When to use: when the user wants more qualified leads than what's currently shown, or when a lead looks promising in leadbay_pull_leads but has an empty qualification_summary. When NOT to use: to qualify a single specific lead \u2014 that's leadbay_qualify_lead (granular, advanced).",
|
|
3423
4494
|
inputSchema: {
|
|
3424
4495
|
type: "object",
|
|
@@ -3444,7 +4515,48 @@ var bulkQualifyLeads = {
|
|
|
3444
4515
|
type: "number",
|
|
3445
4516
|
description: `Total polling budget in ms (default ${DEFAULT_TOTAL_BUDGET_MS2})`
|
|
3446
4517
|
}
|
|
3447
|
-
}
|
|
4518
|
+
},
|
|
4519
|
+
additionalProperties: false
|
|
4520
|
+
},
|
|
4521
|
+
outputSchema: {
|
|
4522
|
+
type: "object",
|
|
4523
|
+
properties: {
|
|
4524
|
+
qualified: {
|
|
4525
|
+
type: "array",
|
|
4526
|
+
description: "Leads whose qualification finished within budget. Each entry: lead_id, qualification_summary{answered,total,avg_qualification_boost}, signals_count.",
|
|
4527
|
+
items: { type: "object" }
|
|
4528
|
+
},
|
|
4529
|
+
still_running: {
|
|
4530
|
+
type: "array",
|
|
4531
|
+
description: "Leads launched but whose qualification did not complete within budget. Re-poll via leadbay_qualify_status with the bulk_id (when present).",
|
|
4532
|
+
items: { type: "object" }
|
|
4533
|
+
},
|
|
4534
|
+
failed: {
|
|
4535
|
+
type: "array",
|
|
4536
|
+
description: "Leads whose web_fetch launch failed (per-lead error).",
|
|
4537
|
+
items: { type: "object" }
|
|
4538
|
+
},
|
|
4539
|
+
quota_exceeded: {
|
|
4540
|
+
type: "boolean",
|
|
4541
|
+
description: "True if 429 was hit mid-fanout. Already-launched leads keep polling; further launches stopped."
|
|
4542
|
+
},
|
|
4543
|
+
exhausted: {
|
|
4544
|
+
type: "boolean",
|
|
4545
|
+
description: "True if the lens's wishlist had no more unqualified leads to qualify."
|
|
4546
|
+
},
|
|
4547
|
+
total_unqualified_found: { type: "number" },
|
|
4548
|
+
message: { type: "string", description: "Human-readable summary; absent on the happy path." },
|
|
4549
|
+
lens_id: {
|
|
4550
|
+
type: "number",
|
|
4551
|
+
description: "The lens id the qualification ran against. Present on every successful return."
|
|
4552
|
+
},
|
|
4553
|
+
_meta: {
|
|
4554
|
+
type: "object",
|
|
4555
|
+
description: "Operator context: region.",
|
|
4556
|
+
properties: { region: { type: "string" } }
|
|
4557
|
+
}
|
|
4558
|
+
},
|
|
4559
|
+
required: ["qualified", "still_running", "failed", "quota_exceeded"]
|
|
3448
4560
|
},
|
|
3449
4561
|
execute: async (client, params, ctx) => {
|
|
3450
4562
|
const wantCount = Math.min(params.count ?? DEFAULT_COUNT, MAX_COUNT);
|
|
@@ -3516,11 +4628,33 @@ var bulkQualifyLeads = {
|
|
|
3516
4628
|
}
|
|
3517
4629
|
}
|
|
3518
4630
|
}
|
|
4631
|
+
let progressDone = 0;
|
|
4632
|
+
const progressTotal = launched.length;
|
|
4633
|
+
if (progressTotal > 0) {
|
|
4634
|
+
ctx?.progress?.({
|
|
4635
|
+
progress: 0,
|
|
4636
|
+
total: progressTotal,
|
|
4637
|
+
message: `Starting qualification for ${progressTotal} lead${progressTotal === 1 ? "" : "s"}`
|
|
4638
|
+
});
|
|
4639
|
+
}
|
|
4640
|
+
const sleepWithSignal = (ms) => new Promise((resolve) => {
|
|
4641
|
+
if (ctx?.signal?.aborted) {
|
|
4642
|
+
resolve();
|
|
4643
|
+
return;
|
|
4644
|
+
}
|
|
4645
|
+
const t = setTimeout(resolve, ms);
|
|
4646
|
+
ctx?.signal?.addEventListener("abort", () => {
|
|
4647
|
+
clearTimeout(t);
|
|
4648
|
+
resolve();
|
|
4649
|
+
}, { once: true });
|
|
4650
|
+
});
|
|
3519
4651
|
const results = await Promise.all(launched.map(async (leadId) => {
|
|
3520
4652
|
const leadDeadline = Math.min(Date.now() + perLeadBudget, totalDeadline);
|
|
3521
4653
|
let lastQual = null;
|
|
3522
4654
|
let lastWf = null;
|
|
3523
4655
|
while (Date.now() < leadDeadline) {
|
|
4656
|
+
if (ctx?.signal?.aborted)
|
|
4657
|
+
break;
|
|
3524
4658
|
try {
|
|
3525
4659
|
const [wfR, qualR] = await Promise.allSettled([
|
|
3526
4660
|
client.request("GET", `/leads/${leadId}/web_fetch`),
|
|
@@ -3531,11 +4665,20 @@ var bulkQualifyLeads = {
|
|
|
3531
4665
|
if (qualR.status === "fulfilled")
|
|
3532
4666
|
lastQual = qualR.value;
|
|
3533
4667
|
const done = lastWf !== null && lastWf.in_progress !== true && Array.isArray(lastQual) && lastQual.length > 0 && lastQual.every((r) => r.score != null);
|
|
3534
|
-
if (done)
|
|
4668
|
+
if (done) {
|
|
4669
|
+
progressDone += 1;
|
|
4670
|
+
ctx?.progress?.({
|
|
4671
|
+
progress: progressDone,
|
|
4672
|
+
total: progressTotal,
|
|
4673
|
+
message: `Qualified lead ${leadId} (${progressDone}/${progressTotal})`
|
|
4674
|
+
});
|
|
3535
4675
|
break;
|
|
4676
|
+
}
|
|
3536
4677
|
} catch {
|
|
3537
4678
|
}
|
|
3538
|
-
|
|
4679
|
+
if (ctx?.signal?.aborted)
|
|
4680
|
+
break;
|
|
4681
|
+
await sleepWithSignal(5e3);
|
|
3539
4682
|
}
|
|
3540
4683
|
const stillRunning = lastWf?.in_progress === true || !lastQual || lastQual.length === 0 || lastQual.some((r) => r.score == null);
|
|
3541
4684
|
const responses = lastQual ?? [];
|
|
@@ -3646,6 +4789,16 @@ function buildFingerprintInput(mappings) {
|
|
|
3646
4789
|
}
|
|
3647
4790
|
var importAndQualify = {
|
|
3648
4791
|
name: "leadbay_import_and_qualify",
|
|
4792
|
+
annotations: {
|
|
4793
|
+
title: "Import + qualify leads",
|
|
4794
|
+
readOnlyHint: false,
|
|
4795
|
+
destructiveHint: true,
|
|
4796
|
+
// Composite of import (idempotent against domain hash) + qualify (which
|
|
4797
|
+
// is silent no-op for already-qualified leads). bulk-store + import
|
|
4798
|
+
// hashes return same handles on retry.
|
|
4799
|
+
idempotentHint: true,
|
|
4800
|
+
openWorldHint: true
|
|
4801
|
+
},
|
|
3649
4802
|
description: `Composite: import a list of leads (CSV-shaped records OR a list of domains), then trigger Leadbay's AI qualification (web research + per-question scoring) on every imported leadId, and return both the import outcome and the per-lead qualification answers \u2014 in one call. Honours a total wall-clock budget; when the budget is exhausted before all leads finish qualifying, returns a \`qualify_id\` UUID handle that survives MCP restart and can be passed to leadbay_qualify_status to retrieve the rest of the answers later.
|
|
3650
4803
|
|
|
3651
4804
|
Inputs:
|
|
@@ -3690,7 +4843,11 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
3690
4843
|
description: "Optional display name override; defaults to the domain."
|
|
3691
4844
|
}
|
|
3692
4845
|
},
|
|
3693
|
-
required: ["domain"]
|
|
4846
|
+
required: ["domain"],
|
|
4847
|
+
// Closed shape — extra keys silently no-op, so reject explicitly.
|
|
4848
|
+
// Parallel surface to import_leads.domains[] (iter 13). Per second-
|
|
4849
|
+
// opinion #2 finding #3.
|
|
4850
|
+
additionalProperties: false
|
|
3694
4851
|
}
|
|
3695
4852
|
},
|
|
3696
4853
|
records: {
|
|
@@ -3712,7 +4869,11 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
3712
4869
|
},
|
|
3713
4870
|
statuses: { type: "object", description: "Optional status string mapping." },
|
|
3714
4871
|
default_status: { type: ["string", "null"], description: "Optional default status." }
|
|
3715
|
-
}
|
|
4872
|
+
},
|
|
4873
|
+
// mappings has a closed shape (fields/custom_fields/statuses/default_status).
|
|
4874
|
+
// Inner objects (fields, custom_fields, statuses) keep open shapes
|
|
4875
|
+
// because their keys are user-defined CSV column names.
|
|
4876
|
+
additionalProperties: false
|
|
3716
4877
|
},
|
|
3717
4878
|
per_lead_budget_ms: {
|
|
3718
4879
|
type: "number",
|
|
@@ -3737,7 +4898,105 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
3737
4898
|
type: "boolean",
|
|
3738
4899
|
description: "When true (default), skips web_fetch launch on leads whose ai_agent_lead_score is already non-null. Saves quota. Set false to force fresh re-qualification."
|
|
3739
4900
|
}
|
|
3740
|
-
}
|
|
4901
|
+
},
|
|
4902
|
+
additionalProperties: false
|
|
4903
|
+
},
|
|
4904
|
+
outputSchema: {
|
|
4905
|
+
type: "object",
|
|
4906
|
+
description: "Two return shapes: kind:'preview' (when dry_run='preview') with mapping hints; kind:'result' (default) with imported + qualified leads + qualify_id handle.",
|
|
4907
|
+
properties: {
|
|
4908
|
+
kind: {
|
|
4909
|
+
type: "string",
|
|
4910
|
+
description: "'result' (full flow) or 'preview' (dry_run='preview' mapping diagnostics)."
|
|
4911
|
+
},
|
|
4912
|
+
// preview-shape keys
|
|
4913
|
+
mapping_hints: {
|
|
4914
|
+
type: "array",
|
|
4915
|
+
description: "Per-column AI-confidence suggestions (preview shape).",
|
|
4916
|
+
items: { type: "object" }
|
|
4917
|
+
},
|
|
4918
|
+
custom_field_candidates: {
|
|
4919
|
+
type: "array",
|
|
4920
|
+
description: "Org custom fields that match unmapped columns (preview shape).",
|
|
4921
|
+
items: { type: "object" }
|
|
4922
|
+
},
|
|
4923
|
+
sample_rows: {
|
|
4924
|
+
type: "array",
|
|
4925
|
+
description: "First few rows of the preprocessed sample (preview shape).",
|
|
4926
|
+
items: { type: "object" }
|
|
4927
|
+
},
|
|
4928
|
+
notes: {
|
|
4929
|
+
type: "array",
|
|
4930
|
+
description: "Operator notes (e.g., catalog fetch errors).",
|
|
4931
|
+
items: { type: "string" }
|
|
4932
|
+
},
|
|
4933
|
+
import_id: {
|
|
4934
|
+
type: "string",
|
|
4935
|
+
description: "Backend file-import handle (preview shape)."
|
|
4936
|
+
},
|
|
4937
|
+
// result-shape keys
|
|
4938
|
+
dry_run: { type: "boolean", description: "True when dry_run:true was passed." },
|
|
4939
|
+
chosen_budgets: {
|
|
4940
|
+
type: "object",
|
|
4941
|
+
description: "Adaptive budgets the composite selected (when caller didn't override): {per_lead_budget_ms, total_budget_ms, per_phase_budget_ms, wall_clock_estimate_ms, strategy}."
|
|
4942
|
+
},
|
|
4943
|
+
qualify_id: {
|
|
4944
|
+
type: ["string", "null"],
|
|
4945
|
+
description: "UUIDv4 handle for polling via leadbay_qualify_status. Null when no leads were qualified."
|
|
4946
|
+
},
|
|
4947
|
+
import_ids: {
|
|
4948
|
+
type: "array",
|
|
4949
|
+
description: "Backend file-import handles (one per chunk).",
|
|
4950
|
+
items: { type: "string" }
|
|
4951
|
+
},
|
|
4952
|
+
imported: {
|
|
4953
|
+
type: "array",
|
|
4954
|
+
description: "Leads that landed in CRM. Each: {leadId, domain?, name, rowId?}.",
|
|
4955
|
+
items: { type: "object" }
|
|
4956
|
+
},
|
|
4957
|
+
not_imported: {
|
|
4958
|
+
type: "array",
|
|
4959
|
+
description: "Inputs that didn't land. Each has a `reason` plus the input echo.",
|
|
4960
|
+
items: { type: "object" }
|
|
4961
|
+
},
|
|
4962
|
+
qualified: {
|
|
4963
|
+
type: "array",
|
|
4964
|
+
description: "Leads whose qualification settled within budgets.",
|
|
4965
|
+
items: { type: "object" }
|
|
4966
|
+
},
|
|
4967
|
+
still_running: {
|
|
4968
|
+
type: "array",
|
|
4969
|
+
description: "Leads still being qualified at deadline; agent calls leadbay_qualify_status with qualify_id.",
|
|
4970
|
+
items: { type: "object" }
|
|
4971
|
+
},
|
|
4972
|
+
failed: {
|
|
4973
|
+
type: "array",
|
|
4974
|
+
description: "Per-lead errors observed during qualification.",
|
|
4975
|
+
items: { type: "object" }
|
|
4976
|
+
},
|
|
4977
|
+
quota_exceeded: { type: "boolean" },
|
|
4978
|
+
skipped_already_qualified: {
|
|
4979
|
+
type: "array",
|
|
4980
|
+
description: "Lead ids skipped because ai_agent_lead_score was already non-null (skip_already_qualified=true).",
|
|
4981
|
+
items: { type: "string" }
|
|
4982
|
+
},
|
|
4983
|
+
not_in_lens: {
|
|
4984
|
+
type: "array",
|
|
4985
|
+
description: "Lead ids that aren't members of the active lens \u2014 backend won't qualify them.",
|
|
4986
|
+
items: { type: "string" }
|
|
4987
|
+
},
|
|
4988
|
+
reused: {
|
|
4989
|
+
type: "boolean",
|
|
4990
|
+
description: "True when an identical qualify_id was launched within the idempotency window."
|
|
4991
|
+
},
|
|
4992
|
+
seconds_since_original: { type: "number" },
|
|
4993
|
+
cancelled: { type: "boolean", description: "True when ctx.signal aborted mid-flight." },
|
|
4994
|
+
budget_exhausted: { type: "boolean", description: "True when total_budget_ms hit before all leads finished." },
|
|
4995
|
+
quota_blocked: { type: "boolean", description: "True when quota was exhausted before launching all leads." },
|
|
4996
|
+
region: { type: "string" },
|
|
4997
|
+
_meta: { type: "object" }
|
|
4998
|
+
},
|
|
4999
|
+
required: ["kind", "region", "_meta"]
|
|
3741
5000
|
},
|
|
3742
5001
|
execute: async (client, params, ctx) => {
|
|
3743
5002
|
const signal = ctx?.signal;
|
|
@@ -3756,6 +5015,11 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
3756
5015
|
if (!ctx?.bulkTracker) {
|
|
3757
5016
|
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_import_and_qualify needs a BulkTracker (qualify_id persistence). Upgrade to @leadbay/mcp \u22650.5.0 or set LEADBAY_BULK_STORE_ALLOW_MEMORY=1.", "");
|
|
3758
5017
|
}
|
|
5018
|
+
ctx?.progress?.({
|
|
5019
|
+
progress: 1,
|
|
5020
|
+
total: 3,
|
|
5021
|
+
message: "Importing leads (phase 1/3 \u2014 preprocess + commit)"
|
|
5022
|
+
});
|
|
3759
5023
|
const importResult = await importLeads.execute(client, {
|
|
3760
5024
|
domains: params.domains,
|
|
3761
5025
|
records: params.records,
|
|
@@ -3825,6 +5089,11 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
3825
5089
|
}
|
|
3826
5090
|
};
|
|
3827
5091
|
}
|
|
5092
|
+
ctx?.progress?.({
|
|
5093
|
+
progress: 2,
|
|
5094
|
+
total: 3,
|
|
5095
|
+
message: "Import committed; preparing qualification (phase 2/3)"
|
|
5096
|
+
});
|
|
3828
5097
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
3829
5098
|
const leadIdsFromImported = imported.map((l) => l.leadId);
|
|
3830
5099
|
const leadIdSet = new Set(leadIdsFromImported);
|
|
@@ -3877,6 +5146,11 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
3877
5146
|
} catch (err) {
|
|
3878
5147
|
ctx?.logger?.warn?.(`qualify: question order unavailable (${err?.code ?? err?.message ?? "unknown"}) \u2014 falling back to alphabetical`);
|
|
3879
5148
|
}
|
|
5149
|
+
ctx?.progress?.({
|
|
5150
|
+
progress: 3,
|
|
5151
|
+
total: 3,
|
|
5152
|
+
message: `Qualifying ${leadIds.length} lead${leadIds.length === 1 ? "" : "s"} (phase 3/3)`
|
|
5153
|
+
});
|
|
3880
5154
|
const fanOut = await fanOutWebFetchAndPoll(client, leadIds, {
|
|
3881
5155
|
perLeadBudgetMs: perLeadBudget,
|
|
3882
5156
|
totalDeadlineMs: totalDeadline,
|
|
@@ -3886,6 +5160,13 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
3886
5160
|
skipAlreadyQualifiedLaunch: skipAlreadyQualified,
|
|
3887
5161
|
...questionOrder ? { questionOrder } : {}
|
|
3888
5162
|
});
|
|
5163
|
+
if (fanOut.cancelled) {
|
|
5164
|
+
try {
|
|
5165
|
+
await ctx.bulkTracker.markCancelled(reservation.record.bulk_id);
|
|
5166
|
+
} catch (err) {
|
|
5167
|
+
ctx?.logger?.warn?.(`import_and_qualify: tracker.markCancelled failed: ${err?.message ?? err}`);
|
|
5168
|
+
}
|
|
5169
|
+
}
|
|
3889
5170
|
const qualified = fanOut.results.filter((r) => !r._stillRunning).map(({ _stillRunning, ...rest }) => rest);
|
|
3890
5171
|
const notInLensSet = new Set(fanOut.not_in_lens);
|
|
3891
5172
|
const stillRunningIds = new Set([
|
|
@@ -3963,6 +5244,11 @@ async function runPreview(client, params, ctx, perPhaseBudget, _totalBudget) {
|
|
|
3963
5244
|
const upload = await client.requestRawBinary("POST", `/imports?file_name=${encodeURIComponent(fileName)}`, "text/csv", csv);
|
|
3964
5245
|
const importId = upload.id;
|
|
3965
5246
|
const signal = ctx?.signal;
|
|
5247
|
+
ctx?.progress?.({
|
|
5248
|
+
progress: 1,
|
|
5249
|
+
total: 3,
|
|
5250
|
+
message: "Preprocessing import (phase 1/3)"
|
|
5251
|
+
});
|
|
3966
5252
|
const deadline = Date.now() + perPhaseBudget;
|
|
3967
5253
|
let fileImport = null;
|
|
3968
5254
|
while (Date.now() < deadline) {
|
|
@@ -3972,6 +5258,11 @@ async function runPreview(client, params, ctx, perPhaseBudget, _totalBudget) {
|
|
|
3972
5258
|
const r = await client.request("GET", `/imports/${importId}`);
|
|
3973
5259
|
if (r.pre_processing?.finished) {
|
|
3974
5260
|
fileImport = r;
|
|
5261
|
+
ctx?.progress?.({
|
|
5262
|
+
progress: 2,
|
|
5263
|
+
total: 3,
|
|
5264
|
+
message: "Preprocess complete; committing import (phase 2/3)"
|
|
5265
|
+
});
|
|
3975
5266
|
break;
|
|
3976
5267
|
}
|
|
3977
5268
|
await new Promise((res) => {
|
|
@@ -4411,6 +5702,18 @@ var LocalBulkStore = class {
|
|
|
4411
5702
|
this.logger?.info?.(`bulk.launch_failed bulk_id=${bulk_id}`);
|
|
4412
5703
|
});
|
|
4413
5704
|
}
|
|
5705
|
+
async markCancelled(bulk_id) {
|
|
5706
|
+
return this.mutex.run(async () => {
|
|
5707
|
+
const all = this.prune(await this.readAll());
|
|
5708
|
+
const idx = all.findIndex((r) => r.bulk_id === bulk_id);
|
|
5709
|
+
if (idx < 0) {
|
|
5710
|
+
return;
|
|
5711
|
+
}
|
|
5712
|
+
all[idx] = { ...all[idx], status: "cancelled" };
|
|
5713
|
+
await this.writeAll(all);
|
|
5714
|
+
this.logger?.info?.(`bulk.cancelled bulk_id=${bulk_id}`);
|
|
5715
|
+
});
|
|
5716
|
+
}
|
|
4414
5717
|
async get(bulk_id) {
|
|
4415
5718
|
return this.mutex.run(async () => {
|
|
4416
5719
|
const all = this.prune(await this.readAll());
|
|
@@ -4470,6 +5773,13 @@ async function createDefaultBulkStore(opts = {}) {
|
|
|
4470
5773
|
// ../core/dist/composite/qualify-status.js
|
|
4471
5774
|
var qualifyStatus = {
|
|
4472
5775
|
name: "leadbay_qualify_status",
|
|
5776
|
+
annotations: {
|
|
5777
|
+
title: "Poll import-and-qualify status",
|
|
5778
|
+
readOnlyHint: true,
|
|
5779
|
+
destructiveHint: false,
|
|
5780
|
+
idempotentHint: true,
|
|
5781
|
+
openWorldHint: true
|
|
5782
|
+
},
|
|
4473
5783
|
description: "Retrieve the current state of an import_and_qualify launch by `qualify_id`. Returns the same `qualified[]` / `still_running[]` shape as the original composite, refreshed against the backend at call time. The handle is persisted to ~/.leadbay/bulks.json with a 30-day TTL and survives MCP restart.\n\nWhen to use: after leadbay_import_and_qualify returned a qualify_id with non-empty `still_running[]`, call this tool a few minutes later (or hours) to retrieve the now-completed qualifications without re-running the import or re-spending qualify quota.\nWhen NOT to use: as a substitute for leadbay_research_lead \u2014 that's a deeper per-lead profile and includes contacts. This tool is purely the qualification answers + signals_count.",
|
|
4474
5784
|
inputSchema: {
|
|
4475
5785
|
type: "object",
|
|
@@ -4479,7 +5789,70 @@ var qualifyStatus = {
|
|
|
4479
5789
|
description: "UUIDv4 returned by leadbay_import_and_qualify when at least one lead was still running."
|
|
4480
5790
|
}
|
|
4481
5791
|
},
|
|
4482
|
-
required: ["qualify_id"]
|
|
5792
|
+
required: ["qualify_id"],
|
|
5793
|
+
additionalProperties: false
|
|
5794
|
+
},
|
|
5795
|
+
outputSchema: {
|
|
5796
|
+
type: "object",
|
|
5797
|
+
properties: {
|
|
5798
|
+
qualify_id: { type: "string", description: "Echoed UUIDv4 handle." },
|
|
5799
|
+
launched_at: { type: "string", description: "ISO timestamp of original launch." },
|
|
5800
|
+
status: { type: "string", description: "'launched' on success (other states surface as error envelopes)." },
|
|
5801
|
+
import_ids: {
|
|
5802
|
+
type: "array",
|
|
5803
|
+
description: "Underlying file-import handle ids (one per chunk).",
|
|
5804
|
+
items: { type: "string" }
|
|
5805
|
+
},
|
|
5806
|
+
lens_id: { type: "number", description: "Lens id the qualification ran against." },
|
|
5807
|
+
lead_ids: {
|
|
5808
|
+
type: "array",
|
|
5809
|
+
description: "Lead UUIDs covered by this qualify_id (echoed from launch).",
|
|
5810
|
+
items: { type: "string" }
|
|
5811
|
+
},
|
|
5812
|
+
qualified: {
|
|
5813
|
+
type: "array",
|
|
5814
|
+
description: "Leads whose qualification has settled. Each entry: {lead_id, qualification_summary, signals_count, ...}.",
|
|
5815
|
+
items: { type: "object" }
|
|
5816
|
+
},
|
|
5817
|
+
still_running: {
|
|
5818
|
+
type: "array",
|
|
5819
|
+
description: "Leads still being qualified at refresh time.",
|
|
5820
|
+
items: { type: "object" }
|
|
5821
|
+
},
|
|
5822
|
+
failed: {
|
|
5823
|
+
type: "array",
|
|
5824
|
+
description: "Per-lead errors observed at refresh (e.g., 404 on /web_fetch + /ai_agent_responses).",
|
|
5825
|
+
items: { type: "object" }
|
|
5826
|
+
},
|
|
5827
|
+
not_in_lens: {
|
|
5828
|
+
type: "array",
|
|
5829
|
+
description: "Lead ids that exist in the org but aren't members of the active lens \u2014 backend won't qualify them; agent should stop polling.",
|
|
5830
|
+
items: { type: "string" }
|
|
5831
|
+
},
|
|
5832
|
+
per_lead_budget_ms: {
|
|
5833
|
+
type: "number",
|
|
5834
|
+
description: "Caller-supplied per-lead timeout (informational only at status time)."
|
|
5835
|
+
},
|
|
5836
|
+
total_budget_ms: {
|
|
5837
|
+
type: "number",
|
|
5838
|
+
description: "Caller-supplied total timeout (informational only at status time)."
|
|
5839
|
+
},
|
|
5840
|
+
region: { type: "string" },
|
|
5841
|
+
_meta: { type: "object" }
|
|
5842
|
+
},
|
|
5843
|
+
required: [
|
|
5844
|
+
"qualify_id",
|
|
5845
|
+
"status",
|
|
5846
|
+
"import_ids",
|
|
5847
|
+
"lens_id",
|
|
5848
|
+
"lead_ids",
|
|
5849
|
+
"qualified",
|
|
5850
|
+
"still_running",
|
|
5851
|
+
"failed",
|
|
5852
|
+
"not_in_lens",
|
|
5853
|
+
"region",
|
|
5854
|
+
"_meta"
|
|
5855
|
+
]
|
|
4483
5856
|
},
|
|
4484
5857
|
execute: async (client, params, ctx) => {
|
|
4485
5858
|
if (!isValidBulkId(params.qualify_id)) {
|
|
@@ -4502,18 +5875,36 @@ var qualifyStatus = {
|
|
|
4502
5875
|
if (record.status === "failed") {
|
|
4503
5876
|
throw client.makeError("BULK_LAUNCH_FAILED", "The original import_and_qualify launch failed; no qualifications were ordered", "Call leadbay_import_and_qualify again \u2014 the failed record won't block a fresh launch.", "");
|
|
4504
5877
|
}
|
|
5878
|
+
if (record.status === "cancelled") {
|
|
5879
|
+
throw client.makeError("BULK_CANCELLED", "The qualify run was cancelled (ctx.signal aborted by the client mid-flight); no further qualifications are in flight", "Call leadbay_import_and_qualify again with the same input to relaunch \u2014 the cancelled record won't block a fresh launch.", "");
|
|
5880
|
+
}
|
|
5881
|
+
ctx?.progress?.({
|
|
5882
|
+
progress: 1,
|
|
5883
|
+
total: 3,
|
|
5884
|
+
message: "Loading qualification questions\u2026"
|
|
5885
|
+
});
|
|
4505
5886
|
let questionOrder = void 0;
|
|
4506
5887
|
try {
|
|
4507
5888
|
const taste = await client.resolveTasteProfile();
|
|
4508
5889
|
questionOrder = buildQuestionOrder(taste.qualificationQuestions ?? []);
|
|
4509
5890
|
} catch {
|
|
4510
5891
|
}
|
|
5892
|
+
ctx?.progress?.({
|
|
5893
|
+
progress: 2,
|
|
5894
|
+
total: 3,
|
|
5895
|
+
message: `Checking lens membership for ${record.lead_ids.length} lead${record.lead_ids.length === 1 ? "" : "s"}\u2026`
|
|
5896
|
+
});
|
|
4511
5897
|
let notInLensSet = /* @__PURE__ */ new Set();
|
|
4512
5898
|
try {
|
|
4513
5899
|
const pre = await prequalifiedLeads(client, record.lead_ids, record.lens_id, ctx);
|
|
4514
5900
|
notInLensSet = pre.not_in_lens;
|
|
4515
5901
|
} catch {
|
|
4516
5902
|
}
|
|
5903
|
+
ctx?.progress?.({
|
|
5904
|
+
progress: 3,
|
|
5905
|
+
total: 3,
|
|
5906
|
+
message: `Refreshing qualification state for ${record.lead_ids.length} lead${record.lead_ids.length === 1 ? "" : "s"}\u2026`
|
|
5907
|
+
});
|
|
4517
5908
|
const fresh = await refreshLeadStates(client, record.lead_ids, questionOrder);
|
|
4518
5909
|
const failed = [];
|
|
4519
5910
|
const qualified = [];
|
|
@@ -4564,6 +5955,18 @@ var qualifyStatus = {
|
|
|
4564
5955
|
var DEFAULT_CANDIDATE_COUNT = 25;
|
|
4565
5956
|
var enrichTitles = {
|
|
4566
5957
|
name: "leadbay_enrich_titles",
|
|
5958
|
+
annotations: {
|
|
5959
|
+
title: "Enrich contact titles across leads",
|
|
5960
|
+
readOnlyHint: false,
|
|
5961
|
+
// Mode A (no titles): non-destructive preview returning candidates.
|
|
5962
|
+
// Mode B (with titles): launches enrichment job. Net classification is
|
|
5963
|
+
// destructive because the dominant flow mutates state.
|
|
5964
|
+
destructiveHint: true,
|
|
5965
|
+
// Idempotent against the same selection + titles set (same hash → same
|
|
5966
|
+
// bulk_id; backend silently no-ops on already-enriched contacts).
|
|
5967
|
+
idempotentHint: true,
|
|
5968
|
+
openWorldHint: true
|
|
5969
|
+
},
|
|
4567
5970
|
description: "Order contact enrichments by job title across many leads. Contacts are NOT returned by default with a lead (Leadbay keeps enrichment out-of-band to control cost); the agent requests them on demand via this tool when it's ready to actually reach out. Two modes: (A) NO titles param \u2014 returns the available titles + Leadbay's title_suggestions + auto_included_titles + a count of enrichable contacts, so the agent can ask the user which titles to enrich. (B) titles given \u2014 calls preview, then launches if there's anything enrichable. On 429 returns {status:'quota_exceeded'} cleanly. Selection lifecycle is wrapped in a try/finally so the user's selection is left clean even on error. When to use: as the agent's go-to enrichment entry point, immediately before proposing outreach. When NOT to use: to enrich a single contact \u2014 that's leadbay_enrich_contacts (granular). Speculatively, before the user has committed to outreaching \u2014 enrichment spends credits.",
|
|
4568
5971
|
inputSchema: {
|
|
4569
5972
|
type: "object",
|
|
@@ -4592,6 +5995,100 @@ var enrichTitles = {
|
|
|
4592
5995
|
type: "boolean",
|
|
4593
5996
|
description: "If true, don't launch \u2014 only preview."
|
|
4594
5997
|
}
|
|
5998
|
+
},
|
|
5999
|
+
additionalProperties: false
|
|
6000
|
+
},
|
|
6001
|
+
outputSchema: {
|
|
6002
|
+
type: "object",
|
|
6003
|
+
description: "Branchy return shape; the `mode` (or `status`) field tells the agent which branch it got. Modes: 'discover' (no titles passed), 'preview_only' (no enrichable contacts), 'dry_run', 'already_launched' (idempotent reuse), 'launched_tracker_pending' (rare, soft-fail), 'launched' (happy path). Status: 'quota_exceeded' (429).",
|
|
6004
|
+
properties: {
|
|
6005
|
+
mode: {
|
|
6006
|
+
type: "string",
|
|
6007
|
+
description: "'discover' | 'preview_only' | 'dry_run' | 'already_launched' | 'launched_tracker_pending' | 'launched'."
|
|
6008
|
+
},
|
|
6009
|
+
status: {
|
|
6010
|
+
type: "string",
|
|
6011
|
+
description: "'quota_exceeded' on 429. Otherwise mode is set instead."
|
|
6012
|
+
},
|
|
6013
|
+
available_titles: {
|
|
6014
|
+
type: "array",
|
|
6015
|
+
description: "Titles available across the selection (discover/preview_only modes).",
|
|
6016
|
+
items: { type: "string" }
|
|
6017
|
+
},
|
|
6018
|
+
recommendations: {
|
|
6019
|
+
type: "array",
|
|
6020
|
+
description: "Backend's title_suggestions (discover mode).",
|
|
6021
|
+
items: { type: "string" }
|
|
6022
|
+
},
|
|
6023
|
+
auto_included: {
|
|
6024
|
+
type: "array",
|
|
6025
|
+
description: "Backend's auto_included_titles (discover mode).",
|
|
6026
|
+
items: { type: "string" }
|
|
6027
|
+
},
|
|
6028
|
+
previously_enriched: {
|
|
6029
|
+
type: "array",
|
|
6030
|
+
description: "Titles previously enriched on this selection (discover mode).",
|
|
6031
|
+
items: { type: "string" }
|
|
6032
|
+
},
|
|
6033
|
+
enrichable_contacts: {
|
|
6034
|
+
type: "number",
|
|
6035
|
+
description: "Count of enrichable contacts at preview time."
|
|
6036
|
+
},
|
|
6037
|
+
selected_lead_count: {
|
|
6038
|
+
type: "number",
|
|
6039
|
+
description: "How many leads the selection covers."
|
|
6040
|
+
},
|
|
6041
|
+
preview: {
|
|
6042
|
+
type: "object",
|
|
6043
|
+
description: "Backend BulkEnrichPreview payload (preview_only/dry_run/launched modes)."
|
|
6044
|
+
},
|
|
6045
|
+
launched: {
|
|
6046
|
+
type: "boolean",
|
|
6047
|
+
description: "True when an enrichment job is in flight on the backend."
|
|
6048
|
+
},
|
|
6049
|
+
would_launch: {
|
|
6050
|
+
type: "object",
|
|
6051
|
+
description: "What dry_run WOULD have launched (titles, email, phone)."
|
|
6052
|
+
},
|
|
6053
|
+
re_used: {
|
|
6054
|
+
type: "boolean",
|
|
6055
|
+
description: "True when an identical bulk was launched within the idempotency window (already_launched mode)."
|
|
6056
|
+
},
|
|
6057
|
+
bulk_id: {
|
|
6058
|
+
type: "string",
|
|
6059
|
+
description: "UUIDv4 to poll via leadbay_bulk_enrich_status."
|
|
6060
|
+
},
|
|
6061
|
+
launched_at: {
|
|
6062
|
+
type: "string",
|
|
6063
|
+
description: "ISO timestamp of the (re-used or fresh) launch."
|
|
6064
|
+
},
|
|
6065
|
+
durability: {
|
|
6066
|
+
type: "string",
|
|
6067
|
+
description: "'file' (persisted bulks.json) or 'memory'."
|
|
6068
|
+
},
|
|
6069
|
+
seconds_since_original_launch: {
|
|
6070
|
+
type: "number",
|
|
6071
|
+
description: "Age of the re-used bulk record (already_launched mode)."
|
|
6072
|
+
},
|
|
6073
|
+
titles: {
|
|
6074
|
+
type: "array",
|
|
6075
|
+
description: "Titles ordered (echoed at launch).",
|
|
6076
|
+
items: { type: "string" }
|
|
6077
|
+
},
|
|
6078
|
+
email: { type: "boolean" },
|
|
6079
|
+
phone: { type: "boolean" },
|
|
6080
|
+
message: {
|
|
6081
|
+
type: "string",
|
|
6082
|
+
description: "Operator-facing summary of what happened."
|
|
6083
|
+
},
|
|
6084
|
+
next_action: {
|
|
6085
|
+
type: "string",
|
|
6086
|
+
description: "Concrete next-step instruction for the agent."
|
|
6087
|
+
},
|
|
6088
|
+
retry_after_seconds: {
|
|
6089
|
+
type: ["number", "null"],
|
|
6090
|
+
description: "Seconds until quota resets (quota_exceeded status)."
|
|
6091
|
+
}
|
|
4595
6092
|
}
|
|
4596
6093
|
},
|
|
4597
6094
|
execute: async (client, params, ctx) => {
|
|
@@ -4622,11 +6119,21 @@ var enrichTitles = {
|
|
|
4622
6119
|
hint: "Pass leadIds explicitly or wait for the wishlist to compute"
|
|
4623
6120
|
};
|
|
4624
6121
|
}
|
|
6122
|
+
ctx?.progress?.({
|
|
6123
|
+
progress: 1,
|
|
6124
|
+
total: 3,
|
|
6125
|
+
message: `Selecting ${leadIds.length} lead${leadIds.length === 1 ? "" : "s"}\u2026`
|
|
6126
|
+
});
|
|
4625
6127
|
await client.acquireSelectionLock();
|
|
4626
6128
|
try {
|
|
4627
6129
|
const qs = leadIds.map((id) => `leadIds=${encodeURIComponent(id)}`).join("&");
|
|
4628
6130
|
await client.requestVoid("POST", `/leads/selection/select?${qs}`);
|
|
4629
6131
|
try {
|
|
6132
|
+
ctx?.progress?.({
|
|
6133
|
+
progress: 2,
|
|
6134
|
+
total: 3,
|
|
6135
|
+
message: "Previewing enrichment (titles + counts)\u2026"
|
|
6136
|
+
});
|
|
4630
6137
|
const availableTitles = await client.request("GET", "/leads/selection/enrichment/job_titles");
|
|
4631
6138
|
if (!params.titles || params.titles.length === 0) {
|
|
4632
6139
|
let suggestions = [];
|
|
@@ -4720,14 +6227,24 @@ var enrichTitles = {
|
|
|
4720
6227
|
};
|
|
4721
6228
|
}
|
|
4722
6229
|
}
|
|
6230
|
+
ctx?.progress?.({
|
|
6231
|
+
progress: 3,
|
|
6232
|
+
total: 3,
|
|
6233
|
+
message: `Launching enrichment for ${params.titles.length} title${params.titles.length === 1 ? "" : "s"}\u2026`
|
|
6234
|
+
});
|
|
4723
6235
|
try {
|
|
4724
6236
|
await client.requestVoid("POST", "/leads/selection/enrichment/launch", { titles: params.titles, email, phone });
|
|
4725
6237
|
} catch (err) {
|
|
6238
|
+
const aborted = err?.name === "AbortError" || ctx?.signal?.aborted === true;
|
|
4726
6239
|
if (bulkRecord && tracker) {
|
|
4727
6240
|
try {
|
|
4728
|
-
|
|
6241
|
+
if (aborted) {
|
|
6242
|
+
await tracker.markCancelled(bulkRecord.bulk_id);
|
|
6243
|
+
} else {
|
|
6244
|
+
await tracker.markFailed(bulkRecord.bulk_id);
|
|
6245
|
+
}
|
|
4729
6246
|
} catch (e) {
|
|
4730
|
-
ctx?.logger?.warn?.(`enrich_titles: tracker
|
|
6247
|
+
ctx?.logger?.warn?.(`enrich_titles: tracker.${aborted ? "markCancelled" : "markFailed"} failed: ${e?.message ?? e}`);
|
|
4731
6248
|
}
|
|
4732
6249
|
}
|
|
4733
6250
|
if (err?.code === "QUOTA_EXCEEDED") {
|
|
@@ -4804,6 +6321,13 @@ async function pMap(items, fn, concurrency) {
|
|
|
4804
6321
|
}
|
|
4805
6322
|
var bulkEnrichStatus = {
|
|
4806
6323
|
name: "leadbay_bulk_enrich_status",
|
|
6324
|
+
annotations: {
|
|
6325
|
+
title: "Poll bulk-enrichment status",
|
|
6326
|
+
readOnlyHint: true,
|
|
6327
|
+
destructiveHint: false,
|
|
6328
|
+
idempotentHint: true,
|
|
6329
|
+
openWorldHint: true
|
|
6330
|
+
},
|
|
4807
6331
|
description: "Check status + per-lead contacts for a bulk enrichment you previously launched via leadbay_enrich_titles. Returns the bulk_id, progress per lead (done/total enrichable contacts), and overall progress. When include_contacts=true (opt-in), includes each contact's email/phone/job_title/enrichment.done. When to use: poll this after leadbay_enrich_titles returns a bulk_id. Default include_contacts=false for cheap status polls; set include_contacts=true once all_done flips for the final read. When NOT to use: as a substitute for leadbay_research_lead \u2014 that already includes enriched contacts for a single lead.",
|
|
4808
6332
|
inputSchema: {
|
|
4809
6333
|
type: "object",
|
|
@@ -4817,7 +6341,55 @@ var bulkEnrichStatus = {
|
|
|
4817
6341
|
description: "If true, return the full contact list per lead (email, phone, enrichment.done). Default false \u2014 cheap status polls."
|
|
4818
6342
|
}
|
|
4819
6343
|
},
|
|
4820
|
-
required: ["bulk_id"]
|
|
6344
|
+
required: ["bulk_id"],
|
|
6345
|
+
additionalProperties: false
|
|
6346
|
+
},
|
|
6347
|
+
outputSchema: {
|
|
6348
|
+
type: "object",
|
|
6349
|
+
properties: {
|
|
6350
|
+
bulk_id: { type: "string", description: "Echoed UUIDv4 handle." },
|
|
6351
|
+
launched_at: { type: "string", description: "ISO timestamp of /enrichment/launch ack." },
|
|
6352
|
+
status: {
|
|
6353
|
+
type: "string",
|
|
6354
|
+
description: "'launched' on success. Errors return error envelopes (handled separately)."
|
|
6355
|
+
},
|
|
6356
|
+
durability: {
|
|
6357
|
+
type: "string",
|
|
6358
|
+
description: "'persistent' (file-backed bulks.json) or 'memory' (LEADBAY_BULK_STORE_ALLOW_MEMORY)."
|
|
6359
|
+
},
|
|
6360
|
+
titles: {
|
|
6361
|
+
type: "array",
|
|
6362
|
+
description: "Titles ordered at launch time (echoed from the original enrich_titles call).",
|
|
6363
|
+
items: { type: "string" }
|
|
6364
|
+
},
|
|
6365
|
+
email: { type: "boolean", description: "True if email enrichment was requested." },
|
|
6366
|
+
phone: { type: "boolean", description: "True if phone enrichment was requested." },
|
|
6367
|
+
lens_id: { type: "number", description: "Lens id used to scope the enrichment." },
|
|
6368
|
+
leads: {
|
|
6369
|
+
type: "array",
|
|
6370
|
+
description: "Per-lead rollup: {lead_id, enrichment_progress:{done,total}, contacts? (when include_contacts=true)}.",
|
|
6371
|
+
items: { type: "object" }
|
|
6372
|
+
},
|
|
6373
|
+
overall_progress: {
|
|
6374
|
+
type: "object",
|
|
6375
|
+
description: "Aggregate progress across all leads.",
|
|
6376
|
+
properties: {
|
|
6377
|
+
done: { type: "number" },
|
|
6378
|
+
total: { type: "number" },
|
|
6379
|
+
done_ratio: { type: "number" }
|
|
6380
|
+
}
|
|
6381
|
+
},
|
|
6382
|
+
all_done: {
|
|
6383
|
+
type: "boolean",
|
|
6384
|
+
description: "True when overall_progress.done === total AND no partial_failures."
|
|
6385
|
+
},
|
|
6386
|
+
partial_failures: {
|
|
6387
|
+
type: "array",
|
|
6388
|
+
description: "Per-lead errors observed during contacts fan-out (omitted when no failures).",
|
|
6389
|
+
items: { type: "object" }
|
|
6390
|
+
}
|
|
6391
|
+
},
|
|
6392
|
+
required: ["bulk_id", "status", "leads", "overall_progress", "all_done"]
|
|
4821
6393
|
},
|
|
4822
6394
|
execute: async (client, params, ctx) => {
|
|
4823
6395
|
if (!isValidBulkId(params.bulk_id)) {
|
|
@@ -4886,6 +6458,18 @@ var bulkEnrichStatus = {
|
|
|
4886
6458
|
launched_at: record.launched_at
|
|
4887
6459
|
};
|
|
4888
6460
|
}
|
|
6461
|
+
if (record.status === "cancelled") {
|
|
6462
|
+
return {
|
|
6463
|
+
error: true,
|
|
6464
|
+
code: "BULK_CANCELLED",
|
|
6465
|
+
message: "The bulk was cancelled (ctx.signal aborted by the client mid-launch). No further work is in flight.",
|
|
6466
|
+
hint: "Call leadbay_enrich_titles again with the same titles to relaunch \u2014 the cancelled record won't block a fresh launch.",
|
|
6467
|
+
bulk_id: record.bulk_id,
|
|
6468
|
+
launched_at: record.launched_at
|
|
6469
|
+
};
|
|
6470
|
+
}
|
|
6471
|
+
let doneSoFar = 0;
|
|
6472
|
+
const totalLeads = record.lead_ids.length;
|
|
4889
6473
|
const results = await pMap(record.lead_ids, async (leadId) => {
|
|
4890
6474
|
try {
|
|
4891
6475
|
const out = await getContacts.execute(client, { leadId });
|
|
@@ -4893,6 +6477,12 @@ var bulkEnrichStatus = {
|
|
|
4893
6477
|
const enrichable = contacts.filter((c) => c && c.enrichment);
|
|
4894
6478
|
const done = enrichable.filter((c) => c.enrichment?.done === true).length;
|
|
4895
6479
|
const total = enrichable.length;
|
|
6480
|
+
doneSoFar += 1;
|
|
6481
|
+
ctx?.progress?.({
|
|
6482
|
+
progress: doneSoFar,
|
|
6483
|
+
total: totalLeads,
|
|
6484
|
+
message: `Fetched contacts for ${leadId} (${doneSoFar}/${totalLeads})`
|
|
6485
|
+
});
|
|
4896
6486
|
return {
|
|
4897
6487
|
kind: "ok",
|
|
4898
6488
|
lead_id: leadId,
|
|
@@ -4901,6 +6491,12 @@ var bulkEnrichStatus = {
|
|
|
4901
6491
|
contacts: includeContacts ? contacts : void 0
|
|
4902
6492
|
};
|
|
4903
6493
|
} catch (err) {
|
|
6494
|
+
doneSoFar += 1;
|
|
6495
|
+
ctx?.progress?.({
|
|
6496
|
+
progress: doneSoFar,
|
|
6497
|
+
total: totalLeads,
|
|
6498
|
+
message: `Fetch failed for ${leadId} (${doneSoFar}/${totalLeads}): ${err?.code ?? "UNKNOWN"}`
|
|
6499
|
+
});
|
|
4904
6500
|
return {
|
|
4905
6501
|
kind: "fail",
|
|
4906
6502
|
lead_id: leadId,
|
|
@@ -5041,6 +6637,17 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
|
|
|
5041
6637
|
}
|
|
5042
6638
|
var adjustAudience = {
|
|
5043
6639
|
name: "leadbay_adjust_audience",
|
|
6640
|
+
annotations: {
|
|
6641
|
+
title: "Adjust lens audience filters",
|
|
6642
|
+
readOnlyHint: false,
|
|
6643
|
+
destructiveHint: true,
|
|
6644
|
+
// Each call MERGES new criteria into the lens config; calling twice
|
|
6645
|
+
// with the same args produces the same final state (last write wins on
|
|
6646
|
+
// overlapping criteria, but the merge is deterministic). Per spec
|
|
6647
|
+
// idempotentHint is about same observable outcome — re-call is safe.
|
|
6648
|
+
idempotentHint: true,
|
|
6649
|
+
openWorldHint: true
|
|
6650
|
+
},
|
|
5044
6651
|
description: "Restrict (or expand) the lens audience by sector / size. Free-text sectors are auto-resolved against the sector taxonomy; ambiguous matches are surfaced to the agent rather than guessed silently. Permission routing is hidden: the default lens auto-clones to a new user lens; an org-level lens defaults to a per-user draft (admins can override with save_for_org:true). Filter MERGES with existing criteria (unrelated criteria are not dropped). When to use: when the user wants to see different kinds of leads (sector / size / etc.). When NOT to use: to refine BEYOND firmographics \u2014 that's leadbay_refine_prompt.",
|
|
5045
6652
|
inputSchema: {
|
|
5046
6653
|
type: "object",
|
|
@@ -5077,7 +6684,34 @@ var adjustAudience = {
|
|
|
5077
6684
|
type: "string",
|
|
5078
6685
|
description: "Name to use when this composite has to clone the default lens (otherwise auto-named)"
|
|
5079
6686
|
}
|
|
5080
|
-
}
|
|
6687
|
+
},
|
|
6688
|
+
additionalProperties: false
|
|
6689
|
+
},
|
|
6690
|
+
outputSchema: {
|
|
6691
|
+
type: "object",
|
|
6692
|
+
description: "Two return shapes: 'ambiguous_sectors' when free-text sectors matched multiple candidates (agent re-calls with sector_ids), 'applied' on success.",
|
|
6693
|
+
properties: {
|
|
6694
|
+
status: {
|
|
6695
|
+
type: "string",
|
|
6696
|
+
description: "'ambiguous_sectors' or 'applied'."
|
|
6697
|
+
},
|
|
6698
|
+
sector_ambiguities: {
|
|
6699
|
+
type: "array",
|
|
6700
|
+
description: "Per ambiguous text: {sector_text, matches:[{id, name, score}]}. Agent picks an id and re-calls.",
|
|
6701
|
+
items: { type: "object" }
|
|
6702
|
+
},
|
|
6703
|
+
message: { type: "string" },
|
|
6704
|
+
lens_used: {
|
|
6705
|
+
type: "object",
|
|
6706
|
+
description: "Resolved lens metadata: {id, name, was_draft, was_new, save_for_org}."
|
|
6707
|
+
},
|
|
6708
|
+
filter_applied: {
|
|
6709
|
+
type: "object",
|
|
6710
|
+
description: "The merged FilterPayload that was POSTed to the lens."
|
|
6711
|
+
},
|
|
6712
|
+
_meta: { type: "object" }
|
|
6713
|
+
},
|
|
6714
|
+
required: ["status"]
|
|
5081
6715
|
},
|
|
5082
6716
|
execute: async (client, params, ctx) => {
|
|
5083
6717
|
const me = await client.resolveMe();
|
|
@@ -5151,7 +6785,7 @@ var adjustAudience = {
|
|
|
5151
6785
|
error: true,
|
|
5152
6786
|
code: "ORPHAN_DRAFT",
|
|
5153
6787
|
message: `Draft ${targetLensId} created but filter update failed; draft cleanup also failed`,
|
|
5154
|
-
hint: `
|
|
6788
|
+
hint: `Use leadbay_promote_lens or leadbay_update_lens to recover, or open https://leadbay.app/lenses to manually delete draft lens ${targetLensId}.`,
|
|
5155
6789
|
orphan_draft_id: targetLensId
|
|
5156
6790
|
};
|
|
5157
6791
|
}
|
|
@@ -5189,6 +6823,16 @@ var DEFAULT_POLL_ATTEMPTS = 2;
|
|
|
5189
6823
|
var DEFAULT_POLL_GAP_MS = 5e3;
|
|
5190
6824
|
var refinePrompt = {
|
|
5191
6825
|
name: "leadbay_refine_prompt",
|
|
6826
|
+
annotations: {
|
|
6827
|
+
title: "Refine the audience prompt",
|
|
6828
|
+
readOnlyHint: false,
|
|
6829
|
+
destructiveHint: true,
|
|
6830
|
+
// Sets the org's user_prompt and may trigger a clarification flow. Each
|
|
6831
|
+
// call replaces the prior prompt — a second call with a different
|
|
6832
|
+
// instruction is NOT idempotent (the second prompt wins).
|
|
6833
|
+
idempotentHint: false,
|
|
6834
|
+
openWorldHint: true
|
|
6835
|
+
},
|
|
5192
6836
|
description: "Refine the kind of leads Leadbay surfaces, beyond firmographics. Free-text instruction (e.g. 'focus on hospitals running their own IT'). Sets the org's user_prompt; if the new prompt produces ambiguous criteria, Leadbay raises a clarification question, which this composite polls for and surfaces. Admin-only on the backend (will return 403 for non-admins). When to use: when audience filters (leadbay_adjust_audience) aren't enough. When NOT to use: to answer a pending clarification \u2014 that's leadbay_answer_clarification.",
|
|
5193
6837
|
inputSchema: {
|
|
5194
6838
|
type: "object",
|
|
@@ -5207,7 +6851,44 @@ var refinePrompt = {
|
|
|
5207
6851
|
description: "If true, return the call shape WITHOUT setting the prompt"
|
|
5208
6852
|
}
|
|
5209
6853
|
},
|
|
5210
|
-
required: ["prompt"]
|
|
6854
|
+
required: ["prompt"],
|
|
6855
|
+
additionalProperties: false
|
|
6856
|
+
},
|
|
6857
|
+
outputSchema: {
|
|
6858
|
+
type: "object",
|
|
6859
|
+
description: "Multiple return shapes by status. dry_run, applied (with optional clarified_via_elicit), or clarification_pending.",
|
|
6860
|
+
properties: {
|
|
6861
|
+
dry_run: { type: "boolean", description: "True when dry_run:true was passed (no state change)." },
|
|
6862
|
+
would_call: {
|
|
6863
|
+
type: "object",
|
|
6864
|
+
description: "Dry-run preview of the POST that would have been issued."
|
|
6865
|
+
},
|
|
6866
|
+
status: {
|
|
6867
|
+
type: "string",
|
|
6868
|
+
description: "'applied' (prompt set; intelligence regenerating) or 'clarification_pending' (telephone path)."
|
|
6869
|
+
},
|
|
6870
|
+
computing_intelligence: {
|
|
6871
|
+
type: "boolean",
|
|
6872
|
+
description: "True when intelligence is regenerating after the prompt set."
|
|
6873
|
+
},
|
|
6874
|
+
clarified_via_elicit: {
|
|
6875
|
+
type: "boolean",
|
|
6876
|
+
description: "True when the clarification was answered via the client's elicitation UI (not via telephone)."
|
|
6877
|
+
},
|
|
6878
|
+
message: {
|
|
6879
|
+
type: "string",
|
|
6880
|
+
description: "Operator-facing summary."
|
|
6881
|
+
},
|
|
6882
|
+
clarification: {
|
|
6883
|
+
type: "object",
|
|
6884
|
+
description: "ClarificationPayload returned by the backend (clarification_pending path)."
|
|
6885
|
+
},
|
|
6886
|
+
next_action: {
|
|
6887
|
+
type: "string",
|
|
6888
|
+
description: "Concrete next-step instruction for the agent."
|
|
6889
|
+
},
|
|
6890
|
+
_meta: { type: "object" }
|
|
6891
|
+
}
|
|
5211
6892
|
},
|
|
5212
6893
|
execute: async (client, params, ctx) => {
|
|
5213
6894
|
const me = await client.resolveMe();
|
|
@@ -5259,6 +6940,58 @@ var refinePrompt = {
|
|
|
5259
6940
|
}
|
|
5260
6941
|
}
|
|
5261
6942
|
if (clarification) {
|
|
6943
|
+
if (ctx?.elicit) {
|
|
6944
|
+
const opts = clarification.options ?? [];
|
|
6945
|
+
const requestedSchema = opts.length > 0 ? {
|
|
6946
|
+
type: "object",
|
|
6947
|
+
properties: {
|
|
6948
|
+
option_id: {
|
|
6949
|
+
type: "string",
|
|
6950
|
+
title: "Pick one",
|
|
6951
|
+
description: "Choose the option that best matches your intent.",
|
|
6952
|
+
enum: opts.filter((o) => o.id).map((o) => o.id),
|
|
6953
|
+
enumNames: opts.filter((o) => o.id).map((o) => o.label)
|
|
6954
|
+
}
|
|
6955
|
+
},
|
|
6956
|
+
required: ["option_id"]
|
|
6957
|
+
} : {
|
|
6958
|
+
type: "object",
|
|
6959
|
+
properties: {
|
|
6960
|
+
text_answer: {
|
|
6961
|
+
type: "string",
|
|
6962
|
+
title: "Answer",
|
|
6963
|
+
description: "Free-text answer to the clarification. Plain English."
|
|
6964
|
+
}
|
|
6965
|
+
},
|
|
6966
|
+
required: ["text_answer"]
|
|
6967
|
+
};
|
|
6968
|
+
try {
|
|
6969
|
+
const elicited = await ctx.elicit({
|
|
6970
|
+
message: clarification.question,
|
|
6971
|
+
requestedSchema
|
|
6972
|
+
});
|
|
6973
|
+
if (elicited.action === "accept" && elicited.content) {
|
|
6974
|
+
const body = typeof elicited.content.option_id === "string" ? { option_id: elicited.content.option_id } : typeof elicited.content.text_answer === "string" ? { text_answer: elicited.content.text_answer } : null;
|
|
6975
|
+
if (body) {
|
|
6976
|
+
try {
|
|
6977
|
+
await client.requestVoid("POST", `/organizations/${orgId}/pick_clarification`, body);
|
|
6978
|
+
client.invalidateMe();
|
|
6979
|
+
return {
|
|
6980
|
+
status: "applied",
|
|
6981
|
+
clarified_via_elicit: true,
|
|
6982
|
+
computing_intelligence: true,
|
|
6983
|
+
message: "Prompt set + clarification answered via the client's elicitation UI. Leadbay is regenerating intelligence.",
|
|
6984
|
+
_meta: { region: client.region }
|
|
6985
|
+
};
|
|
6986
|
+
} catch (err) {
|
|
6987
|
+
ctx?.logger?.warn?.(`refine_prompt: pick_clarification POST failed after elicit: ${err?.message ?? err?.code ?? err}`);
|
|
6988
|
+
}
|
|
6989
|
+
}
|
|
6990
|
+
}
|
|
6991
|
+
} catch (err) {
|
|
6992
|
+
ctx?.logger?.warn?.(`refine_prompt: elicit failed: ${err?.message ?? err?.code ?? err} \u2014 falling back to telephone path`);
|
|
6993
|
+
}
|
|
6994
|
+
}
|
|
5262
6995
|
return {
|
|
5263
6996
|
status: "clarification_pending",
|
|
5264
6997
|
clarification,
|
|
@@ -5278,13 +7011,44 @@ var refinePrompt = {
|
|
|
5278
7011
|
// ../core/dist/composite/answer-clarification.js
|
|
5279
7012
|
var answerClarification = {
|
|
5280
7013
|
name: "leadbay_answer_clarification",
|
|
7014
|
+
annotations: {
|
|
7015
|
+
title: "Answer pending clarification",
|
|
7016
|
+
readOnlyHint: false,
|
|
7017
|
+
destructiveHint: true,
|
|
7018
|
+
// Records a one-time answer that becomes the new user_prompt and
|
|
7019
|
+
// triggers regeneration. Re-calling with a different answer wins;
|
|
7020
|
+
// not idempotent.
|
|
7021
|
+
idempotentHint: false,
|
|
7022
|
+
openWorldHint: true
|
|
7023
|
+
},
|
|
5281
7024
|
description: "Answer the pending clarification question Leadbay raised after a refine_prompt. The answer is stored as the new user_prompt and triggers regeneration. Pass option_id (preferred \u2014 pick from the offered options) or text_answer (free-text). Admin-only. When to use: after leadbay_refine_prompt returns status='clarification_pending'. When NOT to use: to set a brand-new prompt \u2014 use leadbay_refine_prompt.",
|
|
5282
7025
|
inputSchema: {
|
|
5283
7026
|
type: "object",
|
|
5284
7027
|
properties: {
|
|
5285
7028
|
option_id: { type: "string", description: "Id of one of the clarification's options" },
|
|
5286
7029
|
text_answer: { type: "string", description: "Free-text answer (overrides option_id)" }
|
|
5287
|
-
}
|
|
7030
|
+
},
|
|
7031
|
+
additionalProperties: false
|
|
7032
|
+
},
|
|
7033
|
+
outputSchema: {
|
|
7034
|
+
type: "object",
|
|
7035
|
+
properties: {
|
|
7036
|
+
status: {
|
|
7037
|
+
type: "string",
|
|
7038
|
+
description: "'answered' (recorded; intelligence regenerating) or 'no_pending_clarification' (nothing to answer)."
|
|
7039
|
+
},
|
|
7040
|
+
recorded_as_user_prompt: {
|
|
7041
|
+
type: "boolean",
|
|
7042
|
+
description: "True when the answer was stored as the org's new user_prompt."
|
|
7043
|
+
},
|
|
7044
|
+
message: { type: "string" },
|
|
7045
|
+
hint: {
|
|
7046
|
+
type: "string",
|
|
7047
|
+
description: "Operator-facing next-step (no_pending_clarification path)."
|
|
7048
|
+
},
|
|
7049
|
+
_meta: { type: "object" }
|
|
7050
|
+
},
|
|
7051
|
+
required: ["status"]
|
|
5288
7052
|
},
|
|
5289
7053
|
execute: async (client, params, ctx) => {
|
|
5290
7054
|
if (!params.option_id && !params.text_answer) {
|
|
@@ -5341,6 +7105,13 @@ function formatNoteWithVerification(note, v) {
|
|
|
5341
7105
|
}
|
|
5342
7106
|
var reportOutreach = {
|
|
5343
7107
|
name: "leadbay_report_outreach",
|
|
7108
|
+
annotations: {
|
|
7109
|
+
title: "Report outreach to Leadbay",
|
|
7110
|
+
readOnlyHint: false,
|
|
7111
|
+
destructiveHint: true,
|
|
7112
|
+
idempotentHint: false,
|
|
7113
|
+
openWorldHint: true
|
|
7114
|
+
},
|
|
5344
7115
|
description: "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.). VERIFICATION REQUIRED: every call must include verification={source: 'gmail_message_id'|'calendar_event_id'|'user_confirmed', ref: '<id-or-confirmation>'} to prevent hallucinated outreach poisoning the pipeline. The verification is appended to the note body. Bulk variant: pass lead_ids=[uuid,...] instead of lead_id (epilogue is bulk-native; notes fan out per-lead). When to use: AFTER actually emailing/calling/meeting/messaging a contact, OR after a substantive decision the user wants logged (skip, save, hand off). When NOT to use: BEFORE doing the outreach (use dry_run:true to validate args first); without verification (call will be rejected); from a flow where the user did not consent to having actions logged automatically.",
|
|
5345
7116
|
optional: true,
|
|
5346
7117
|
write: true,
|
|
@@ -5368,14 +7139,78 @@ var reportOutreach = {
|
|
|
5368
7139
|
source: { type: "string" },
|
|
5369
7140
|
ref: { type: "string" }
|
|
5370
7141
|
},
|
|
5371
|
-
required: ["source", "ref"]
|
|
7142
|
+
required: ["source", "ref"],
|
|
7143
|
+
// Security-load-bearing: the verification field prevents the agent from
|
|
7144
|
+
// poisoning the SDR pipeline with hallucinated outreach. Extra keys here
|
|
7145
|
+
// would create an injection vector (e.g., agent passes
|
|
7146
|
+
// verification.bypass="true"). Hard-rejected per second-opinion #3.
|
|
7147
|
+
additionalProperties: false
|
|
5372
7148
|
},
|
|
5373
7149
|
dry_run: {
|
|
5374
7150
|
type: "boolean",
|
|
5375
7151
|
description: "If true, return what WOULD be called without writing anything"
|
|
5376
7152
|
}
|
|
5377
7153
|
},
|
|
5378
|
-
required: ["note", "verification"]
|
|
7154
|
+
required: ["note", "verification"],
|
|
7155
|
+
additionalProperties: false
|
|
7156
|
+
},
|
|
7157
|
+
outputSchema: {
|
|
7158
|
+
type: "object",
|
|
7159
|
+
description: "Either the dry_run shape (dry_run:true with would_write_notes / would_set_epilogue) OR the live result (notes:{succeeded,failed} + epilogue:{status,applied,error?} + verification + _meta). Schema declares both shapes; the dry_run discriminator picks which sub-shape applies.",
|
|
7160
|
+
properties: {
|
|
7161
|
+
// dry_run discriminator + dry_run subshape (from before iter 13)
|
|
7162
|
+
dry_run: { type: "boolean" },
|
|
7163
|
+
would_write_notes: {
|
|
7164
|
+
type: "array",
|
|
7165
|
+
description: "On dry_run: the per-lead POST shapes that WOULD be issued.",
|
|
7166
|
+
items: { type: "object" }
|
|
7167
|
+
},
|
|
7168
|
+
would_set_epilogue: {
|
|
7169
|
+
type: ["object", "null"],
|
|
7170
|
+
description: "On dry_run: the epilogue POST shape that WOULD be issued."
|
|
7171
|
+
},
|
|
7172
|
+
// Live subshape — what execute() actually returns when dry_run is false.
|
|
7173
|
+
notes: {
|
|
7174
|
+
type: "object",
|
|
7175
|
+
description: "Per-lead note-write outcome (split into succeeded / failed sub-arrays).",
|
|
7176
|
+
properties: {
|
|
7177
|
+
succeeded: {
|
|
7178
|
+
type: "array",
|
|
7179
|
+
items: { type: "object" }
|
|
7180
|
+
},
|
|
7181
|
+
failed: {
|
|
7182
|
+
type: "array",
|
|
7183
|
+
items: { type: "object" }
|
|
7184
|
+
}
|
|
7185
|
+
}
|
|
7186
|
+
},
|
|
7187
|
+
epilogue: {
|
|
7188
|
+
type: "object",
|
|
7189
|
+
description: "Epilogue status outcome: status (the wire-format string written to /leads/epilogue, or null when not requested), applied (true/false), error (when applied=false).",
|
|
7190
|
+
properties: {
|
|
7191
|
+
status: { type: ["string", "null"] },
|
|
7192
|
+
applied: { type: "boolean" },
|
|
7193
|
+
error: { type: "string" }
|
|
7194
|
+
}
|
|
7195
|
+
},
|
|
7196
|
+
verification: {
|
|
7197
|
+
type: "object",
|
|
7198
|
+
description: `Effective verification used (after elicit override). Useful for the client UI to render "logged with proof X". When source=user_confirmed AND ctx.elicit was available, ref is the user's literal text typed into the client; otherwise it's the agent-supplied ref.`,
|
|
7199
|
+
properties: {
|
|
7200
|
+
source: { type: "string" },
|
|
7201
|
+
ref: { type: "string" }
|
|
7202
|
+
}
|
|
7203
|
+
},
|
|
7204
|
+
confirmed_via: {
|
|
7205
|
+
type: "string",
|
|
7206
|
+
description: "Audit trail of how verification was obtained: 'elicit' (user typed into client UI \u2014 anti-poisoning), 'agent_supplied' (legacy path; user_confirmed source with no elicit), 'non_user_confirmed' (gmail_message_id or calendar_event_id \u2014 agent can't fabricate these)."
|
|
7207
|
+
},
|
|
7208
|
+
_meta: {
|
|
7209
|
+
type: "object",
|
|
7210
|
+
description: "Operator context: region.",
|
|
7211
|
+
properties: { region: { type: "string" } }
|
|
7212
|
+
}
|
|
7213
|
+
}
|
|
5379
7214
|
},
|
|
5380
7215
|
execute: async (client, params, ctx) => {
|
|
5381
7216
|
if (!params.verification || !params.verification.source || !params.verification.ref) {
|
|
@@ -5386,6 +7221,16 @@ var reportOutreach = {
|
|
|
5386
7221
|
hint: "Provide verification.source as one of: gmail_message_id (the Gmail message id from sending), calendar_event_id (the event id from booking), or user_confirmed (set verification.ref to the user's literal confirmation in chat)."
|
|
5387
7222
|
};
|
|
5388
7223
|
}
|
|
7224
|
+
const verificationKeys = Object.keys(params.verification);
|
|
7225
|
+
const extraKeys = verificationKeys.filter((k) => k !== "source" && k !== "ref");
|
|
7226
|
+
if (extraKeys.length > 0) {
|
|
7227
|
+
return {
|
|
7228
|
+
error: true,
|
|
7229
|
+
code: "VERIFICATION_EXTRA_KEYS",
|
|
7230
|
+
message: `verification accepts only {source, ref}; rejected extra key(s): ${extraKeys.join(", ")}`,
|
|
7231
|
+
hint: "Drop the extra key(s). Verification is security-sensitive \u2014 extra fields are not silently accepted."
|
|
7232
|
+
};
|
|
7233
|
+
}
|
|
5389
7234
|
if (!VALID_SOURCES.has(params.verification.source)) {
|
|
5390
7235
|
return {
|
|
5391
7236
|
error: true,
|
|
@@ -5399,10 +7244,59 @@ var reportOutreach = {
|
|
|
5399
7244
|
error: true,
|
|
5400
7245
|
code: "BAD_INPUT",
|
|
5401
7246
|
message: "Provide lead_id (single) or lead_ids (bulk)",
|
|
5402
|
-
hint: "lead_id
|
|
7247
|
+
hint: "Set lead_id to one UUID for a single-lead call, or pass lead_ids: [uuid, ...] for a bulk call. Use leadbay_pull_leads to discover candidate IDs."
|
|
5403
7248
|
};
|
|
5404
7249
|
}
|
|
5405
|
-
|
|
7250
|
+
let confirmedVia = params.verification.source === "user_confirmed" ? "agent_supplied" : "non_user_confirmed";
|
|
7251
|
+
let effectiveVerification = params.verification;
|
|
7252
|
+
if (!params.dry_run && params.verification.source === "user_confirmed" && typeof ctx?.elicit === "function") {
|
|
7253
|
+
try {
|
|
7254
|
+
const targetIds = params.lead_ids ?? [params.lead_id];
|
|
7255
|
+
const leadCount = targetIds.length;
|
|
7256
|
+
const elicitMsg = leadCount === 1 ? `An AI agent wants to log outreach on lead ${targetIds[0]}: "${params.note}". The agent claims you confirmed this. Type your literal confirmation to proceed; cancel to reject.` : `An AI agent wants to log outreach on ${leadCount} leads: "${params.note}". The agent claims you confirmed this. Type your literal confirmation to proceed; cancel to reject.`;
|
|
7257
|
+
const result = await ctx.elicit({
|
|
7258
|
+
message: elicitMsg,
|
|
7259
|
+
requestedSchema: {
|
|
7260
|
+
type: "object",
|
|
7261
|
+
properties: {
|
|
7262
|
+
confirmation: {
|
|
7263
|
+
type: "string",
|
|
7264
|
+
title: "Your confirmation",
|
|
7265
|
+
description: "Type a few words confirming the outreach actually happened. This text becomes the audit-trail entry."
|
|
7266
|
+
}
|
|
7267
|
+
},
|
|
7268
|
+
required: ["confirmation"]
|
|
7269
|
+
}
|
|
7270
|
+
});
|
|
7271
|
+
if (result.action === "accept") {
|
|
7272
|
+
const userText = String(result.content?.confirmation ?? "").trim();
|
|
7273
|
+
if (userText.length > 0) {
|
|
7274
|
+
effectiveVerification = {
|
|
7275
|
+
source: "user_confirmed",
|
|
7276
|
+
ref: userText
|
|
7277
|
+
};
|
|
7278
|
+
confirmedVia = "elicit";
|
|
7279
|
+
} else {
|
|
7280
|
+
return {
|
|
7281
|
+
error: true,
|
|
7282
|
+
code: "OUTREACH_USER_CANCELLED",
|
|
7283
|
+
message: "User confirmation was empty; outreach not logged.",
|
|
7284
|
+
hint: "Re-call leadbay_report_outreach after the user types a non-empty confirmation, or use a gmail_message_id / calendar_event_id source instead."
|
|
7285
|
+
};
|
|
7286
|
+
}
|
|
7287
|
+
} else {
|
|
7288
|
+
return {
|
|
7289
|
+
error: true,
|
|
7290
|
+
code: "OUTREACH_USER_CANCELLED",
|
|
7291
|
+
message: `User ${result.action === "decline" ? "declined" : "cancelled"} the outreach confirmation; nothing was logged.`,
|
|
7292
|
+
hint: "Re-call leadbay_report_outreach with verification.source set to gmail_message_id or calendar_event_id when the user is unwilling to type a confirmation."
|
|
7293
|
+
};
|
|
7294
|
+
}
|
|
7295
|
+
} catch (err) {
|
|
7296
|
+
ctx?.logger?.warn?.(`report_outreach: ctx.elicit failed (${err?.code ?? err?.message ?? err}) \u2014 falling back to agent-supplied verification`);
|
|
7297
|
+
}
|
|
7298
|
+
}
|
|
7299
|
+
const noteBody = formatNoteWithVerification(params.note, effectiveVerification);
|
|
5406
7300
|
let epilogueWire = null;
|
|
5407
7301
|
if (params.epilogue_status) {
|
|
5408
7302
|
const w = EPILOGUE_LABEL_MAP[params.epilogue_status];
|
|
@@ -5469,7 +7363,17 @@ var reportOutreach = {
|
|
|
5469
7363
|
status: epilogueWire,
|
|
5470
7364
|
...epilogueResult
|
|
5471
7365
|
},
|
|
5472
|
-
verification:
|
|
7366
|
+
verification: effectiveVerification,
|
|
7367
|
+
// iter-22: audit-trail field. Tells the SDR team which path was taken
|
|
7368
|
+
// for this call:
|
|
7369
|
+
// "elicit" = the user typed the confirmation directly via the
|
|
7370
|
+
// client UI (anti-poisoning shape).
|
|
7371
|
+
// "agent_supplied" = source was user_confirmed but ctx.elicit was
|
|
7372
|
+
// unavailable / failed; agent's ref was accepted.
|
|
7373
|
+
// "non_user_confirmed" = source was gmail_message_id or
|
|
7374
|
+
// calendar_event_id (agent doesn't get to fabricate
|
|
7375
|
+
// these — they're external ids).
|
|
7376
|
+
confirmed_via: confirmedVia,
|
|
5473
7377
|
_meta: { region: client.region }
|
|
5474
7378
|
};
|
|
5475
7379
|
}
|