@leadbay/mcp 0.6.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -1
- package/dist/bin.js +334 -37
- package/dist/{chunk-NLG7GUZ3.js → chunk-3WNCQ7MP.js} +1744 -231
- package/dist/{dist-JUTSXWBL.js → dist-2RTYPHB3.js} +7 -1
- package/package.json +2 -1
|
@@ -532,6 +532,444 @@ function parseRetryAfter(value) {
|
|
|
532
532
|
return null;
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
+
// ../core/dist/tool-descriptions.generated.js
|
|
536
|
+
var leadbay_account_status = `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.
|
|
537
|
+
|
|
538
|
+
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.
|
|
539
|
+
|
|
540
|
+
WHEN NOT TO USE: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating.
|
|
541
|
+
`;
|
|
542
|
+
var leadbay_add_note = `Add a note to a lead. Notes are visible to the whole organization in Leadbay.
|
|
543
|
+
|
|
544
|
+
WHEN TO USE: low-level \u2014 for free-form notes not tied to outreach actions, including meaningful per-lead notes/context preserved from an imported file after the import returns lead IDs.
|
|
545
|
+
|
|
546
|
+
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.
|
|
547
|
+
|
|
548
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
549
|
+
`;
|
|
550
|
+
var leadbay_adjust_audience = `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).
|
|
551
|
+
|
|
552
|
+
WHEN TO USE: when the user wants to see different kinds of leads (sector / size / etc.).
|
|
553
|
+
|
|
554
|
+
WHEN NOT TO USE: to refine BEYOND firmographics \u2014 that's leadbay_refine_prompt.
|
|
555
|
+
|
|
556
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
557
|
+
`;
|
|
558
|
+
var leadbay_answer_clarification = `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.
|
|
559
|
+
|
|
560
|
+
WHEN TO USE: after leadbay_refine_prompt returns \`status='clarification_pending'\`.
|
|
561
|
+
|
|
562
|
+
WHEN NOT TO USE: to set a brand-new prompt \u2014 use leadbay_refine_prompt.
|
|
563
|
+
|
|
564
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
565
|
+
`;
|
|
566
|
+
var leadbay_bulk_enrich_status = `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.
|
|
567
|
+
|
|
568
|
+
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.
|
|
569
|
+
|
|
570
|
+
WHEN NOT TO USE: as a substitute for leadbay_research_lead \u2014 that already includes enriched contacts for a single lead.
|
|
571
|
+
`;
|
|
572
|
+
var leadbay_bulk_qualify_leads = `Pick the next N unqualified leads in the active lens and qualify them (run AI rescore + web fetch). Pass \`wait_for_completion:false\` to return quickly with \`{status:'running', qualify_id}\`; poll leadbay_qualify_status with that id. With \`wait_for_completion\` omitted/true, the legacy behavior polls until the answers are populated or a budget is exhausted. Already-qualified leads (those with a non-null \`ai_agent_lead_score\`) are silently no-ops on the backend, so this composite paginates past them to find fresh candidates. On 429 mid-fanout, stops launching but keeps polling already-launched leads.
|
|
573
|
+
|
|
574
|
+
Context: Leadbay auto-qualifies roughly the top 10 of each daily batch. Leads below the top ~10 are NOT worse \u2014 the system is saving resources. This tool is how the agent spends more resources to go deeper on promising-looking leads the user hasn't had time to surface yet.
|
|
575
|
+
|
|
576
|
+
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\`.
|
|
577
|
+
|
|
578
|
+
WHEN NOT TO USE: to qualify a single specific lead \u2014 that's leadbay_qualify_lead (granular, advanced).
|
|
579
|
+
|
|
580
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
581
|
+
`;
|
|
582
|
+
var leadbay_clear_selection = `Clear the user's transient selection.
|
|
583
|
+
|
|
584
|
+
WHEN TO USE: cleanup after manual selection work, or recovery from a stuck composite.
|
|
585
|
+
|
|
586
|
+
WHEN NOT TO USE: in normal flow \u2014 composites clear in their own finally blocks.
|
|
587
|
+
|
|
588
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
589
|
+
`;
|
|
590
|
+
var leadbay_clear_user_prompt = `Remove the org's intelligence-refinement prompt (revert to AI-only generation). Admin-only. Triggers full intelligence regeneration.
|
|
591
|
+
|
|
592
|
+
WHEN TO USE: when a refinement turned out to be the wrong direction.
|
|
593
|
+
|
|
594
|
+
WHEN NOT TO USE: to replace with a different prompt \u2014 just call leadbay_refine_prompt; that overwrites.
|
|
595
|
+
|
|
596
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
597
|
+
`;
|
|
598
|
+
var leadbay_create_custom_field = `Create an org-level CRM custom field for imports, then use the returned \`mapping_value\` in leadbay_import_leads / leadbay_import_and_qualify mappings. Use when the user's file contains valuable columns that do not fit Leadbay's standard fields, such as source-system deep links, source record IDs, campaign provenance, or user-requested enrichment attributes.
|
|
599
|
+
|
|
600
|
+
For HubSpot record links, prefer \`type:'EXTERNAL_ID'\` with \`config.url_template\` and import only the stable HubSpot id as the CSV value. Example: create field name 'HubSpot Contact', type 'EXTERNAL_ID', config \`{url_template:'https://app.hubspot.com/contacts/<portal-id>/record/0-1/{value}'}\`; then map \`hubspot_id\` to the returned \`mapping_value\`. If only a full URL column exists and the id cannot be safely extracted, use a TEXT field instead.
|
|
601
|
+
|
|
602
|
+
WHEN TO USE: after leadbay_list_mappable_fields shows no suitable existing custom field and preserving the column matters to the user's goal.
|
|
603
|
+
|
|
604
|
+
WHEN NOT TO USE: for standard company/contact data that maps to LEAD_WEBSITE, LEAD_NAME, CONTACT_EMAIL, etc.; do not create custom fields for noisy scraper notes unless the user explicitly asks to preserve them.
|
|
605
|
+
`;
|
|
606
|
+
var leadbay_create_lens = `Create a new user-level lens by cloning an existing lens's filter/scoring as the starting point.
|
|
607
|
+
|
|
608
|
+
WHEN TO USE: when adjust_audience determined the current lens cannot be edited (e.g. it's the org default).
|
|
609
|
+
|
|
610
|
+
WHEN NOT TO USE: to update an existing lens \u2014 use leadbay_update_lens or leadbay_update_lens_filter.
|
|
611
|
+
|
|
612
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
613
|
+
`;
|
|
614
|
+
var leadbay_create_lens_draft = `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.
|
|
615
|
+
|
|
616
|
+
WHEN TO USE: when a non-admin needs to modify an org-level lens \u2014 make a draft, edit the draft.
|
|
617
|
+
|
|
618
|
+
WHEN NOT TO USE: from agent flow \u2014 leadbay_adjust_audience handles the draft-routing transparently.
|
|
619
|
+
|
|
620
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
621
|
+
`;
|
|
622
|
+
var leadbay_deselect_leads = `Remove leads from the user's transient selection.
|
|
623
|
+
|
|
624
|
+
WHEN TO USE: when narrowing a previously-built selection without clearing it entirely.
|
|
625
|
+
|
|
626
|
+
WHEN NOT TO USE: in normal flow \u2014 leadbay_enrich_titles handles selection lifecycle.
|
|
627
|
+
|
|
628
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
629
|
+
`;
|
|
630
|
+
var leadbay_discover_leads = `Get AI-recommended leads from Leadbay. Returns paginated lead summaries with scores, AI summaries, tags, and recommended contacts. Auto-resolves to the active lens if \`lensId\` is omitted; \`count\` is capped at 50 per page.
|
|
631
|
+
|
|
632
|
+
WHEN TO USE: low-level \u2014 when you need raw paginated wishlist access without the qualification_summary attached by leadbay_pull_leads.
|
|
633
|
+
|
|
634
|
+
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.
|
|
635
|
+
`;
|
|
636
|
+
var leadbay_dismiss_clarification = `Dismiss the pending clarification without answering. Leadbay proceeds with its best guess. Admin-only.
|
|
637
|
+
|
|
638
|
+
WHEN TO USE: when the user explicitly doesn't want to answer the disambiguation.
|
|
639
|
+
|
|
640
|
+
WHEN NOT TO USE: as a default \u2014 answering with even a free-text reason gives Leadbay better signal.
|
|
641
|
+
|
|
642
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
643
|
+
`;
|
|
644
|
+
var leadbay_enrich_contacts = `Order email and/or phone enrichment for a specific contact. Performs an advisory credit check, then tries the paid-contact path and falls back to the org-contact path on NOT_FOUND. Consumes enrichment credits.
|
|
645
|
+
|
|
646
|
+
WHEN TO USE: when you have a specific \`contact_id\` (from leadbay_get_contacts) and want to enrich just that one.
|
|
647
|
+
|
|
648
|
+
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.
|
|
649
|
+
|
|
650
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
651
|
+
`;
|
|
652
|
+
var leadbay_enrich_titles = `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.
|
|
653
|
+
|
|
654
|
+
WHEN TO USE: as the agent's go-to enrichment entry point, immediately before proposing outreach.
|
|
655
|
+
|
|
656
|
+
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.
|
|
657
|
+
|
|
658
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
659
|
+
`;
|
|
660
|
+
var leadbay_get_clarification = `Check whether Leadbay has a pending clarification question \u2014 a question raised when refining the intelligence prompt produced contradictory or ambiguous criteria. Returns \`{pending: false, clarification: null}\` when nothing is pending (the backend returns 204).
|
|
661
|
+
|
|
662
|
+
WHEN TO USE: after leadbay_refine_prompt, to see if Leadbay needs the user to disambiguate.
|
|
663
|
+
|
|
664
|
+
WHEN NOT TO USE: to answer the question \u2014 use leadbay_answer_clarification.
|
|
665
|
+
`;
|
|
666
|
+
var leadbay_get_contacts = `Get contacts for a lead, including enriched email and phone data. Returns both organization contacts and enrichable contacts with IDs, tagged with \`source:'org'|'paid'\`.
|
|
667
|
+
|
|
668
|
+
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.
|
|
669
|
+
|
|
670
|
+
WHEN NOT TO USE: as a substitute for leadbay_research_lead, which already includes enriched contacts in its return.
|
|
671
|
+
`;
|
|
672
|
+
var leadbay_get_enrichment_job_titles = `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.
|
|
673
|
+
|
|
674
|
+
WHEN TO USE: after leadbay_select_leads, to know which titles are even available before launching a bulk enrichment.
|
|
675
|
+
|
|
676
|
+
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.
|
|
677
|
+
`;
|
|
678
|
+
var leadbay_get_epilogue_responses = `Read the lead's epilogue history \u2014 what status (still chasing, meeting booked, etc.) was set when, and by whom. Paginated (\`count\` 1-200 default 20, \`page\` 0-indexed).
|
|
679
|
+
|
|
680
|
+
WHEN TO USE: to see the lead's outreach progression before deciding the next step.
|
|
681
|
+
|
|
682
|
+
WHEN NOT TO USE: when the lead summary's \`epilogue_actions_count\` is 0.
|
|
683
|
+
`;
|
|
684
|
+
var leadbay_get_lead_activities = `Get prospecting activity history for a lead (emails sent, calls made, status changes, notes). Each entry is \`{type, date}\`; older activities are trimmed by \`count\` (max 100, default 50).
|
|
685
|
+
|
|
686
|
+
WHEN TO USE: to avoid redundant outreach and understand where this lead is in the sales process.
|
|
687
|
+
|
|
688
|
+
WHEN NOT TO USE: when leadbay_research_lead has already been called \u2014 it includes recent prospecting actions in its engagement block.
|
|
689
|
+
`;
|
|
690
|
+
var leadbay_get_lead_notes = `Read existing notes on a lead \u2014 context the human team or prior agent runs have already captured.
|
|
691
|
+
|
|
692
|
+
WHEN TO USE: before adding a note via leadbay_report_outreach, to avoid duplicating or overwriting context the SDR already wrote.
|
|
693
|
+
|
|
694
|
+
WHEN NOT TO USE: when the lead summary's \`notes_count\` is 0 \u2014 there's nothing to fetch.
|
|
695
|
+
`;
|
|
696
|
+
var leadbay_get_lead_profile = `Get a full lead profile including company details, AI qualification scores, web insights, and contacts. Also marks the lead as SEEN+CLICKED in the user's lens so it ages out of the "new" Discover view (fire-and-forget; profile fetch never blocks on it).
|
|
697
|
+
|
|
698
|
+
WHEN TO USE: low-level \u2014 for fine-grained access to the raw shape of the lead profile.
|
|
699
|
+
|
|
700
|
+
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.
|
|
701
|
+
`;
|
|
702
|
+
var leadbay_get_lens_filter = `Read the firmographic filter (sectors, sizes, locations) currently applied to a lens.
|
|
703
|
+
|
|
704
|
+
WHEN TO USE: before adjusting an audience \u2014 see what's already restricted so changes are diffs, not full replacements.
|
|
705
|
+
|
|
706
|
+
WHEN NOT TO USE: to actually apply changes \u2014 use the leadbay_adjust_audience composite, which handles permissions transparently.
|
|
707
|
+
`;
|
|
708
|
+
var leadbay_get_lens_scoring = `Read the AI-scoring criteria configured on a lens (what makes a lead score 100 vs 30).
|
|
709
|
+
|
|
710
|
+
WHEN TO USE: when explaining why a lead got the score it did.
|
|
711
|
+
|
|
712
|
+
WHEN NOT TO USE: to mutate scoring \u2014 that's an admin/setup operation, not part of the agent loop.
|
|
713
|
+
`;
|
|
714
|
+
var leadbay_get_prospecting_actions = `Read the CRM-style activity log for a lead (calls, emails, meetings \u2014 actions performed by humans or prior agent runs). Paginated (\`count\` 1-200 default 20, \`page\` 0-indexed).
|
|
715
|
+
|
|
716
|
+
WHEN TO USE: before contacting the lead, to avoid duplicating outreach the team already did.
|
|
717
|
+
|
|
718
|
+
WHEN NOT TO USE: when the lead summary's \`prospecting_actions_count\` is 0.
|
|
719
|
+
`;
|
|
720
|
+
var leadbay_get_quota = `Read remaining quota / spend across daily, weekly, and monthly windows for the org's resources (\`llm_completion\`, \`ai_rescore\`, \`web_fetch\`). Each entry shows \`current_units\` vs \`max_units\` and \`resets_at\`.
|
|
721
|
+
|
|
722
|
+
WHEN TO USE: after a 429 error, to explain to the user which window was hit and when it resets.
|
|
723
|
+
|
|
724
|
+
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.
|
|
725
|
+
`;
|
|
726
|
+
var leadbay_get_selection_ids = `List the lead ids currently in the user's selection (the transient set that bulk operations like enrichment act on).
|
|
727
|
+
|
|
728
|
+
WHEN TO USE: to verify the selection state before/after bulk ops if a composite call has misbehaved.
|
|
729
|
+
|
|
730
|
+
WHEN NOT TO USE: in the normal flow \u2014 leadbay_enrich_titles manages selection lifecycle automatically (select \u2192 action \u2192 clear).
|
|
731
|
+
`;
|
|
732
|
+
var leadbay_get_taste_profile = `Get the user's Ideal Buyer Profile, purchase-intent tags, and qualification questions. The result is cached on the client. Returns an operator \`hint\` when no profile is configured yet.
|
|
733
|
+
|
|
734
|
+
WHEN TO USE: at the very start of a session to understand what kind of leads the user is looking for.
|
|
735
|
+
|
|
736
|
+
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).
|
|
737
|
+
`;
|
|
738
|
+
var leadbay_get_user_prompt = `Read the org's intelligence-refinement prompt (free-text instruction that steers lead recommendations beyond firmographics). Returns \`{prompt: null, set: false}\` when none is configured (the backend returns 204 in that case).
|
|
739
|
+
|
|
740
|
+
WHEN TO USE: to know what's currently steering the agent's recommendations before suggesting a refine.
|
|
741
|
+
|
|
742
|
+
WHEN NOT TO USE: to set/change the prompt \u2014 use leadbay_refine_prompt.
|
|
743
|
+
`;
|
|
744
|
+
var leadbay_get_web_fetch = `Read the AI-generated web-research summary for a lead \u2014 company profile, business signals, prospecting clues, each with sources and "hot" flags marking high-signal recent items. The content is dictioned by emoji-prefixed section labels in the raw API.
|
|
745
|
+
|
|
746
|
+
WHEN TO USE: when the agent already qualified this lead and wants the underlying research to reason from.
|
|
747
|
+
|
|
748
|
+
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.
|
|
749
|
+
`;
|
|
750
|
+
var leadbay_import_and_qualify = `Import + qualify leads in one call. Pass either \`domains: [{domain, name?}]\` (Mode A) OR \`records[]\` with \`mappings\` (Mode B). At least one mapped field must be LEADBAY_ID, CRM_ID, SIREN, LEAD_NAME, or LEAD_WEBSITE. Discover the org's mappable surface via \`leadbay_list_mappable_fields\`. For messy files, prefer the \`leadbay_import_file\` prompt which walks an agent through scan \u2192 resolve \u2192 preserve \u2192 commit phases.
|
|
751
|
+
|
|
752
|
+
WHEN TO USE: agent has a list of companies (domains, or CSV-shaped rows from the user's CRM) and wants the full AI qualification \u2014 qualification answers, web-research signals \u2014 without orchestrating import + bulk_qualify_leads + lead_profile chains by hand.
|
|
753
|
+
|
|
754
|
+
WHEN NOT TO USE: discovery (use leadbay_pull_leads); single-lead deep dive (use leadbay_research_lead); high-cadence or untrusted automation \u2014 this mutates user state and consumes ai_rescore + web_fetch quota.
|
|
755
|
+
|
|
756
|
+
Budgets: \`total_budget_ms\` caps wall-clock; \`per_lead_budget_ms\` caps each lead's poll. For short transport timeouts, pass \`wait_for_completion:false\` and poll \`leadbay_import_status\`. Outputs \`qualified[]\`, \`still_running[]\`, \`not_imported[]\`, \`qualify_id\` (resumable handle). Idempotent within a 5-min window. \`dry_run:'preview'\` returns mapping hints + custom-field candidates without importing.
|
|
757
|
+
|
|
758
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role; active billing. Imported leads are NOT auto-promoted to the Monitor view; lens-scoring threshold decides.
|
|
762
|
+
`;
|
|
763
|
+
var leadbay_import_leads = `Import leads into Leadbay's CRM via the file-import wizard. Returns stable Leadbay leadIds for downstream chaining into leadbay_bulk_qualify_leads / leadbay_research_lead. For MCP clients with short transport timeouts, pass \`wait_for_completion:false\` to return quickly with \`{status:'running', handle_id}\`; poll leadbay_import_status with that handle. For end-to-end import+qualify in one call, prefer leadbay_import_and_qualify. For messy files, prefer the \`leadbay_import_file\` prompt which walks an agent through scan \u2192 resolve \u2192 preserve \u2192 commit phases.
|
|
764
|
+
|
|
765
|
+
TWO MODES: (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. (B) Custom records + mapping \u2014 pass \`records: [{Col1, Col2, ...}]\` plus \`mappings.fields: {Col1: 'LEAD_NAME', ...}\`. \`mappings.fields\` must include LEADBAY_ID, CRM_ID, SIREN, LEAD_NAME, or LEAD_WEBSITE (resolver needs at least one identity key). Pass exactly one of \`domains\` / \`records\`. Reserved column \`MCP_ROW_ID\` cannot appear in records/mappings \u2014 the tool injects it for stable reconciliation.
|
|
766
|
+
|
|
767
|
+
MUTATES USER STATE: each call creates a row in the user's CRM-imports list (visible in the web UI) and touches onboarding state. Suitable for occasional automation, NOT for high-cadence (>5 calls/day). Imported leads are NOT auto-promoted to the user's Monitor view; lens-scoring threshold decides. For messy files call leadbay_resolve_import_rows first, then pass \`records_for_import\`/\`mappings_for_import\` here. Agents should inspect every column, build a preservation plan, and pass an explicit final mapping. For each meaningful column decide standard field, CONTACT_* field, Leadbay note, custom field, derived helper, or skip with a reason. For contact-only exports, derive a company-domain column from CONTACT_EMAIL only when it's a real business domain. Multiple rows can share the same LEADBAY_ID and import as separate contacts on that lead. Custom fields use \`CUSTOM.<id>\` in \`mappings.fields\` or the \`mappings.custom_fields\` shorthand. For source-system deep links create a custom field via leadbay_create_custom_field first (prefer EXTERNAL_ID + url_template). Preserve meaningful per-lead notes by calling leadbay_add_note after import returns lead IDs.
|
|
768
|
+
|
|
769
|
+
WHEN TO USE: you have a list of company domains from another system (CRM, analytics, email correspondents) and need stable Leadbay leadIds; or CRM-shaped rows with custom columns and want to drive the wizard with explicit field mappings.
|
|
770
|
+
|
|
771
|
+
WHEN 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; when you also want qualification in the same call (use leadbay_import_and_qualify).
|
|
772
|
+
|
|
773
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
774
|
+
|
|
775
|
+
|
|
776
|
+
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role on the Leadbay account; active billing.
|
|
777
|
+
`;
|
|
778
|
+
var leadbay_import_status = `Retrieve the current state of an async lead import. Pass \`handle_id\` returned by \`leadbay_import_leads({wait_for_completion:false})\`, or pass legacy \`importIds[]\` to inspect backend wizard rows. This status call performs a single refresh pass and never polls in a loop.
|
|
779
|
+
|
|
780
|
+
WHEN TO USE: after leadbay_import_leads or leadbay_import_and_qualify returns \`{status:'running', handle_id}\` for the import phase, call this tool later to retrieve progress or the final import result without re-running the import.
|
|
781
|
+
|
|
782
|
+
WHEN NOT TO USE: for qualification handles returned as \`qualify_id\` \u2014 use leadbay_qualify_status for those; or when you still want the legacy blocking behavior from leadbay_import_leads with \`wait_for_completion=true\`.
|
|
783
|
+
`;
|
|
784
|
+
var leadbay_launch_bulk_enrichment = `Launch a bulk-enrichment job against the current selection. The backend requires \`email=true\` OR \`phone=true\` (both can be true). Returns 204 with no body \u2014 there is no bulk_id and no per-job status endpoint. Track results by polling individual leads via leadbay_get_contacts after ~60s; \`contact.enrichment.done\` flips to true. \`dry_run:true\` returns the call shape without contacting the backend.
|
|
785
|
+
|
|
786
|
+
WHEN TO USE: low-level.
|
|
787
|
+
|
|
788
|
+
WHEN NOT TO USE: from agent flow \u2014 leadbay_enrich_titles handles selection lifecycle, preview, launch, and cleanup.
|
|
789
|
+
|
|
790
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
791
|
+
`;
|
|
792
|
+
var leadbay_list_lenses = `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.
|
|
793
|
+
|
|
794
|
+
WHEN TO USE: when the user wants to switch lens or asks "what lenses do I have".
|
|
795
|
+
|
|
796
|
+
WHEN NOT TO USE: in normal flow \u2014 composites auto-resolve the active lens via \`/me.last_requested_lens\`.
|
|
797
|
+
`;
|
|
798
|
+
var leadbay_list_mappable_fields = `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.
|
|
799
|
+
|
|
800
|
+
For contact exports, map person data to CONTACT_* fields and still provide parent-company identity via LEADBAY_ID/LEAD_WEBSITE/LEAD_NAME/CRM_ID/SIREN. When contact emails contain business domains, agents may derive a clean company-domain column for LEAD_WEBSITE only when the domain agrees with the row's company/deal/brand context, while preserving the original email as CONTACT_EMAIL. For import files, audit every meaningful source column. If no standard/contact field fits, preserve the data by creating or reusing a custom field unless the column is blank, duplicate plumbing, raw unparsed noise after useful extraction, or harmful to data quality. For HubSpot or other source-system deep links, create or reuse an EXTERNAL_ID/TEXT custom field with leadbay_create_custom_field, then map the source id/link to the returned \`mapping_value\`. Backend mapping_hints are advisory only; for contact files, do not accept hints such as first_name -> LEAD_NAME when the column is clearly a person field.
|
|
801
|
+
|
|
802
|
+
Optional \`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). Saves a separate preview round-trip.
|
|
803
|
+
|
|
804
|
+
WHEN TO USE: before authoring an import mapping, especially when the CSV has columns that aren't obvious matches for standard fields.
|
|
805
|
+
|
|
806
|
+
WHEN NOT TO USE: when you already know the mapping \u2014 this call is cheap (~50ms without for_records, ~5\u201310s with) but unnecessary if the agent has already cached the catalog within the same conversation.
|
|
807
|
+
`;
|
|
808
|
+
var leadbay_list_sectors = `List the sector taxonomy (id + display name in the requested language). Default: \`lang\` follows the caller's language; \`includeInvisible=false\` returns ~1,091 visible sectors.
|
|
809
|
+
|
|
810
|
+
WHEN TO USE: to resolve a free-text sector name (e.g. "Healthcare") into the sector ids that leadbay_adjust_audience needs.
|
|
811
|
+
|
|
812
|
+
WHEN NOT TO USE: when you already have sector ids \u2014 pass them directly.
|
|
813
|
+
`;
|
|
814
|
+
var leadbay_login = `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. On success, sets the client's bearer token and switches the active region if needed.
|
|
815
|
+
|
|
816
|
+
WHEN TO USE: at the start of a session if no token is preconfigured (cfg.token / LEADBAY_TOKEN).
|
|
817
|
+
|
|
818
|
+
WHEN NOT TO USE: if a token is already preconfigured \u2014 you'll just overwrite it. The user needs a Leadbay account first; they can register at https://wow.leadbay.ai/?register=true.
|
|
819
|
+
|
|
820
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
821
|
+
`;
|
|
822
|
+
var leadbay_pick_clarification = `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.
|
|
823
|
+
|
|
824
|
+
WHEN TO USE: low-level.
|
|
825
|
+
|
|
826
|
+
WHEN NOT TO USE: from agent flow \u2014 use leadbay_answer_clarification.
|
|
827
|
+
|
|
828
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
829
|
+
`;
|
|
830
|
+
var leadbay_prepare_outreach = `Prepare an outreach package for a single lead: recommended contact + enriched contact details + AI summary. Optionally trigger contact enrichment in-flight (\`enrich:true\`); enrichment is async, so poll leadbay_get_contacts after ~60s if you need the result inline.
|
|
831
|
+
|
|
832
|
+
WHEN TO USE: when the agent is about to draft outreach for ONE specific lead and needs the contact's email/phone.
|
|
833
|
+
|
|
834
|
+
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).
|
|
835
|
+
`;
|
|
836
|
+
var leadbay_preview_bulk_enrichment = `Preview a bulk-enrichment cost given a set of job titles applied to the current selection. Returns \`{selected_leads, enriched_contacts, enrichable_contacts, title_suggestions, auto_included_titles, previously_enriched_titles}\`. \`previously_enriched_titles\` is a newer field (in prod soon) \u2014 when present, the agent can recommend repeating those titles for new leads.
|
|
837
|
+
|
|
838
|
+
WHEN TO USE: between selecting leads and launching, to know what the enrichment will cost.
|
|
839
|
+
|
|
840
|
+
WHEN NOT TO USE: from agent flow \u2014 leadbay_enrich_titles wraps preview + launch with the right safety checks.
|
|
841
|
+
`;
|
|
842
|
+
var leadbay_promote_lens = `Promote a user-level lens (or draft) to org-level so all teammates see it. Admin-only.
|
|
843
|
+
|
|
844
|
+
WHEN TO USE: rare \u2014 when an admin user has built a lens (or refined a draft) and wants to share it org-wide.
|
|
845
|
+
|
|
846
|
+
WHEN NOT TO USE: as a non-admin (will fail with 403); for personal lens changes (those stay user-scoped).
|
|
847
|
+
|
|
848
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
849
|
+
`;
|
|
850
|
+
var leadbay_pull_leads = `Pull up new leads from the user's last-active lens \u2014 the canonical "show me today's prospects" tool. Leadbay works like an inbox: each time the user logs back in, a fresh batch is delivered, paced by how many leads they've actually acted on recently. Pulling more won't produce more; user outreach/skips/saves does. Each returned lead carries a one-line \`qualification_summary\` built from leadbay_ai_agent_responses, plus the rich tags / scores / recommended_contact_title / engagement counters / in-flight flags from the lead summary.
|
|
851
|
+
|
|
852
|
+
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.
|
|
853
|
+
|
|
854
|
+
WHEN TO USE: as the agent's default opening move when the user wants to see leads, or as a daily check-in for what's new today.
|
|
855
|
+
|
|
856
|
+
WHEN NOT TO USE: when the user has named a specific lens \u2014 pass \`lensId\` to override the auto-resolution. Replaces the older leadbay_find_prospects (removed in v0.2.0).
|
|
857
|
+
`;
|
|
858
|
+
var leadbay_qualify_lead = `Trigger AI qualification for a single lead (web fetch + AI rescore). The operation is asynchronous \u2014 results take ~60s. \`forceFetch:true\` re-runs even if recent data exists.
|
|
859
|
+
|
|
860
|
+
WHEN TO USE: low-level \u2014 when you need to kick qualification on exactly one lead without composite orchestration.
|
|
861
|
+
|
|
862
|
+
WHEN NOT TO USE: as the agent's bulk-qualify path \u2014 use leadbay_bulk_qualify_leads, which paginates past already-qualified leads, fans out, polls, and bails out cleanly on 429.
|
|
863
|
+
|
|
864
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
865
|
+
`;
|
|
866
|
+
var leadbay_qualify_status = `Retrieve the current state of an import_and_qualify (or bulk_qualify_leads) 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.
|
|
867
|
+
|
|
868
|
+
WHEN TO USE: after leadbay_import_and_qualify or leadbay_bulk_qualify_leads 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.
|
|
869
|
+
|
|
870
|
+
WHEN 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.
|
|
871
|
+
`;
|
|
872
|
+
var leadbay_recall_ordered_titles = `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 \u2014 a selection-scoped preview call that reads \`previously_enriched_titles\` from the backend (newer prod field). (2) FALLBACK \u2014 live aggregation across each lead's enriched contacts. The composite picks transparently.
|
|
873
|
+
|
|
874
|
+
WHEN TO USE: before leadbay_enrich_titles, to plan which titles to order.
|
|
875
|
+
|
|
876
|
+
WHEN NOT TO USE: when you already know the exact titles you want to enrich.
|
|
877
|
+
`;
|
|
878
|
+
var leadbay_refine_prompt = `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).
|
|
879
|
+
|
|
880
|
+
WHEN TO USE: when audience filters (leadbay_adjust_audience) aren't enough.
|
|
881
|
+
|
|
882
|
+
WHEN NOT TO USE: to answer a pending clarification \u2014 that's leadbay_answer_clarification.
|
|
883
|
+
|
|
884
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
885
|
+
`;
|
|
886
|
+
var leadbay_remove_epilogue = `Bulk-clear the epilogue status from a set of leads.
|
|
887
|
+
|
|
888
|
+
WHEN TO USE: when an outreach action was logged in error and needs to be undone.
|
|
889
|
+
|
|
890
|
+
WHEN NOT TO USE: to change status \u2014 call leadbay_set_epilogue_status with the new status (it overwrites).
|
|
891
|
+
|
|
892
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
893
|
+
`;
|
|
894
|
+
var leadbay_report_outreach = `Log an outreach action (email, call, message, meeting) on a lead so the human team using Leadbay sees the progress in their UI. Writes a NOTE on the lead and (optionally) sets an EPILOGUE status (still chasing, meeting booked, etc.). Bulk variant: pass \`lead_ids=[uuid,...]\` instead of \`lead_id\` (epilogue is bulk-native; notes fan out per-lead).
|
|
895
|
+
|
|
896
|
+
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. Skipping or fabricating verification poisons the human team's pipeline.
|
|
897
|
+
|
|
898
|
+
WHEN TO USE: AFTER actually emailing/calling/meeting/messaging a contact, OR after a substantive decision the user wants logged (skip, save, hand off).
|
|
899
|
+
|
|
900
|
+
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.
|
|
901
|
+
|
|
902
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
903
|
+
`;
|
|
904
|
+
var leadbay_research_company = `Deep-dive research on a specific company by NAME (fuzzy match against the active lens's wishlist). Pass \`companyName\` (matches the top-scoring lead with that name) or \`leadId\` (takes precedence when both supplied).
|
|
905
|
+
|
|
906
|
+
WHEN TO USE: when the user references a company by name and you don't yet have its \`lead_id\`.
|
|
907
|
+
|
|
908
|
+
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).
|
|
909
|
+
`;
|
|
910
|
+
var leadbay_research_lead = `Tell me everything decision-relevant about a single lead. Bundles the lens-scoped lead profile, the AI qualification answers (the agent's knowledge-base food), the structured web-research signals (with hot flags + sources), the enriched contacts, and the recent notes/epilogue/prospecting activity in one call. Order is deliberate: qualification first, then signals, then firmographics, then contacts, then engagement.
|
|
911
|
+
|
|
912
|
+
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.
|
|
913
|
+
|
|
914
|
+
WHEN TO USE: when picking up a single lead from leadbay_pull_leads to decide whether to act on it.
|
|
915
|
+
|
|
916
|
+
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.)
|
|
917
|
+
`;
|
|
918
|
+
var leadbay_resolve_import_rows = `Resolve messy CSV-shaped lead rows against Leadbay before file import. The tool sends each row's available identity signals to \`POST /leads/resolve\`, returns matched lead IDs or ambiguous candidate IDs, and produces \`records_for_import\` plus a SAFE identity-only \`mappings_for_import\` starting point for leadbay_import_leads / leadbay_import_and_qualify. This tool deliberately does not try to understand every CSV dialect; the agent should inspect the file, derive clean helper columns when useful, pass explicit \`identity_mappings\`, and build the final CRM mapping from \`mapping_guidance\`.
|
|
919
|
+
|
|
920
|
+
WHEN TO USE: before importing user-supplied files when domains, names, CRM IDs, registry numbers, or Leadbay IDs may be inconsistently formatted; when the agent needs to pre-resolve messy rows, inspect ambiguous candidates, or prepare LEADBAY_ID values for the import composites. For contact-only files, first derive company website/domain from business contact emails where possible, while ignoring consumer mailbox domains. Deterministic matches get a LEADBAY_ID column inserted so the standard import commits immediately. Ambiguous rows are deliberately left without LEADBAY_ID; inspect candidates and choose one only when the evidence is good. Rows with websites but no match can still be imported; Leadbay may crawl and match them later, and leadbay_import_status can surface late matches.
|
|
921
|
+
|
|
922
|
+
WHEN NOT TO USE: for prospect discovery from scratch (use leadbay_pull_leads); for one known company profile (use leadbay_research_company / leadbay_research_lead); or when the file already has clean, final LEADBAY_ID/CRM_ID/SIREN mappings and no row-level identity disambiguation is needed.
|
|
923
|
+
`;
|
|
924
|
+
var leadbay_select_leads = `Add leads to the user's transient selection (used by selection-scoped bulk operations). Accepts 1-1000 \`leadIds\` per call.
|
|
925
|
+
|
|
926
|
+
WHEN TO USE: low-level. The user's selection is a per-token global state \u2014 be careful when invoking directly.
|
|
927
|
+
|
|
928
|
+
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.
|
|
929
|
+
|
|
930
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
931
|
+
`;
|
|
932
|
+
var leadbay_set_active_lens = `Mark a lens as last-used. Subsequent \`/me\` reads return it as \`last_requested_lens\`, so all composite tools default to it.
|
|
933
|
+
|
|
934
|
+
WHEN TO USE: after the user explicitly switched contexts (e.g. created a new lens via leadbay_create_lens).
|
|
935
|
+
|
|
936
|
+
WHEN NOT TO USE: in normal flow \u2014 leadbay_pull_leads and leadbay_adjust_audience auto-set the right lens.
|
|
937
|
+
|
|
938
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
939
|
+
`;
|
|
940
|
+
var leadbay_set_epilogue_status = `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.
|
|
941
|
+
|
|
942
|
+
WHEN TO USE: low-level.
|
|
943
|
+
|
|
944
|
+
WHEN NOT TO USE: from agent flow \u2014 leadbay_report_outreach pairs this with a note + verification, which is what humans actually need to see in Leadbay.
|
|
945
|
+
|
|
946
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
947
|
+
`;
|
|
948
|
+
var leadbay_set_user_prompt = `Set the org's intelligence-refinement prompt \u2014 free-text instruction that steers Leadbay's lead recommendations beyond firmographics. Admin-only. Setting this clears any pending clarification and triggers a full intelligence regeneration (web search + high-reasoning). \`dry_run:true\` returns the call shape without contacting the backend.
|
|
949
|
+
|
|
950
|
+
WHEN TO USE: low-level.
|
|
951
|
+
|
|
952
|
+
WHEN NOT TO USE: from agent flow \u2014 use leadbay_refine_prompt, which polls for follow-up clarifications.
|
|
953
|
+
|
|
954
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
955
|
+
`;
|
|
956
|
+
var leadbay_update_lens = `Update lens metadata (name, description, mode flags). Does NOT change the audience filter \u2014 use leadbay_update_lens_filter for that.
|
|
957
|
+
|
|
958
|
+
WHEN TO USE: rename a lens or toggle \`multi_product_mode\` / \`use_hq_only\`.
|
|
959
|
+
|
|
960
|
+
WHEN NOT TO USE: to change which leads the lens shows \u2014 that's a filter operation.
|
|
961
|
+
|
|
962
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
963
|
+
`;
|
|
964
|
+
var leadbay_update_lens_filter = `Replace the audience filter (sectors, sizes, locations) on a lens. Body is the full \`Filter\` object \u2014 this is a REPLACE, not a merge. Returns 400 \`default_lens\` if applied to the org default lens (clone it first). \`dry_run:true\` returns the call shape without contacting the backend.
|
|
965
|
+
|
|
966
|
+
WHEN TO USE: low-level mutation when you've already prepared the merged filter.
|
|
967
|
+
|
|
968
|
+
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.
|
|
969
|
+
|
|
970
|
+
This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible for confirming intent before invocation; the MCP server does not soft-prompt for confirmation. See \`annotations.destructiveHint\`.
|
|
971
|
+
`;
|
|
972
|
+
|
|
535
973
|
// ../core/dist/tools/login.js
|
|
536
974
|
var login = {
|
|
537
975
|
name: "leadbay_login",
|
|
@@ -542,7 +980,7 @@ var login = {
|
|
|
542
980
|
idempotentHint: false,
|
|
543
981
|
openWorldHint: true
|
|
544
982
|
},
|
|
545
|
-
description:
|
|
983
|
+
description: leadbay_login,
|
|
546
984
|
inputSchema: {
|
|
547
985
|
type: "object",
|
|
548
986
|
properties: {
|
|
@@ -593,7 +1031,7 @@ var listLenses = {
|
|
|
593
1031
|
idempotentHint: true,
|
|
594
1032
|
openWorldHint: true
|
|
595
1033
|
},
|
|
596
|
-
description:
|
|
1034
|
+
description: leadbay_list_lenses,
|
|
597
1035
|
inputSchema: {
|
|
598
1036
|
type: "object",
|
|
599
1037
|
properties: {},
|
|
@@ -641,7 +1079,7 @@ var discoverLeads = {
|
|
|
641
1079
|
idempotentHint: true,
|
|
642
1080
|
openWorldHint: true
|
|
643
1081
|
},
|
|
644
|
-
description:
|
|
1082
|
+
description: leadbay_discover_leads,
|
|
645
1083
|
inputSchema: {
|
|
646
1084
|
type: "object",
|
|
647
1085
|
properties: {
|
|
@@ -705,7 +1143,7 @@ var getLeadProfile = {
|
|
|
705
1143
|
idempotentHint: true,
|
|
706
1144
|
openWorldHint: true
|
|
707
1145
|
},
|
|
708
|
-
description:
|
|
1146
|
+
description: leadbay_get_lead_profile,
|
|
709
1147
|
inputSchema: {
|
|
710
1148
|
type: "object",
|
|
711
1149
|
properties: {
|
|
@@ -837,7 +1275,7 @@ var getContacts = {
|
|
|
837
1275
|
idempotentHint: true,
|
|
838
1276
|
openWorldHint: true
|
|
839
1277
|
},
|
|
840
|
-
description:
|
|
1278
|
+
description: leadbay_get_contacts,
|
|
841
1279
|
inputSchema: {
|
|
842
1280
|
type: "object",
|
|
843
1281
|
properties: {
|
|
@@ -897,7 +1335,7 @@ var getQuota = {
|
|
|
897
1335
|
idempotentHint: true,
|
|
898
1336
|
openWorldHint: true
|
|
899
1337
|
},
|
|
900
|
-
description:
|
|
1338
|
+
description: leadbay_get_quota,
|
|
901
1339
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
902
1340
|
outputSchema: {
|
|
903
1341
|
type: "object",
|
|
@@ -926,7 +1364,7 @@ var getTasteProfile = {
|
|
|
926
1364
|
idempotentHint: true,
|
|
927
1365
|
openWorldHint: true
|
|
928
1366
|
},
|
|
929
|
-
description:
|
|
1367
|
+
description: leadbay_get_taste_profile,
|
|
930
1368
|
inputSchema: {
|
|
931
1369
|
type: "object",
|
|
932
1370
|
properties: {},
|
|
@@ -990,7 +1428,7 @@ var qualifyLead = {
|
|
|
990
1428
|
idempotentHint: true,
|
|
991
1429
|
openWorldHint: true
|
|
992
1430
|
},
|
|
993
|
-
description:
|
|
1431
|
+
description: leadbay_qualify_lead,
|
|
994
1432
|
optional: true,
|
|
995
1433
|
inputSchema: {
|
|
996
1434
|
type: "object",
|
|
@@ -1027,7 +1465,7 @@ var enrichContacts = {
|
|
|
1027
1465
|
idempotentHint: true,
|
|
1028
1466
|
openWorldHint: true
|
|
1029
1467
|
},
|
|
1030
|
-
description:
|
|
1468
|
+
description: leadbay_enrich_contacts,
|
|
1031
1469
|
optional: true,
|
|
1032
1470
|
inputSchema: {
|
|
1033
1471
|
type: "object",
|
|
@@ -1101,7 +1539,7 @@ var addNote = {
|
|
|
1101
1539
|
idempotentHint: false,
|
|
1102
1540
|
openWorldHint: true
|
|
1103
1541
|
},
|
|
1104
|
-
description:
|
|
1542
|
+
description: leadbay_add_note,
|
|
1105
1543
|
optional: true,
|
|
1106
1544
|
inputSchema: {
|
|
1107
1545
|
type: "object",
|
|
@@ -1151,7 +1589,7 @@ var getLeadActivities = {
|
|
|
1151
1589
|
idempotentHint: true,
|
|
1152
1590
|
openWorldHint: true
|
|
1153
1591
|
},
|
|
1154
|
-
description:
|
|
1592
|
+
description: leadbay_get_lead_activities,
|
|
1155
1593
|
inputSchema: {
|
|
1156
1594
|
type: "object",
|
|
1157
1595
|
properties: {
|
|
@@ -1211,7 +1649,7 @@ var getLensFilter = {
|
|
|
1211
1649
|
idempotentHint: true,
|
|
1212
1650
|
openWorldHint: true
|
|
1213
1651
|
},
|
|
1214
|
-
description:
|
|
1652
|
+
description: leadbay_get_lens_filter,
|
|
1215
1653
|
inputSchema: {
|
|
1216
1654
|
type: "object",
|
|
1217
1655
|
properties: {
|
|
@@ -1235,7 +1673,7 @@ var getLensScoring = {
|
|
|
1235
1673
|
idempotentHint: true,
|
|
1236
1674
|
openWorldHint: true
|
|
1237
1675
|
},
|
|
1238
|
-
description:
|
|
1676
|
+
description: leadbay_get_lens_scoring,
|
|
1239
1677
|
inputSchema: {
|
|
1240
1678
|
type: "object",
|
|
1241
1679
|
properties: { lensId: { type: "number", description: "Lens id (required)" } },
|
|
@@ -1257,7 +1695,7 @@ var listSectors = {
|
|
|
1257
1695
|
idempotentHint: true,
|
|
1258
1696
|
openWorldHint: true
|
|
1259
1697
|
},
|
|
1260
|
-
description:
|
|
1698
|
+
description: leadbay_list_sectors,
|
|
1261
1699
|
inputSchema: {
|
|
1262
1700
|
type: "object",
|
|
1263
1701
|
properties: {
|
|
@@ -1295,7 +1733,7 @@ var getUserPrompt = {
|
|
|
1295
1733
|
idempotentHint: true,
|
|
1296
1734
|
openWorldHint: true
|
|
1297
1735
|
},
|
|
1298
|
-
description:
|
|
1736
|
+
description: leadbay_get_user_prompt,
|
|
1299
1737
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1300
1738
|
outputSchema: {
|
|
1301
1739
|
type: "object",
|
|
@@ -1335,7 +1773,7 @@ var getClarification = {
|
|
|
1335
1773
|
idempotentHint: true,
|
|
1336
1774
|
openWorldHint: true
|
|
1337
1775
|
},
|
|
1338
|
-
description:
|
|
1776
|
+
description: leadbay_get_clarification,
|
|
1339
1777
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1340
1778
|
outputSchema: {
|
|
1341
1779
|
type: "object",
|
|
@@ -1377,7 +1815,7 @@ var getLeadNotes = {
|
|
|
1377
1815
|
idempotentHint: true,
|
|
1378
1816
|
openWorldHint: true
|
|
1379
1817
|
},
|
|
1380
|
-
description:
|
|
1818
|
+
description: leadbay_get_lead_notes,
|
|
1381
1819
|
inputSchema: {
|
|
1382
1820
|
type: "object",
|
|
1383
1821
|
properties: { leadId: { type: "string", description: "Lead UUID (required)" } },
|
|
@@ -1399,7 +1837,7 @@ var getEpilogueResponses = {
|
|
|
1399
1837
|
idempotentHint: true,
|
|
1400
1838
|
openWorldHint: true
|
|
1401
1839
|
},
|
|
1402
|
-
description:
|
|
1840
|
+
description: leadbay_get_epilogue_responses,
|
|
1403
1841
|
inputSchema: {
|
|
1404
1842
|
type: "object",
|
|
1405
1843
|
properties: {
|
|
@@ -1427,7 +1865,7 @@ var getProspectingActions = {
|
|
|
1427
1865
|
idempotentHint: true,
|
|
1428
1866
|
openWorldHint: true
|
|
1429
1867
|
},
|
|
1430
|
-
description:
|
|
1868
|
+
description: leadbay_get_prospecting_actions,
|
|
1431
1869
|
inputSchema: {
|
|
1432
1870
|
type: "object",
|
|
1433
1871
|
properties: {
|
|
@@ -1455,7 +1893,7 @@ var getWebFetch = {
|
|
|
1455
1893
|
idempotentHint: true,
|
|
1456
1894
|
openWorldHint: true
|
|
1457
1895
|
},
|
|
1458
|
-
description:
|
|
1896
|
+
description: leadbay_get_web_fetch,
|
|
1459
1897
|
inputSchema: {
|
|
1460
1898
|
type: "object",
|
|
1461
1899
|
properties: { leadId: { type: "string", description: "Lead UUID (required)" } },
|
|
@@ -1498,7 +1936,7 @@ var getSelectionIds = {
|
|
|
1498
1936
|
idempotentHint: true,
|
|
1499
1937
|
openWorldHint: true
|
|
1500
1938
|
},
|
|
1501
|
-
description:
|
|
1939
|
+
description: leadbay_get_selection_ids,
|
|
1502
1940
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1503
1941
|
execute: async (client) => {
|
|
1504
1942
|
return await client.request("GET", "/leads/selection/ids");
|
|
@@ -1515,7 +1953,7 @@ var getEnrichmentJobTitles = {
|
|
|
1515
1953
|
idempotentHint: true,
|
|
1516
1954
|
openWorldHint: true
|
|
1517
1955
|
},
|
|
1518
|
-
description:
|
|
1956
|
+
description: leadbay_get_enrichment_job_titles,
|
|
1519
1957
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
1520
1958
|
execute: async (client) => {
|
|
1521
1959
|
return await client.request("GET", "/leads/selection/enrichment/job_titles");
|
|
@@ -1871,7 +2309,10 @@ async function refreshLeadStates(client, leadIds, questionOrder) {
|
|
|
1871
2309
|
}
|
|
1872
2310
|
|
|
1873
2311
|
// ../core/dist/composite/import-leads.js
|
|
1874
|
-
import { randomUUID } from "crypto";
|
|
2312
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
2313
|
+
function isImportLeadsRunningResult(result) {
|
|
2314
|
+
return "status" in result && result.status === "running";
|
|
2315
|
+
}
|
|
1875
2316
|
var CHUNK_SIZE = 100;
|
|
1876
2317
|
var POLL_INTERVAL_MS2 = 2e3;
|
|
1877
2318
|
var DEFAULT_PER_PHASE_BUDGET_MS = 6e4;
|
|
@@ -1880,6 +2321,13 @@ var STABILIZATION_POLLS = 2;
|
|
|
1880
2321
|
var MAX_COLUMN_NAME_LEN = 128;
|
|
1881
2322
|
var RESERVED_COLUMN_RE = /^mcp_row_id$/i;
|
|
1882
2323
|
var CUSTOM_FIELD_RE = /^CUSTOM\.(\d+)$/;
|
|
2324
|
+
var IMPORT_RESOLVER_FIELDS = /* @__PURE__ */ new Set([
|
|
2325
|
+
"LEADBAY_ID",
|
|
2326
|
+
"CRM_ID",
|
|
2327
|
+
"LEAD_NAME",
|
|
2328
|
+
"LEAD_WEBSITE",
|
|
2329
|
+
"SIREN"
|
|
2330
|
+
]);
|
|
1883
2331
|
function isCustomFieldMappingValue(v) {
|
|
1884
2332
|
return CUSTOM_FIELD_RE.test(v);
|
|
1885
2333
|
}
|
|
@@ -1972,6 +2420,29 @@ function chunkAt100(items) {
|
|
|
1972
2420
|
}
|
|
1973
2421
|
return chunks;
|
|
1974
2422
|
}
|
|
2423
|
+
function stableStringify(value) {
|
|
2424
|
+
if (value === null || typeof value !== "object")
|
|
2425
|
+
return JSON.stringify(value);
|
|
2426
|
+
if (Array.isArray(value))
|
|
2427
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
2428
|
+
const obj = value;
|
|
2429
|
+
return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
2430
|
+
}
|
|
2431
|
+
function importFingerprint(params, prep) {
|
|
2432
|
+
const payload = {
|
|
2433
|
+
mode: prep.mode,
|
|
2434
|
+
rows: prep.validInputs.map((i) => ({
|
|
2435
|
+
domain: i.domain,
|
|
2436
|
+
outputDomain: i.outputDomain,
|
|
2437
|
+
row: i.row
|
|
2438
|
+
})),
|
|
2439
|
+
malformed: prep.malformedDomains,
|
|
2440
|
+
header: prep.header,
|
|
2441
|
+
mappings: prep.mappings,
|
|
2442
|
+
dry_run: Boolean(params.dry_run)
|
|
2443
|
+
};
|
|
2444
|
+
return createHash2("sha256").update(stableStringify(payload)).digest("hex");
|
|
2445
|
+
}
|
|
1975
2446
|
function checkAborted(signal) {
|
|
1976
2447
|
if (signal?.aborted) {
|
|
1977
2448
|
throw Object.assign(new Error("aborted"), { name: "AbortError" });
|
|
@@ -2099,8 +2570,8 @@ function prepareRecordsMode(client, records, mappings, customFieldCatalog) {
|
|
|
2099
2570
|
throw client.makeError("IMPORT_MAPPING_REQUIRED", "mappings.fields must contain at least one column \u2192 CRM field entry", "Map at least one CSV column to LEAD_NAME or LEAD_WEBSITE.", "POST /imports");
|
|
2100
2571
|
}
|
|
2101
2572
|
const targets = new Set(fieldEntries.map(([, v]) => v));
|
|
2102
|
-
if (!targets.
|
|
2103
|
-
throw client.makeError("IMPORT_MAPPING_NO_RESOLVER", "mappings.fields must include LEAD_NAME or LEAD_WEBSITE", "The wizard needs at least one
|
|
2573
|
+
if (![...targets].some((t) => IMPORT_RESOLVER_FIELDS.has(t))) {
|
|
2574
|
+
throw client.makeError("IMPORT_MAPPING_NO_RESOLVER", "mappings.fields must include LEADBAY_ID, CRM_ID, SIREN, LEAD_NAME, or LEAD_WEBSITE", "The wizard needs at least one identity field to match a lead. Use leadbay_resolve_import_rows to prepare LEADBAY_ID values when the input file is messy.", "POST /imports");
|
|
2104
2575
|
}
|
|
2105
2576
|
const targetCounts = /* @__PURE__ */ new Map();
|
|
2106
2577
|
for (const [col, target] of fieldEntries) {
|
|
@@ -2339,6 +2810,10 @@ async function pollRecordsToTerminal(client, importId, budgetMs, expectedRowCoun
|
|
|
2339
2810
|
}
|
|
2340
2811
|
}
|
|
2341
2812
|
async function runOneChunk(client, chunk, chunkIdx, totalChunks, header, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal, onImportId) {
|
|
2813
|
+
const upload = await uploadOneChunk(client, chunk, chunkIdx, totalChunks, header, ctx, onImportId);
|
|
2814
|
+
return completeUploadedChunk(client, upload, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal);
|
|
2815
|
+
}
|
|
2816
|
+
async function uploadOneChunk(client, chunk, chunkIdx, totalChunks, header, ctx, onImportId) {
|
|
2342
2817
|
const csv = synthesizeCsv(header, chunk.map((c) => c.row));
|
|
2343
2818
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2344
2819
|
const fileName = `mcp-import-${ts}-${chunkIdx}.csv`;
|
|
@@ -2346,6 +2821,10 @@ async function runOneChunk(client, chunk, chunkIdx, totalChunks, header, mapping
|
|
|
2346
2821
|
const upload = await client.requestRawBinary("POST", `/imports?file_name=${encodeURIComponent(fileName)}`, "text/csv", csv);
|
|
2347
2822
|
const importId = upload.id;
|
|
2348
2823
|
onImportId(importId);
|
|
2824
|
+
return { importId, chunk, chunkIdx, totalChunks };
|
|
2825
|
+
}
|
|
2826
|
+
async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal) {
|
|
2827
|
+
const { importId, chunk } = upload;
|
|
2349
2828
|
const phaseBudget = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
2350
2829
|
await pollPreprocess(client, importId, phaseBudget, ctx, signal);
|
|
2351
2830
|
ctx?.logger?.info?.(`import-leads: preprocess done for importId=${importId}`);
|
|
@@ -2416,6 +2895,82 @@ function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
|
2416
2895
|
}
|
|
2417
2896
|
}
|
|
2418
2897
|
}
|
|
2898
|
+
function buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled) {
|
|
2899
|
+
const leads = [];
|
|
2900
|
+
const not_imported = [];
|
|
2901
|
+
if (dryRun) {
|
|
2902
|
+
for (const inp of prep.validInputs) {
|
|
2903
|
+
if (prep.mode === "domains") {
|
|
2904
|
+
not_imported.push({ domain: inp.outputDomain, reason: "dry_run" });
|
|
2905
|
+
} else {
|
|
2906
|
+
const entry = { rowId: inp.rowId, reason: "dry_run" };
|
|
2907
|
+
if (inp.outputDomain)
|
|
2908
|
+
entry.domain = inp.outputDomain;
|
|
2909
|
+
not_imported.push(entry);
|
|
2910
|
+
}
|
|
2911
|
+
}
|
|
2912
|
+
} else {
|
|
2913
|
+
for (const inp of prep.validInputs) {
|
|
2914
|
+
const m = matched.get(inp.index);
|
|
2915
|
+
if (m) {
|
|
2916
|
+
if (prep.mode === "domains") {
|
|
2917
|
+
leads.push({
|
|
2918
|
+
domain: inp.outputDomain,
|
|
2919
|
+
leadId: m.leadId,
|
|
2920
|
+
name: m.name
|
|
2921
|
+
});
|
|
2922
|
+
} else {
|
|
2923
|
+
const e = {
|
|
2924
|
+
rowId: inp.rowId,
|
|
2925
|
+
leadId: m.leadId,
|
|
2926
|
+
name: m.name
|
|
2927
|
+
};
|
|
2928
|
+
if (m.domain ?? inp.outputDomain)
|
|
2929
|
+
e.domain = m.domain ?? inp.outputDomain;
|
|
2930
|
+
leads.push(e);
|
|
2931
|
+
}
|
|
2932
|
+
continue;
|
|
2933
|
+
}
|
|
2934
|
+
const ni = notImported.get(inp.index);
|
|
2935
|
+
if (ni) {
|
|
2936
|
+
if (prep.mode === "domains") {
|
|
2937
|
+
not_imported.push({ domain: inp.outputDomain, reason: ni.reason });
|
|
2938
|
+
} else {
|
|
2939
|
+
const e = { rowId: inp.rowId, reason: ni.reason };
|
|
2940
|
+
if (ni.domain ?? inp.outputDomain)
|
|
2941
|
+
e.domain = ni.domain ?? inp.outputDomain;
|
|
2942
|
+
not_imported.push(e);
|
|
2943
|
+
}
|
|
2944
|
+
continue;
|
|
2945
|
+
}
|
|
2946
|
+
if (prep.mode === "domains") {
|
|
2947
|
+
not_imported.push({ domain: inp.outputDomain, reason: "internal_error" });
|
|
2948
|
+
} else {
|
|
2949
|
+
const e = { rowId: inp.rowId, reason: "internal_error" };
|
|
2950
|
+
if (inp.outputDomain)
|
|
2951
|
+
e.domain = inp.outputDomain;
|
|
2952
|
+
not_imported.push(e);
|
|
2953
|
+
}
|
|
2954
|
+
}
|
|
2955
|
+
}
|
|
2956
|
+
for (const m of prep.malformedDomains) {
|
|
2957
|
+
not_imported.push({ domain: m, reason: "malformed" });
|
|
2958
|
+
}
|
|
2959
|
+
return {
|
|
2960
|
+
leads,
|
|
2961
|
+
not_imported,
|
|
2962
|
+
importIds,
|
|
2963
|
+
region: client.region,
|
|
2964
|
+
cancelled: cancelled || void 0,
|
|
2965
|
+
dry_run: dryRun || void 0,
|
|
2966
|
+
_meta: client.lastMeta ?? {
|
|
2967
|
+
region: client.region,
|
|
2968
|
+
endpoint: "POST /imports",
|
|
2969
|
+
latency_ms: null,
|
|
2970
|
+
retry_after: null
|
|
2971
|
+
}
|
|
2972
|
+
};
|
|
2973
|
+
}
|
|
2419
2974
|
var importLeads = {
|
|
2420
2975
|
name: "leadbay_import_leads",
|
|
2421
2976
|
annotations: {
|
|
@@ -2428,7 +2983,7 @@ var importLeads = {
|
|
|
2428
2983
|
idempotentHint: true,
|
|
2429
2984
|
openWorldHint: true
|
|
2430
2985
|
},
|
|
2431
|
-
description:
|
|
2986
|
+
description: leadbay_import_leads,
|
|
2432
2987
|
write: true,
|
|
2433
2988
|
version: "0.3.0",
|
|
2434
2989
|
inputSchema: {
|
|
@@ -2466,7 +3021,7 @@ var importLeads = {
|
|
|
2466
3021
|
properties: {
|
|
2467
3022
|
fields: {
|
|
2468
3023
|
type: "object",
|
|
2469
|
-
description: "Object whose keys are CSV column names (matching keys in `records`) and whose values are either Leadbay's StandardCrmFieldType (LEAD_NAME, LEAD_WEBSITE, LEAD_STATUS, LEAD_LOCATION, LEAD_LOCATION_*, LEAD_SECTOR, LEAD_SIZE, CRM_ID, LEADBAY_ID, EMAIL, DEAL_CRM_ID, CONTACT_FIRST_NAME, CONTACT_LAST_NAME, CONTACT_EMAIL, CONTACT_PHONE_NUMBER, CONTACT_TITLE, CONTACT_LINKEDIN, LEAD_STATUS_DATE, OWNER, SCORE, SIREN) or the wire-format string 'CUSTOM.<id>' for org-defined custom fields. At least one entry must target LEAD_NAME or LEAD_WEBSITE \u2014 the wizard needs
|
|
3024
|
+
description: "Object whose keys are CSV column names (matching keys in `records`) and whose values are either Leadbay's StandardCrmFieldType (LEAD_NAME, LEAD_WEBSITE, LEAD_STATUS, LEAD_LOCATION, LEAD_LOCATION_*, LEAD_SECTOR, LEAD_SIZE, CRM_ID, LEADBAY_ID, EMAIL, DEAL_CRM_ID, CONTACT_FIRST_NAME, CONTACT_LAST_NAME, CONTACT_EMAIL, CONTACT_PHONE_NUMBER, CONTACT_TITLE, CONTACT_LINKEDIN, LEAD_STATUS_DATE, OWNER, SCORE, SIREN) or the wire-format string 'CUSTOM.<id>' for org-defined custom fields. At least one entry must target LEADBAY_ID, CRM_ID, SIREN, LEAD_NAME, or LEAD_WEBSITE \u2014 the wizard needs an identity field to find leads. Use leadbay_resolve_import_rows to prepare LEADBAY_ID values, and leadbay_list_mappable_fields to discover the org's custom fields. Contact rows should include both parent lead identity fields and CONTACT_* fields; repeated LEADBAY_ID/company values create multiple contacts on the same lead. Preserve HubSpot/source links by mapping them to a CUSTOM.<id> field returned by leadbay_create_custom_field when no suitable custom field already exists."
|
|
2470
3025
|
},
|
|
2471
3026
|
custom_fields: {
|
|
2472
3027
|
type: "object",
|
|
@@ -2494,6 +3049,10 @@ var importLeads = {
|
|
|
2494
3049
|
total_budget_ms: {
|
|
2495
3050
|
type: "number",
|
|
2496
3051
|
description: `Overall cap across all phases (default ${DEFAULT_TOTAL_BUDGET_MS}ms).`
|
|
3052
|
+
},
|
|
3053
|
+
wait_for_completion: {
|
|
3054
|
+
type: "boolean",
|
|
3055
|
+
description: "When false, validate and enqueue the import in the background, then return `{status:'running', handle_id}` immediately. Poll leadbay_import_status(handle_id). Default is true for 0.6.x backwards compatibility."
|
|
2497
3056
|
}
|
|
2498
3057
|
},
|
|
2499
3058
|
// Neither field is "required" at the schema level; xor + presence is
|
|
@@ -2508,6 +3067,18 @@ var importLeads = {
|
|
|
2508
3067
|
description: "Imported leads. Domains mode: [{domain, leadId, name}]. Records mode: [{rowId, domain?, leadId, name}].",
|
|
2509
3068
|
items: { type: "object" }
|
|
2510
3069
|
},
|
|
3070
|
+
status: {
|
|
3071
|
+
type: "string",
|
|
3072
|
+
description: "`running` when wait_for_completion=false; absent on the legacy blocking result."
|
|
3073
|
+
},
|
|
3074
|
+
handle_id: {
|
|
3075
|
+
type: "string",
|
|
3076
|
+
description: "Persisted UUID handle to pass to leadbay_import_status."
|
|
3077
|
+
},
|
|
3078
|
+
progress: {
|
|
3079
|
+
type: "object",
|
|
3080
|
+
description: "Current async import progress when wait_for_completion=false."
|
|
3081
|
+
},
|
|
2511
3082
|
not_imported: {
|
|
2512
3083
|
type: "array",
|
|
2513
3084
|
description: "Inputs that did NOT yield a leadId. Each entry has a `reason` ('malformed', 'NO_MATCH', 'TIMEOUT', etc.) plus the input echo.",
|
|
@@ -2529,7 +3100,11 @@ var importLeads = {
|
|
|
2529
3100
|
},
|
|
2530
3101
|
_meta: { type: "object" }
|
|
2531
3102
|
},
|
|
2532
|
-
required: ["
|
|
3103
|
+
required: ["importIds", "region", "_meta"],
|
|
3104
|
+
anyOf: [
|
|
3105
|
+
{ required: ["leads", "not_imported", "importIds", "region", "_meta"] },
|
|
3106
|
+
{ required: ["status", "handle_id", "importIds", "progress", "region", "_meta"] }
|
|
3107
|
+
]
|
|
2533
3108
|
},
|
|
2534
3109
|
execute: async (client, params, ctx) => {
|
|
2535
3110
|
const signal = ctx?.signal;
|
|
@@ -2537,6 +3112,7 @@ var importLeads = {
|
|
|
2537
3112
|
const perPhaseBudget = params.per_phase_budget_ms ?? DEFAULT_PER_PHASE_BUDGET_MS;
|
|
2538
3113
|
const totalBudget = params.total_budget_ms ?? DEFAULT_TOTAL_BUDGET_MS;
|
|
2539
3114
|
const totalDeadline = Date.now() + totalBudget;
|
|
3115
|
+
const waitForCompletion = params.wait_for_completion ?? true;
|
|
2540
3116
|
const hasDomains = Array.isArray(params.domains) && params.domains.length > 0;
|
|
2541
3117
|
const hasRecords = Array.isArray(params.records) && params.records.length > 0;
|
|
2542
3118
|
if (hasDomains && hasRecords) {
|
|
@@ -2563,13 +3139,13 @@ var importLeads = {
|
|
|
2563
3139
|
}
|
|
2564
3140
|
const prep = hasDomains ? prepareDomainsMode(client, params.domains) : prepareRecordsMode(client, params.records, params.mappings, customFieldCatalog);
|
|
2565
3141
|
if (prep.validInputs.length === 0) {
|
|
2566
|
-
const
|
|
3142
|
+
const not_imported = prep.malformedDomains.map((d) => ({
|
|
2567
3143
|
domain: d,
|
|
2568
3144
|
reason: "malformed"
|
|
2569
3145
|
}));
|
|
2570
3146
|
return {
|
|
2571
3147
|
leads: [],
|
|
2572
|
-
not_imported
|
|
3148
|
+
not_imported,
|
|
2573
3149
|
importIds: [],
|
|
2574
3150
|
region: client.region,
|
|
2575
3151
|
dry_run: dryRun || void 0,
|
|
@@ -2582,6 +3158,67 @@ var importLeads = {
|
|
|
2582
3158
|
};
|
|
2583
3159
|
}
|
|
2584
3160
|
const chunks = chunkAt100(prep.validInputs);
|
|
3161
|
+
if (!waitForCompletion) {
|
|
3162
|
+
if (!ctx?.bulkTracker) {
|
|
3163
|
+
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_import_leads wait_for_completion=false needs a BulkTracker so the handle survives restart.", "");
|
|
3164
|
+
}
|
|
3165
|
+
const reservation = await ctx.bulkTracker.findOrCreatePendingImport({
|
|
3166
|
+
import_fingerprint: importFingerprint(params, prep),
|
|
3167
|
+
mode: prep.mode,
|
|
3168
|
+
dry_run: dryRun,
|
|
3169
|
+
records_total: prep.validInputs.length
|
|
3170
|
+
});
|
|
3171
|
+
const importIds2 = [...reservation.record.import_ids];
|
|
3172
|
+
const uploadedChunks = [];
|
|
3173
|
+
if (!reservation.reused || reservation.record.import_ids.length === 0) {
|
|
3174
|
+
try {
|
|
3175
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
3176
|
+
const upload = await uploadOneChunk(client, chunks[i], i, chunks.length, prep.header, ctx, (id) => {
|
|
3177
|
+
if (!importIds2.includes(id))
|
|
3178
|
+
importIds2.push(id);
|
|
3179
|
+
});
|
|
3180
|
+
uploadedChunks.push(upload);
|
|
3181
|
+
await ctx.bulkTracker.setImportIds(reservation.record.bulk_id, importIds2);
|
|
3182
|
+
}
|
|
3183
|
+
await ctx.bulkTracker.setImportProgress(reservation.record.bulk_id, {
|
|
3184
|
+
phase: "preprocess",
|
|
3185
|
+
records_processed: 0,
|
|
3186
|
+
records_total: prep.validInputs.length
|
|
3187
|
+
});
|
|
3188
|
+
} catch (err) {
|
|
3189
|
+
await ctx.bulkTracker.markImportFailed(reservation.record.bulk_id, err?.message ?? err?.code ?? "unknown");
|
|
3190
|
+
throw err;
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
3193
|
+
if (uploadedChunks.length > 0) {
|
|
3194
|
+
void runImportInBackground(client, prep, uploadedChunks, {
|
|
3195
|
+
dryRun,
|
|
3196
|
+
perPhaseBudget,
|
|
3197
|
+
totalBudget
|
|
3198
|
+
}, ctx, reservation.record.bulk_id);
|
|
3199
|
+
}
|
|
3200
|
+
return {
|
|
3201
|
+
status: "running",
|
|
3202
|
+
handle_id: reservation.record.bulk_id,
|
|
3203
|
+
importIds: importIds2,
|
|
3204
|
+
progress: {
|
|
3205
|
+
phase: reservation.record.status === "complete" ? "complete" : importIds2.length > 0 ? "preprocess" : "queued",
|
|
3206
|
+
records_processed: reservation.record.status === "complete" ? reservation.record.records_total : 0,
|
|
3207
|
+
records_total: reservation.record.records_total
|
|
3208
|
+
},
|
|
3209
|
+
region: client.region,
|
|
3210
|
+
...reservation.reused ? {
|
|
3211
|
+
reused: true,
|
|
3212
|
+
seconds_since_original: reservation.seconds_since_original
|
|
3213
|
+
} : {},
|
|
3214
|
+
_meta: client.lastMeta ?? {
|
|
3215
|
+
region: client.region,
|
|
3216
|
+
endpoint: "POST /imports",
|
|
3217
|
+
latency_ms: null,
|
|
3218
|
+
retry_after: null
|
|
3219
|
+
}
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
2585
3222
|
ctx?.logger?.info?.(`import-leads(${prep.mode}): ${prep.validInputs.length} rows \u2192 ${chunks.length} chunk(s); dry_run=${dryRun}, totalBudgetMs=${totalBudget}`);
|
|
2586
3223
|
const importIds = [];
|
|
2587
3224
|
const matched = /* @__PURE__ */ new Map();
|
|
@@ -2615,88 +3252,51 @@ var importLeads = {
|
|
|
2615
3252
|
throw err;
|
|
2616
3253
|
}
|
|
2617
3254
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
rowId: inp.rowId,
|
|
2644
|
-
leadId: m.leadId,
|
|
2645
|
-
name: m.name
|
|
2646
|
-
};
|
|
2647
|
-
if (m.domain ?? inp.outputDomain)
|
|
2648
|
-
e.domain = m.domain ?? inp.outputDomain;
|
|
2649
|
-
leads.push(e);
|
|
2650
|
-
}
|
|
2651
|
-
continue;
|
|
2652
|
-
}
|
|
2653
|
-
const ni = notImported.get(inp.index);
|
|
2654
|
-
if (ni) {
|
|
2655
|
-
if (prep.mode === "domains") {
|
|
2656
|
-
not_imported.push({ domain: inp.outputDomain, reason: ni.reason });
|
|
2657
|
-
} else {
|
|
2658
|
-
const e = { rowId: inp.rowId, reason: ni.reason };
|
|
2659
|
-
if (ni.domain ?? inp.outputDomain)
|
|
2660
|
-
e.domain = ni.domain ?? inp.outputDomain;
|
|
2661
|
-
not_imported.push(e);
|
|
3255
|
+
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled);
|
|
3256
|
+
}
|
|
3257
|
+
};
|
|
3258
|
+
async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, handleId) {
|
|
3259
|
+
const tracker = ctx.bulkTracker;
|
|
3260
|
+
if (!tracker)
|
|
3261
|
+
return;
|
|
3262
|
+
void tracker.setImportProgress(handleId, {
|
|
3263
|
+
phase: "preprocess",
|
|
3264
|
+
records_processed: 0,
|
|
3265
|
+
records_total: prep.validInputs.length
|
|
3266
|
+
}).catch(() => {
|
|
3267
|
+
});
|
|
3268
|
+
setTimeout(() => {
|
|
3269
|
+
void (async () => {
|
|
3270
|
+
const bgCtx = { logger: ctx.logger, bulkTracker: tracker };
|
|
3271
|
+
const importIds = uploadedChunks.map((chunk) => chunk.importId);
|
|
3272
|
+
const matched = /* @__PURE__ */ new Map();
|
|
3273
|
+
const notImported = /* @__PURE__ */ new Map();
|
|
3274
|
+
try {
|
|
3275
|
+
const totalDeadline = Date.now() + opts.totalBudget;
|
|
3276
|
+
for (const upload of uploadedChunks) {
|
|
3277
|
+
const out = await completeUploadedChunk(client, upload, prep.mappings, opts.dryRun, opts.perPhaseBudget, totalDeadline, bgCtx, void 0);
|
|
3278
|
+
if (!opts.dryRun) {
|
|
3279
|
+
reconcileOneChunk(prep, out, matched, notImported);
|
|
2662
3280
|
}
|
|
2663
|
-
continue;
|
|
2664
|
-
}
|
|
2665
|
-
if (prep.mode === "domains") {
|
|
2666
|
-
not_imported.push({ domain: inp.outputDomain, reason: "internal_error" });
|
|
2667
|
-
} else {
|
|
2668
|
-
const e = { rowId: inp.rowId, reason: "internal_error" };
|
|
2669
|
-
if (inp.outputDomain)
|
|
2670
|
-
e.domain = inp.outputDomain;
|
|
2671
|
-
not_imported.push(e);
|
|
2672
3281
|
}
|
|
3282
|
+
const result = buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false);
|
|
3283
|
+
await tracker.markImportComplete(handleId, {
|
|
3284
|
+
leads: result.leads,
|
|
3285
|
+
not_imported: result.not_imported,
|
|
3286
|
+
importIds: result.importIds
|
|
3287
|
+
});
|
|
3288
|
+
} catch (err) {
|
|
3289
|
+
await tracker.markImportFailed(handleId, err?.message ?? err?.code ?? "unknown");
|
|
2673
3290
|
}
|
|
2674
|
-
}
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
}
|
|
2678
|
-
return {
|
|
2679
|
-
leads,
|
|
2680
|
-
not_imported,
|
|
2681
|
-
importIds,
|
|
2682
|
-
region: client.region,
|
|
2683
|
-
cancelled: cancelled || void 0,
|
|
2684
|
-
dry_run: dryRun || void 0,
|
|
2685
|
-
_meta: client.lastMeta ?? {
|
|
2686
|
-
region: client.region,
|
|
2687
|
-
endpoint: "POST /imports",
|
|
2688
|
-
latency_ms: null,
|
|
2689
|
-
retry_after: null
|
|
2690
|
-
}
|
|
2691
|
-
};
|
|
2692
|
-
}
|
|
2693
|
-
};
|
|
3291
|
+
})();
|
|
3292
|
+
}, 0);
|
|
3293
|
+
}
|
|
2694
3294
|
|
|
2695
3295
|
// ../core/dist/tools/list-mappable-fields.js
|
|
2696
3296
|
var STANDARD_FIELDS = [
|
|
2697
3297
|
{ name: "LEAD_NAME", description: "Company name. Required for fuzzy match." },
|
|
2698
3298
|
{ name: "LEAD_WEBSITE", description: "Company domain (preferred matcher; protocol/path auto-stripped)." },
|
|
2699
|
-
{ name: "EMAIL", description: "
|
|
3299
|
+
{ name: "EMAIL", description: "Lead/company email \u2014 domain part may be used as a website-fallback matcher. For a person's email, use CONTACT_EMAIL and optionally derive a separate business-domain column for LEAD_WEBSITE." },
|
|
2700
3300
|
{ name: "CRM_ID", description: "Your CRM's stable lead identifier (round-trips back as crm_id on the lead)." },
|
|
2701
3301
|
{ name: "LEADBAY_ID", description: "Leadbay UUID, if you already have one (matches by id, no fuzzy needed)." },
|
|
2702
3302
|
{ name: "DEAL_CRM_ID", description: "Your CRM's deal id (one deal per row; combined with LEAD_STATUS forms a sales record)." },
|
|
@@ -2714,7 +3314,7 @@ var STANDARD_FIELDS = [
|
|
|
2714
3314
|
{ name: "SIREN", description: "French SIREN registry number (9 digits) \u2014 auto-matches against the FR registry." },
|
|
2715
3315
|
{ name: "CONTACT_FIRST_NAME", description: "Contact first name (creates an org contact)." },
|
|
2716
3316
|
{ name: "CONTACT_LAST_NAME", description: "Contact last name." },
|
|
2717
|
-
{ name: "CONTACT_EMAIL", description: "Contact email." },
|
|
3317
|
+
{ name: "CONTACT_EMAIL", description: "Contact email. Does not replace the parent company's LEAD_WEBSITE; derive a company domain from this only when it is a business domain, not a personal mailbox provider." },
|
|
2718
3318
|
{ name: "CONTACT_PHONE_NUMBER", description: "Contact phone (free-form)." },
|
|
2719
3319
|
{ name: "CONTACT_TITLE", description: "Contact job title." },
|
|
2720
3320
|
{ name: "CONTACT_LINKEDIN", description: "Contact LinkedIn URL." }
|
|
@@ -2732,7 +3332,7 @@ function describeCustomField(f) {
|
|
|
2732
3332
|
case "DATETIME":
|
|
2733
3333
|
return `Custom DATETIME field${f.config?.format ? ` (format: ${f.config.format})` : " (ISO datetime)"}.`;
|
|
2734
3334
|
case "EXTERNAL_ID":
|
|
2735
|
-
return `Custom EXTERNAL_ID field \u2014 opaque id${f.config?.urlTemplate ? ` (deep-link template configured)` : ""}.`;
|
|
3335
|
+
return `Custom EXTERNAL_ID field \u2014 opaque id${f.config?.url_template || f.config?.urlTemplate ? ` (deep-link template configured)` : ""}.`;
|
|
2736
3336
|
default:
|
|
2737
3337
|
return `Custom field of unrecognized type "${f.type}" \u2014 pass values as strings.`;
|
|
2738
3338
|
}
|
|
@@ -2748,7 +3348,7 @@ var listMappableFields = {
|
|
|
2748
3348
|
idempotentHint: true,
|
|
2749
3349
|
openWorldHint: true
|
|
2750
3350
|
},
|
|
2751
|
-
description:
|
|
3351
|
+
description: leadbay_list_mappable_fields,
|
|
2752
3352
|
inputSchema: {
|
|
2753
3353
|
type: "object",
|
|
2754
3354
|
properties: {
|
|
@@ -2826,7 +3426,10 @@ var listMappableFields = {
|
|
|
2826
3426
|
}
|
|
2827
3427
|
};
|
|
2828
3428
|
if (Array.isArray(params.for_records) && params.for_records.length > 0) {
|
|
2829
|
-
const notes = [
|
|
3429
|
+
const notes = [
|
|
3430
|
+
"mapping_hints are backend suggestions, not final truth. Inspect values semantically before importing; person columns like first_name/last_name should map to CONTACT_* fields, not LEAD_NAME.",
|
|
3431
|
+
"If mapping_hints disagree with the user's file semantics, ignore the hint. Use leadbay_resolve_import_rows with explicit identity_mappings for identity matching, then author final mappings yourself."
|
|
3432
|
+
];
|
|
2830
3433
|
try {
|
|
2831
3434
|
const sample = params.for_records.slice(0, PREVIEW_SAMPLE_CAP);
|
|
2832
3435
|
const headerSet = /* @__PURE__ */ new Set();
|
|
@@ -2910,7 +3513,7 @@ var selectLeads = {
|
|
|
2910
3513
|
idempotentHint: true,
|
|
2911
3514
|
openWorldHint: true
|
|
2912
3515
|
},
|
|
2913
|
-
description:
|
|
3516
|
+
description: leadbay_select_leads,
|
|
2914
3517
|
optional: true,
|
|
2915
3518
|
write: true,
|
|
2916
3519
|
inputSchema: {
|
|
@@ -2954,7 +3557,7 @@ var deselectLeads = {
|
|
|
2954
3557
|
idempotentHint: true,
|
|
2955
3558
|
openWorldHint: true
|
|
2956
3559
|
},
|
|
2957
|
-
description:
|
|
3560
|
+
description: leadbay_deselect_leads,
|
|
2958
3561
|
optional: true,
|
|
2959
3562
|
write: true,
|
|
2960
3563
|
inputSchema: {
|
|
@@ -2987,7 +3590,7 @@ var clearSelection = {
|
|
|
2987
3590
|
idempotentHint: true,
|
|
2988
3591
|
openWorldHint: true
|
|
2989
3592
|
},
|
|
2990
|
-
description:
|
|
3593
|
+
description: leadbay_clear_selection,
|
|
2991
3594
|
optional: true,
|
|
2992
3595
|
write: true,
|
|
2993
3596
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
@@ -3007,7 +3610,7 @@ var setActiveLens = {
|
|
|
3007
3610
|
idempotentHint: true,
|
|
3008
3611
|
openWorldHint: true
|
|
3009
3612
|
},
|
|
3010
|
-
description:
|
|
3613
|
+
description: leadbay_set_active_lens,
|
|
3011
3614
|
optional: true,
|
|
3012
3615
|
write: true,
|
|
3013
3616
|
inputSchema: {
|
|
@@ -3034,7 +3637,7 @@ var createLens = {
|
|
|
3034
3637
|
idempotentHint: false,
|
|
3035
3638
|
openWorldHint: true
|
|
3036
3639
|
},
|
|
3037
|
-
description:
|
|
3640
|
+
description: leadbay_create_lens,
|
|
3038
3641
|
optional: true,
|
|
3039
3642
|
write: true,
|
|
3040
3643
|
inputSchema: {
|
|
@@ -3081,7 +3684,7 @@ var updateLens = {
|
|
|
3081
3684
|
idempotentHint: true,
|
|
3082
3685
|
openWorldHint: true
|
|
3083
3686
|
},
|
|
3084
|
-
description:
|
|
3687
|
+
description: leadbay_update_lens,
|
|
3085
3688
|
optional: true,
|
|
3086
3689
|
write: true,
|
|
3087
3690
|
inputSchema: {
|
|
@@ -3114,7 +3717,7 @@ var updateLensFilter = {
|
|
|
3114
3717
|
idempotentHint: true,
|
|
3115
3718
|
openWorldHint: true
|
|
3116
3719
|
},
|
|
3117
|
-
description:
|
|
3720
|
+
description: leadbay_update_lens_filter,
|
|
3118
3721
|
optional: true,
|
|
3119
3722
|
write: true,
|
|
3120
3723
|
inputSchema: {
|
|
@@ -3160,7 +3763,7 @@ var createLensDraft = {
|
|
|
3160
3763
|
idempotentHint: false,
|
|
3161
3764
|
openWorldHint: true
|
|
3162
3765
|
},
|
|
3163
|
-
description:
|
|
3766
|
+
description: leadbay_create_lens_draft,
|
|
3164
3767
|
optional: true,
|
|
3165
3768
|
write: true,
|
|
3166
3769
|
inputSchema: {
|
|
@@ -3184,7 +3787,7 @@ var promoteLens = {
|
|
|
3184
3787
|
idempotentHint: false,
|
|
3185
3788
|
openWorldHint: true
|
|
3186
3789
|
},
|
|
3187
|
-
description:
|
|
3790
|
+
description: leadbay_promote_lens,
|
|
3188
3791
|
optional: true,
|
|
3189
3792
|
write: true,
|
|
3190
3793
|
inputSchema: {
|
|
@@ -3210,7 +3813,7 @@ var setUserPrompt = {
|
|
|
3210
3813
|
idempotentHint: true,
|
|
3211
3814
|
openWorldHint: true
|
|
3212
3815
|
},
|
|
3213
|
-
description:
|
|
3816
|
+
description: leadbay_set_user_prompt,
|
|
3214
3817
|
optional: true,
|
|
3215
3818
|
write: true,
|
|
3216
3819
|
inputSchema: {
|
|
@@ -3255,7 +3858,7 @@ var clearUserPrompt = {
|
|
|
3255
3858
|
idempotentHint: true,
|
|
3256
3859
|
openWorldHint: true
|
|
3257
3860
|
},
|
|
3258
|
-
description:
|
|
3861
|
+
description: leadbay_clear_user_prompt,
|
|
3259
3862
|
optional: true,
|
|
3260
3863
|
write: true,
|
|
3261
3864
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
@@ -3277,7 +3880,7 @@ var pickClarification = {
|
|
|
3277
3880
|
idempotentHint: false,
|
|
3278
3881
|
openWorldHint: true
|
|
3279
3882
|
},
|
|
3280
|
-
description:
|
|
3883
|
+
description: leadbay_pick_clarification,
|
|
3281
3884
|
optional: true,
|
|
3282
3885
|
write: true,
|
|
3283
3886
|
inputSchema: {
|
|
@@ -3324,7 +3927,7 @@ var dismissClarification = {
|
|
|
3324
3927
|
idempotentHint: false,
|
|
3325
3928
|
openWorldHint: true
|
|
3326
3929
|
},
|
|
3327
|
-
description:
|
|
3930
|
+
description: leadbay_dismiss_clarification,
|
|
3328
3931
|
optional: true,
|
|
3329
3932
|
write: true,
|
|
3330
3933
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
@@ -3357,7 +3960,7 @@ var setEpilogueStatus = {
|
|
|
3357
3960
|
idempotentHint: true,
|
|
3358
3961
|
openWorldHint: true
|
|
3359
3962
|
},
|
|
3360
|
-
description:
|
|
3963
|
+
description: leadbay_set_epilogue_status,
|
|
3361
3964
|
optional: true,
|
|
3362
3965
|
write: true,
|
|
3363
3966
|
inputSchema: {
|
|
@@ -3404,7 +4007,7 @@ var removeEpilogue = {
|
|
|
3404
4007
|
idempotentHint: true,
|
|
3405
4008
|
openWorldHint: true
|
|
3406
4009
|
},
|
|
3407
|
-
description:
|
|
4010
|
+
description: leadbay_remove_epilogue,
|
|
3408
4011
|
optional: true,
|
|
3409
4012
|
write: true,
|
|
3410
4013
|
inputSchema: {
|
|
@@ -3437,7 +4040,7 @@ var previewBulkEnrichment = {
|
|
|
3437
4040
|
idempotentHint: true,
|
|
3438
4041
|
openWorldHint: true
|
|
3439
4042
|
},
|
|
3440
|
-
description:
|
|
4043
|
+
description: leadbay_preview_bulk_enrichment,
|
|
3441
4044
|
optional: true,
|
|
3442
4045
|
write: true,
|
|
3443
4046
|
inputSchema: {
|
|
@@ -3467,7 +4070,7 @@ var launchBulkEnrichment = {
|
|
|
3467
4070
|
idempotentHint: true,
|
|
3468
4071
|
openWorldHint: true
|
|
3469
4072
|
},
|
|
3470
|
-
description:
|
|
4073
|
+
description: leadbay_launch_bulk_enrichment,
|
|
3471
4074
|
optional: true,
|
|
3472
4075
|
write: true,
|
|
3473
4076
|
inputSchema: {
|
|
@@ -3520,6 +4123,98 @@ var launchBulkEnrichment = {
|
|
|
3520
4123
|
}
|
|
3521
4124
|
};
|
|
3522
4125
|
|
|
4126
|
+
// ../core/dist/tools/create-custom-field.js
|
|
4127
|
+
var createCustomField = {
|
|
4128
|
+
name: "leadbay_create_custom_field",
|
|
4129
|
+
annotations: {
|
|
4130
|
+
title: "Create CRM custom field",
|
|
4131
|
+
readOnlyHint: false,
|
|
4132
|
+
destructiveHint: false,
|
|
4133
|
+
idempotentHint: true,
|
|
4134
|
+
openWorldHint: true
|
|
4135
|
+
},
|
|
4136
|
+
description: leadbay_create_custom_field,
|
|
4137
|
+
write: true,
|
|
4138
|
+
version: "0.6.4",
|
|
4139
|
+
inputSchema: {
|
|
4140
|
+
type: "object",
|
|
4141
|
+
properties: {
|
|
4142
|
+
name: {
|
|
4143
|
+
type: "string",
|
|
4144
|
+
description: "User-visible custom field name, e.g. 'HubSpot Contact'."
|
|
4145
|
+
},
|
|
4146
|
+
type: {
|
|
4147
|
+
type: "string",
|
|
4148
|
+
description: "Custom field type: TEXT, NUMBER, PRICE, DATE, DATETIME, or EXTERNAL_ID. Defaults to TEXT."
|
|
4149
|
+
},
|
|
4150
|
+
config: {
|
|
4151
|
+
type: ["object", "null"],
|
|
4152
|
+
description: "Type-specific config. EXTERNAL_ID requires {url_template:'https://.../{value}'}; PRICE requires {currency:'USD'}; DATE/DATETIME may set {format}."
|
|
4153
|
+
},
|
|
4154
|
+
if_not_exists: {
|
|
4155
|
+
type: "boolean",
|
|
4156
|
+
description: "Default true. If a custom field with the same name already exists, return it instead of creating a duplicate."
|
|
4157
|
+
}
|
|
4158
|
+
},
|
|
4159
|
+
required: ["name"],
|
|
4160
|
+
additionalProperties: false
|
|
4161
|
+
},
|
|
4162
|
+
outputSchema: {
|
|
4163
|
+
type: "object",
|
|
4164
|
+
properties: {
|
|
4165
|
+
id: { type: "string" },
|
|
4166
|
+
name: { type: "string" },
|
|
4167
|
+
type: { type: "string" },
|
|
4168
|
+
config: { type: ["object", "null"] },
|
|
4169
|
+
mapping_value: {
|
|
4170
|
+
type: "string",
|
|
4171
|
+
description: "Wire mapping value to use in import mappings, e.g. CUSTOM.123."
|
|
4172
|
+
},
|
|
4173
|
+
existed: {
|
|
4174
|
+
type: "boolean",
|
|
4175
|
+
description: "True when if_not_exists reused an existing custom field."
|
|
4176
|
+
}
|
|
4177
|
+
},
|
|
4178
|
+
required: ["id", "name", "type", "mapping_value", "existed"]
|
|
4179
|
+
},
|
|
4180
|
+
execute: async (client, params) => {
|
|
4181
|
+
const name = params.name?.trim();
|
|
4182
|
+
if (!name) {
|
|
4183
|
+
throw client.makeError("CUSTOM_FIELD_NAME_REQUIRED", "name must be a non-empty string", "Pass a user-visible custom field name, e.g. 'HubSpot Contact'.", "POST /crm/custom_fields");
|
|
4184
|
+
}
|
|
4185
|
+
const type = params.type ?? "TEXT";
|
|
4186
|
+
const config = params.config ?? null;
|
|
4187
|
+
if (type === "EXTERNAL_ID") {
|
|
4188
|
+
const urlTemplate = config?.url_template ?? config?.urlTemplate;
|
|
4189
|
+
if (!urlTemplate || !urlTemplate.includes("{value}")) {
|
|
4190
|
+
throw client.makeError("CUSTOM_FIELD_EXTERNAL_ID_TEMPLATE_REQUIRED", "EXTERNAL_ID custom fields require config.url_template containing {value}", "Use a URL template like https://app.hubspot.com/contacts/<portal-id>/record/0-1/{value}.", "POST /crm/custom_fields");
|
|
4191
|
+
}
|
|
4192
|
+
}
|
|
4193
|
+
if (params.if_not_exists ?? true) {
|
|
4194
|
+
const existing = await client.request("GET", "/crm/custom_fields");
|
|
4195
|
+
const found = (existing ?? []).find((f) => f.name.toLowerCase() === name.toLowerCase());
|
|
4196
|
+
if (found) {
|
|
4197
|
+
return {
|
|
4198
|
+
...found,
|
|
4199
|
+
mapping_value: `CUSTOM.${found.id}`,
|
|
4200
|
+
existed: true
|
|
4201
|
+
};
|
|
4202
|
+
}
|
|
4203
|
+
}
|
|
4204
|
+
const body = {
|
|
4205
|
+
name,
|
|
4206
|
+
type,
|
|
4207
|
+
...config ? { config } : {}
|
|
4208
|
+
};
|
|
4209
|
+
const created = await client.request("POST", "/crm/custom_fields", body);
|
|
4210
|
+
return {
|
|
4211
|
+
...created,
|
|
4212
|
+
mapping_value: `CUSTOM.${created.id}`,
|
|
4213
|
+
existed: false
|
|
4214
|
+
};
|
|
4215
|
+
}
|
|
4216
|
+
};
|
|
4217
|
+
|
|
3523
4218
|
// ../core/dist/composite/research-company.js
|
|
3524
4219
|
var researchCompany = {
|
|
3525
4220
|
name: "leadbay_research_company",
|
|
@@ -3530,7 +4225,7 @@ var researchCompany = {
|
|
|
3530
4225
|
idempotentHint: true,
|
|
3531
4226
|
openWorldHint: true
|
|
3532
4227
|
},
|
|
3533
|
-
description:
|
|
4228
|
+
description: leadbay_research_company,
|
|
3534
4229
|
inputSchema: {
|
|
3535
4230
|
type: "object",
|
|
3536
4231
|
properties: {
|
|
@@ -3614,7 +4309,7 @@ var prepareOutreach = {
|
|
|
3614
4309
|
idempotentHint: true,
|
|
3615
4310
|
openWorldHint: true
|
|
3616
4311
|
},
|
|
3617
|
-
description:
|
|
4312
|
+
description: leadbay_prepare_outreach,
|
|
3618
4313
|
optional: true,
|
|
3619
4314
|
inputSchema: {
|
|
3620
4315
|
type: "object",
|
|
@@ -3729,7 +4424,7 @@ var pullLeads = {
|
|
|
3729
4424
|
idempotentHint: true,
|
|
3730
4425
|
openWorldHint: true
|
|
3731
4426
|
},
|
|
3732
|
-
description:
|
|
4427
|
+
description: leadbay_pull_leads,
|
|
3733
4428
|
inputSchema: {
|
|
3734
4429
|
type: "object",
|
|
3735
4430
|
properties: {
|
|
@@ -3987,7 +4682,7 @@ var researchLead = {
|
|
|
3987
4682
|
idempotentHint: true,
|
|
3988
4683
|
openWorldHint: true
|
|
3989
4684
|
},
|
|
3990
|
-
description:
|
|
4685
|
+
description: leadbay_research_lead,
|
|
3991
4686
|
inputSchema: {
|
|
3992
4687
|
type: "object",
|
|
3993
4688
|
properties: {
|
|
@@ -4272,7 +4967,7 @@ var recallOrderedTitles = {
|
|
|
4272
4967
|
idempotentHint: true,
|
|
4273
4968
|
openWorldHint: true
|
|
4274
4969
|
},
|
|
4275
|
-
description:
|
|
4970
|
+
description: leadbay_recall_ordered_titles,
|
|
4276
4971
|
inputSchema: {
|
|
4277
4972
|
type: "object",
|
|
4278
4973
|
properties: {
|
|
@@ -4393,7 +5088,7 @@ var accountStatus = {
|
|
|
4393
5088
|
idempotentHint: true,
|
|
4394
5089
|
openWorldHint: true
|
|
4395
5090
|
},
|
|
4396
|
-
description:
|
|
5091
|
+
description: leadbay_account_status,
|
|
4397
5092
|
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
4398
5093
|
outputSchema: {
|
|
4399
5094
|
type: "object",
|
|
@@ -4490,7 +5185,7 @@ var bulkQualifyLeads = {
|
|
|
4490
5185
|
idempotentHint: true,
|
|
4491
5186
|
openWorldHint: true
|
|
4492
5187
|
},
|
|
4493
|
-
description:
|
|
5188
|
+
description: leadbay_bulk_qualify_leads,
|
|
4494
5189
|
inputSchema: {
|
|
4495
5190
|
type: "object",
|
|
4496
5191
|
properties: {
|
|
@@ -4514,6 +5209,10 @@ var bulkQualifyLeads = {
|
|
|
4514
5209
|
total_budget_ms: {
|
|
4515
5210
|
type: "number",
|
|
4516
5211
|
description: `Total polling budget in ms (default ${DEFAULT_TOTAL_BUDGET_MS2})`
|
|
5212
|
+
},
|
|
5213
|
+
wait_for_completion: {
|
|
5214
|
+
type: "boolean",
|
|
5215
|
+
description: "When false, launch qualification and return `{status:'running', qualify_id}` immediately. Poll leadbay_qualify_status. Default is true for 0.6.x backwards compatibility."
|
|
4517
5216
|
}
|
|
4518
5217
|
},
|
|
4519
5218
|
additionalProperties: false
|
|
@@ -4526,6 +5225,14 @@ var bulkQualifyLeads = {
|
|
|
4526
5225
|
description: "Leads whose qualification finished within budget. Each entry: lead_id, qualification_summary{answered,total,avg_qualification_boost}, signals_count.",
|
|
4527
5226
|
items: { type: "object" }
|
|
4528
5227
|
},
|
|
5228
|
+
status: {
|
|
5229
|
+
type: "string",
|
|
5230
|
+
description: "`running` when wait_for_completion=false; absent on the legacy blocking result."
|
|
5231
|
+
},
|
|
5232
|
+
handle_id: { type: "string", description: "Alias of qualify_id for handle-oriented callers." },
|
|
5233
|
+
qualify_id: { type: "string", description: "UUIDv4 to poll via leadbay_qualify_status." },
|
|
5234
|
+
lead_ids: { type: "array", items: { type: "string" } },
|
|
5235
|
+
launched_count: { type: "number" },
|
|
4529
5236
|
still_running: {
|
|
4530
5237
|
type: "array",
|
|
4531
5238
|
description: "Leads launched but whose qualification did not complete within budget. Re-poll via leadbay_qualify_status with the bulk_id (when present).",
|
|
@@ -4556,13 +5263,30 @@ var bulkQualifyLeads = {
|
|
|
4556
5263
|
properties: { region: { type: "string" } }
|
|
4557
5264
|
}
|
|
4558
5265
|
},
|
|
4559
|
-
required: ["
|
|
5266
|
+
required: ["failed", "quota_exceeded"],
|
|
5267
|
+
anyOf: [
|
|
5268
|
+
{ required: ["qualified", "still_running", "failed", "quota_exceeded"] },
|
|
5269
|
+
{
|
|
5270
|
+
required: [
|
|
5271
|
+
"status",
|
|
5272
|
+
"handle_id",
|
|
5273
|
+
"qualify_id",
|
|
5274
|
+
"lead_ids",
|
|
5275
|
+
"launched_count",
|
|
5276
|
+
"failed",
|
|
5277
|
+
"quota_exceeded",
|
|
5278
|
+
"lens_id",
|
|
5279
|
+
"_meta"
|
|
5280
|
+
]
|
|
5281
|
+
}
|
|
5282
|
+
]
|
|
4560
5283
|
},
|
|
4561
5284
|
execute: async (client, params, ctx) => {
|
|
4562
5285
|
const wantCount = Math.min(params.count ?? DEFAULT_COUNT, MAX_COUNT);
|
|
4563
5286
|
const perLeadBudget = params.per_lead_budget_ms ?? DEFAULT_PER_LEAD_BUDGET_MS;
|
|
4564
5287
|
const totalBudget = params.total_budget_ms ?? DEFAULT_TOTAL_BUDGET_MS2;
|
|
4565
5288
|
const totalDeadline = Date.now() + totalBudget;
|
|
5289
|
+
const waitForCompletion = params.wait_for_completion ?? true;
|
|
4566
5290
|
let candidates;
|
|
4567
5291
|
let exhausted = false;
|
|
4568
5292
|
let totalUnqualifiedFound = 0;
|
|
@@ -4605,6 +5329,58 @@ var bulkQualifyLeads = {
|
|
|
4605
5329
|
message: "No unqualified leads found in this lens \u2014 either all leads have been qualified, or the wishlist is still computing (check leadbay_account_status for computing_wishlist)."
|
|
4606
5330
|
};
|
|
4607
5331
|
}
|
|
5332
|
+
if (!waitForCompletion) {
|
|
5333
|
+
if (!ctx?.bulkTracker) {
|
|
5334
|
+
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_bulk_qualify_leads wait_for_completion=false needs a BulkTracker so qualify_id survives restart.", "");
|
|
5335
|
+
}
|
|
5336
|
+
const reservation = await ctx.bulkTracker.findOrCreatePendingQualify({
|
|
5337
|
+
lead_ids: candidates,
|
|
5338
|
+
import_ids: [],
|
|
5339
|
+
lens_id: lensId,
|
|
5340
|
+
mapping_fingerprint: "bulk_qualify_leads",
|
|
5341
|
+
per_lead_budget_ms: perLeadBudget,
|
|
5342
|
+
total_budget_ms: totalBudget
|
|
5343
|
+
});
|
|
5344
|
+
const launched2 = [];
|
|
5345
|
+
const failed2 = [];
|
|
5346
|
+
let quotaExceeded2 = false;
|
|
5347
|
+
if (!reservation.reused) {
|
|
5348
|
+
for (const leadId of candidates) {
|
|
5349
|
+
if (quotaExceeded2)
|
|
5350
|
+
break;
|
|
5351
|
+
try {
|
|
5352
|
+
await client.requestVoid("POST", `/leads/${leadId}/web_fetch?force_fetch=false`);
|
|
5353
|
+
launched2.push(leadId);
|
|
5354
|
+
} catch (err) {
|
|
5355
|
+
if (err?.code === "QUOTA_EXCEEDED") {
|
|
5356
|
+
quotaExceeded2 = true;
|
|
5357
|
+
} else if (err?.code === "NOT_FOUND") {
|
|
5358
|
+
failed2.push({ lead_id: leadId, error: "lead not found" });
|
|
5359
|
+
} else {
|
|
5360
|
+
failed2.push({
|
|
5361
|
+
lead_id: leadId,
|
|
5362
|
+
error: err?.message ?? err?.code ?? "unknown"
|
|
5363
|
+
});
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
}
|
|
5367
|
+
if (failed2.length === candidates.length || launched2.length > 0 || quotaExceeded2) {
|
|
5368
|
+
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id);
|
|
5369
|
+
}
|
|
5370
|
+
}
|
|
5371
|
+
const out = {
|
|
5372
|
+
status: "running",
|
|
5373
|
+
handle_id: reservation.record.bulk_id,
|
|
5374
|
+
qualify_id: reservation.record.bulk_id,
|
|
5375
|
+
lead_ids: candidates,
|
|
5376
|
+
launched_count: reservation.reused ? reservation.record.lead_ids.length : launched2.length,
|
|
5377
|
+
failed: failed2,
|
|
5378
|
+
quota_exceeded: quotaExceeded2,
|
|
5379
|
+
lens_id: lensId,
|
|
5380
|
+
_meta: { region: client.region }
|
|
5381
|
+
};
|
|
5382
|
+
return out;
|
|
5383
|
+
}
|
|
4608
5384
|
const launched = [];
|
|
4609
5385
|
const failed = [];
|
|
4610
5386
|
let quotaExceeded = false;
|
|
@@ -4648,64 +5424,382 @@ var bulkQualifyLeads = {
|
|
|
4648
5424
|
resolve();
|
|
4649
5425
|
}, { once: true });
|
|
4650
5426
|
});
|
|
4651
|
-
const results = await Promise.all(launched.map(async (leadId) => {
|
|
4652
|
-
const leadDeadline = Math.min(Date.now() + perLeadBudget, totalDeadline);
|
|
4653
|
-
let lastQual = null;
|
|
4654
|
-
let lastWf = null;
|
|
4655
|
-
while (Date.now() < leadDeadline) {
|
|
4656
|
-
if (ctx?.signal?.aborted)
|
|
4657
|
-
break;
|
|
4658
|
-
try {
|
|
4659
|
-
const [wfR, qualR] = await Promise.allSettled([
|
|
4660
|
-
client.request("GET", `/leads/${leadId}/web_fetch`),
|
|
4661
|
-
client.request("GET", `/leads/${leadId}/ai_agent_responses`)
|
|
4662
|
-
]);
|
|
4663
|
-
if (wfR.status === "fulfilled")
|
|
4664
|
-
lastWf = wfR.value;
|
|
4665
|
-
if (qualR.status === "fulfilled")
|
|
4666
|
-
lastQual = qualR.value;
|
|
4667
|
-
const done = lastWf !== null && lastWf.in_progress !== true && Array.isArray(lastQual) && lastQual.length > 0 && lastQual.every((r) => r.score != null);
|
|
4668
|
-
if (done) {
|
|
4669
|
-
progressDone += 1;
|
|
4670
|
-
ctx?.progress?.({
|
|
4671
|
-
progress: progressDone,
|
|
4672
|
-
total: progressTotal,
|
|
4673
|
-
message: `Qualified lead ${leadId} (${progressDone}/${progressTotal})`
|
|
4674
|
-
});
|
|
4675
|
-
break;
|
|
4676
|
-
}
|
|
4677
|
-
} catch {
|
|
5427
|
+
const results = await Promise.all(launched.map(async (leadId) => {
|
|
5428
|
+
const leadDeadline = Math.min(Date.now() + perLeadBudget, totalDeadline);
|
|
5429
|
+
let lastQual = null;
|
|
5430
|
+
let lastWf = null;
|
|
5431
|
+
while (Date.now() < leadDeadline) {
|
|
5432
|
+
if (ctx?.signal?.aborted)
|
|
5433
|
+
break;
|
|
5434
|
+
try {
|
|
5435
|
+
const [wfR, qualR] = await Promise.allSettled([
|
|
5436
|
+
client.request("GET", `/leads/${leadId}/web_fetch`),
|
|
5437
|
+
client.request("GET", `/leads/${leadId}/ai_agent_responses`)
|
|
5438
|
+
]);
|
|
5439
|
+
if (wfR.status === "fulfilled")
|
|
5440
|
+
lastWf = wfR.value;
|
|
5441
|
+
if (qualR.status === "fulfilled")
|
|
5442
|
+
lastQual = qualR.value;
|
|
5443
|
+
const done = lastWf !== null && lastWf.in_progress !== true && Array.isArray(lastQual) && lastQual.length > 0 && lastQual.every((r) => r.score != null);
|
|
5444
|
+
if (done) {
|
|
5445
|
+
progressDone += 1;
|
|
5446
|
+
ctx?.progress?.({
|
|
5447
|
+
progress: progressDone,
|
|
5448
|
+
total: progressTotal,
|
|
5449
|
+
message: `Qualified lead ${leadId} (${progressDone}/${progressTotal})`
|
|
5450
|
+
});
|
|
5451
|
+
break;
|
|
5452
|
+
}
|
|
5453
|
+
} catch {
|
|
5454
|
+
}
|
|
5455
|
+
if (ctx?.signal?.aborted)
|
|
5456
|
+
break;
|
|
5457
|
+
await sleepWithSignal(5e3);
|
|
5458
|
+
}
|
|
5459
|
+
const stillRunning = lastWf?.in_progress === true || !lastQual || lastQual.length === 0 || lastQual.some((r) => r.score == null);
|
|
5460
|
+
const responses = lastQual ?? [];
|
|
5461
|
+
const scores = responses.map((r) => r.score).filter((s) => s != null);
|
|
5462
|
+
const avg = scores.length > 0 ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length * 10) / 10 : null;
|
|
5463
|
+
return {
|
|
5464
|
+
lead_id: leadId,
|
|
5465
|
+
qualification_summary: responses.length > 0 ? {
|
|
5466
|
+
answered: responses.filter((r) => r.score != null).length,
|
|
5467
|
+
total: responses.length,
|
|
5468
|
+
avg_qualification_boost: avg
|
|
5469
|
+
} : null,
|
|
5470
|
+
signals_count: lastWf?.content ? Object.values(lastWf.content).reduce((acc, arr) => acc + (Array.isArray(arr) ? arr.length : 0), 0) : null,
|
|
5471
|
+
_stillRunning: stillRunning
|
|
5472
|
+
};
|
|
5473
|
+
}));
|
|
5474
|
+
const qualified = results.filter((r) => !r._stillRunning).map(({ _stillRunning, ...rest }) => rest);
|
|
5475
|
+
const still_running = results.filter((r) => r._stillRunning).map(({ _stillRunning, ...rest }) => rest);
|
|
5476
|
+
return {
|
|
5477
|
+
qualified,
|
|
5478
|
+
still_running,
|
|
5479
|
+
failed,
|
|
5480
|
+
quota_exceeded: quotaExceeded,
|
|
5481
|
+
exhausted,
|
|
5482
|
+
total_unqualified_found: totalUnqualifiedFound,
|
|
5483
|
+
lens_id: lensId,
|
|
5484
|
+
_meta: { region: client.region }
|
|
5485
|
+
};
|
|
5486
|
+
}
|
|
5487
|
+
};
|
|
5488
|
+
|
|
5489
|
+
// ../core/dist/composite/resolve-import-rows.js
|
|
5490
|
+
var SOCIAL_FIELDS = /* @__PURE__ */ new Set(["linkedin", "facebook", "instagram", "twitter", "tiktok"]);
|
|
5491
|
+
var RESOLVER_TARGETS = /* @__PURE__ */ new Set(["LEADBAY_ID", "CRM_ID", "LEAD_NAME", "LEAD_WEBSITE", "SIREN"]);
|
|
5492
|
+
var DEFAULT_CANDIDATE_PROFILE_LIMIT = 5;
|
|
5493
|
+
function coerceCell2(v) {
|
|
5494
|
+
if (v == null)
|
|
5495
|
+
return "";
|
|
5496
|
+
if (typeof v === "string")
|
|
5497
|
+
return v;
|
|
5498
|
+
if (typeof v === "number" || typeof v === "boolean")
|
|
5499
|
+
return String(v);
|
|
5500
|
+
return JSON.stringify(v) ?? "";
|
|
5501
|
+
}
|
|
5502
|
+
function compactMappings(mappings) {
|
|
5503
|
+
const out = {};
|
|
5504
|
+
for (const [k, v] of Object.entries(mappings)) {
|
|
5505
|
+
if (v)
|
|
5506
|
+
out[k] = v;
|
|
5507
|
+
}
|
|
5508
|
+
return out;
|
|
5509
|
+
}
|
|
5510
|
+
function payloadForRecord(row, mappings) {
|
|
5511
|
+
const payload = {};
|
|
5512
|
+
const socials = {};
|
|
5513
|
+
for (const [field, column] of Object.entries(mappings)) {
|
|
5514
|
+
const value = (row[column] ?? "").trim();
|
|
5515
|
+
if (!value)
|
|
5516
|
+
continue;
|
|
5517
|
+
if (SOCIAL_FIELDS.has(field)) {
|
|
5518
|
+
socials[field] = value;
|
|
5519
|
+
} else {
|
|
5520
|
+
payload[field] = value;
|
|
5521
|
+
}
|
|
5522
|
+
}
|
|
5523
|
+
if (Object.keys(socials).length > 0)
|
|
5524
|
+
payload.socials = socials;
|
|
5525
|
+
return payload;
|
|
5526
|
+
}
|
|
5527
|
+
function identityMappingsForImport(records, identityMappings) {
|
|
5528
|
+
const fields = {};
|
|
5529
|
+
if (records.some((r) => (r.LEADBAY_ID ?? "").trim() !== "")) {
|
|
5530
|
+
fields.LEADBAY_ID = "LEADBAY_ID";
|
|
5531
|
+
}
|
|
5532
|
+
const add = (field, target) => {
|
|
5533
|
+
const column = identityMappings[field];
|
|
5534
|
+
if (!column || fields[column])
|
|
5535
|
+
return;
|
|
5536
|
+
if (!records.some((r) => (r[column] ?? "").trim() !== ""))
|
|
5537
|
+
return;
|
|
5538
|
+
fields[column] = target;
|
|
5539
|
+
};
|
|
5540
|
+
add("leadbay_id", "LEADBAY_ID");
|
|
5541
|
+
add("crm_id", "CRM_ID");
|
|
5542
|
+
add("registry_number", "SIREN");
|
|
5543
|
+
add("name", "LEAD_NAME");
|
|
5544
|
+
add("website", "LEAD_WEBSITE");
|
|
5545
|
+
return { fields, statuses: {}, default_status: null };
|
|
5546
|
+
}
|
|
5547
|
+
function disambiguationPolicy() {
|
|
5548
|
+
return [
|
|
5549
|
+
"Use `matched` lead_id values directly; the tool already writes those into LEADBAY_ID.",
|
|
5550
|
+
"For `ambiguous` rows, do not choose a candidate from score alone. Score is a tied evidence-band, not a confidence percentage.",
|
|
5551
|
+
"For every ambiguous row you resolve, keep a short decision note: selected candidate id, evidence used, conflicting evidence checked, or why LEADBAY_ID stayed blank. Report counts and examples to the user.",
|
|
5552
|
+
"Try to disambiguate relentlessly before giving up: rerun the row with include_candidate_profiles=true and a larger candidate_profile_limit if candidate facts are truncated, and include every trustworthy source signal available (website, full address, postcode, city, phone, registry/CRM id, source URL path, neighborhood/location words).",
|
|
5553
|
+
"Compare addresses intelligently as a human would. Recognize ordinary formatting, abbreviation, spelling, punctuation, casing, accent, direction, ordinal, and suite/unit differences without treating address comparison as a rigid rule checklist. A clear same-place street address match is strong evidence.",
|
|
5554
|
+
"Auto-select an ambiguous candidate when hydrated candidate facts uniquely agree with the source row on strong evidence: exact registry number, exact CRM ID, exact canonical website/domain with only one candidate, exact phone, or name plus clear same-place address match with postcode/city and no conflicting evidence.",
|
|
5555
|
+
"If several candidates share the same website/domain, do not fail fast. Treat it as a chain/multi-location problem: use source street address, postcode, city/neighborhood, phone, source URL path/location slug, and location words in the source name to pick the specific location when exactly one candidate matches.",
|
|
5556
|
+
"Postcode/city alone is not enough, and brand/root-domain alone is not enough for multi-location sources. If several candidates remain plausible after checking location/phone/path evidence, leave LEADBAY_ID blank.",
|
|
5557
|
+
"A domain derived from a contact email is useful only when it is a business domain (not gmail/hotmail/outlook/yahoo/icloud/proton/aol/etc.) and the company/contact context agrees with the candidate. If the domain looks like a POS/vendor/agency/group domain or conflicts with row notes, do not use it for LEADBAY_ID selection.",
|
|
5558
|
+
"If evidence is name-only, fuzzy-name-only, generic directory website, or multiple candidates remain plausible after exhausting location/phone/path evidence, leave LEADBAY_ID blank and import with website/name so Leadbay can crawl or late-match later.",
|
|
5559
|
+
"When the user asked for qualification after import, qualify only the lead IDs that the import returns. Late website matches may appear later via leadbay_import_status."
|
|
5560
|
+
];
|
|
5561
|
+
}
|
|
5562
|
+
function mappingGuidance() {
|
|
5563
|
+
return [
|
|
5564
|
+
"Treat mappings_for_import as a safe identity starting point, not a complete CRM mapping.",
|
|
5565
|
+
"Before importing, inspect every user column and sample values, then make a preservation plan: standard field, CONTACT_* field, Leadbay note, custom field, derived helper, or skip with a reason. The model, not this helper, should decide the complete mapping.",
|
|
5566
|
+
"Default to preserving client-provided business data. For meaningful columns with no standard Leadbay field, call leadbay_list_mappable_fields and create/reuse custom fields instead of silently dropping them. Skip only blank placeholders, duplicate plumbing, raw unparsed blobs after useful values are extracted, or values that would actively harm data quality.",
|
|
5567
|
+
"Always include LEADBAY_ID when records_for_import contains it; it makes deterministic matches import immediately.",
|
|
5568
|
+
"Also map the best available source identity columns: website/domain/url -> LEAD_WEBSITE, company/account/restaurant name -> LEAD_NAME, CRM/system id -> CRM_ID, registry/SIREN/SIRET/company number -> SIREN.",
|
|
5569
|
+
"For contact-only or HubSpot contact exports, derive a separate company_domain/company_website column from CONTACT_EMAIL only when the email domain is a real business domain and agrees with the company/deal/brand context. Do not use POS/vendor/group domains that conflict with the row, and do not derive company identity from private mailbox domains such as gmail.com, hotmail.com, outlook.com, yahoo.com, icloud.com, proton.me/protonmail.com, aol.com, or similar consumer email providers.",
|
|
5570
|
+
"Map contact-person columns when the file contains people: first_name -> CONTACT_FIRST_NAME, last_name -> CONTACT_LAST_NAME, job_title/title -> CONTACT_TITLE, contact email -> CONTACT_EMAIL, contact phone -> CONTACT_PHONE_NUMBER, contact LinkedIn -> CONTACT_LINKEDIN. If a company/restaurant row contains structured owners, decision makers, or contact lists, expand those people into additional import rows that repeat the parent lead identity and contain one CONTACT_* person per row. Multiple rows may point to the same LEADBAY_ID/company; import them as separate contacts on that lead.",
|
|
5571
|
+
"Preserve valuable source-system links. For HubSpot URLs, prefer extracting the stable object id into a clean column and mapping it to an existing or newly created EXTERNAL_ID custom field. Reuse an existing HubSpot linked-id field when present. Preserve raw source identifiers such as hubspot_id and associated_deal in custom fields when they are not already represented by a better standard/custom field. Use TEXT only when no stable id/template can be recovered.",
|
|
5572
|
+
"Clean source-system deal names before using them as LEAD_NAME: strip import campaign suffixes such as BYOC, BYOC only, DD, Uber, trailing separators, and duplicate pipeline labels, while preserving the original associated deal/source value in a custom field when it is meaningful to the user's workflow.",
|
|
5573
|
+
"Drop blank-header columns and placeholder values like `couldn't find`, `yes`, empty arrays, and raw JSON blobs unless you first extract meaningful scalar fields.",
|
|
5574
|
+
"Leadbay has CONTACT_PHONE_NUMBER but no standard LEAD_PHONE field in this surface. Preserve establishment/company phone only via an intentional custom field, not by pretending it is a contact phone.",
|
|
5575
|
+
"Preserve meaningful client notes, data-quality warnings that affect outreach, source record links, and owner/evidence URLs when they help the user's workflow. Do not map noisy scraper plumbing, duplicate blank columns, placeholder values, or long reasoning text.",
|
|
5576
|
+
"If the file contains meaningful per-lead notes/context, keep that text aside during import and add it to the imported/resolved leads with leadbay_add_note after import when that tool is available. For dry runs, report which notes would be written. If notes cannot be written and the user asked to preserve the text, create/reuse an import-notes custom field.",
|
|
5577
|
+
"For scraped owner/email JSON columns, extract the best scalar values into new clean columns before import; do not pass raw JSON blobs as core CRM fields.",
|
|
5578
|
+
"If no confident standard/custom mapping exists for a meaningful user column, create or reuse a custom field unless the column is blank/noisy/duplicate and record why it was skipped."
|
|
5579
|
+
];
|
|
5580
|
+
}
|
|
5581
|
+
async function hydrateCandidateProfiles(client, candidates, lensId, limit) {
|
|
5582
|
+
const selected = candidates.slice(0, Math.max(0, limit));
|
|
5583
|
+
const settled = await Promise.allSettled(selected.map((c) => client.request("GET", `/lenses/${lensId}/leads/${c.lead_id}`)));
|
|
5584
|
+
const out = [];
|
|
5585
|
+
settled.forEach((r, i) => {
|
|
5586
|
+
if (r.status !== "fulfilled")
|
|
5587
|
+
return;
|
|
5588
|
+
const lead = r.value;
|
|
5589
|
+
out.push({
|
|
5590
|
+
lead_id: selected[i].lead_id,
|
|
5591
|
+
name: lead.name ?? null,
|
|
5592
|
+
website: lead.website ?? null,
|
|
5593
|
+
location: lead.location ? {
|
|
5594
|
+
full: lead.location.full ?? null,
|
|
5595
|
+
city: lead.location.city ?? null,
|
|
5596
|
+
state: lead.location.state ?? null,
|
|
5597
|
+
country: lead.location.country ?? null
|
|
5598
|
+
} : null,
|
|
5599
|
+
phone_numbers: Array.isArray(lead.phone_numbers) ? lead.phone_numbers : [],
|
|
5600
|
+
description: typeof lead.description === "string" ? lead.description.slice(0, 400) : null
|
|
5601
|
+
});
|
|
5602
|
+
});
|
|
5603
|
+
return out;
|
|
5604
|
+
}
|
|
5605
|
+
function hasResolverTarget(mappings) {
|
|
5606
|
+
return Object.values(mappings.fields).some((v) => RESOLVER_TARGETS.has(v));
|
|
5607
|
+
}
|
|
5608
|
+
var resolveImportRows = {
|
|
5609
|
+
name: "leadbay_resolve_import_rows",
|
|
5610
|
+
annotations: {
|
|
5611
|
+
title: "Resolve import row identities",
|
|
5612
|
+
readOnlyHint: true,
|
|
5613
|
+
destructiveHint: false,
|
|
5614
|
+
idempotentHint: true,
|
|
5615
|
+
openWorldHint: true
|
|
5616
|
+
},
|
|
5617
|
+
description: leadbay_resolve_import_rows,
|
|
5618
|
+
write: false,
|
|
5619
|
+
version: "0.6.4",
|
|
5620
|
+
inputSchema: {
|
|
5621
|
+
type: "object",
|
|
5622
|
+
properties: {
|
|
5623
|
+
records: {
|
|
5624
|
+
type: "array",
|
|
5625
|
+
description: "CSV-shaped rows from the user file. Values may be strings, numbers, booleans, null, arrays, or objects; non-scalars are JSON-stringified for resolver/import preparation.",
|
|
5626
|
+
items: { type: "object" }
|
|
5627
|
+
},
|
|
5628
|
+
identity_mappings: {
|
|
5629
|
+
type: "object",
|
|
5630
|
+
description: "Resolver field -> source column map chosen by the agent after inspecting the file, e.g. {website:'company_domain', name:'Company', crm_id:'Salesforce ID', registry_number:'SIREN'}. May point to clean columns the agent derived before calling this tool. This tool does not infer mappings from header names.",
|
|
5631
|
+
properties: {
|
|
5632
|
+
leadbay_id: { type: "string" },
|
|
5633
|
+
crm_id: { type: "string" },
|
|
5634
|
+
name: { type: "string" },
|
|
5635
|
+
website: { type: "string" },
|
|
5636
|
+
phone: { type: "string" },
|
|
5637
|
+
email: { type: "string" },
|
|
5638
|
+
registry_number: { type: "string" },
|
|
5639
|
+
registry_type: { type: "string" },
|
|
5640
|
+
address: { type: "string" },
|
|
5641
|
+
city: { type: "string" },
|
|
5642
|
+
postcode: { type: "string" },
|
|
5643
|
+
country: { type: "string" },
|
|
5644
|
+
linkedin: { type: "string" },
|
|
5645
|
+
facebook: { type: "string" },
|
|
5646
|
+
instagram: { type: "string" },
|
|
5647
|
+
twitter: { type: "string" },
|
|
5648
|
+
tiktok: { type: "string" }
|
|
5649
|
+
},
|
|
5650
|
+
additionalProperties: false
|
|
5651
|
+
},
|
|
5652
|
+
include_candidate_profiles: {
|
|
5653
|
+
type: "boolean",
|
|
5654
|
+
description: "When true, hydrate ambiguous candidate IDs with lightweight lead facts from the active lens. Use on small batches or rerun on only ambiguous rows; large ambiguous files can return many candidates."
|
|
5655
|
+
},
|
|
5656
|
+
candidate_profile_limit: {
|
|
5657
|
+
type: "number",
|
|
5658
|
+
description: `Maximum candidates to hydrate per ambiguous row when include_candidate_profiles=true (default ${DEFAULT_CANDIDATE_PROFILE_LIMIT}).`
|
|
5659
|
+
},
|
|
5660
|
+
lensId: {
|
|
5661
|
+
type: "number",
|
|
5662
|
+
description: "Lens ID used for candidate profile hydration. Defaults to the user's active lens."
|
|
5663
|
+
}
|
|
5664
|
+
},
|
|
5665
|
+
required: ["records"],
|
|
5666
|
+
additionalProperties: false
|
|
5667
|
+
},
|
|
5668
|
+
outputSchema: {
|
|
5669
|
+
type: "object",
|
|
5670
|
+
properties: {
|
|
5671
|
+
rows: {
|
|
5672
|
+
type: "array",
|
|
5673
|
+
description: "Per-input resolution result. Matched rows include lead_id; ambiguous rows include candidates; none/unidentifiable rows explain what extra signal would help.",
|
|
5674
|
+
items: { type: "object" }
|
|
5675
|
+
},
|
|
5676
|
+
records_for_import: {
|
|
5677
|
+
type: "array",
|
|
5678
|
+
description: "Import-ready records. Matched rows include LEADBAY_ID. Ambiguous/unresolved rows preserve user data and rely on website/name/CRM fields for normal or late matching.",
|
|
5679
|
+
items: { type: "object" }
|
|
5680
|
+
},
|
|
5681
|
+
mappings_for_import: {
|
|
5682
|
+
type: "object",
|
|
5683
|
+
description: "Safe identity-only mapping starter. The agent should review/extend this using mapping_guidance and leadbay_list_mappable_fields before importing."
|
|
5684
|
+
},
|
|
5685
|
+
identity_mappings_used: { type: "object" },
|
|
5686
|
+
mapping_guidance: {
|
|
5687
|
+
type: "array",
|
|
5688
|
+
description: "Instructions for building the final import mappings from the source columns.",
|
|
5689
|
+
items: { type: "string" }
|
|
5690
|
+
},
|
|
5691
|
+
disambiguation_policy: {
|
|
5692
|
+
type: "array",
|
|
5693
|
+
description: "Rules the agent should follow before writing LEADBAY_ID onto ambiguous rows.",
|
|
5694
|
+
items: { type: "string" }
|
|
5695
|
+
},
|
|
5696
|
+
summary: { type: "object" },
|
|
5697
|
+
next_action: { type: "string" },
|
|
5698
|
+
region: { type: "string" },
|
|
5699
|
+
_meta: { type: "object" }
|
|
5700
|
+
},
|
|
5701
|
+
required: [
|
|
5702
|
+
"rows",
|
|
5703
|
+
"records_for_import",
|
|
5704
|
+
"mappings_for_import",
|
|
5705
|
+
"identity_mappings_used",
|
|
5706
|
+
"mapping_guidance",
|
|
5707
|
+
"disambiguation_policy",
|
|
5708
|
+
"summary",
|
|
5709
|
+
"next_action",
|
|
5710
|
+
"region",
|
|
5711
|
+
"_meta"
|
|
5712
|
+
]
|
|
5713
|
+
},
|
|
5714
|
+
execute: async (client, params) => {
|
|
5715
|
+
if (!Array.isArray(params.records) || params.records.length === 0) {
|
|
5716
|
+
throw client.makeError("RESOLVE_IMPORT_EMPTY_INPUT", "records[] must contain at least one row", "Pass the rows from the user file, then import records_for_import.", "POST /leads/resolve");
|
|
5717
|
+
}
|
|
5718
|
+
const rows = params.records.map((rec, i) => {
|
|
5719
|
+
if (rec == null || typeof rec !== "object" || Array.isArray(rec)) {
|
|
5720
|
+
throw client.makeError("RESOLVE_IMPORT_INVALID_ROW", `records[${i}] must be a plain object`, "Pass each input row as { ColumnName: value, ... }.", "POST /leads/resolve");
|
|
5721
|
+
}
|
|
5722
|
+
const out = {};
|
|
5723
|
+
for (const [k, v] of Object.entries(rec))
|
|
5724
|
+
out[k] = coerceCell2(v);
|
|
5725
|
+
return out;
|
|
5726
|
+
});
|
|
5727
|
+
const identityMappings = compactMappings(params.identity_mappings ?? {});
|
|
5728
|
+
const allColumns = new Set(rows.flatMap((r) => Object.keys(r)));
|
|
5729
|
+
for (const [field, column] of Object.entries(identityMappings)) {
|
|
5730
|
+
if (!allColumns.has(column)) {
|
|
5731
|
+
throw client.makeError("RESOLVE_IMPORT_MAPPING_KEY_UNKNOWN", `identity_mappings.${field} points to missing column ${JSON.stringify(column)}`, "Use a source column that exists in at least one input record.", "POST /leads/resolve");
|
|
5732
|
+
}
|
|
5733
|
+
}
|
|
5734
|
+
const outputs = [];
|
|
5735
|
+
const recordsForImport = [];
|
|
5736
|
+
const results = await Promise.all(rows.map(async (row) => {
|
|
5737
|
+
const payload = payloadForRecord(row, identityMappings);
|
|
5738
|
+
const result = await client.request("POST", "/leads/resolve", payload);
|
|
5739
|
+
return { payload, result };
|
|
5740
|
+
}));
|
|
5741
|
+
let matched = 0;
|
|
5742
|
+
let ambiguous = 0;
|
|
5743
|
+
let none = 0;
|
|
5744
|
+
let unidentifiable = 0;
|
|
5745
|
+
const hydrateProfiles = params.include_candidate_profiles === true;
|
|
5746
|
+
const candidateProfileLimit = params.candidate_profile_limit ?? DEFAULT_CANDIDATE_PROFILE_LIMIT;
|
|
5747
|
+
const hydrationLensId = hydrateProfiles ? params.lensId ?? await client.resolveDefaultLens() : null;
|
|
5748
|
+
for (let index = 0; index < results.length; index++) {
|
|
5749
|
+
const { payload, result } = results[index];
|
|
5750
|
+
const importRecord = { ...rows[index] };
|
|
5751
|
+
const rowOut = {
|
|
5752
|
+
index,
|
|
5753
|
+
type: result.type,
|
|
5754
|
+
resolver_payload: payload,
|
|
5755
|
+
import_record: importRecord
|
|
5756
|
+
};
|
|
5757
|
+
if (result.type === "matched") {
|
|
5758
|
+
matched++;
|
|
5759
|
+
importRecord.LEADBAY_ID = result.lead_id;
|
|
5760
|
+
rowOut.lead_id = result.lead_id;
|
|
5761
|
+
rowOut.matched_on = result.matched_on;
|
|
5762
|
+
} else if (result.type === "ambiguous") {
|
|
5763
|
+
ambiguous++;
|
|
5764
|
+
rowOut.candidates = result.candidates;
|
|
5765
|
+
if (hydrateProfiles && hydrationLensId !== null) {
|
|
5766
|
+
rowOut.candidate_profiles = await hydrateCandidateProfiles(client, result.candidates, hydrationLensId, candidateProfileLimit);
|
|
4678
5767
|
}
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
5768
|
+
} else if (result.type === "none") {
|
|
5769
|
+
none++;
|
|
5770
|
+
rowOut.would_help = result.would_help;
|
|
5771
|
+
} else {
|
|
5772
|
+
unidentifiable++;
|
|
5773
|
+
rowOut.reason = result.reason;
|
|
4682
5774
|
}
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
lead_id: leadId,
|
|
4689
|
-
qualification_summary: responses.length > 0 ? {
|
|
4690
|
-
answered: responses.filter((r) => r.score != null).length,
|
|
4691
|
-
total: responses.length,
|
|
4692
|
-
avg_qualification_boost: avg
|
|
4693
|
-
} : null,
|
|
4694
|
-
signals_count: lastWf?.content ? Object.values(lastWf.content).reduce((acc, arr) => acc + (Array.isArray(arr) ? arr.length : 0), 0) : null,
|
|
4695
|
-
_stillRunning: stillRunning
|
|
4696
|
-
};
|
|
4697
|
-
}));
|
|
4698
|
-
const qualified = results.filter((r) => !r._stillRunning).map(({ _stillRunning, ...rest }) => rest);
|
|
4699
|
-
const still_running = results.filter((r) => r._stillRunning).map(({ _stillRunning, ...rest }) => rest);
|
|
5775
|
+
recordsForImport.push(importRecord);
|
|
5776
|
+
outputs.push(rowOut);
|
|
5777
|
+
}
|
|
5778
|
+
const mappingsForImport = identityMappingsForImport(recordsForImport, identityMappings);
|
|
5779
|
+
const readyForImport = hasResolverTarget(mappingsForImport);
|
|
4700
5780
|
return {
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
5781
|
+
rows: outputs,
|
|
5782
|
+
records_for_import: recordsForImport,
|
|
5783
|
+
mappings_for_import: mappingsForImport,
|
|
5784
|
+
identity_mappings_used: identityMappings,
|
|
5785
|
+
mapping_guidance: mappingGuidance(),
|
|
5786
|
+
disambiguation_policy: disambiguationPolicy(),
|
|
5787
|
+
summary: {
|
|
5788
|
+
total: rows.length,
|
|
5789
|
+
matched,
|
|
5790
|
+
ambiguous,
|
|
5791
|
+
none,
|
|
5792
|
+
unidentifiable,
|
|
5793
|
+
ready_for_import: readyForImport
|
|
5794
|
+
},
|
|
5795
|
+
next_action: readyForImport ? "Review ambiguous candidates using disambiguation_policy. Build the final mapping from mappings_for_import plus mapping_guidance, then call leadbay_import_leads or leadbay_import_and_qualify with records_for_import and the reviewed mapping." : "Add or map at least one import resolver column (LEADBAY_ID, CRM_ID, LEAD_NAME, LEAD_WEBSITE, or SIREN), then call leadbay_import_leads.",
|
|
5796
|
+
region: client.region,
|
|
5797
|
+
_meta: client.lastMeta ?? {
|
|
5798
|
+
region: client.region,
|
|
5799
|
+
endpoint: "POST /leads/resolve",
|
|
5800
|
+
latency_ms: null,
|
|
5801
|
+
retry_after: null
|
|
5802
|
+
}
|
|
4709
5803
|
};
|
|
4710
5804
|
}
|
|
4711
5805
|
};
|
|
@@ -4799,28 +5893,7 @@ var importAndQualify = {
|
|
|
4799
5893
|
idempotentHint: true,
|
|
4800
5894
|
openWorldHint: true
|
|
4801
5895
|
},
|
|
4802
|
-
description:
|
|
4803
|
-
|
|
4804
|
-
Inputs:
|
|
4805
|
-
- \`domains\`: list of \`{domain, name?}\` (Mode A) \u2014 mutually exclusive with \`records\`.
|
|
4806
|
-
- \`records\`: list of CSV-shaped objects (Mode B), accompanied by \`mappings\`. Use \`mappings.fields\` with StandardCrmFieldType names or 'CUSTOM.<id>' wire values; or \`mappings.custom_fields\` with field id or name shorthand. Discover the org's mappable surface via leadbay_list_mappable_fields.
|
|
4807
|
-
- Budgets: \`total_budget_ms\` (default ${DEFAULT_TOTAL_BUDGET_MS3 / 6e4} min) caps the entire wall-clock; \`per_lead_budget_ms\` (default ${DEFAULT_PER_LEAD_BUDGET_MS2 / 1e3}s) caps each lead's individual qualification poll.
|
|
4808
|
-
|
|
4809
|
-
Outputs include \`qualified[]\` (per-lead question answers), \`still_running[]\` (lead ids whose qualification exceeded the budget), \`not_imported[]\` (rows the wizard couldn't match), and \`qualify_id\` (the resumable handle when at least one lead is still running). Idempotent within a 5-min window: re-calling with the same records+mapping returns the same qualify_id (\`reused: true\`). The result has a \`kind\` discriminator (\`'result' | 'preview'\`); preview-mode (\`dry_run: 'preview'\`) returns mapping hints + custom-field candidates instead of importing. Pass \`dry_run: true\` for input-validation only (top-level \`dry_run: true\` appears in the result so the agent can distinguish from all-malformed input).
|
|
4810
|
-
|
|
4811
|
-
When to use: the agent has a list of companies (domains, or CSV-shaped rows from the user's CRM) and wants Leadbay's full AI qualification \u2014 qualification answers, web-research signals \u2014 without orchestrating import + bulk_qualify_leads + lead_profile chains by hand.
|
|
4812
|
-
When NOT to use: discovery (use leadbay_pull_leads); single-lead deep dive (use leadbay_research_lead); high-cadence or untrusted automation \u2014 this mutates user state by creating CRM-import rows and consumes ai_rescore + web_fetch quota.
|
|
4813
|
-
|
|
4814
|
-
\u26A0\uFE0F MUTATES USER STATE. Each call:
|
|
4815
|
-
- creates a CRM-imports row (visible in the web UI)
|
|
4816
|
-
- touches onboarding state
|
|
4817
|
-
- launches up to N\xD7ai_rescore + N\xD7web_fetch quota where N = imported lead count (unless \`skip_already_qualified: true\` (default) excludes already-scored leads)
|
|
4818
|
-
|
|
4819
|
-
\u2139\uFE0F Monitor-tab membership: imported leads are NOT auto-promoted to the user's Monitor view. The lens-scoring rule decides \u2014 only leads that score above the lens threshold get \`in_monitor: true\` server-side. Lower-scoring imports stay invisible to the Monitor tab. Tell the user this if they ask 'where did my import go?' \u2014 answer is the CRM-imports list, not Monitor.
|
|
4820
|
-
|
|
4821
|
-
\u2139\uFE0F In-lens-but-unscored leads: a lead may be admitted to the active lens (lens GET returns 200) but the lens-scoring job may not materialize for it (qualification never starts). The composite today surfaces hard-rejected leads (404 from lens GET) in \`not_in_lens[]\`, but in-lens-unscored leads currently sit in \`still_running[]\` \u2014 there's no per-lead 'scoring queued vs won't_compute' signal from the backend yet (tracked: leadbay/product#3571). If still_running stays non-empty for a lead more than 5 minutes after a successful import, suggest the user verify the lens covers that lead's profile (e.g., sector match) \u2014 qualify_status's poll won't ever produce answers.
|
|
4822
|
-
|
|
4823
|
-
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role; active billing.`,
|
|
5896
|
+
description: leadbay_import_and_qualify,
|
|
4824
5897
|
write: true,
|
|
4825
5898
|
version: "0.2.0",
|
|
4826
5899
|
inputSchema: {
|
|
@@ -4861,7 +5934,7 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
4861
5934
|
properties: {
|
|
4862
5935
|
fields: {
|
|
4863
5936
|
type: "object",
|
|
4864
|
-
description: "Object whose keys are CSV column names and whose values are either StandardCrmFieldType (LEAD_NAME, LEAD_WEBSITE, ..., CONTACT_TITLE) or 'CUSTOM.<id>'. Discover via leadbay_list_mappable_fields. At least one entry must target LEAD_NAME or LEAD_WEBSITE."
|
|
5937
|
+
description: "Object whose keys are CSV column names and whose values are either StandardCrmFieldType (LEAD_NAME, LEAD_WEBSITE, ..., CONTACT_TITLE) or 'CUSTOM.<id>'. Discover via leadbay_list_mappable_fields. At least one entry must target LEADBAY_ID, CRM_ID, SIREN, LEAD_NAME, or LEAD_WEBSITE. Use leadbay_resolve_import_rows to prepare LEADBAY_ID values from messy user files. Contact exports and embedded owner/contact lists should map CONTACT_EMAIL/PHONE/TITLE/name fields while preserving parent lead identity; expand structured people into repeated parent rows. HubSpot/source links should map to CUSTOM.<id> fields created or discovered before import."
|
|
4865
5938
|
},
|
|
4866
5939
|
custom_fields: {
|
|
4867
5940
|
type: "object",
|
|
@@ -4887,6 +5960,10 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
4887
5960
|
type: "number",
|
|
4888
5961
|
description: `Per-phase budget for the import wizard (default ${DEFAULT_PER_PHASE_BUDGET_MS2}); mirrors leadbay_import_leads.`
|
|
4889
5962
|
},
|
|
5963
|
+
wait_for_completion: {
|
|
5964
|
+
type: "boolean",
|
|
5965
|
+
description: "When false, enqueue the import phase and return `{kind:'result', status:'running', handle_id}` immediately. Poll leadbay_import_status. Default is true for 0.6.x backwards compatibility."
|
|
5966
|
+
},
|
|
4890
5967
|
lensId: {
|
|
4891
5968
|
type: "number",
|
|
4892
5969
|
description: "Lens id (escape hatch \u2014 defaults to active)."
|
|
@@ -4909,6 +5986,14 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
4909
5986
|
type: "string",
|
|
4910
5987
|
description: "'result' (full flow) or 'preview' (dry_run='preview' mapping diagnostics)."
|
|
4911
5988
|
},
|
|
5989
|
+
status: {
|
|
5990
|
+
type: "string",
|
|
5991
|
+
description: "`running` when wait_for_completion=false."
|
|
5992
|
+
},
|
|
5993
|
+
handle_id: {
|
|
5994
|
+
type: "string",
|
|
5995
|
+
description: "Import handle to pass to leadbay_import_status when wait_for_completion=false."
|
|
5996
|
+
},
|
|
4912
5997
|
// preview-shape keys
|
|
4913
5998
|
mapping_hints: {
|
|
4914
5999
|
type: "array",
|
|
@@ -5015,19 +6100,76 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
5015
6100
|
if (!ctx?.bulkTracker) {
|
|
5016
6101
|
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.", "");
|
|
5017
6102
|
}
|
|
6103
|
+
if (params.wait_for_completion === false) {
|
|
6104
|
+
const queued = await importLeads.execute(client, {
|
|
6105
|
+
domains: params.domains,
|
|
6106
|
+
records: params.records,
|
|
6107
|
+
mappings: params.mappings,
|
|
6108
|
+
per_phase_budget_ms: perPhaseBudget,
|
|
6109
|
+
total_budget_ms: totalBudget,
|
|
6110
|
+
...params.dry_run === true ? { dry_run: true } : {},
|
|
6111
|
+
wait_for_completion: false
|
|
6112
|
+
}, ctx);
|
|
6113
|
+
if (!isImportLeadsRunningResult(queued)) {
|
|
6114
|
+
return {
|
|
6115
|
+
kind: "result",
|
|
6116
|
+
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
6117
|
+
qualify_id: null,
|
|
6118
|
+
import_ids: queued.importIds,
|
|
6119
|
+
imported: queued.leads.map((l) => ({
|
|
6120
|
+
leadId: l.leadId,
|
|
6121
|
+
...l.domain ? { domain: l.domain } : {},
|
|
6122
|
+
name: l.name ?? null,
|
|
6123
|
+
...l.rowId ? { rowId: l.rowId } : {}
|
|
6124
|
+
})),
|
|
6125
|
+
not_imported: queued.not_imported.map(toNotImportedEntry),
|
|
6126
|
+
qualified: [],
|
|
6127
|
+
still_running: [],
|
|
6128
|
+
failed: [],
|
|
6129
|
+
quota_exceeded: false,
|
|
6130
|
+
skipped_already_qualified: [],
|
|
6131
|
+
not_in_lens: [],
|
|
6132
|
+
region: client.region,
|
|
6133
|
+
_meta: queued._meta
|
|
6134
|
+
};
|
|
6135
|
+
}
|
|
6136
|
+
return {
|
|
6137
|
+
kind: "result",
|
|
6138
|
+
status: "running",
|
|
6139
|
+
handle_id: queued.handle_id,
|
|
6140
|
+
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
6141
|
+
qualify_id: null,
|
|
6142
|
+
import_ids: queued.importIds,
|
|
6143
|
+
imported: [],
|
|
6144
|
+
not_imported: [],
|
|
6145
|
+
qualified: [],
|
|
6146
|
+
still_running: [],
|
|
6147
|
+
failed: [],
|
|
6148
|
+
quota_exceeded: false,
|
|
6149
|
+
skipped_already_qualified: [],
|
|
6150
|
+
not_in_lens: [],
|
|
6151
|
+
region: client.region,
|
|
6152
|
+
_meta: queued._meta
|
|
6153
|
+
};
|
|
6154
|
+
}
|
|
5018
6155
|
ctx?.progress?.({
|
|
5019
6156
|
progress: 1,
|
|
5020
6157
|
total: 3,
|
|
5021
6158
|
message: "Importing leads (phase 1/3 \u2014 preprocess + commit)"
|
|
5022
6159
|
});
|
|
5023
|
-
const
|
|
6160
|
+
const importResultRaw = await importLeads.execute(client, {
|
|
5024
6161
|
domains: params.domains,
|
|
5025
6162
|
records: params.records,
|
|
5026
6163
|
mappings: params.mappings,
|
|
5027
6164
|
per_phase_budget_ms: perPhaseBudget,
|
|
5028
6165
|
total_budget_ms: totalBudget,
|
|
5029
|
-
...params.dry_run === true ? { dry_run: true } : {}
|
|
6166
|
+
...params.dry_run === true ? { dry_run: true } : {},
|
|
6167
|
+
wait_for_completion: true
|
|
5030
6168
|
}, ctx);
|
|
6169
|
+
if (isImportLeadsRunningResult(importResultRaw)) {
|
|
6170
|
+
throw client.makeError("IMPORT_ASYNC_UNEXPECTED", "Import returned an async handle while import_and_qualify was waiting for completion", "Retry with wait_for_completion=false and poll leadbay_import_status, or retry the blocking call.", "POST /imports");
|
|
6171
|
+
}
|
|
6172
|
+
const importResult = importResultRaw;
|
|
5031
6173
|
if (importResult.cancelled) {
|
|
5032
6174
|
return {
|
|
5033
6175
|
kind: "result",
|
|
@@ -5320,7 +6462,7 @@ import { mkdir as mkdirAsync, lstat, open as fsOpen, readFile, rename, stat, unl
|
|
|
5320
6462
|
import { constants as fsConstants } from "fs";
|
|
5321
6463
|
import { dirname, resolve as resolvePath } from "path";
|
|
5322
6464
|
import { homedir, platform } from "os";
|
|
5323
|
-
import { createHash as
|
|
6465
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
5324
6466
|
var DEFAULT_IDEMPOTENCY_WINDOW_MS = 5 * 60 * 1e3;
|
|
5325
6467
|
var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
5326
6468
|
var UUIDV4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
@@ -5335,7 +6477,7 @@ function computeIdempotencyKey(args) {
|
|
|
5335
6477
|
args.phone ? "p1" : "p0",
|
|
5336
6478
|
`l${args.lens_id}`
|
|
5337
6479
|
];
|
|
5338
|
-
return
|
|
6480
|
+
return createHash3("sha256").update(parts.join("|")).digest("hex");
|
|
5339
6481
|
}
|
|
5340
6482
|
function computeQualifyIdempotencyKey(args) {
|
|
5341
6483
|
const parts = [
|
|
@@ -5345,7 +6487,16 @@ function computeQualifyIdempotencyKey(args) {
|
|
|
5345
6487
|
`l${args.lens_id}`,
|
|
5346
6488
|
args.mapping_fingerprint
|
|
5347
6489
|
];
|
|
5348
|
-
return
|
|
6490
|
+
return createHash3("sha256").update(parts.join("|")).digest("hex");
|
|
6491
|
+
}
|
|
6492
|
+
function computeImportIdempotencyKey(args) {
|
|
6493
|
+
const parts = [
|
|
6494
|
+
"import",
|
|
6495
|
+
args.mode,
|
|
6496
|
+
args.dry_run ? "dry1" : "dry0",
|
|
6497
|
+
args.import_fingerprint
|
|
6498
|
+
];
|
|
6499
|
+
return createHash3("sha256").update(parts.join("|")).digest("hex");
|
|
5349
6500
|
}
|
|
5350
6501
|
function normalizeLaunchInputs(args) {
|
|
5351
6502
|
return {
|
|
@@ -5483,7 +6634,7 @@ var LocalBulkStore = class {
|
|
|
5483
6634
|
throw new Error("missing launched_at");
|
|
5484
6635
|
if (!Array.isArray(r.lead_ids) || !r.lead_ids.every((x) => typeof x === "string"))
|
|
5485
6636
|
throw new Error("invalid lead_ids");
|
|
5486
|
-
if (r.status !== "pending" && r.status !== "launched" && r.status !== "failed")
|
|
6637
|
+
if (r.status !== "pending" && r.status !== "launched" && r.status !== "complete" && r.status !== "failed" && r.status !== "cancelled")
|
|
5487
6638
|
throw new Error("invalid status");
|
|
5488
6639
|
if (typeof r.idempotency_key !== "string")
|
|
5489
6640
|
throw new Error("invalid idempotency_key");
|
|
@@ -5510,6 +6661,52 @@ var LocalBulkStore = class {
|
|
|
5510
6661
|
out.total_budget_ms = r.total_budget_ms;
|
|
5511
6662
|
return out;
|
|
5512
6663
|
}
|
|
6664
|
+
if (kind === "import") {
|
|
6665
|
+
if (!Array.isArray(r.import_ids) || !r.import_ids.every((x) => typeof x === "string"))
|
|
6666
|
+
throw new Error("invalid import_ids");
|
|
6667
|
+
if (r.mode !== "domains" && r.mode !== "records")
|
|
6668
|
+
throw new Error("invalid mode");
|
|
6669
|
+
if (typeof r.dry_run !== "boolean")
|
|
6670
|
+
throw new Error("invalid dry_run");
|
|
6671
|
+
if (typeof r.records_total !== "number")
|
|
6672
|
+
throw new Error("invalid records_total");
|
|
6673
|
+
const out = {
|
|
6674
|
+
kind: "import",
|
|
6675
|
+
bulk_id: r.bulk_id,
|
|
6676
|
+
launched_at: r.launched_at,
|
|
6677
|
+
lead_ids: r.lead_ids,
|
|
6678
|
+
import_ids: r.import_ids,
|
|
6679
|
+
mode: r.mode,
|
|
6680
|
+
dry_run: r.dry_run,
|
|
6681
|
+
records_total: r.records_total,
|
|
6682
|
+
status: r.status,
|
|
6683
|
+
idempotency_key: r.idempotency_key,
|
|
6684
|
+
durability: this.backend
|
|
6685
|
+
};
|
|
6686
|
+
if (r.result && typeof r.result === "object") {
|
|
6687
|
+
const result = r.result;
|
|
6688
|
+
if (Array.isArray(result.leads) && Array.isArray(result.not_imported) && Array.isArray(result.importIds) && result.importIds.every((x) => typeof x === "string")) {
|
|
6689
|
+
out.result = {
|
|
6690
|
+
leads: result.leads,
|
|
6691
|
+
not_imported: result.not_imported,
|
|
6692
|
+
importIds: result.importIds
|
|
6693
|
+
};
|
|
6694
|
+
}
|
|
6695
|
+
}
|
|
6696
|
+
if (r.progress && typeof r.progress === "object") {
|
|
6697
|
+
const p = r.progress;
|
|
6698
|
+
if (typeof p.phase === "string" && typeof p.records_processed === "number" && typeof p.records_total === "number") {
|
|
6699
|
+
out.progress = {
|
|
6700
|
+
phase: p.phase,
|
|
6701
|
+
records_processed: p.records_processed,
|
|
6702
|
+
records_total: p.records_total
|
|
6703
|
+
};
|
|
6704
|
+
}
|
|
6705
|
+
}
|
|
6706
|
+
if (typeof r.error === "string")
|
|
6707
|
+
out.error = r.error;
|
|
6708
|
+
return out;
|
|
6709
|
+
}
|
|
5513
6710
|
if (kind === "enrich") {
|
|
5514
6711
|
if (!Array.isArray(r.titles) || !r.titles.every((x) => typeof x === "string"))
|
|
5515
6712
|
throw new Error("invalid titles");
|
|
@@ -5673,10 +6870,119 @@ var LocalBulkStore = class {
|
|
|
5673
6870
|
return { record, reused: false };
|
|
5674
6871
|
});
|
|
5675
6872
|
}
|
|
6873
|
+
async findOrCreatePendingImport(args) {
|
|
6874
|
+
const idempotency_key = computeImportIdempotencyKey({
|
|
6875
|
+
import_fingerprint: args.import_fingerprint,
|
|
6876
|
+
mode: args.mode,
|
|
6877
|
+
dry_run: args.dry_run
|
|
6878
|
+
});
|
|
6879
|
+
const window = args.idempotency_window_ms ?? DEFAULT_IDEMPOTENCY_WINDOW_MS;
|
|
6880
|
+
return this.mutex.run(async () => {
|
|
6881
|
+
const all = this.prune(await this.readAll());
|
|
6882
|
+
const nowMs = this.now();
|
|
6883
|
+
const existing = all.find((r) => r.kind === "import" && r.idempotency_key === idempotency_key && r.status !== "failed" && r.status !== "cancelled" && nowMs - Date.parse(r.launched_at) < window);
|
|
6884
|
+
if (existing) {
|
|
6885
|
+
this.logger?.info?.(`bulk.reused kind=import bulk_id=${existing.bulk_id} seconds_since_original=${Math.round((nowMs - Date.parse(existing.launched_at)) / 1e3)}`);
|
|
6886
|
+
return {
|
|
6887
|
+
record: existing,
|
|
6888
|
+
reused: true,
|
|
6889
|
+
seconds_since_original: Math.round((nowMs - Date.parse(existing.launched_at)) / 1e3)
|
|
6890
|
+
};
|
|
6891
|
+
}
|
|
6892
|
+
const record = {
|
|
6893
|
+
kind: "import",
|
|
6894
|
+
bulk_id: randomUUID2(),
|
|
6895
|
+
launched_at: new Date(nowMs).toISOString(),
|
|
6896
|
+
lead_ids: [],
|
|
6897
|
+
import_ids: [],
|
|
6898
|
+
mode: args.mode,
|
|
6899
|
+
dry_run: args.dry_run,
|
|
6900
|
+
records_total: args.records_total,
|
|
6901
|
+
progress: {
|
|
6902
|
+
phase: "queued",
|
|
6903
|
+
records_processed: 0,
|
|
6904
|
+
records_total: args.records_total
|
|
6905
|
+
},
|
|
6906
|
+
status: "pending",
|
|
6907
|
+
idempotency_key,
|
|
6908
|
+
durability: this.backend
|
|
6909
|
+
};
|
|
6910
|
+
all.push(record);
|
|
6911
|
+
await this.writeAll(all);
|
|
6912
|
+
this.logger?.info?.(`bulk.registered kind=import bulk_id=${record.bulk_id} mode=${record.mode} records_total=${record.records_total} durability=${record.durability}`);
|
|
6913
|
+
return { record, reused: false };
|
|
6914
|
+
});
|
|
6915
|
+
}
|
|
5676
6916
|
async getQualify(bulk_id) {
|
|
5677
6917
|
const r = await this.get(bulk_id);
|
|
5678
6918
|
return r && r.kind === "qualify" ? r : void 0;
|
|
5679
6919
|
}
|
|
6920
|
+
async getImport(bulk_id) {
|
|
6921
|
+
const r = await this.get(bulk_id);
|
|
6922
|
+
return r && r.kind === "import" ? r : void 0;
|
|
6923
|
+
}
|
|
6924
|
+
async setImportIds(bulk_id, import_ids) {
|
|
6925
|
+
return this.mutex.run(async () => {
|
|
6926
|
+
const all = this.prune(await this.readAll());
|
|
6927
|
+
const idx = all.findIndex((r) => r.bulk_id === bulk_id && r.kind === "import");
|
|
6928
|
+
if (idx < 0)
|
|
6929
|
+
throw new Error(`import bulk_id not found: ${bulk_id}`);
|
|
6930
|
+
const record = all[idx];
|
|
6931
|
+
all[idx] = {
|
|
6932
|
+
...record,
|
|
6933
|
+
import_ids: [...new Set(import_ids)].sort(),
|
|
6934
|
+
status: record.status === "pending" ? "launched" : record.status
|
|
6935
|
+
};
|
|
6936
|
+
await this.writeAll(all);
|
|
6937
|
+
return all[idx];
|
|
6938
|
+
});
|
|
6939
|
+
}
|
|
6940
|
+
async setImportProgress(bulk_id, progress) {
|
|
6941
|
+
return this.mutex.run(async () => {
|
|
6942
|
+
const all = this.prune(await this.readAll());
|
|
6943
|
+
const idx = all.findIndex((r) => r.bulk_id === bulk_id && r.kind === "import");
|
|
6944
|
+
if (idx < 0)
|
|
6945
|
+
throw new Error(`import bulk_id not found: ${bulk_id}`);
|
|
6946
|
+
const record = all[idx];
|
|
6947
|
+
all[idx] = { ...record, progress };
|
|
6948
|
+
await this.writeAll(all);
|
|
6949
|
+
return all[idx];
|
|
6950
|
+
});
|
|
6951
|
+
}
|
|
6952
|
+
async markImportComplete(bulk_id, result) {
|
|
6953
|
+
return this.mutex.run(async () => {
|
|
6954
|
+
const all = this.prune(await this.readAll());
|
|
6955
|
+
const idx = all.findIndex((r) => r.bulk_id === bulk_id && r.kind === "import");
|
|
6956
|
+
if (idx < 0)
|
|
6957
|
+
throw new Error(`import bulk_id not found: ${bulk_id}`);
|
|
6958
|
+
const record = all[idx];
|
|
6959
|
+
all[idx] = {
|
|
6960
|
+
...record,
|
|
6961
|
+
import_ids: [...new Set(result.importIds)].sort(),
|
|
6962
|
+
result,
|
|
6963
|
+
progress: {
|
|
6964
|
+
phase: "complete",
|
|
6965
|
+
records_processed: record.records_total,
|
|
6966
|
+
records_total: record.records_total
|
|
6967
|
+
},
|
|
6968
|
+
status: "complete"
|
|
6969
|
+
};
|
|
6970
|
+
await this.writeAll(all);
|
|
6971
|
+
this.logger?.info?.(`bulk.import_complete bulk_id=${bulk_id}`);
|
|
6972
|
+
return all[idx];
|
|
6973
|
+
});
|
|
6974
|
+
}
|
|
6975
|
+
async markImportFailed(bulk_id, error) {
|
|
6976
|
+
return this.mutex.run(async () => {
|
|
6977
|
+
const all = this.prune(await this.readAll());
|
|
6978
|
+
const idx = all.findIndex((r) => r.bulk_id === bulk_id && r.kind === "import");
|
|
6979
|
+
if (idx < 0)
|
|
6980
|
+
return;
|
|
6981
|
+
all[idx] = { ...all[idx], status: "failed", error };
|
|
6982
|
+
await this.writeAll(all);
|
|
6983
|
+
this.logger?.info?.(`bulk.import_failed bulk_id=${bulk_id}`);
|
|
6984
|
+
});
|
|
6985
|
+
}
|
|
5680
6986
|
async markLaunched(bulk_id) {
|
|
5681
6987
|
return this.mutex.run(async () => {
|
|
5682
6988
|
const all = this.prune(await this.readAll());
|
|
@@ -5770,6 +7076,200 @@ async function createDefaultBulkStore(opts = {}) {
|
|
|
5770
7076
|
}
|
|
5771
7077
|
}
|
|
5772
7078
|
|
|
7079
|
+
// ../core/dist/composite/import-status.js
|
|
7080
|
+
function summarizeImports(imports, dryRun) {
|
|
7081
|
+
let recordsTotal = 0;
|
|
7082
|
+
let recordsProcessed = 0;
|
|
7083
|
+
let hasPreprocess = false;
|
|
7084
|
+
let hasProcess = false;
|
|
7085
|
+
let hasFailed = false;
|
|
7086
|
+
for (const imp of imports) {
|
|
7087
|
+
recordsTotal += Number(imp.total_records ?? 0);
|
|
7088
|
+
recordsProcessed += Number(imp.imported_records ?? 0);
|
|
7089
|
+
if (!imp.pre_processing?.finished) {
|
|
7090
|
+
hasPreprocess = true;
|
|
7091
|
+
continue;
|
|
7092
|
+
}
|
|
7093
|
+
if (imp.pre_processing?.error) {
|
|
7094
|
+
hasFailed = true;
|
|
7095
|
+
continue;
|
|
7096
|
+
}
|
|
7097
|
+
if (dryRun === true) {
|
|
7098
|
+
continue;
|
|
7099
|
+
}
|
|
7100
|
+
if (!imp.processing?.finished) {
|
|
7101
|
+
if (dryRun === false || imp.processing != null)
|
|
7102
|
+
hasProcess = true;
|
|
7103
|
+
continue;
|
|
7104
|
+
}
|
|
7105
|
+
if (imp.processing?.error) {
|
|
7106
|
+
hasFailed = true;
|
|
7107
|
+
}
|
|
7108
|
+
}
|
|
7109
|
+
const phase = hasFailed ? "failed" : hasPreprocess ? "preprocess" : hasProcess ? "process" : imports.length > 0 ? "complete" : "queued";
|
|
7110
|
+
return {
|
|
7111
|
+
phase,
|
|
7112
|
+
records_processed: recordsProcessed,
|
|
7113
|
+
records_total: recordsTotal
|
|
7114
|
+
};
|
|
7115
|
+
}
|
|
7116
|
+
var importStatus = {
|
|
7117
|
+
name: "leadbay_import_status",
|
|
7118
|
+
annotations: {
|
|
7119
|
+
title: "Poll import status",
|
|
7120
|
+
readOnlyHint: true,
|
|
7121
|
+
destructiveHint: false,
|
|
7122
|
+
idempotentHint: true,
|
|
7123
|
+
openWorldHint: true
|
|
7124
|
+
},
|
|
7125
|
+
description: leadbay_import_status,
|
|
7126
|
+
inputSchema: {
|
|
7127
|
+
type: "object",
|
|
7128
|
+
properties: {
|
|
7129
|
+
handle_id: {
|
|
7130
|
+
type: "string",
|
|
7131
|
+
description: "UUIDv4 handle returned by leadbay_import_leads when wait_for_completion=false."
|
|
7132
|
+
},
|
|
7133
|
+
importIds: {
|
|
7134
|
+
type: "array",
|
|
7135
|
+
description: "Legacy backend file-import ids to inspect directly.",
|
|
7136
|
+
items: { type: "string" }
|
|
7137
|
+
}
|
|
7138
|
+
},
|
|
7139
|
+
additionalProperties: false
|
|
7140
|
+
},
|
|
7141
|
+
outputSchema: {
|
|
7142
|
+
type: "object",
|
|
7143
|
+
properties: {
|
|
7144
|
+
status: { type: "string", description: "running, complete, or failed." },
|
|
7145
|
+
handle_id: { type: "string" },
|
|
7146
|
+
importIds: { type: "array", items: { type: "string" } },
|
|
7147
|
+
progress: { type: "object" },
|
|
7148
|
+
result: {
|
|
7149
|
+
type: "object",
|
|
7150
|
+
description: "Final import result when the handle has completed in this MCP instance."
|
|
7151
|
+
},
|
|
7152
|
+
error: { type: "string" },
|
|
7153
|
+
region: { type: "string" },
|
|
7154
|
+
_meta: { type: "object" }
|
|
7155
|
+
},
|
|
7156
|
+
required: ["status", "importIds", "progress", "region", "_meta"]
|
|
7157
|
+
},
|
|
7158
|
+
execute: async (client, params, ctx) => {
|
|
7159
|
+
let handleId = params.handle_id;
|
|
7160
|
+
let importIds = params.importIds ?? [];
|
|
7161
|
+
let handleDryRun;
|
|
7162
|
+
if (handleId) {
|
|
7163
|
+
if (!isValidBulkId(handleId)) {
|
|
7164
|
+
throw client.makeError("BULK_INVALID_ID", "handle_id is not a valid UUIDv4", "Pass the handle_id returned by leadbay_import_leads verbatim.", "");
|
|
7165
|
+
}
|
|
7166
|
+
if (!ctx?.bulkTracker) {
|
|
7167
|
+
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_import_status needs a BulkTracker to resolve handle_id. Pass importIds[] directly as a fallback.", "");
|
|
7168
|
+
}
|
|
7169
|
+
const record = await ctx.bulkTracker.getImport(handleId);
|
|
7170
|
+
if (!record) {
|
|
7171
|
+
const any = await ctx.bulkTracker.get(handleId);
|
|
7172
|
+
if (any && any.kind !== "import") {
|
|
7173
|
+
throw client.makeError("BULK_WRONG_KIND", "This handle was not created by leadbay_import_leads", "Use leadbay_qualify_status for qualify ids or leadbay_bulk_enrich_status for enrich ids.", "");
|
|
7174
|
+
}
|
|
7175
|
+
throw client.makeError("BULK_NOT_FOUND", "No import record for that handle_id", "It may have expired (30-day TTL) or the MCP process was restarted without persistence.", "");
|
|
7176
|
+
}
|
|
7177
|
+
importIds = record.import_ids;
|
|
7178
|
+
handleDryRun = record.dry_run;
|
|
7179
|
+
if (record.status === "complete" && record.result) {
|
|
7180
|
+
return {
|
|
7181
|
+
status: "complete",
|
|
7182
|
+
handle_id: handleId,
|
|
7183
|
+
importIds,
|
|
7184
|
+
progress: record.progress ?? {
|
|
7185
|
+
phase: "complete",
|
|
7186
|
+
records_processed: record.records_total,
|
|
7187
|
+
records_total: record.records_total
|
|
7188
|
+
},
|
|
7189
|
+
result: record.result,
|
|
7190
|
+
region: client.region,
|
|
7191
|
+
_meta: client.lastMeta ?? {
|
|
7192
|
+
region: client.region,
|
|
7193
|
+
endpoint: "bulk-store",
|
|
7194
|
+
latency_ms: null,
|
|
7195
|
+
retry_after: null
|
|
7196
|
+
}
|
|
7197
|
+
};
|
|
7198
|
+
}
|
|
7199
|
+
if (record.status === "failed") {
|
|
7200
|
+
return {
|
|
7201
|
+
status: "failed",
|
|
7202
|
+
handle_id: handleId,
|
|
7203
|
+
importIds,
|
|
7204
|
+
progress: record.progress ?? {
|
|
7205
|
+
phase: "failed",
|
|
7206
|
+
records_processed: 0,
|
|
7207
|
+
records_total: record.records_total
|
|
7208
|
+
},
|
|
7209
|
+
error: record.error ?? "import failed",
|
|
7210
|
+
region: client.region,
|
|
7211
|
+
_meta: client.lastMeta ?? {
|
|
7212
|
+
region: client.region,
|
|
7213
|
+
endpoint: "bulk-store",
|
|
7214
|
+
latency_ms: null,
|
|
7215
|
+
retry_after: null
|
|
7216
|
+
}
|
|
7217
|
+
};
|
|
7218
|
+
}
|
|
7219
|
+
if (importIds.length === 0) {
|
|
7220
|
+
return {
|
|
7221
|
+
status: "running",
|
|
7222
|
+
handle_id: handleId,
|
|
7223
|
+
importIds,
|
|
7224
|
+
progress: record.progress ?? {
|
|
7225
|
+
phase: "queued",
|
|
7226
|
+
records_processed: 0,
|
|
7227
|
+
records_total: record.records_total
|
|
7228
|
+
},
|
|
7229
|
+
region: client.region,
|
|
7230
|
+
_meta: client.lastMeta ?? {
|
|
7231
|
+
region: client.region,
|
|
7232
|
+
endpoint: "bulk-store",
|
|
7233
|
+
latency_ms: null,
|
|
7234
|
+
retry_after: null
|
|
7235
|
+
}
|
|
7236
|
+
};
|
|
7237
|
+
}
|
|
7238
|
+
}
|
|
7239
|
+
if (importIds.length === 0) {
|
|
7240
|
+
throw client.makeError("IMPORT_STATUS_INPUT_REQUIRED", "Pass either handle_id or importIds[]", "Call leadbay_import_leads with wait_for_completion=false first, then pass its handle_id.", "");
|
|
7241
|
+
}
|
|
7242
|
+
const imports = await Promise.all(importIds.map((id) => client.request("GET", `/imports/${id}`)));
|
|
7243
|
+
const progress = summarizeImports(imports, handleDryRun);
|
|
7244
|
+
const failed = imports.find((i) => i.pre_processing?.error || i.processing?.error);
|
|
7245
|
+
const complete = imports.every((i) => {
|
|
7246
|
+
if (i.pre_processing?.error || i.processing?.error)
|
|
7247
|
+
return false;
|
|
7248
|
+
if (handleDryRun === true)
|
|
7249
|
+
return Boolean(i.pre_processing?.finished);
|
|
7250
|
+
if (handleDryRun === false)
|
|
7251
|
+
return Boolean(i.processing?.finished);
|
|
7252
|
+
return Boolean(i.processing?.finished || i.pre_processing?.finished && !i.processing);
|
|
7253
|
+
});
|
|
7254
|
+
return {
|
|
7255
|
+
status: failed ? "failed" : complete ? "complete" : "running",
|
|
7256
|
+
...handleId ? { handle_id: handleId } : {},
|
|
7257
|
+
importIds,
|
|
7258
|
+
progress,
|
|
7259
|
+
...failed ? {
|
|
7260
|
+
error: failed.pre_processing?.error ?? failed.processing?.error ?? "import failed"
|
|
7261
|
+
} : {},
|
|
7262
|
+
region: client.region,
|
|
7263
|
+
_meta: client.lastMeta ?? {
|
|
7264
|
+
region: client.region,
|
|
7265
|
+
endpoint: "GET /imports/<id>",
|
|
7266
|
+
latency_ms: null,
|
|
7267
|
+
retry_after: null
|
|
7268
|
+
}
|
|
7269
|
+
};
|
|
7270
|
+
}
|
|
7271
|
+
};
|
|
7272
|
+
|
|
5773
7273
|
// ../core/dist/composite/qualify-status.js
|
|
5774
7274
|
var qualifyStatus = {
|
|
5775
7275
|
name: "leadbay_qualify_status",
|
|
@@ -5780,7 +7280,7 @@ var qualifyStatus = {
|
|
|
5780
7280
|
idempotentHint: true,
|
|
5781
7281
|
openWorldHint: true
|
|
5782
7282
|
},
|
|
5783
|
-
description:
|
|
7283
|
+
description: leadbay_qualify_status,
|
|
5784
7284
|
inputSchema: {
|
|
5785
7285
|
type: "object",
|
|
5786
7286
|
properties: {
|
|
@@ -5865,7 +7365,8 @@ var qualifyStatus = {
|
|
|
5865
7365
|
if (!record) {
|
|
5866
7366
|
const any = await ctx.bulkTracker.get(params.qualify_id);
|
|
5867
7367
|
if (any && any.kind !== "qualify") {
|
|
5868
|
-
|
|
7368
|
+
const hint = any.kind === "import" ? "Call leadbay_import_status with this id instead." : "Call leadbay_bulk_enrich_status with this id instead.";
|
|
7369
|
+
throw client.makeError("BULK_WRONG_KIND", `This bulk_id was created by ${any.kind}, not leadbay_import_and_qualify`, hint, "");
|
|
5869
7370
|
}
|
|
5870
7371
|
throw client.makeError("BULK_NOT_FOUND", "No qualify record for that qualify_id", "It may have expired (30-day TTL) or the MCP process was restarted without persistence. Re-launch via leadbay_import_and_qualify.", "");
|
|
5871
7372
|
}
|
|
@@ -5927,7 +7428,7 @@ var qualifyStatus = {
|
|
|
5927
7428
|
const out = {
|
|
5928
7429
|
qualify_id: record.bulk_id,
|
|
5929
7430
|
launched_at: record.launched_at,
|
|
5930
|
-
status: record.status,
|
|
7431
|
+
status: record.status === "complete" ? "launched" : record.status,
|
|
5931
7432
|
import_ids: record.import_ids,
|
|
5932
7433
|
lens_id: record.lens_id,
|
|
5933
7434
|
lead_ids: record.lead_ids,
|
|
@@ -5967,7 +7468,7 @@ var enrichTitles = {
|
|
|
5967
7468
|
idempotentHint: true,
|
|
5968
7469
|
openWorldHint: true
|
|
5969
7470
|
},
|
|
5970
|
-
description:
|
|
7471
|
+
description: leadbay_enrich_titles,
|
|
5971
7472
|
inputSchema: {
|
|
5972
7473
|
type: "object",
|
|
5973
7474
|
properties: {
|
|
@@ -6328,7 +7829,7 @@ var bulkEnrichStatus = {
|
|
|
6328
7829
|
idempotentHint: true,
|
|
6329
7830
|
openWorldHint: true
|
|
6330
7831
|
},
|
|
6331
|
-
description:
|
|
7832
|
+
description: leadbay_bulk_enrich_status,
|
|
6332
7833
|
inputSchema: {
|
|
6333
7834
|
type: "object",
|
|
6334
7835
|
properties: {
|
|
@@ -6429,12 +7930,12 @@ var bulkEnrichStatus = {
|
|
|
6429
7930
|
hint: "The record may have aged out (30-day TTL) or the MCP process was restarted without persistence. Launch a new enrichment via leadbay_enrich_titles."
|
|
6430
7931
|
};
|
|
6431
7932
|
}
|
|
6432
|
-
if (record.kind
|
|
7933
|
+
if (record.kind !== "enrich") {
|
|
6433
7934
|
return {
|
|
6434
7935
|
error: true,
|
|
6435
7936
|
code: "BULK_WRONG_KIND",
|
|
6436
|
-
message:
|
|
6437
|
-
hint: "Call leadbay_qualify_status with this id instead.",
|
|
7937
|
+
message: `This bulk_id was created by ${record.kind === "qualify" ? "leadbay_import_and_qualify" : "leadbay_import_leads"}, not leadbay_enrich_titles.`,
|
|
7938
|
+
hint: record.kind === "qualify" ? "Call leadbay_qualify_status with this id instead." : "Call leadbay_import_status with this id instead.",
|
|
6438
7939
|
bulk_id: record.bulk_id
|
|
6439
7940
|
};
|
|
6440
7941
|
}
|
|
@@ -6648,7 +8149,7 @@ var adjustAudience = {
|
|
|
6648
8149
|
idempotentHint: true,
|
|
6649
8150
|
openWorldHint: true
|
|
6650
8151
|
},
|
|
6651
|
-
description:
|
|
8152
|
+
description: leadbay_adjust_audience,
|
|
6652
8153
|
inputSchema: {
|
|
6653
8154
|
type: "object",
|
|
6654
8155
|
properties: {
|
|
@@ -6833,7 +8334,7 @@ var refinePrompt = {
|
|
|
6833
8334
|
idempotentHint: false,
|
|
6834
8335
|
openWorldHint: true
|
|
6835
8336
|
},
|
|
6836
|
-
description:
|
|
8337
|
+
description: leadbay_refine_prompt,
|
|
6837
8338
|
inputSchema: {
|
|
6838
8339
|
type: "object",
|
|
6839
8340
|
properties: {
|
|
@@ -7021,7 +8522,7 @@ var answerClarification = {
|
|
|
7021
8522
|
idempotentHint: false,
|
|
7022
8523
|
openWorldHint: true
|
|
7023
8524
|
},
|
|
7024
|
-
description:
|
|
8525
|
+
description: leadbay_answer_clarification,
|
|
7025
8526
|
inputSchema: {
|
|
7026
8527
|
type: "object",
|
|
7027
8528
|
properties: {
|
|
@@ -7112,7 +8613,7 @@ var reportOutreach = {
|
|
|
7112
8613
|
idempotentHint: false,
|
|
7113
8614
|
openWorldHint: true
|
|
7114
8615
|
},
|
|
7115
|
-
description:
|
|
8616
|
+
description: leadbay_report_outreach,
|
|
7116
8617
|
optional: true,
|
|
7117
8618
|
write: true,
|
|
7118
8619
|
inputSchema: {
|
|
@@ -7421,7 +8922,8 @@ var granularWriteTools = [
|
|
|
7421
8922
|
setEpilogueStatus,
|
|
7422
8923
|
removeEpilogue,
|
|
7423
8924
|
previewBulkEnrichment,
|
|
7424
|
-
launchBulkEnrichment
|
|
8925
|
+
launchBulkEnrichment,
|
|
8926
|
+
createCustomField
|
|
7425
8927
|
];
|
|
7426
8928
|
var granularTools = [
|
|
7427
8929
|
login,
|
|
@@ -7438,6 +8940,8 @@ var compositeReadTools = [
|
|
|
7438
8940
|
accountStatus,
|
|
7439
8941
|
bulkEnrichStatus,
|
|
7440
8942
|
qualifyStatus,
|
|
8943
|
+
importStatus,
|
|
8944
|
+
resolveImportRows,
|
|
7441
8945
|
// listMappableFields is granular-shaped but the import composites depend on
|
|
7442
8946
|
// it for discoverability; expose it always-on so agents can find custom fields
|
|
7443
8947
|
// without needing LEADBAY_MCP_ADVANCED=1.
|
|
@@ -7454,7 +8958,13 @@ var compositeWriteTools = [
|
|
|
7454
8958
|
answerClarification,
|
|
7455
8959
|
reportOutreach,
|
|
7456
8960
|
importLeads,
|
|
7457
|
-
importAndQualify
|
|
8961
|
+
importAndQualify,
|
|
8962
|
+
// createCustomField is granular-shaped but file-import prompts depend on it
|
|
8963
|
+
// to preserve source-system links without requiring advanced-tool exposure.
|
|
8964
|
+
createCustomField,
|
|
8965
|
+
// addNote is granular-shaped but file-import prompts depend on it to preserve
|
|
8966
|
+
// meaningful source-file notes after imports return lead ids.
|
|
8967
|
+
addNote
|
|
7458
8968
|
];
|
|
7459
8969
|
var compositeTools = [
|
|
7460
8970
|
...compositeReadTools,
|
|
@@ -7511,6 +9021,7 @@ export {
|
|
|
7511
9021
|
removeEpilogue,
|
|
7512
9022
|
previewBulkEnrichment,
|
|
7513
9023
|
launchBulkEnrichment,
|
|
9024
|
+
createCustomField,
|
|
7514
9025
|
researchCompany,
|
|
7515
9026
|
prepareOutreach,
|
|
7516
9027
|
pullLeads,
|
|
@@ -7518,11 +9029,13 @@ export {
|
|
|
7518
9029
|
recallOrderedTitles,
|
|
7519
9030
|
accountStatus,
|
|
7520
9031
|
bulkQualifyLeads,
|
|
9032
|
+
resolveImportRows,
|
|
7521
9033
|
importAndQualify,
|
|
7522
9034
|
isValidBulkId,
|
|
7523
9035
|
LocalBulkStore,
|
|
7524
9036
|
InMemoryBulkStore,
|
|
7525
9037
|
createDefaultBulkStore,
|
|
9038
|
+
importStatus,
|
|
7526
9039
|
qualifyStatus,
|
|
7527
9040
|
enrichTitles,
|
|
7528
9041
|
bulkEnrichStatus,
|