@leadbay/mcp 0.22.0 → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/dist/bin.js +1093 -81
- package/dist/http-server.js +925 -6
- package/dist/installer-electron.js +59 -21
- package/dist/installer-gui.js +59 -21
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -5140,9 +5140,13 @@ var init_composite_file_names = __esm({
|
|
|
5140
5140
|
"leadbay_campaign_call_sheet",
|
|
5141
5141
|
"leadbay_campaign_progression",
|
|
5142
5142
|
"leadbay_create_campaign",
|
|
5143
|
+
"leadbay_create_custom_field",
|
|
5144
|
+
"leadbay_delete_custom_field",
|
|
5143
5145
|
"leadbay_enrich_titles",
|
|
5144
5146
|
"leadbay_extend_lens",
|
|
5145
5147
|
"leadbay_followups_map",
|
|
5148
|
+
"leadbay_get_lead_custom_fields",
|
|
5149
|
+
"leadbay_get_qualification_questions",
|
|
5146
5150
|
"leadbay_import_and_qualify",
|
|
5147
5151
|
"leadbay_import_leads",
|
|
5148
5152
|
"leadbay_import_status",
|
|
@@ -5163,7 +5167,9 @@ var init_composite_file_names = __esm({
|
|
|
5163
5167
|
"leadbay_resolve_import_rows",
|
|
5164
5168
|
"leadbay_scan_portfolio_signals",
|
|
5165
5169
|
"leadbay_seed_candidates",
|
|
5170
|
+
"leadbay_set_qualification_questions",
|
|
5166
5171
|
"leadbay_team_activity",
|
|
5172
|
+
"leadbay_update_custom_field",
|
|
5167
5173
|
"leadbay_tour_plan"
|
|
5168
5174
|
]);
|
|
5169
5175
|
}
|
|
@@ -5468,7 +5474,7 @@ var init_notifications = __esm({
|
|
|
5468
5474
|
});
|
|
5469
5475
|
|
|
5470
5476
|
// ../core/dist/tool-descriptions.generated.js
|
|
5471
|
-
var leadbay_account_history, leadbay_account_status, leadbay_acknowledge_notification, leadbay_add_contact, leadbay_add_leads_to_campaign, leadbay_add_note, leadbay_adjust_audience, leadbay_agent_memory_capture, leadbay_agent_memory_recall, leadbay_agent_memory_review, leadbay_answer_clarification, leadbay_artifact_kit, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_pin_contact, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_contact, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_scan_portfolio_signals, leadbay_seed_candidates, leadbay_select_leads, leadbay_send_feedback, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_user_prompt, leadbay_team_activity, leadbay_tour_plan, leadbay_unpin_contact, leadbay_update_contact, leadbay_update_lens, leadbay_update_lens_filter;
|
|
5477
|
+
var leadbay_account_history, leadbay_account_status, leadbay_acknowledge_notification, leadbay_add_contact, leadbay_add_leads_to_campaign, leadbay_add_note, leadbay_adjust_audience, leadbay_agent_memory_capture, leadbay_agent_memory_recall, leadbay_agent_memory_review, leadbay_answer_clarification, leadbay_artifact_kit, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_delete_custom_field, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_custom_fields, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_qualification_questions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_pin_contact, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_contact, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_scan_portfolio_signals, leadbay_seed_candidates, leadbay_select_leads, leadbay_send_feedback, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_qualification_questions, leadbay_set_user_prompt, leadbay_team_activity, leadbay_tour_plan, leadbay_unpin_contact, leadbay_update_contact, leadbay_update_custom_field, leadbay_update_lens, leadbay_update_lens_filter;
|
|
5472
5478
|
var init_tool_descriptions_generated = __esm({
|
|
5473
5479
|
"../core/dist/tool-descriptions.generated.js"() {
|
|
5474
5480
|
"use strict";
|
|
@@ -6312,6 +6318,23 @@ Do NOT call this tool defensively before any quota error \u2014 only after a rea
|
|
|
6312
6318
|
WHEN TO USE: when a quota error has blocked the user's intent and they want to keep working today rather than wait for the window reset; OR when the user explicitly asks for a top-up link.
|
|
6313
6319
|
|
|
6314
6320
|
WHEN NOT TO USE: pre-flight (the agent is not paying \u2014 the user is); for subscription / plan changes use \`leadbay_open_billing_portal\` instead; for read-only quota diagnostics use \`leadbay_account_status\` / \`leadbay_get_quota\`.
|
|
6321
|
+
`;
|
|
6322
|
+
leadbay_delete_custom_field = `Delete an org-level CRM custom field. Use when the user explicitly wants to remove a custom field from their account \u2014 e.g. "delete the old 'Legacy Source' field".
|
|
6323
|
+
|
|
6324
|
+
**This is destructive.** Removing the field drops its stored values from every lead and breaks any import mapping that targets \`CUSTOM.<id>\`. For that reason the tool has a safety gate:
|
|
6325
|
+
|
|
6326
|
+
- Call with \`id\` only \u2192 the tool returns the field that WOULD be deleted (\`{id, name, type, deleted:false, hint}\`) and changes nothing. Surface the hint to the user and ask them to confirm.
|
|
6327
|
+
- Call with \`id\` + \`confirm:true\` \u2192 the field is deleted (\`{id, name, type, deleted:true}\`).
|
|
6328
|
+
|
|
6329
|
+
\`id\` is the numeric custom-field id from \`leadbay_list_mappable_fields\` \u2014 NOT the \`CUSTOM.<id>\` mapping value.
|
|
6330
|
+
|
|
6331
|
+
WHEN TO USE: the user explicitly asks to delete/remove a custom field \u2014 and only fire with confirm:true after they confirm.
|
|
6332
|
+
|
|
6333
|
+
WHEN NOT TO USE: to rename or retype a field (use leadbay_update_custom_field) or to create one (use leadbay_create_custom_field).
|
|
6334
|
+
|
|
6335
|
+
### RENDERING
|
|
6336
|
+
|
|
6337
|
+
Before deleting (no confirm), render the \`hint\` and ask the user to confirm \u2014 do NOT auto-confirm on the user's behalf. After a confirmed delete, acknowledge in one line: **"Deleted custom field #12 'Legacy Source'."**
|
|
6315
6338
|
`;
|
|
6316
6339
|
leadbay_deselect_leads = `Remove leads from the user's transient selection.
|
|
6317
6340
|
|
|
@@ -6715,6 +6738,68 @@ WHEN NOT TO USE: when the lead summary's \`epilogue_actions_count\` is 0.
|
|
|
6715
6738
|
WHEN TO USE: to avoid redundant outreach and understand where this lead is in the sales process.
|
|
6716
6739
|
|
|
6717
6740
|
WHEN NOT TO USE: when leadbay_research_lead_by_id has already been called \u2014 it includes recent prospecting actions in its engagement block.
|
|
6741
|
+
`;
|
|
6742
|
+
leadbay_get_lead_custom_fields = `## WHEN TO USE
|
|
6743
|
+
|
|
6744
|
+
Trigger phrases: "what custom fields are on this lead", "show the CRM custom field values for <Company>", "what's the <custom field name> on this lead", "get lead <UUID>'s custom fields", "does this lead have any custom field values".
|
|
6745
|
+
|
|
6746
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
6747
|
+
|
|
6748
|
+
Do NOT use for: "what custom fields exist on my account" \u2192 \`leadbay_list_mappable_fields\`; "give me the full research dossier on this lead" \u2192 \`leadbay_research_lead_by_id\`.
|
|
6749
|
+
|
|
6750
|
+
Prefer when: user wants the custom-field VALUES on ONE lead; pass \`leadId\`
|
|
6751
|
+
|
|
6752
|
+
Examples that SHOULD invoke this tool:
|
|
6753
|
+
- "What custom field values are stored on this lead?"
|
|
6754
|
+
- "Show me the CRM custom fields for that company."
|
|
6755
|
+
|
|
6756
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
6757
|
+
- "What custom fields are defined on my account?"
|
|
6758
|
+
- "Give me the full research breakdown on Acme Corp."
|
|
6759
|
+
|
|
6760
|
+
## RENDER (quick)
|
|
6761
|
+
|
|
6762
|
+
3-column markdown table: Field | Type | Value, one row per entry. When
|
|
6763
|
+
\`custom_fields\` is empty, render the \`hint\` sentence instead of an empty
|
|
6764
|
+
table.
|
|
6765
|
+
|
|
6766
|
+
---
|
|
6767
|
+
|
|
6768
|
+
Retrieve the CRM custom-field **values** stored on a single lead \u2014 the actual
|
|
6769
|
+
data (\`value\`) per custom field, not the field definitions.
|
|
6770
|
+
|
|
6771
|
+
This is distinct from **leadbay_list_mappable_fields**, which returns the org's
|
|
6772
|
+
custom-field *catalog* (the definitions: id/name/type, used for import
|
|
6773
|
+
mapping). This tool answers "what does *this* lead hold for each custom field".
|
|
6774
|
+
|
|
6775
|
+
Params: \`leadId\` (required UUID); \`lensId\` (optional escape hatch \u2014 normally
|
|
6776
|
+
omit; auto-resolves to the active lens).
|
|
6777
|
+
|
|
6778
|
+
Returns:
|
|
6779
|
+
|
|
6780
|
+
- **\`custom_fields\`** \u2014 one row per value: \`{id, name, type, value}\`. The lead
|
|
6781
|
+
payload embeds each field's definition, so rows are fully named without a
|
|
6782
|
+
separate catalog lookup. Empty array when the org has no custom fields or
|
|
6783
|
+
none are set on this lead.
|
|
6784
|
+
- **\`count\`** \u2014 number of values.
|
|
6785
|
+
- **\`hint\`** \u2014 empty-state guidance (points at \`leadbay_list_mappable_fields\`
|
|
6786
|
+
and the import path), or a degradation note in the rare case a value arrived
|
|
6787
|
+
without an embedded definition and the catalog fallback failed.
|
|
6788
|
+
|
|
6789
|
+
Reading a lead marks it seen (LEAD_SEEN) the same way the research tools do, so
|
|
6790
|
+
it ages out of the 'new' Discover view.
|
|
6791
|
+
|
|
6792
|
+
Companion tools: **leadbay_list_mappable_fields** for the catalog/definitions;
|
|
6793
|
+
**leadbay_research_lead_by_id** for the full lead dossier (signals, contacts,
|
|
6794
|
+
qualification answers).
|
|
6795
|
+
|
|
6796
|
+
### RENDERING
|
|
6797
|
+
|
|
6798
|
+
Render \`custom_fields\` as a 3-column markdown table: **Field** (\`name\`, or the
|
|
6799
|
+
\`id\` when name is null) \xB7 **Type** (\`type\`) \xB7 **Value** (\`value\`, or "\u2014" when
|
|
6800
|
+
null/empty). One row per entry, in the order returned. When \`custom_fields\` is
|
|
6801
|
+
empty, render the \`hint\` sentence instead of an empty table. Don't fabricate
|
|
6802
|
+
fields or values \u2014 render verbatim.
|
|
6718
6803
|
`;
|
|
6719
6804
|
leadbay_get_lead_notes = `Read existing notes on a lead \u2014 context the human team or prior agent runs have already captured.
|
|
6720
6805
|
|
|
@@ -6745,6 +6830,70 @@ WHEN NOT TO USE: to mutate scoring \u2014 that's an admin/setup operation, not p
|
|
|
6745
6830
|
WHEN TO USE: before contacting the lead, to avoid duplicating outreach the team already did.
|
|
6746
6831
|
|
|
6747
6832
|
WHEN NOT TO USE: when the lead summary's \`prospecting_actions_count\` is 0.
|
|
6833
|
+
`;
|
|
6834
|
+
leadbay_get_qualification_questions = `## WHEN TO USE
|
|
6835
|
+
|
|
6836
|
+
Trigger phrases: "what are my qualification questions", "what questions does Leadbay ask about each lead", "show me the org qualification questions", "how are my leads being qualified", "what's the qualification criteria".
|
|
6837
|
+
|
|
6838
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
6839
|
+
|
|
6840
|
+
Do NOT use for: "how did this lead score on the qualification questions" \u2192 \`leadbay_research_lead_by_id\`; "show my ideal buyer profile and intent tags" \u2192 \`leadbay_get_taste_profile\`.
|
|
6841
|
+
|
|
6842
|
+
Prefer when: user wants the ORG-level qualification questions catalog, no lead and no buyer profile
|
|
6843
|
+
|
|
6844
|
+
Examples that SHOULD invoke this tool:
|
|
6845
|
+
- "What qualification questions does Leadbay use to score my leads?"
|
|
6846
|
+
- "Show me my org's qualification questions."
|
|
6847
|
+
|
|
6848
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
6849
|
+
- "How did Acme Corp answer the qualification questions?"
|
|
6850
|
+
- "What's my ideal buyer profile?"
|
|
6851
|
+
|
|
6852
|
+
## RENDER (quick)
|
|
6853
|
+
|
|
6854
|
+
Numbered list of the questions (chat-native markdown), each one line. When
|
|
6855
|
+
\`is_admin\` is true, append the \`hint\` as a footnote (points at
|
|
6856
|
+
leadbay_set_qualification_questions for editing). When the list is empty,
|
|
6857
|
+
render the \`hint\` instead.
|
|
6858
|
+
|
|
6859
|
+
---
|
|
6860
|
+
|
|
6861
|
+
Retrieve the organization's **qualification questions** \u2014 the AI-agent questions
|
|
6862
|
+
Leadbay scores every lead against. These are the org-level questions that drive
|
|
6863
|
+
each lead's qualification boost; the per-lead ANSWERS to them surface inside
|
|
6864
|
+
\`leadbay_research_lead_by_id\`.
|
|
6865
|
+
|
|
6866
|
+
Returns:
|
|
6867
|
+
|
|
6868
|
+
- **\`qualification_questions\`** \u2014 the catalog. Each: \`{question, created_at,
|
|
6869
|
+
lang}\`. Ordered as the backend returns them.
|
|
6870
|
+
- **\`count\`** \u2014 number of configured questions.
|
|
6871
|
+
- **\`is_admin\`** \u2014 whether the current user is an org admin. Modifying the
|
|
6872
|
+
questions (\`leadbay_set_qualification_questions\`) is an org-admin action; for
|
|
6873
|
+
admins a \`hint\` points there.
|
|
6874
|
+
- **\`hint\`** \u2014 operator note: the modify pointer, or an empty-state message
|
|
6875
|
+
when no questions are configured.
|
|
6876
|
+
|
|
6877
|
+
This tool only READS. To change the questions, use
|
|
6878
|
+
**leadbay_set_qualification_questions** (add / remove / replace). The result is
|
|
6879
|
+
cached on the client (it reuses the same taste-profile fetch as
|
|
6880
|
+
\`leadbay_get_taste_profile\`), so repeated calls in a session are cheap.
|
|
6881
|
+
|
|
6882
|
+
Companion tools: **leadbay_set_qualification_questions** to modify the questions;
|
|
6883
|
+
**leadbay_get_taste_profile** when the user also wants the Ideal Buyer Profile +
|
|
6884
|
+
purchase-intent tags; **leadbay_research_lead_by_id** for how a SPECIFIC lead
|
|
6885
|
+
answered these questions; **leadbay_refine_prompt** to shape the AI agent's
|
|
6886
|
+
behaviour.
|
|
6887
|
+
|
|
6888
|
+
### RENDERING
|
|
6889
|
+
|
|
6890
|
+
Render \`qualification_questions\` as a numbered list \u2014 one question per line, in
|
|
6891
|
+
the order returned. Lead with a short heading like **"Qualification questions
|
|
6892
|
+
(N)"**. When \`qualification_questions\` is empty, render the \`hint\` sentence
|
|
6893
|
+
instead of an empty list. When \`is_admin\` is true and there are questions,
|
|
6894
|
+
append the \`hint\` as a one-line footnote (points at
|
|
6895
|
+
leadbay_set_qualification_questions). Do not invent questions or reword them \u2014
|
|
6896
|
+
render verbatim.
|
|
6748
6897
|
`;
|
|
6749
6898
|
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\`.
|
|
6750
6899
|
|
|
@@ -8907,6 +9056,28 @@ WHEN TO USE: the user explicitly says they want to defer a lead (or a small set
|
|
|
8907
9056
|
WHEN NOT TO USE: the user is just skipping ONE outreach attempt \u2014 that's a normal \`EPILOGUE_COULD_NOT_REACH_STILL_TRYING\` outcome via \`leadbay_report_outreach\`, not a snooze.
|
|
8908
9057
|
|
|
8909
9058
|
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\`.
|
|
9059
|
+
`;
|
|
9060
|
+
leadbay_set_qualification_questions = `Modify the organization's **qualification questions** \u2014 the AI-agent questions Leadbay scores every lead against. Use when the user wants to add, remove, or rewrite their qualification questions \u2014 e.g. "add a question about whether they run install crews", "remove the flooring question", "replace my questions with these three".
|
|
9061
|
+
|
|
9062
|
+
The backend stores the list as a whole, so this tool reads the current questions and applies your change:
|
|
9063
|
+
|
|
9064
|
+
- **\`add\`** \u2014 append questions (deduped against the current list).
|
|
9065
|
+
- **\`remove\`** \u2014 drop the exact question strings you pass.
|
|
9066
|
+
- **\`questions\`** \u2014 replace the ENTIRE list (mutually exclusive with add/remove).
|
|
9067
|
+
|
|
9068
|
+
Leadbay allows **at most 5** qualification questions. If a change would exceed 5, the tool rejects with a clear limit message \u2014 remove some before adding.
|
|
9069
|
+
|
|
9070
|
+
**Dropping any existing question is destructive** \u2014 it changes how every lead is scored. Any change that removes a current question requires \`confirm:true\` \u2014 including a same-count **swap** (remove one + add one) or a \`questions\` replacement that omits a current question, not only when the list gets shorter. Without \`confirm\`, the tool previews what would be removed and applies nothing. Pure additions never need confirm.
|
|
9071
|
+
|
|
9072
|
+
Returns the resulting \`{qualification_questions, count, previous_count, changed}\`. Phrase questions as the yes/no scoring prompts Leadbay uses (e.g. "Is the company likely to \u2026?").
|
|
9073
|
+
|
|
9074
|
+
WHEN TO USE: the user wants to change the org's qualification questions.
|
|
9075
|
+
|
|
9076
|
+
WHEN NOT TO USE: to READ the questions (use leadbay_get_qualification_questions) or to change a single lead's data. This is org-level \u2014 it affects scoring for ALL leads.
|
|
9077
|
+
|
|
9078
|
+
### RENDERING
|
|
9079
|
+
|
|
9080
|
+
After a change, confirm in one line \u2014 e.g. **"Added 1 question \u2014 you now score leads against 4 questions."** or **"Removed 'the flooring question' \u2014 3 questions remain."** Then list the resulting questions as a numbered list. When the result is a non-changing preview (a removal awaiting confirmation), surface the \`hint\` (what would be removed) and ask the user to confirm \u2014 do NOT auto-confirm.
|
|
8910
9081
|
`;
|
|
8911
9082
|
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.
|
|
8912
9083
|
|
|
@@ -9133,6 +9304,20 @@ Backend: \`POST /contacts/{contact_id}/update\` (snake_case body) \u2192 200 wit
|
|
|
9133
9304
|
Returns \`{ updated: true, contact_id, contact: { id, first_name, last_name, job_title, linkedin_page, email, phone_number } }\`.
|
|
9134
9305
|
|
|
9135
9306
|
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw).
|
|
9307
|
+
`;
|
|
9308
|
+
leadbay_update_custom_field = `Update an org-level CRM custom field in place. Use when the user wants to rename a custom field or change its type/config \u2014 e.g. "rename the 'Tier' field to 'Account Tier'" or "make the ARR field a PRICE in USD".
|
|
9309
|
+
|
|
9310
|
+
Pass \`id\` (the numeric custom-field id from \`leadbay_list_mappable_fields\` \u2014 NOT the \`CUSTOM.<id>\` mapping value) plus any of \`name\`, \`type\`, \`config\`. The update is a partial merge over the current definition: a rename-only call keeps the existing type; a retype-only call keeps the name. At least one of \`name\` / \`type\` / \`config\` is required.
|
|
9311
|
+
|
|
9312
|
+
Type + config rules mirror creation: \`EXTERNAL_ID\` requires \`config.url_template\` containing \`{value}\`; \`PRICE\` requires \`config.currency\`; \`DATE\`/\`DATETIME\` may set \`config.format\`. Returns the updated \`{id, name, type, config, mapping_value}\`.
|
|
9313
|
+
|
|
9314
|
+
WHEN TO USE: the user wants to change an existing custom field's name or type.
|
|
9315
|
+
|
|
9316
|
+
WHEN NOT TO USE: to CREATE a new field (use leadbay_create_custom_field) or to DELETE one (use leadbay_delete_custom_field). To read a lead's custom-field values use leadbay_get_lead_custom_fields; to list the catalog use leadbay_list_mappable_fields.
|
|
9317
|
+
|
|
9318
|
+
### RENDERING
|
|
9319
|
+
|
|
9320
|
+
Confirm the change in one line naming the field and what changed, e.g. **"Renamed custom field #12 \u2192 'Account Tier' (TEXT)."** or **"Updated #13 'ARR' \u2192 PRICE (USD)."** Don't dump the raw payload.
|
|
9136
9321
|
`;
|
|
9137
9322
|
leadbay_update_lens = `Update lens metadata (name, description, mode flags). Does NOT change the audience filter \u2014 use leadbay_update_lens_filter for that.
|
|
9138
9323
|
|
|
@@ -13403,12 +13588,50 @@ var init_launch_bulk_enrichment = __esm({
|
|
|
13403
13588
|
}
|
|
13404
13589
|
});
|
|
13405
13590
|
|
|
13406
|
-
// ../core/dist/
|
|
13591
|
+
// ../core/dist/composite/_custom-field-config.js
|
|
13592
|
+
function sanitizeConfigForType(type, rawConfig) {
|
|
13593
|
+
if (!rawConfig)
|
|
13594
|
+
return null;
|
|
13595
|
+
let config;
|
|
13596
|
+
if (typeof rawConfig === "string") {
|
|
13597
|
+
try {
|
|
13598
|
+
const parsed = JSON.parse(rawConfig);
|
|
13599
|
+
if (!parsed || typeof parsed !== "object")
|
|
13600
|
+
return null;
|
|
13601
|
+
config = parsed;
|
|
13602
|
+
} catch {
|
|
13603
|
+
return null;
|
|
13604
|
+
}
|
|
13605
|
+
} else {
|
|
13606
|
+
config = rawConfig;
|
|
13607
|
+
}
|
|
13608
|
+
switch (type) {
|
|
13609
|
+
case "PRICE":
|
|
13610
|
+
return config.currency != null ? { currency: config.currency } : null;
|
|
13611
|
+
case "DATE":
|
|
13612
|
+
case "DATETIME":
|
|
13613
|
+
return "format" in config ? { format: config.format ?? null } : null;
|
|
13614
|
+
case "EXTERNAL_ID": {
|
|
13615
|
+
const url = config.url_template ?? config.urlTemplate;
|
|
13616
|
+
return url != null ? { url_template: url } : null;
|
|
13617
|
+
}
|
|
13618
|
+
default:
|
|
13619
|
+
return null;
|
|
13620
|
+
}
|
|
13621
|
+
}
|
|
13622
|
+
var init_custom_field_config = __esm({
|
|
13623
|
+
"../core/dist/composite/_custom-field-config.js"() {
|
|
13624
|
+
"use strict";
|
|
13625
|
+
}
|
|
13626
|
+
});
|
|
13627
|
+
|
|
13628
|
+
// ../core/dist/composite/create-custom-field.js
|
|
13407
13629
|
var createCustomField;
|
|
13408
13630
|
var init_create_custom_field = __esm({
|
|
13409
|
-
"../core/dist/
|
|
13631
|
+
"../core/dist/composite/create-custom-field.js"() {
|
|
13410
13632
|
"use strict";
|
|
13411
13633
|
init_tool_descriptions_generated();
|
|
13634
|
+
init_custom_field_config();
|
|
13412
13635
|
createCustomField = {
|
|
13413
13636
|
name: "leadbay_create_custom_field",
|
|
13414
13637
|
annotations: {
|
|
@@ -13433,8 +13656,8 @@ var init_create_custom_field = __esm({
|
|
|
13433
13656
|
description: "Custom field type: TEXT, NUMBER, PRICE, DATE, DATETIME, or EXTERNAL_ID. Defaults to TEXT."
|
|
13434
13657
|
},
|
|
13435
13658
|
config: {
|
|
13436
|
-
type: ["object", "null"],
|
|
13437
|
-
description: "Type-specific config. EXTERNAL_ID requires {url_template:'https://.../{value}'}; PRICE requires {currency:'USD'}; DATE/DATETIME may set {format}."
|
|
13659
|
+
type: ["object", "string", "null"],
|
|
13660
|
+
description: "Type-specific config, as an object (preferred) or a JSON string. EXTERNAL_ID requires {url_template:'https://.../{value}'}; PRICE requires {currency:'USD'}; DATE/DATETIME may set {format}."
|
|
13438
13661
|
},
|
|
13439
13662
|
if_not_exists: {
|
|
13440
13663
|
type: "boolean",
|
|
@@ -13468,9 +13691,9 @@ var init_create_custom_field = __esm({
|
|
|
13468
13691
|
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");
|
|
13469
13692
|
}
|
|
13470
13693
|
const type = params.type ?? "TEXT";
|
|
13471
|
-
const config = params.config ?? null;
|
|
13694
|
+
const config = sanitizeConfigForType(type, params.config ?? null);
|
|
13472
13695
|
if (type === "EXTERNAL_ID") {
|
|
13473
|
-
const urlTemplate = config?.url_template
|
|
13696
|
+
const urlTemplate = config?.url_template;
|
|
13474
13697
|
if (!urlTemplate || !urlTemplate.includes("{value}")) {
|
|
13475
13698
|
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");
|
|
13476
13699
|
}
|
|
@@ -13502,6 +13725,185 @@ var init_create_custom_field = __esm({
|
|
|
13502
13725
|
}
|
|
13503
13726
|
});
|
|
13504
13727
|
|
|
13728
|
+
// ../core/dist/composite/update-custom-field.js
|
|
13729
|
+
var updateCustomField;
|
|
13730
|
+
var init_update_custom_field = __esm({
|
|
13731
|
+
"../core/dist/composite/update-custom-field.js"() {
|
|
13732
|
+
"use strict";
|
|
13733
|
+
init_tool_descriptions_generated();
|
|
13734
|
+
init_custom_field_config();
|
|
13735
|
+
updateCustomField = {
|
|
13736
|
+
name: "leadbay_update_custom_field",
|
|
13737
|
+
annotations: {
|
|
13738
|
+
title: "Update CRM custom field",
|
|
13739
|
+
readOnlyHint: false,
|
|
13740
|
+
destructiveHint: false,
|
|
13741
|
+
idempotentHint: true,
|
|
13742
|
+
openWorldHint: true
|
|
13743
|
+
},
|
|
13744
|
+
description: leadbay_update_custom_field,
|
|
13745
|
+
write: true,
|
|
13746
|
+
inputSchema: {
|
|
13747
|
+
type: "object",
|
|
13748
|
+
properties: {
|
|
13749
|
+
id: {
|
|
13750
|
+
type: "string",
|
|
13751
|
+
description: "Custom field id to update (the numeric id from leadbay_list_mappable_fields, NOT the 'CUSTOM.<id>' mapping value)."
|
|
13752
|
+
},
|
|
13753
|
+
name: {
|
|
13754
|
+
type: "string",
|
|
13755
|
+
description: "New user-visible name. Omit to keep the current name."
|
|
13756
|
+
},
|
|
13757
|
+
type: {
|
|
13758
|
+
type: "string",
|
|
13759
|
+
description: "New type: TEXT, NUMBER, PRICE, DATE, DATETIME, or EXTERNAL_ID. Omit to keep the current type."
|
|
13760
|
+
},
|
|
13761
|
+
config: {
|
|
13762
|
+
type: ["object", "string", "null"],
|
|
13763
|
+
description: "New type-specific config, as an object (preferred) or a JSON string. EXTERNAL_ID requires {url_template:'https://.../{value}'}; PRICE requires {currency:'USD'}; DATE/DATETIME may set {format}. Omit to keep current config."
|
|
13764
|
+
}
|
|
13765
|
+
},
|
|
13766
|
+
required: ["id"],
|
|
13767
|
+
additionalProperties: false
|
|
13768
|
+
},
|
|
13769
|
+
outputSchema: {
|
|
13770
|
+
type: "object",
|
|
13771
|
+
properties: {
|
|
13772
|
+
id: { type: "string" },
|
|
13773
|
+
name: { type: "string" },
|
|
13774
|
+
type: { type: "string" },
|
|
13775
|
+
config: { type: ["object", "null"] },
|
|
13776
|
+
mapping_value: {
|
|
13777
|
+
type: "string",
|
|
13778
|
+
description: "Wire mapping value for import mappings, e.g. CUSTOM.123."
|
|
13779
|
+
}
|
|
13780
|
+
},
|
|
13781
|
+
required: ["id", "name", "type", "mapping_value"]
|
|
13782
|
+
},
|
|
13783
|
+
execute: async (client, params) => {
|
|
13784
|
+
const id = String(params.id ?? "").trim();
|
|
13785
|
+
if (!id) {
|
|
13786
|
+
throw client.makeError("CUSTOM_FIELD_ID_REQUIRED", "id must be a non-empty string", "Pass the custom field id from leadbay_list_mappable_fields (the numeric id, not 'CUSTOM.<id>').", "POST /crm/custom_fields/{id}");
|
|
13787
|
+
}
|
|
13788
|
+
if (params.name === void 0 && params.type === void 0 && params.config === void 0) {
|
|
13789
|
+
throw client.makeError("CUSTOM_FIELD_NO_CHANGE", "no field to update \u2014 pass at least one of name, type, config", "Provide a new name and/or type (and config when the type needs it).", "POST /crm/custom_fields/{id}");
|
|
13790
|
+
}
|
|
13791
|
+
const catalog = await client.request("GET", "/crm/custom_fields");
|
|
13792
|
+
const current = (catalog ?? []).find((f) => String(f.id) === id);
|
|
13793
|
+
if (!current) {
|
|
13794
|
+
throw client.makeError("CUSTOM_FIELD_NOT_FOUND", `no custom field with id ${id} on this org`, "Call leadbay_list_mappable_fields to see the available custom fields and their ids.", "POST /crm/custom_fields/{id}");
|
|
13795
|
+
}
|
|
13796
|
+
const name = params.name !== void 0 ? params.name.trim() : current.name;
|
|
13797
|
+
if (!name) {
|
|
13798
|
+
throw client.makeError("CUSTOM_FIELD_NAME_REQUIRED", "name must be a non-empty string", "Pass a user-visible custom field name, or omit name to keep the current one.", "POST /crm/custom_fields/{id}");
|
|
13799
|
+
}
|
|
13800
|
+
const type = params.type !== void 0 ? params.type : current.type;
|
|
13801
|
+
const rawConfig = params.config !== void 0 ? params.config : current.config ?? null;
|
|
13802
|
+
const config = sanitizeConfigForType(type, rawConfig);
|
|
13803
|
+
if (type === "EXTERNAL_ID") {
|
|
13804
|
+
const urlTemplate = config?.url_template;
|
|
13805
|
+
if (!urlTemplate || !urlTemplate.includes("{value}")) {
|
|
13806
|
+
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/{id}");
|
|
13807
|
+
}
|
|
13808
|
+
}
|
|
13809
|
+
const body = {
|
|
13810
|
+
name,
|
|
13811
|
+
type,
|
|
13812
|
+
...config ? { config } : {}
|
|
13813
|
+
};
|
|
13814
|
+
await client.requestVoid("POST", `/crm/custom_fields/${id}`, body);
|
|
13815
|
+
return {
|
|
13816
|
+
id,
|
|
13817
|
+
name,
|
|
13818
|
+
type,
|
|
13819
|
+
config: config ?? null,
|
|
13820
|
+
mapping_value: `CUSTOM.${id}`
|
|
13821
|
+
};
|
|
13822
|
+
}
|
|
13823
|
+
};
|
|
13824
|
+
}
|
|
13825
|
+
});
|
|
13826
|
+
|
|
13827
|
+
// ../core/dist/composite/delete-custom-field.js
|
|
13828
|
+
var deleteCustomField;
|
|
13829
|
+
var init_delete_custom_field = __esm({
|
|
13830
|
+
"../core/dist/composite/delete-custom-field.js"() {
|
|
13831
|
+
"use strict";
|
|
13832
|
+
init_tool_descriptions_generated();
|
|
13833
|
+
deleteCustomField = {
|
|
13834
|
+
name: "leadbay_delete_custom_field",
|
|
13835
|
+
annotations: {
|
|
13836
|
+
title: "Delete CRM custom field",
|
|
13837
|
+
readOnlyHint: false,
|
|
13838
|
+
destructiveHint: true,
|
|
13839
|
+
idempotentHint: false,
|
|
13840
|
+
openWorldHint: true
|
|
13841
|
+
},
|
|
13842
|
+
description: leadbay_delete_custom_field,
|
|
13843
|
+
write: true,
|
|
13844
|
+
inputSchema: {
|
|
13845
|
+
type: "object",
|
|
13846
|
+
properties: {
|
|
13847
|
+
id: {
|
|
13848
|
+
type: "string",
|
|
13849
|
+
description: "Custom field id to delete (the numeric id from leadbay_list_mappable_fields, NOT the 'CUSTOM.<id>' mapping value)."
|
|
13850
|
+
},
|
|
13851
|
+
confirm: {
|
|
13852
|
+
type: "boolean",
|
|
13853
|
+
description: "Must be true to actually delete. Without it the tool returns the field that WOULD be deleted and does nothing \u2014 a safety gate, because deletion drops the field's values from every lead."
|
|
13854
|
+
}
|
|
13855
|
+
},
|
|
13856
|
+
required: ["id"],
|
|
13857
|
+
additionalProperties: false
|
|
13858
|
+
},
|
|
13859
|
+
outputSchema: {
|
|
13860
|
+
type: "object",
|
|
13861
|
+
properties: {
|
|
13862
|
+
id: { type: "string" },
|
|
13863
|
+
name: { type: "string" },
|
|
13864
|
+
type: { type: "string" },
|
|
13865
|
+
deleted: {
|
|
13866
|
+
type: "boolean",
|
|
13867
|
+
description: "True when the field was deleted; false when confirm was not set."
|
|
13868
|
+
},
|
|
13869
|
+
hint: {
|
|
13870
|
+
type: "string",
|
|
13871
|
+
description: "Guidance \u2014 present when confirm was missing (re-call with confirm:true)."
|
|
13872
|
+
}
|
|
13873
|
+
},
|
|
13874
|
+
required: ["id", "deleted"]
|
|
13875
|
+
},
|
|
13876
|
+
execute: async (client, params) => {
|
|
13877
|
+
const id = String(params.id ?? "").trim();
|
|
13878
|
+
if (!id) {
|
|
13879
|
+
throw client.makeError("CUSTOM_FIELD_ID_REQUIRED", "id must be a non-empty string", "Pass the custom field id from leadbay_list_mappable_fields (the numeric id, not 'CUSTOM.<id>').", "DELETE /crm/custom_fields/{id}");
|
|
13880
|
+
}
|
|
13881
|
+
const catalog = await client.request("GET", "/crm/custom_fields");
|
|
13882
|
+
const current = (catalog ?? []).find((f) => String(f.id) === id);
|
|
13883
|
+
if (!current) {
|
|
13884
|
+
throw client.makeError("CUSTOM_FIELD_NOT_FOUND", `no custom field with id ${id} on this org`, "Call leadbay_list_mappable_fields to see the available custom fields and their ids.", "DELETE /crm/custom_fields/{id}");
|
|
13885
|
+
}
|
|
13886
|
+
if (params.confirm !== true) {
|
|
13887
|
+
return {
|
|
13888
|
+
id,
|
|
13889
|
+
name: current.name,
|
|
13890
|
+
type: current.type,
|
|
13891
|
+
deleted: false,
|
|
13892
|
+
hint: `Deleting "${current.name}" removes its values from every lead and breaks any import mapping using CUSTOM.${id}. Re-call with confirm:true to proceed.`
|
|
13893
|
+
};
|
|
13894
|
+
}
|
|
13895
|
+
await client.requestVoid("DELETE", `/crm/custom_fields/${id}`);
|
|
13896
|
+
return {
|
|
13897
|
+
id,
|
|
13898
|
+
name: current.name,
|
|
13899
|
+
type: current.type,
|
|
13900
|
+
deleted: true
|
|
13901
|
+
};
|
|
13902
|
+
}
|
|
13903
|
+
};
|
|
13904
|
+
}
|
|
13905
|
+
});
|
|
13906
|
+
|
|
13505
13907
|
// ../core/dist/tools/like-lead.js
|
|
13506
13908
|
var likeLead;
|
|
13507
13909
|
var init_like_lead = __esm({
|
|
@@ -15952,80 +16354,400 @@ var researchLeadByNameFuzzy;
|
|
|
15952
16354
|
var init_research_lead_by_name_fuzzy = __esm({
|
|
15953
16355
|
"../core/dist/composite/research-lead-by-name-fuzzy.js"() {
|
|
15954
16356
|
"use strict";
|
|
15955
|
-
init_discover_leads();
|
|
15956
|
-
init_research_lead_by_id();
|
|
16357
|
+
init_discover_leads();
|
|
16358
|
+
init_research_lead_by_id();
|
|
16359
|
+
init_tool_descriptions_generated();
|
|
16360
|
+
researchLeadByNameFuzzy = {
|
|
16361
|
+
name: "leadbay_research_lead_by_name_fuzzy",
|
|
16362
|
+
annotations: {
|
|
16363
|
+
title: "Look up a lead by company name (fuzzy)",
|
|
16364
|
+
readOnlyHint: true,
|
|
16365
|
+
destructiveHint: false,
|
|
16366
|
+
idempotentHint: true,
|
|
16367
|
+
openWorldHint: true
|
|
16368
|
+
},
|
|
16369
|
+
description: leadbay_research_lead_by_name_fuzzy,
|
|
16370
|
+
inputSchema: {
|
|
16371
|
+
type: "object",
|
|
16372
|
+
properties: {
|
|
16373
|
+
companyName: {
|
|
16374
|
+
type: "string",
|
|
16375
|
+
description: "Company name to look up. Substring fuzzy-match against the top 50 of the active lens's wishlist; ties broken by descending lead score."
|
|
16376
|
+
},
|
|
16377
|
+
lensId: {
|
|
16378
|
+
type: "number",
|
|
16379
|
+
description: "Lens id (escape hatch \u2014 normally omit; auto-resolves to the active lens)"
|
|
16380
|
+
},
|
|
16381
|
+
concise: {
|
|
16382
|
+
type: "boolean",
|
|
16383
|
+
description: "Forwarded to leadbay_research_lead_by_id. If true, trims signals to hot=true items only."
|
|
16384
|
+
},
|
|
16385
|
+
response_format: {
|
|
16386
|
+
type: "string",
|
|
16387
|
+
enum: ["json", "markdown"],
|
|
16388
|
+
description: "Forwarded to leadbay_research_lead_by_id. Default 'json'."
|
|
16389
|
+
}
|
|
16390
|
+
},
|
|
16391
|
+
required: ["companyName"],
|
|
16392
|
+
additionalProperties: false
|
|
16393
|
+
},
|
|
16394
|
+
// Output shape matches leadbay_research_lead_by_id; the only additions are
|
|
16395
|
+
// _meta.resolved_from / resolved_query / match_candidates which are
|
|
16396
|
+
// documented on _by_id's output schema. Defer to _by_id for the schema —
|
|
16397
|
+
// duplicating it would just rot.
|
|
16398
|
+
outputSchema: {
|
|
16399
|
+
type: "object",
|
|
16400
|
+
description: "Same shape as leadbay_research_lead_by_id, with _meta.resolved_from='companyName', _meta.resolved_query='<needle>', and _meta.match_candidates=[{leadId,name,score}] populated.",
|
|
16401
|
+
additionalProperties: true
|
|
16402
|
+
},
|
|
16403
|
+
execute: async (client, params, ctx) => {
|
|
16404
|
+
if (!params.companyName || typeof params.companyName !== "string") {
|
|
16405
|
+
throw client.makeError("INVALID_PARAMS", "companyName is required", "Pass the company name as a string. If you already have the lead UUID, call leadbay_research_lead_by_id directly.");
|
|
16406
|
+
}
|
|
16407
|
+
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
16408
|
+
const results = await discoverLeads.execute(client, { lensId, count: 50, page: 0 }, ctx);
|
|
16409
|
+
const allLeads = results.leads;
|
|
16410
|
+
const ranked = rankSubstringMatches(params.companyName, allLeads);
|
|
16411
|
+
if (ranked.length === 0) {
|
|
16412
|
+
const nearestNames = [...allLeads].sort((a, b) => (b.score ?? -Infinity) - (a.score ?? -Infinity)).slice(0, 5).map((l) => ({ leadId: l.id, name: l.name, score: l.score }));
|
|
16413
|
+
throw client.makeError("LEAD_NOT_FOUND", `No lead matching "${params.companyName}" in the current lens (top-50 wishlist)`, `Call leadbay_pull_leads to see what's available. Top-scoring leads currently in this lens: ${nearestNames.map((n) => n.name).join(", ")}.`);
|
|
16414
|
+
}
|
|
16415
|
+
const [primary, ...rest] = ranked;
|
|
16416
|
+
const candidates = rest.slice(0, 4).map((m) => ({
|
|
16417
|
+
leadId: m.id,
|
|
16418
|
+
name: m.name,
|
|
16419
|
+
score: m.score
|
|
16420
|
+
}));
|
|
16421
|
+
return await researchLeadById.execute(client, {
|
|
16422
|
+
leadId: primary.id,
|
|
16423
|
+
lensId,
|
|
16424
|
+
concise: params.concise,
|
|
16425
|
+
response_format: params.response_format,
|
|
16426
|
+
_resolved: {
|
|
16427
|
+
from: "companyName",
|
|
16428
|
+
query: params.companyName,
|
|
16429
|
+
candidates
|
|
16430
|
+
}
|
|
16431
|
+
}, ctx);
|
|
16432
|
+
}
|
|
16433
|
+
};
|
|
16434
|
+
}
|
|
16435
|
+
});
|
|
16436
|
+
|
|
16437
|
+
// ../core/dist/composite/get-qualification-questions.js
|
|
16438
|
+
var getQualificationQuestions;
|
|
16439
|
+
var init_get_qualification_questions = __esm({
|
|
16440
|
+
"../core/dist/composite/get-qualification-questions.js"() {
|
|
16441
|
+
"use strict";
|
|
16442
|
+
init_agent_memory();
|
|
16443
|
+
init_tool_descriptions_generated();
|
|
16444
|
+
getQualificationQuestions = {
|
|
16445
|
+
name: "leadbay_get_qualification_questions",
|
|
16446
|
+
annotations: {
|
|
16447
|
+
title: "Read the org's qualification questions",
|
|
16448
|
+
readOnlyHint: true,
|
|
16449
|
+
destructiveHint: false,
|
|
16450
|
+
idempotentHint: true,
|
|
16451
|
+
openWorldHint: true
|
|
16452
|
+
},
|
|
16453
|
+
description: leadbay_get_qualification_questions,
|
|
16454
|
+
inputSchema: {
|
|
16455
|
+
type: "object",
|
|
16456
|
+
properties: {},
|
|
16457
|
+
additionalProperties: false
|
|
16458
|
+
},
|
|
16459
|
+
outputSchema: {
|
|
16460
|
+
type: "object",
|
|
16461
|
+
properties: {
|
|
16462
|
+
qualification_questions: {
|
|
16463
|
+
type: "array",
|
|
16464
|
+
description: "Org-level questions Leadbay scores every lead against. Each: {question, created_at, lang}.",
|
|
16465
|
+
items: { type: "object" }
|
|
16466
|
+
},
|
|
16467
|
+
count: {
|
|
16468
|
+
type: "number",
|
|
16469
|
+
description: "Number of qualification questions configured."
|
|
16470
|
+
},
|
|
16471
|
+
is_admin: {
|
|
16472
|
+
type: "boolean",
|
|
16473
|
+
description: "Whether the current bearer-token holder is an org admin. Admins can modify the questions via leadbay_set_qualification_questions."
|
|
16474
|
+
},
|
|
16475
|
+
region: { type: "string" },
|
|
16476
|
+
hint: {
|
|
16477
|
+
type: "string",
|
|
16478
|
+
description: "Operator note \u2014 admin edit pointer, or the empty-state message when no questions are configured."
|
|
16479
|
+
},
|
|
16480
|
+
_meta: { type: "object" }
|
|
16481
|
+
},
|
|
16482
|
+
required: ["qualification_questions"]
|
|
16483
|
+
},
|
|
16484
|
+
execute: async (client, _params, ctx) => {
|
|
16485
|
+
const me = await client.resolveMe().catch(() => null);
|
|
16486
|
+
const isAdmin = me?.admin ?? false;
|
|
16487
|
+
const orgId = me?.organization?.id ?? await client.resolveOrgId();
|
|
16488
|
+
const questions = await client.request("GET", `/organizations/${orgId}/ai_agent_questions`) ?? [];
|
|
16489
|
+
let hint;
|
|
16490
|
+
if (questions.length === 0) {
|
|
16491
|
+
hint = "No qualification questions configured yet. Use leadbay_set_qualification_questions to add some, or leadbay_refine_prompt to shape the AI agent.";
|
|
16492
|
+
} else if (isAdmin) {
|
|
16493
|
+
hint = "You're an org admin \u2014 use leadbay_set_qualification_questions to add, remove, or replace these questions.";
|
|
16494
|
+
}
|
|
16495
|
+
return withAgentMemoryMeta(client, {
|
|
16496
|
+
qualification_questions: questions.map((q) => ({
|
|
16497
|
+
question: q.question,
|
|
16498
|
+
created_at: q.created_at,
|
|
16499
|
+
lang: q.lang
|
|
16500
|
+
})),
|
|
16501
|
+
count: questions.length,
|
|
16502
|
+
is_admin: isAdmin,
|
|
16503
|
+
region: client.region,
|
|
16504
|
+
...hint ? { hint } : {}
|
|
16505
|
+
}, ctx);
|
|
16506
|
+
}
|
|
16507
|
+
};
|
|
16508
|
+
}
|
|
16509
|
+
});
|
|
16510
|
+
|
|
16511
|
+
// ../core/dist/composite/set-qualification-questions.js
|
|
16512
|
+
var setQualificationQuestions;
|
|
16513
|
+
var init_set_qualification_questions = __esm({
|
|
16514
|
+
"../core/dist/composite/set-qualification-questions.js"() {
|
|
16515
|
+
"use strict";
|
|
16516
|
+
init_agent_memory();
|
|
16517
|
+
init_tool_descriptions_generated();
|
|
16518
|
+
setQualificationQuestions = {
|
|
16519
|
+
name: "leadbay_set_qualification_questions",
|
|
16520
|
+
annotations: {
|
|
16521
|
+
title: "Modify the org's qualification questions",
|
|
16522
|
+
readOnlyHint: false,
|
|
16523
|
+
destructiveHint: true,
|
|
16524
|
+
idempotentHint: false,
|
|
16525
|
+
openWorldHint: true
|
|
16526
|
+
},
|
|
16527
|
+
description: leadbay_set_qualification_questions,
|
|
16528
|
+
write: true,
|
|
16529
|
+
inputSchema: {
|
|
16530
|
+
type: "object",
|
|
16531
|
+
properties: {
|
|
16532
|
+
questions: {
|
|
16533
|
+
type: "array",
|
|
16534
|
+
items: { type: "string" },
|
|
16535
|
+
description: "Full replacement list of qualification questions (replaces ALL current questions). Mutually exclusive with add/remove."
|
|
16536
|
+
},
|
|
16537
|
+
add: {
|
|
16538
|
+
type: "array",
|
|
16539
|
+
items: { type: "string" },
|
|
16540
|
+
description: "Questions to append to the current list (deduped). Mutually exclusive with `questions`."
|
|
16541
|
+
},
|
|
16542
|
+
remove: {
|
|
16543
|
+
type: "array",
|
|
16544
|
+
items: { type: "string" },
|
|
16545
|
+
description: "Exact question strings to remove from the current list. Mutually exclusive with `questions`. A removal requires confirm:true."
|
|
16546
|
+
},
|
|
16547
|
+
confirm: {
|
|
16548
|
+
type: "boolean",
|
|
16549
|
+
description: "Required whenever the change DROPS ANY existing question \u2014 including a same-count swap or a `questions` replacement that omits a current question, not only when the list gets shorter (removing a question changes how every lead is scored). Without it, such a change is previewed and not applied. Pure additions never need confirm."
|
|
16550
|
+
}
|
|
16551
|
+
},
|
|
16552
|
+
additionalProperties: false
|
|
16553
|
+
},
|
|
16554
|
+
outputSchema: {
|
|
16555
|
+
type: "object",
|
|
16556
|
+
properties: {
|
|
16557
|
+
qualification_questions: {
|
|
16558
|
+
type: "array",
|
|
16559
|
+
description: "The questions AFTER the change. Each: {question}.",
|
|
16560
|
+
items: { type: "object" }
|
|
16561
|
+
},
|
|
16562
|
+
count: { type: "number" },
|
|
16563
|
+
previous_count: { type: "number" },
|
|
16564
|
+
changed: {
|
|
16565
|
+
type: "boolean",
|
|
16566
|
+
description: "True when the list was actually written; false on a no-op or an unconfirmed shrink."
|
|
16567
|
+
},
|
|
16568
|
+
region: { type: "string" },
|
|
16569
|
+
hint: {
|
|
16570
|
+
type: "string",
|
|
16571
|
+
description: "Operator note \u2014 confirm prompt on a shrink, or a no-op explanation."
|
|
16572
|
+
},
|
|
16573
|
+
_meta: { type: "object" }
|
|
16574
|
+
},
|
|
16575
|
+
required: ["qualification_questions", "count", "changed"]
|
|
16576
|
+
},
|
|
16577
|
+
execute: async (client, params, ctx) => {
|
|
16578
|
+
const hasSet = Array.isArray(params.questions);
|
|
16579
|
+
const hasAdd = Array.isArray(params.add) && params.add.length > 0;
|
|
16580
|
+
const hasRemove = Array.isArray(params.remove) && params.remove.length > 0;
|
|
16581
|
+
if (hasSet && (hasAdd || hasRemove)) {
|
|
16582
|
+
throw client.makeError("QUALIFICATION_QUESTIONS_BAD_ARGS", "`questions` (full replace) is mutually exclusive with add/remove", "Pass EITHER `questions` (the full new list) OR `add`/`remove`, not both.", "POST /organizations/{orgId}");
|
|
16583
|
+
}
|
|
16584
|
+
if (!hasSet && !hasAdd && !hasRemove) {
|
|
16585
|
+
throw client.makeError("QUALIFICATION_QUESTIONS_NO_CHANGE", "nothing to change \u2014 pass `questions`, `add`, or `remove`", "Provide a full `questions` list, or `add`/`remove` entries.", "POST /organizations/{orgId}");
|
|
16586
|
+
}
|
|
16587
|
+
const orgId = await client.resolveOrgId();
|
|
16588
|
+
const current = await client.request("GET", `/organizations/${orgId}/ai_agent_questions`);
|
|
16589
|
+
const currentQs = (current ?? []).map((q) => q.question);
|
|
16590
|
+
const norm = (s) => s.trim();
|
|
16591
|
+
let next;
|
|
16592
|
+
if (hasSet) {
|
|
16593
|
+
next = params.questions.map(norm).filter((s) => s.length > 0);
|
|
16594
|
+
} else {
|
|
16595
|
+
next = [...currentQs];
|
|
16596
|
+
if (hasRemove) {
|
|
16597
|
+
const drop = new Set(params.remove.map(norm));
|
|
16598
|
+
next = next.filter((q) => !drop.has(norm(q)));
|
|
16599
|
+
}
|
|
16600
|
+
if (hasAdd) {
|
|
16601
|
+
const seen2 = new Set(next.map(norm));
|
|
16602
|
+
for (const q of params.add.map(norm)) {
|
|
16603
|
+
if (q.length > 0 && !seen2.has(q)) {
|
|
16604
|
+
next.push(q);
|
|
16605
|
+
seen2.add(q);
|
|
16606
|
+
}
|
|
16607
|
+
}
|
|
16608
|
+
}
|
|
16609
|
+
}
|
|
16610
|
+
const seen = /* @__PURE__ */ new Set();
|
|
16611
|
+
next = next.filter((q) => {
|
|
16612
|
+
const k = norm(q);
|
|
16613
|
+
if (seen.has(k))
|
|
16614
|
+
return false;
|
|
16615
|
+
seen.add(k);
|
|
16616
|
+
return true;
|
|
16617
|
+
});
|
|
16618
|
+
const MAX_QUESTIONS = 5;
|
|
16619
|
+
if (next.length > MAX_QUESTIONS) {
|
|
16620
|
+
throw client.makeError("QUALIFICATION_QUESTIONS_LIMIT", `too many questions: ${next.length} (max ${MAX_QUESTIONS})`, `Leadbay allows at most ${MAX_QUESTIONS} qualification questions. Remove some first (pass fewer in \`questions\`, or use \`remove\`), then add.`, "POST /organizations/{orgId}");
|
|
16621
|
+
}
|
|
16622
|
+
const previousCount = currentQs.length;
|
|
16623
|
+
const noChange = next.length === currentQs.length && next.every((q, i) => norm(q) === norm(currentQs[i] ?? ""));
|
|
16624
|
+
if (noChange) {
|
|
16625
|
+
return withAgentMemoryMeta(client, {
|
|
16626
|
+
qualification_questions: currentQs.map((q) => ({ question: q })),
|
|
16627
|
+
count: currentQs.length,
|
|
16628
|
+
previous_count: previousCount,
|
|
16629
|
+
changed: false,
|
|
16630
|
+
region: client.region,
|
|
16631
|
+
hint: "No change \u2014 the resulting list is identical to the current one. Pass different `add`/`remove` entries, or call leadbay_get_qualification_questions to review the current questions."
|
|
16632
|
+
}, ctx);
|
|
16633
|
+
}
|
|
16634
|
+
const removed = currentQs.filter((q) => !next.some((n) => norm(n) === norm(q)));
|
|
16635
|
+
if (removed.length > 0 && params.confirm !== true) {
|
|
16636
|
+
return withAgentMemoryMeta(client, {
|
|
16637
|
+
qualification_questions: currentQs.map((q) => ({ question: q })),
|
|
16638
|
+
count: currentQs.length,
|
|
16639
|
+
previous_count: previousCount,
|
|
16640
|
+
changed: false,
|
|
16641
|
+
region: client.region,
|
|
16642
|
+
hint: `Re-call with confirm:true to apply. This would remove ${removed.length} question(s): ${removed.map((q) => `"${q}"`).join(", ")}. Removing a question changes how every lead is scored.`
|
|
16643
|
+
}, ctx);
|
|
16644
|
+
}
|
|
16645
|
+
await client.requestVoid("POST", `/organizations/${orgId}`, {
|
|
16646
|
+
ai_agent_lead_questions: next
|
|
16647
|
+
});
|
|
16648
|
+
client.invalidateTasteProfile();
|
|
16649
|
+
return withAgentMemoryMeta(client, {
|
|
16650
|
+
qualification_questions: next.map((q) => ({ question: q })),
|
|
16651
|
+
count: next.length,
|
|
16652
|
+
previous_count: previousCount,
|
|
16653
|
+
changed: true,
|
|
16654
|
+
region: client.region
|
|
16655
|
+
}, ctx);
|
|
16656
|
+
}
|
|
16657
|
+
};
|
|
16658
|
+
}
|
|
16659
|
+
});
|
|
16660
|
+
|
|
16661
|
+
// ../core/dist/composite/get-lead-custom-fields.js
|
|
16662
|
+
var getLeadCustomFields;
|
|
16663
|
+
var init_get_lead_custom_fields = __esm({
|
|
16664
|
+
"../core/dist/composite/get-lead-custom-fields.js"() {
|
|
16665
|
+
"use strict";
|
|
16666
|
+
init_agent_memory();
|
|
15957
16667
|
init_tool_descriptions_generated();
|
|
15958
|
-
|
|
15959
|
-
name: "
|
|
16668
|
+
getLeadCustomFields = {
|
|
16669
|
+
name: "leadbay_get_lead_custom_fields",
|
|
15960
16670
|
annotations: {
|
|
15961
|
-
title: "
|
|
16671
|
+
title: "Read a lead's custom-field values",
|
|
15962
16672
|
readOnlyHint: true,
|
|
15963
16673
|
destructiveHint: false,
|
|
15964
16674
|
idempotentHint: true,
|
|
15965
16675
|
openWorldHint: true
|
|
15966
16676
|
},
|
|
15967
|
-
description:
|
|
16677
|
+
description: leadbay_get_lead_custom_fields,
|
|
15968
16678
|
inputSchema: {
|
|
15969
16679
|
type: "object",
|
|
15970
16680
|
properties: {
|
|
15971
|
-
|
|
15972
|
-
type: "string",
|
|
15973
|
-
description: "Company name to look up. Substring fuzzy-match against the top 50 of the active lens's wishlist; ties broken by descending lead score."
|
|
15974
|
-
},
|
|
16681
|
+
leadId: { type: "string", description: "Lead UUID (required)" },
|
|
15975
16682
|
lensId: {
|
|
15976
16683
|
type: "number",
|
|
15977
16684
|
description: "Lens id (escape hatch \u2014 normally omit; auto-resolves to the active lens)"
|
|
15978
|
-
},
|
|
15979
|
-
concise: {
|
|
15980
|
-
type: "boolean",
|
|
15981
|
-
description: "Forwarded to leadbay_research_lead_by_id. If true, trims signals to hot=true items only."
|
|
15982
|
-
},
|
|
15983
|
-
response_format: {
|
|
15984
|
-
type: "string",
|
|
15985
|
-
enum: ["json", "markdown"],
|
|
15986
|
-
description: "Forwarded to leadbay_research_lead_by_id. Default 'json'."
|
|
15987
16685
|
}
|
|
15988
16686
|
},
|
|
15989
|
-
required: ["
|
|
16687
|
+
required: ["leadId"],
|
|
15990
16688
|
additionalProperties: false
|
|
15991
16689
|
},
|
|
15992
|
-
// Output shape matches leadbay_research_lead_by_id; the only additions are
|
|
15993
|
-
// _meta.resolved_from / resolved_query / match_candidates which are
|
|
15994
|
-
// documented on _by_id's output schema. Defer to _by_id for the schema —
|
|
15995
|
-
// duplicating it would just rot.
|
|
15996
16690
|
outputSchema: {
|
|
15997
16691
|
type: "object",
|
|
15998
|
-
|
|
15999
|
-
|
|
16692
|
+
properties: {
|
|
16693
|
+
lead_id: { type: "string" },
|
|
16694
|
+
custom_fields: {
|
|
16695
|
+
type: "array",
|
|
16696
|
+
description: "Custom-field VALUES on this lead. Each: {id, name, type, value}. Empty when the org has no custom fields or none are set on this lead.",
|
|
16697
|
+
items: { type: "object" }
|
|
16698
|
+
},
|
|
16699
|
+
count: { type: "number" },
|
|
16700
|
+
region: { type: "string" },
|
|
16701
|
+
hint: {
|
|
16702
|
+
type: "string",
|
|
16703
|
+
description: "Operator note \u2014 empty-state guidance, or a degradation note when the lead fetch returned entries without embedded definitions."
|
|
16704
|
+
},
|
|
16705
|
+
_meta: { type: "object" }
|
|
16706
|
+
},
|
|
16707
|
+
required: ["custom_fields", "lead_id"]
|
|
16000
16708
|
},
|
|
16001
16709
|
execute: async (client, params, ctx) => {
|
|
16002
|
-
if (!params.companyName || typeof params.companyName !== "string") {
|
|
16003
|
-
throw client.makeError("INVALID_PARAMS", "companyName is required", "Pass the company name as a string. If you already have the lead UUID, call leadbay_research_lead_by_id directly.");
|
|
16004
|
-
}
|
|
16005
16710
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
16013
|
-
const
|
|
16014
|
-
|
|
16015
|
-
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
|
|
16019
|
-
|
|
16020
|
-
leadId: primary.id,
|
|
16021
|
-
lensId,
|
|
16022
|
-
concise: params.concise,
|
|
16023
|
-
response_format: params.response_format,
|
|
16024
|
-
_resolved: {
|
|
16025
|
-
from: "companyName",
|
|
16026
|
-
query: params.companyName,
|
|
16027
|
-
candidates
|
|
16711
|
+
void client.request("POST", "/interactions", [
|
|
16712
|
+
{ type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
|
|
16713
|
+
{ type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
|
|
16714
|
+
]).catch(() => {
|
|
16715
|
+
});
|
|
16716
|
+
const lead = await client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`);
|
|
16717
|
+
const entries = lead.custom_fields ?? [];
|
|
16718
|
+
const needsCatalog = entries.some((e) => !e?.field?.name);
|
|
16719
|
+
let catalog = null;
|
|
16720
|
+
if (needsCatalog) {
|
|
16721
|
+
try {
|
|
16722
|
+
catalog = await client.request("GET", "/crm/custom_fields");
|
|
16723
|
+
} catch (err) {
|
|
16724
|
+
ctx?.logger?.warn?.(`get_lead_custom_fields: catalog fallback failed: ${err?.message ?? err?.code ?? err}`);
|
|
16028
16725
|
}
|
|
16726
|
+
}
|
|
16727
|
+
const byId = new Map((catalog ?? []).map((f) => [f.id, f]));
|
|
16728
|
+
const rows = entries.map((e) => {
|
|
16729
|
+
const bareId = e.id;
|
|
16730
|
+
const id = e.field?.id ?? bareId ?? "";
|
|
16731
|
+
const def = e.field ? void 0 : byId.get(String(id));
|
|
16732
|
+
return {
|
|
16733
|
+
id: String(id),
|
|
16734
|
+
name: e.field?.name ?? def?.name ?? null,
|
|
16735
|
+
type: e.field?.type ?? def?.type ?? null,
|
|
16736
|
+
value: e.value ?? null
|
|
16737
|
+
};
|
|
16738
|
+
});
|
|
16739
|
+
let hint;
|
|
16740
|
+
if (rows.length === 0) {
|
|
16741
|
+
hint = "This lead has no custom-field values. The org may have no custom fields defined \u2014 see leadbay_list_mappable_fields for the catalog, or set values via import (map a column to CUSTOM.<id>).";
|
|
16742
|
+
} else if (needsCatalog && rows.some((r) => r.name === null)) {
|
|
16743
|
+
hint = "Some custom fields could not be named (the lead payload omitted definitions and the catalog fetch failed). Retry, or check leadbay_list_mappable_fields.";
|
|
16744
|
+
}
|
|
16745
|
+
return withAgentMemoryMeta(client, {
|
|
16746
|
+
lead_id: params.leadId,
|
|
16747
|
+
custom_fields: rows,
|
|
16748
|
+
count: rows.length,
|
|
16749
|
+
region: client.region,
|
|
16750
|
+
...hint ? { hint } : {}
|
|
16029
16751
|
}, ctx);
|
|
16030
16752
|
}
|
|
16031
16753
|
};
|
|
@@ -21767,6 +22489,7 @@ __export(dist_exports, {
|
|
|
21767
22489
|
createLens: () => createLens,
|
|
21768
22490
|
createLensDraft: () => createLensDraft,
|
|
21769
22491
|
createTopupLink: () => createTopupLink,
|
|
22492
|
+
deleteCustomField: () => deleteCustomField,
|
|
21770
22493
|
deselectLeads: () => deselectLeads,
|
|
21771
22494
|
discoverLeads: () => discoverLeads,
|
|
21772
22495
|
dislikeLead: () => dislikeLead,
|
|
@@ -21782,12 +22505,14 @@ __export(dist_exports, {
|
|
|
21782
22505
|
getEnrichmentJobTitles: () => getEnrichmentJobTitles,
|
|
21783
22506
|
getEpilogueResponses: () => getEpilogueResponses,
|
|
21784
22507
|
getLeadActivities: () => getLeadActivities,
|
|
22508
|
+
getLeadCustomFields: () => getLeadCustomFields,
|
|
21785
22509
|
getLeadNotes: () => getLeadNotes,
|
|
21786
22510
|
getLeadProfile: () => getLeadProfile,
|
|
21787
22511
|
getLensFilter: () => getLensFilter,
|
|
21788
22512
|
getLensScoring: () => getLensScoring,
|
|
21789
22513
|
getMockJournal: () => getMockJournal,
|
|
21790
22514
|
getProspectingActions: () => getProspectingActions,
|
|
22515
|
+
getQualificationQuestions: () => getQualificationQuestions,
|
|
21791
22516
|
getQuota: () => getQuota,
|
|
21792
22517
|
getSelectionIds: () => getSelectionIds,
|
|
21793
22518
|
getTasteProfile: () => getTasteProfile,
|
|
@@ -21849,11 +22574,13 @@ __export(dist_exports, {
|
|
|
21849
22574
|
setActiveLens: () => setActiveLens,
|
|
21850
22575
|
setEpilogueStatus: () => setEpilogueStatus,
|
|
21851
22576
|
setPushback: () => setPushback,
|
|
22577
|
+
setQualificationQuestions: () => setQualificationQuestions,
|
|
21852
22578
|
setUserPrompt: () => setUserPrompt,
|
|
21853
22579
|
teamActivity: () => teamActivity,
|
|
21854
22580
|
toInboxEntry: () => toInboxEntry,
|
|
21855
22581
|
tools: () => tools,
|
|
21856
22582
|
tourPlan: () => tourPlan,
|
|
22583
|
+
updateCustomField: () => updateCustomField,
|
|
21857
22584
|
updateLens: () => updateLens,
|
|
21858
22585
|
updateLensFilter: () => updateLensFilter,
|
|
21859
22586
|
withAgentMemoryMeta: () => withAgentMemoryMeta
|
|
@@ -21917,6 +22644,8 @@ var init_dist = __esm({
|
|
|
21917
22644
|
init_preview_bulk_enrichment();
|
|
21918
22645
|
init_launch_bulk_enrichment();
|
|
21919
22646
|
init_create_custom_field();
|
|
22647
|
+
init_update_custom_field();
|
|
22648
|
+
init_delete_custom_field();
|
|
21920
22649
|
init_like_lead();
|
|
21921
22650
|
init_dislike_lead();
|
|
21922
22651
|
init_add_contact();
|
|
@@ -21937,6 +22666,9 @@ var init_dist = __esm({
|
|
|
21937
22666
|
init_campaign_call_sheet();
|
|
21938
22667
|
init_research_lead_by_id();
|
|
21939
22668
|
init_research_lead_by_name_fuzzy();
|
|
22669
|
+
init_get_qualification_questions();
|
|
22670
|
+
init_set_qualification_questions();
|
|
22671
|
+
init_get_lead_custom_fields();
|
|
21940
22672
|
init_account_history();
|
|
21941
22673
|
init_scan_portfolio_signals();
|
|
21942
22674
|
init_recall_ordered_titles();
|
|
@@ -22034,6 +22766,15 @@ var init_dist = __esm({
|
|
|
22034
22766
|
campaignCallSheet,
|
|
22035
22767
|
researchLeadById,
|
|
22036
22768
|
researchLeadByNameFuzzy,
|
|
22769
|
+
// Org qualification questions — the AI-agent questions every lead is scored
|
|
22770
|
+
// against. ALWAYS exposed (default surface): "how are my leads qualified"
|
|
22771
|
+
// is a first-session question, and the underlying get_taste_profile is
|
|
22772
|
+
// ADVANCED-gated. Read-only; no MCP edit endpoint exists (issue #3768).
|
|
22773
|
+
getQualificationQuestions,
|
|
22774
|
+
// Per-lead custom-field VALUES. ALWAYS exposed: complements the always-on
|
|
22775
|
+
// list_mappable_fields (which returns DEFINITIONS only). The lead payload
|
|
22776
|
+
// embeds each field's definition, so no catalog join is needed (issue #3768).
|
|
22777
|
+
getLeadCustomFields,
|
|
22037
22778
|
// accountHistory layers FULL notes + activity timeline on top of research
|
|
22038
22779
|
// so the agent can write the US4 "why has this dormant account resurfaced"
|
|
22039
22780
|
// narrative in ONE call. ALWAYS exposed (compositeReadTools) — the underlying
|
|
@@ -22127,6 +22868,15 @@ var init_dist = __esm({
|
|
|
22127
22868
|
// createCustomField is granular-shaped but file-import prompts depend on it
|
|
22128
22869
|
// to preserve source-system links without requiring advanced-tool exposure.
|
|
22129
22870
|
createCustomField,
|
|
22871
|
+
// update/delete custom field — same default-surface rationale as create.
|
|
22872
|
+
// delete is destructive (requires confirm:true). Both gated behind
|
|
22873
|
+
// LEADBAY_MCP_WRITE=1 in MCP.
|
|
22874
|
+
updateCustomField,
|
|
22875
|
+
deleteCustomField,
|
|
22876
|
+
// Modify the org's qualification questions (AI-agent questions). Full-replace
|
|
22877
|
+
// endpoint (POST /organizations/{orgId} {ai_agent_lead_questions:[...]}); the
|
|
22878
|
+
// tool reads current + applies add/remove/set. Shrinking requires confirm.
|
|
22879
|
+
setQualificationQuestions,
|
|
22130
22880
|
// addNote is granular-shaped but file-import prompts depend on it to preserve
|
|
22131
22881
|
// meaningful source-file notes after imports return lead ids.
|
|
22132
22882
|
addNote,
|
|
@@ -22184,6 +22934,204 @@ import {
|
|
|
22184
22934
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
22185
22935
|
|
|
22186
22936
|
// src/prompts.generated.ts
|
|
22937
|
+
var leadbay_build_campaign = `
|
|
22938
|
+
## MEMORY
|
|
22939
|
+
|
|
22940
|
+
Before responding, glance at any \`_meta.agent_memory.summary\` returned by tool calls earlier in this session and reflect its top signals in your reasoning ("Filtering by your stated preference for healthcare"). After any material new signal from the user this conversation (sector, region, deal size, communication style, qualification rule, explicit retraction, or recurrence / scheduling preference such as "I do this every day" or "remind me every morning"), call \`leadbay_agent_memory_capture\` to persist it: \`source:"user_stated"\` if literal, \`source:"inferred"\` with confidence <=6 if inferred.
|
|
22941
|
+
|
|
22942
|
+
|
|
22943
|
+
Build me a Leadbay campaign from scratch{{arg:campaign_name_paren}}. {{arg:audience_block}}
|
|
22944
|
+
|
|
22945
|
+
GATE \u2014 DEFER TO TOOL RENDERING. When you call a Leadbay composite that ships its own RENDERING block (every composite in 0.9.0+ does), render the response using that block's recipe verbatim \u2014 score bars, glyph palette, column order, hide-list, link priorities, all of it. Do NOT substitute prose, a numbered list, or a different column structure even when an orchestrating prompt's body suggests alternate framing. Prompt-specific commentary (motivational nudges, summaries, next-action recommendations) belongs ABOVE or BELOW the canonical table, never in place of it.
|
|
22946
|
+
|
|
22947
|
+
If the prompt's body and the tool's RENDERING appear to conflict, the tool's RENDERING wins for the structural layout; the prompt's voice wins for the commentary that surrounds it.
|
|
22948
|
+
|
|
22949
|
+
|
|
22950
|
+
# Resilience rules for Leadbay long-running tools
|
|
22951
|
+
|
|
22952
|
+
These four rules apply to every Leadbay workflow that calls \`leadbay_pull_leads\`, \`leadbay_bulk_qualify_leads\`, \`leadbay_research_lead_by_id\`, \`leadbay_import_and_qualify\`, or \`leadbay_enrich_titles\`. **Treat timeouts and stream-closed errors as transient, not as signals to replan.**
|
|
22953
|
+
|
|
22954
|
+
## Rule 1 \u2014 Pin the lens
|
|
22955
|
+
|
|
22956
|
+
After your first \`leadbay_pull_leads\` call, capture \`response.lens.id\` into your working memory and **pass it explicitly as the \`lensId\` argument to every subsequent call** in this session \u2014 including any re-pulls, bulk qualifies, or research calls that accept it. (Field-name caveat: the response nests it as \`lens.id\`; the parameter on subsequent calls is \`lensId\`.) The active lens can shift between calls (5-minute client cache + backend \`last_requested_lens\` can change if the user touches the web UI). A lens shift mid-workflow throws away your top-10 work.
|
|
22957
|
+
|
|
22958
|
+
## Rule 2 \u2014 Prefer async for bulk operations
|
|
22959
|
+
|
|
22960
|
+
\`leadbay_bulk_qualify_leads\` and \`leadbay_import_and_qualify\` accept \`wait_for_completion:false\`, which returns \`{status:'running', qualify_id}\` immediately. Then poll \`leadbay_qualify_status\` (or \`leadbay_import_status\`) every ~10s until the job completes. **Use the async pattern by default** \u2014 the blocking default can exceed the MCP client's per-call timeout on large batches and produce a misleading \`"Request timed out"\` even though the server is still working.
|
|
22961
|
+
|
|
22962
|
+
## Rule 3 \u2014 Serialize \`leadbay_research_lead_by_id\` fan-out
|
|
22963
|
+
|
|
22964
|
+
\`leadbay_research_lead_by_id\` is composite and reads many sub-resources. Calling it on 10 leads in parallel can saturate the transport and produce \`"Tool permission stream closed"\` errors that look like permission failures but are really backpressure. **Call it sequentially**, or at most 3 in parallel. If one call fails with a stream/timeout error, retry that one call once before moving on; on a second failure, note the lead and continue \u2014 do not abandon the remaining leads.
|
|
22965
|
+
|
|
22966
|
+
## Rule 4 \u2014 Retry, don't replan
|
|
22967
|
+
|
|
22968
|
+
If a Leadbay tool returns \`"Request timed out"\`, \`"stream closed"\`, or any other transport-level error (distinct from a Leadbay-issued error payload), the work may still be running server-side. Do this in order:
|
|
22969
|
+
|
|
22970
|
+
1. For bulk tools \u2014 retry with \`wait_for_completion:false\` and poll the status tool with the returned id. Don't re-pull leads; that can shift the lens.
|
|
22971
|
+
2. For single-lead tools \u2014 retry the same call once. If it still fails, record the lead id and continue with the rest of the workflow.
|
|
22972
|
+
3. **Do not** switch strategies (e.g. "the endpoint is broken, let me re-pull from scratch"). The earlier work is still valid; the timeout was the wire.
|
|
22973
|
+
|
|
22974
|
+
If \`pull_leads\` itself fails and you have no prior batch, then yes \u2014 retry it, explicitly pass the lensId you captured (if any), and continue.
|
|
22975
|
+
|
|
22976
|
+
|
|
22977
|
+
# PHASE 0 \u2014 STATE + AUDIENCE
|
|
22978
|
+
|
|
22979
|
+
Call \`leadbay_account_status\` to see my remaining quota, my **enrichment credits**, and my **active lens**. Note \`web_fetch_remaining\` (qualification budget) and the enrichment credit balance \u2014 Phase 3 spends credits.
|
|
22980
|
+
|
|
22981
|
+
Resolve the audience:
|
|
22982
|
+
|
|
22983
|
+
- **Default \u2014 use my active lens.** If I didn't name a fresh audience, the active lens IS the audience. Do NOT create a new lens.
|
|
22984
|
+
- **Fresh-audience fork.** Only if I described a NEW audience the active lens doesn't already cover, set it up first: \`leadbay_adjust_audience\` for sector/size tweaks, or \`leadbay_new_lens\` to create a brand-new named lens \u2014 then continue on that lens. Do NOT silently overwrite my existing lens; confirm once before switching.
|
|
22985
|
+
|
|
22986
|
+
# PHASE 1 \u2014 DISCOVER
|
|
22987
|
+
|
|
22988
|
+
Call \`leadbay_pull_leads\` on the resolved lens. **Capture \`response.lens.id\` and pass it as an explicit \`lensId\` on every later call this session** \u2014 a mid-session lens shift would discard the cohort I'm about to pick. Render the batch with the canonical layout:
|
|
22989
|
+
|
|
22990
|
+
## RENDERING \u2014 markdown table, three columns, score-bar driven
|
|
22991
|
+
|
|
22992
|
+
Present the response as a markdown table sorted by \`score\` descending, with exactly three columns. Do not summarize in prose. Do not show the numeric score anywhere.
|
|
22993
|
+
|
|
22994
|
+
## Score-bar (10-segment, inline-code wrapped)
|
|
22995
|
+
|
|
22996
|
+
Wrap a 10-glyph bar in a SINGLE inline-code span (backticks). The inline-code styling is what gives the bar contrast in most chat renderers \u2014 HTML \`<span>\` is stripped inside table cells.
|
|
22997
|
+
|
|
22998
|
+
Glyphs (use these exact characters; do not substitute):
|
|
22999
|
+
|
|
23000
|
+
- \`\u25B0\` \u2014 firmographic-only fill
|
|
23001
|
+
- \`\u2756\` \u2014 AI-booster cap (placed at the RIGHT END of the filled run, never the front)
|
|
23002
|
+
- \`\u25B1\` \u2014 empty
|
|
23003
|
+
|
|
23004
|
+
Computation:
|
|
23005
|
+
|
|
23006
|
+
\`\`\`
|
|
23007
|
+
total_filled = round(score / 10), clamped to 0..10
|
|
23008
|
+
ai_segments = round(qualification_summary.avg_qualification_boost / 3.3),
|
|
23009
|
+
clamped to [0, total_filled]
|
|
23010
|
+
normal_filled = total_filled \u2212 ai_segments
|
|
23011
|
+
bar = "\u25B0" \xD7 normal_filled
|
|
23012
|
+
+ "\u2756" \xD7 ai_segments
|
|
23013
|
+
+ "\u25B1" \xD7 (10 \u2212 total_filled)
|
|
23014
|
+
\`\`\`
|
|
23015
|
+
|
|
23016
|
+
If \`qualification_summary.answered == 0\` or \`avg_qualification_boost\` is null, set \`ai_segments = 0\` (no \u2756). Always wrap the bar in backticks. Print the legend \`\` \`\u25B0\` firmographic \xB7 \`\u2756\` AI booster cap \xB7 \`\u25B1\` unfilled \`\` once below the table.
|
|
23017
|
+
|
|
23018
|
+
|
|
23019
|
+
**Column 1 \u2014 Company**
|
|
23020
|
+
|
|
23021
|
+
- Line 1: the 10-segment score bar in inline-code backticks (see the score-bar snippet above for the algorithm).
|
|
23022
|
+
- Insert \`<br>\` between lines.
|
|
23023
|
+
- Line 2: linked company name + \` \xB7 \` + short location + \` \xB7 \` + compact size.
|
|
23024
|
+
- Link target: \`website\` (prefix \`https://\` if it's a bare hostname). Don't synthesize an app deep-link.
|
|
23025
|
+
- Location: shorten "City of New York" \u2192 "NYC"; otherwise "City ST"; state alone only when city missing.
|
|
23026
|
+
- Size: \`"Xk+"\` when \`size.min >= 1000\`, \`"min\u2013max"\` otherwise.
|
|
23027
|
+
|
|
23028
|
+
**Column 2 \u2014 Why it fits**
|
|
23029
|
+
|
|
23030
|
+
- One sentence, \u2264 20 words.
|
|
23031
|
+
- Synthesize from (in priority order, whichever is present) the lead's \`short_description\`, top 2 \`tags[].display_name\`, and the gist of \`qualification_summary.best_response_excerpt\`. The trim payload does NOT carry the longer \`description\` field \u2014 for that, agent must call \`leadbay_research_lead_by_id\` or \`leadbay_research_lead_by_name_fuzzy\`.
|
|
23032
|
+
- Do NOT append \`(boost N)\` \u2014 the \u2756 cap in column 1 already carries that signal.
|
|
23033
|
+
- No bullet lists, no line breaks inside the cell.
|
|
23034
|
+
|
|
23035
|
+
**Column 3 \u2014 Contact**
|
|
23036
|
+
|
|
23037
|
+
\`[Contact name](LINK) \xB7 short job title\`. The \`[Contact name](LINK)\` markdown link wrapping is mandatory \u2014 never render the name as plain text. See linking/contact-linkedin for the URL priority (real profile \u2192 constructed people-search) and the \xB0-flag fallback.
|
|
23038
|
+
|
|
23039
|
+
**Hide from the user (never include in any cell):** \`id\`, \`location.pos\`, \`location.country\` (unless city/state both missing), \`sector_id\`, \`is_hq\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`highlighted_fields\`, \`custom_fields\`, \`contacts_count\` when 0, \`notes_count\` / \`epilogue_actions_count\` / \`prospecting_actions_count\` when 0, \`stale_at\`, \`deal_insights\`, \`social_presence\` booleans (except as the \xB0-flag signal), \`need_attention\` flags, any field whose value is the string \`"null"\`.
|
|
23040
|
+
|
|
23041
|
+
## Linking a contact's name
|
|
23042
|
+
|
|
23043
|
+
**MANDATORY: every contact name in your output \u2014 table cells, prose, headers, "Reach <Name>" callouts \u2014 MUST be wrapped in markdown link syntax \`[Name](URL)\`. Never render a contact name as bare text. A plain-text name is a broken contact card; the underlined name is the user's primary affordance for "take me to this person's profile". No "no URL available" exception \u2014 the search URL below is always constructable from name + company.**
|
|
23044
|
+
|
|
23045
|
+
URL priority (first applicable wins):
|
|
23046
|
+
|
|
23047
|
+
1. **Real profile** \u2014 \`contact.linkedin_page\` when it's a string starting with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it).
|
|
23048
|
+
2. **Constructed people-search** \u2014 \`https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>\`. URL-encode params. Strip Inc / LLC / Corp / Ltd / GmbH / Co / S.A. / S.L. / PLC / AG / SAS / SARL suffixes from the company. Append a trailing \` \xB0\` to the rendered name ONLY when this fallback is in use AND \`social_presence.linkedin == false\`. Never append \`\xB0\` when a real \`linkedin_page\` was used.
|
|
23049
|
+
|
|
23050
|
+
Never link a person's name to the company's LinkedIn page (and vice versa) \u2014 the two surfaces are different and conflating them quietly degrades the workflow.
|
|
23051
|
+
|
|
23052
|
+
## Linking the company
|
|
23053
|
+
|
|
23054
|
+
Use the lead's \`website\` as the company-name link target \u2014 prefix \`https://\` if the value is a bare hostname. (The MCP does NOT synthesize a Leadbay-app deep-link URL; the team has not standardized one. Linking to \`website\` is always real data.)
|
|
23055
|
+
|
|
23056
|
+
When the response carries \`social_urls\` (the post-fix multi-platform URL block on rich-lead responses), render every non-null platform as a pill chip in the company-info row. Iterate over \`social_urls\`'s keys \u2014 never hardcode a fixed list \u2014 and emit each as \`[<platform-label>](<url>)\`. Skip platforms whose URL is null.
|
|
23057
|
+
|
|
23058
|
+
\`social_presence\` carries booleans for the same 6 platforms (crunchbase, facebook, instagram, linkedin, tiktok, twitter) \u2014 useful when you only care that the company has a profile somewhere. Use it as the \xB0-flag signal in the contact people-search fallback (see linking/contact-linkedin).
|
|
23059
|
+
|
|
23060
|
+
|
|
23061
|
+
|
|
23062
|
+
If the batch is thin (fewer than ~10 workable leads) or I ask for more depth, top it up: call \`leadbay_bulk_qualify_leads({lensId:<captured>, count:<deficit, max 25>, wait_for_completion:false})\`, poll \`leadbay_qualify_status\` until done, then re-pull with the same \`lensId\`. Never re-pull without \`lensId\`.
|
|
23063
|
+
|
|
23064
|
+
# PHASE 2 \u2014 PICK AN ICP CANDIDATE POOL
|
|
23065
|
+
|
|
23066
|
+
A campaign is only as good as the leads in it \u2014 AND only as good as whether each lead has a reachable BUYER (see Phase 3). So pick a **generous candidate pool** now, not the final cohort: aim for ~1.5\xD7 the target size of in-ICP leads (highest \`ai_agent_lead_score\`), so Phase 3 can drop any lead that turns out to have no buyer-persona contact and still hit the target. If the batch is short, top up via \`leadbay_bulk_qualify_leads\` / \`leadbay_extend_lens\`.
|
|
23067
|
+
|
|
23068
|
+
If I named specific leads, use those (but still apply the Phase 3 buyer-coverage check and tell me which lack a buyer). Otherwise recommend the pool and show it. Capture the candidate \`leadIds\`. Confirm the count ("12 candidates \u2014 I'll lock the final ~8 after checking each has a buyer"). Do NOT create the campaign yet \u2014 the final cohort is decided after Phase 3's coverage check.
|
|
23069
|
+
|
|
23070
|
+
# PHASE 3 \u2014 ENRICH THE RIGHT CONTACTS (load-bearing)
|
|
23071
|
+
|
|
23072
|
+
This is the phase that decides whether the campaign is worth a salesperson's time. Contacts aren't attached by default and enrichment is paid \u2014 so spend it ONLY on the people who would actually **buy what I sell**, not on whoever is most senior.
|
|
23073
|
+
|
|
23074
|
+
**Step A \u2014 work out MY buyer persona (do this before touching titles).**
|
|
23075
|
+
Figure out what *I* sell and therefore who, inside the target company, owns the decision to buy it:
|
|
23076
|
+
|
|
23077
|
+
- Infer my product / value-prop from my context: my org and account (\`leadbay_account_status\`), and especially my lens's qualification criteria \u2014 the \`qualification_summary\` on the leads tells you *why* these companies are good targets for me, which implies what I'm offering them.
|
|
23078
|
+
- Map value-prop \u2192 the **buying department/persona**, NOT seniority:
|
|
23079
|
+
- A sales / prospecting / lead-gen / outbound / marketing / GTM / revenue tool \u2192 the **revenue org**: VP / Head / Director of Sales, Business Development, Account/Carrier Sales, CRO, CMO / VP Marketing, Head of Growth / Demand Gen, RevOps. (This is Leadbay's own persona.)
|
|
23080
|
+
- An operations / logistics tool \u2192 operations leaders. A finance tool \u2192 finance. A dev tool \u2192 engineering. Etc.
|
|
23081
|
+
- **Company size caveat:** Founder / CEO / Owner is a real buyer at small companies (\u2264~50), but at larger ones they are not \u2014 there the functional leader (e.g. VP Sales) is the buyer.
|
|
23082
|
+
- **State the persona in one line and confirm it with me** before spending (see Step C). I can correct it.
|
|
23083
|
+
|
|
23084
|
+
**ANTI-PATTERN \u2014 do NOT do this:** picking the most senior or most "decision-maker-sounding" title regardless of department. A Director of Operations, COO, Mgr of Logistics, CFO, or CTO will **never** buy a sales tool \u2014 enriching them wastes credits and hands me a useless list. Seniority is not the same as being my buyer.
|
|
23085
|
+
|
|
23086
|
+
**Step B \u2014 find the persona-matching, enrichable contacts.**
|
|
23087
|
+
Call \`leadbay_recall_ordered_titles({leadIds, lensId})\` and \`leadbay_enrich_titles({leadIds, lensId})\` in **discovery mode** (no \`titles\`). These return previously-enriched titles, \`title_suggestions\`, \`auto_included_titles\`, \`available_in_selection\`, \`enrichable_contacts\`, and \`credits_remaining\`. Treat them as a **menu to filter against my persona \u2014 not the answer.** If past-enriched titles or suggestions are off-persona (e.g. operations roles for a sales tool), do NOT repeat them. Select the titles that match my buyer persona AND are actually enrichable.
|
|
23088
|
+
|
|
23089
|
+
**Step B.5 \u2014 coverage guarantee (lock the final cohort here).** A campaign where half the leads have no buyer is a failed campaign. So before enriching, determine for each candidate lead whether it actually has an **enrichable buyer-persona contact** \u2014 use the discovery data plus, where it's ambiguous, a quick \`leadbay_research_lead_by_id\` to see that lead's available contact titles. Then:
|
|
23090
|
+
|
|
23091
|
+
- **KEEP** candidates that have \u22651 enrichable buyer-persona contact.
|
|
23092
|
+
- **SWAP OUT** candidates whose only contacts are off-persona (e.g. ops/dispatch/finance only) or who have no enrichable contact at all. Replace each with the **highest-\`ai_agent_lead_score\` in-ICP candidate** from the pool that DOES have a buyer, until the cohort hits the target size (default 20; here capped by the enrichment budget).
|
|
23093
|
+
- **Do NOT trade ICP fit for coverage.** A lead with a buyer but weak ICP fit (low \`ai_agent_lead_score\`, a vertical that doesn't match what I sell) is still the wrong lead \u2014 coverage is a filter applied AFTER ICP, never a reason to admit an off-ICP company. The final cohort must be both high-ICP AND buyer-covered.
|
|
23094
|
+
- If the lens genuinely can't supply enough buyer-ready, in-ICP leads, say so honestly and offer to widen/extend rather than padding the campaign with no-buyer leads OR with off-ICP ones.
|
|
23095
|
+
|
|
23096
|
+
Tell me what you swapped in one line ("dropped Corbett + RBS \u2014 ops-only; swapped in Acme + Globex which have Sales VPs"). The goal is a final cohort where EVERY lead has a real buyer to call.
|
|
23097
|
+
|
|
23098
|
+
**Step C \u2014 show the spend + persona and confirm.** State the persona, the chosen titles, and "You have {credits_remaining} credits; this enriches {enrichable_contacts} contacts." Confirm via \`ask_user_input_v0\` ("Enrich these {enrichable_contacts} <persona> contacts now?" \u2192 ["Yes, enrich", "No, skip", "Change the persona/titles"]). Never launch a paid run without this. Enrich up to the campaign size (default 20) best persona-matching contacts.
|
|
23099
|
+
|
|
23100
|
+
**Step D \u2014 launch + poll.** On yes: \`leadbay_enrich_titles({leadIds, lensId, titles:[...chosen], email:true, phone:true})\` to launch, then poll \`leadbay_bulk_enrich_status\` until done (enrichment can take several minutes \u2014 keep polling, don't render an empty sheet prematurely). Append one small line at the very end of your reply: \`_(N credits remaining)_\`.
|
|
23101
|
+
|
|
23102
|
+
If I skip enrichment, continue \u2014 the campaign can be enriched later from the call sheet.
|
|
23103
|
+
|
|
23104
|
+
# PHASE 4 \u2014 CREATE THE CAMPAIGN
|
|
23105
|
+
|
|
23106
|
+
Derive a name (\`<lens or audience> \u2013 <today's date>\`) or use the one I gave. Call \`leadbay_create_campaign({name, lead_ids:<final buyer-ready cohort from Phase 3>})\` \u2014 this creates AND seeds in one call. Seed the cohort you actually enriched buyers on, not the original raw pool. Confirm the returned campaign name + id in one line. (If I later ask to add more leads to it, that's \`leadbay_add_leads_to_campaign\`.)
|
|
23107
|
+
|
|
23108
|
+
# PHASE 5 \u2014 THE VIEW (call / email ready)
|
|
23109
|
+
|
|
23110
|
+
If you launched enrichment in Phase 3, **poll \`leadbay_bulk_enrich_status\` until it's actually done before rendering** \u2014 do not render a "still enriching" sheet with empty contact cells; the whole point is the landed phones/emails. Enrichment can take several minutes; keep polling.
|
|
23111
|
+
|
|
23112
|
+
Then call \`leadbay_campaign_call_sheet({campaign_id})\` and render it per its RENDERING block \u2014 one card per lead, contacts with \`[phone](tel:)\` + \`[email](mailto:)\` one-tap links, the readiness chip at the top, map optional. This is the view I work from: scan \u2192 tap to call \u2192 tap to email.
|
|
23113
|
+
|
|
23114
|
+
**Flag suspect contacts** so I don't email the wrong person blind: mark with \u26A0 any enriched contact whose email domain doesn't match the company's website, or who shows up on more than one lead in this campaign (a sign of a mis-attributed enrichment). Keep the phone (it's usually still right) but tell me the email looks off.
|
|
23115
|
+
|
|
23116
|
+
# PHASE 6 \u2014 HANDOFF + STOP
|
|
23117
|
+
|
|
23118
|
+
The campaign exists and is call/email ready. End by offering, via \`ask_user_input_v0\`:
|
|
23119
|
+
|
|
23120
|
+
- "Start working it now" \u2192 run \`leadbay_work_campaign\` on this campaign (the calling/email session + outcome-logging loop).
|
|
23121
|
+
- "See the pulse" \u2192 \`leadbay_campaign_progression\` for per-lead status.
|
|
23122
|
+
|
|
23123
|
+
Then STOP. Building a campaign is NOT outreaching \u2014 do not send anything and do not call \`leadbay_report_outreach\`. When I come back later to log calls, see previous statuses, and do follow-ups, that is \`leadbay_work_campaign\`, not this prompt.
|
|
23124
|
+
|
|
23125
|
+
# Iron laws
|
|
23126
|
+
|
|
23127
|
+
- Enrichment targets MY buyer persona \u2014 the people who would actually buy what *I* sell, derived from my product/ICP \u2014 NOT generic seniority. For a sales/prospecting tool that means the revenue org (sales / BD / growth / marketing leaders); a Director of Operations, COO, or logistics manager is useless no matter how senior. Get the persona right or the campaign is worthless.
|
|
23128
|
+
- Selection is DATA-DRIVEN (\`leadbay_recall_ordered_titles\` + \`leadbay_enrich_titles\` discovery) but FILTERED to the persona \u2014 never blindly repeat past-enriched or suggested titles that don't match who buys my product.
|
|
23129
|
+
- The FINAL cohort must be all buyer-ready: drop/swap any lead with no enrichable buyer-persona contact (Phase 3 Step B.5) rather than shipping it empty. A campaign where half the leads have no buyer to call is a failed campaign. Pick a generous pool in Phase 2 so swaps are possible.
|
|
23130
|
+
- NEVER launch paid enrichment without showing \`credits_remaining\` + \`enrichable_contacts\`, naming the persona, and getting a yes.
|
|
23131
|
+
- Qualify / pick BEFORE \`leadbay_create_campaign\` \u2014 never seed a campaign with unvetted leads.
|
|
23132
|
+
- Carry the captured \`lensId\` on every call. A lens shift loses the cohort.
|
|
23133
|
+
- End at the rendered call sheet, then hand off to \`leadbay_work_campaign\`. Do NOT re-implement the calling / follow-up loop here, and do NOT call \`leadbay_report_outreach\`.
|
|
23134
|
+
`;
|
|
22187
23135
|
var leadbay_daily_check_in = `
|
|
22188
23136
|
## MEMORY
|
|
22189
23137
|
|
|
@@ -23208,6 +24156,7 @@ Optional: offer to review the \`leadbay_campaign_progression\` for the same camp
|
|
|
23208
24156
|
- If the user dictates an outcome that doesn't cleanly map to one of the four epilogue values, ASK ONCE before guessing.
|
|
23209
24157
|
`;
|
|
23210
24158
|
var PROMPT_META = {
|
|
24159
|
+
leadbay_build_campaign: { "name": "leadbay_build_campaign", "short_description": 'Build a sales campaign from scratch in one guided flow: discover on the\nactive lens, qualify and pick a cohort, enrich the contacts most likely to\nengage, save it via `leadbay_create_campaign`, then show a one-tap call/email\nview via `leadbay_campaign_call_sheet`. Trigger on "build me a campaign",\n"set up a new campaign", "create a campaign from scratch". WORK an existing\ncampaign with `leadbay_work_campaign`; split leads across reps with\n`leadbay_setup_team_prospecting`.\n', "arguments": [{ "name": "audience", "description": "Optional: a fresh audience to target (e.g. 'dental clinics in Texas'). Omit to build from your ACTIVE lens \u2014 the default.", "required": false }, { "name": "campaign_name", "description": "Optional: a name for the campaign. Omit and one is derived from the lens/audience + date (or the backend AI-names it).", "required": false }], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_bulk_qualify_leads", "leadbay_qualify_status", "leadbay_recall_ordered_titles", "leadbay_enrich_titles", "leadbay_bulk_enrich_status", "leadbay_create_campaign", "leadbay_add_leads_to_campaign", "leadbay_campaign_call_sheet", "leadbay_campaign_progression", "leadbay_new_lens", "leadbay_adjust_audience"], "failure_modes": ["Enriches by seniority instead of by buyer persona \u2014 picks COO / Director of Operations / Mgr of Logistics / CFO / CTO because they sound senior, when the user sells a SALES tool whose buyer is the revenue org (VP/Head/Director of Sales, BD, growth, marketing). Operations people never buy a sales tool; this hands the salesperson a useless list.", "Fails to derive the user's buyer persona from their product/ICP before choosing titles \u2014 jumps straight to titles without working out who buys what THIS user sells, then defaults to generic exec titles.", "Blindly repeats leadbay_recall_ordered_titles / discovery suggestions even when they are off-persona (e.g. operations roles a prior session wrongly enriched) \u2014 recall is a filtered input, not the answer.", "Poor coverage \u2014 leaves many picked leads with no persona-matching contact (or 0 enrichments on some leads) and doesn't flag it, so the salesperson opens the campaign to half-empty rows.", "Launches paid enrichment without first showing credits_remaining + enrichable_contacts, naming the persona, and getting a yes \u2014 the spend-visibility gate is mandatory.", "Creates the campaign before qualifying / picking \u2014 seeds a campaign with unvetted leads. Qualify and let the user pick (or AI-recommend) FIRST, then leadbay_create_campaign.", "Ends at 'campaign created' without rendering the leadbay_campaign_call_sheet view \u2014 the ready-to-work view IS the deliverable; stopping short is purpose drift.", "Re-implements the calling / outcome / follow-up loop instead of handing off to leadbay_work_campaign \u2014 that prompt already owns it. This prompt BUILDS; work_campaign WORKS.", "Auto-sends outreach or calls leadbay_report_outreach \u2014 building a campaign is not outreaching. No send, no log; the user works it afterward.", "Re-pulls leadbay_pull_leads without the captured lensId \u2014 a mid-session lens shift discards the picked cohort.", "Silently overwrites the user's active lens when forking to a fresh audience \u2014 confirm once before switching lenses.", "Renders the picked leads or the call sheet as prose instead of the canonical per-tool RENDERING layout."] },
|
|
23211
24160
|
leadbay_daily_check_in: { "name": "leadbay_daily_check_in", "short_description": 'Morning DISCOVERY workflow \u2014 new leads from the lens wishlist. Trigger\non "show me leads", "what\'s new today", "let\'s prospect", "run my check-in",\n"my morning check-in", "I do this every day", "every morning". Recurrence\nlanguage always means this prompt. Do NOT trigger on follow-up phrasings\n("follow up", "before my trip") \u2014 those go to `leadbay_followup_check_in`.\n', "arguments": [], "expected_calls": ["leadbay_account_status", "leadbay_pull_leads", "leadbay_research_lead_by_id", "leadbay_bulk_qualify_leads", "leadbay_enrich_contacts"], "failure_modes": ["Calls leadbay_report_outreach without explicit user authorization", "Surfaces fewer than 10 leads when more are available, or fails to top up via leadbay_qualify_top_n when the batch is short", `Replaces the canonical pull_leads table layout with prose per row (the per-tool RENDERING block is the structural contract; "Today's nudges" goes above it, not in place of it)`, "Skips the nudge paragraph entirely \u2014 the table alone is fine but adding the nudge is the value-add", `Skips deep research on promising leads (Phase 4) \u2014 the agent must call leadbay_research_lead_by_id on each when the user's intent is to research specific leads; Phase 4 is intentionally skipped for batch-view requests ("show me today's leads", "run my morning check-in") per the Phase 4 skip gate`, "Triggers contact enrichment without asking the user first (it consumes quota)", "Skips the STOP byproduct and proposes next actions on its own", 'Fires 10 parallel leadbay_research_lead_by_id calls and treats "stream closed" errors as terminal \u2014 must serialize and retry singletons', "Re-pulls leadbay_pull_leads without passing the captured lensId, allowing a backend lens shift to discard the Phase 2 batch", 'Treats a "Request timed out" from leadbay_bulk_qualify_leads as terminal instead of retrying with wait_for_completion:false + qualify_status polling', 'Triggers on a follow-up query (e.g., "leads I should follow up with") that should have routed to `leadbay_followup_check_in` \u2014 the two entry points are different data sources (Discover wishlist vs Monitor view) per \xA71.6'] },
|
|
23212
24161
|
leadbay_extend_my_lens: { "name": "leadbay_extend_my_lens", "short_description": "Add more leads to the current lens on demand \u2014 for users whose appetite\nexceeds the standard daily fill. The agent picks seeds silently from\nwhat's already on the lens, fires the extra refill, and surfaces the\nqueue confirmation. The user never reviews the seed list.\n", "arguments": [{ "name": "extra_count", "description": "How many extra leads to add. Optional. Omit to use the backend default.", "required": false }], "expected_calls": ["leadbay_account_status", "leadbay_seed_candidates", "leadbay_extend_lens", "leadbay_pull_leads"], "failure_modes": ["Surfaces the seed candidate list to the user instead of picking silently \u2014 the user asked for MORE LEADS, not a candidate review meeting", "Skips the seeded path and calls `leadbay_extend_lens` with no `seed_lead_ids`, losing the bias signal the recommender needs", "On 429, silently retries instead of surfacing the three options (smaller / wait / upgrade) via your host's choice widget (`ask_user_input_v0` or `AskUserQuestion`)", "Forgets to pre-check `LENS_EXTRA_REFILL` quota in `leadbay_account_status` and burns a wasted API call", "Skips the post-queue pull-leads suggestion, so the user doesn't see what just got added"] },
|
|
23213
24162
|
leadbay_followup_check_in: { "name": "leadbay_followup_check_in", "short_description": 'Follow-up check-in: surface KNOWN leads from the Monitor view needing\nre-engagement. Trigger on "follow up", "already known leads", "what\'s\noverdue", "before my trip", "who should I re-engage". Do NOT trigger on\n"show me today\'s leads", "my morning check-in", "run my check-in",\n"I do this every day", "every morning" \u2014 those go to\n`leadbay_daily_check_in`.\n', "arguments": [], "expected_calls": ["leadbay_pull_followups", "leadbay_research_lead_by_id", "leadbay_prepare_outreach"], "failure_modes": ["Calls leadbay_pull_leads (the Discover entry point) instead of leadbay_pull_followups \u2014 these are different data sources; the Discover queue does NOT contain Monitor's known-but-cold pipeline", 'Iterates pages of leadbay_pull_leads filtering by engagement_count to "fake" a follow-up view (a real bug observed in 0.9.0 \u2014 the right move is to call pull_followups directly)', "Replaces the canonical pull_followups table layout with prose per row (the per-tool RENDERING block is the structural contract; commentary belongs above or below)", 'Skips the cross-mode pivot offer at the end ("Want to see NEW leads from your wishlist instead?" routes to leadbay_pull_leads)'] },
|
|
@@ -23229,6 +24178,7 @@ should I follow up on" to "I'll send via lemlist".
|
|
|
23229
24178
|
};
|
|
23230
24179
|
var PROMPT_CATALOG_HEADER = `This server exposes the following workflow prompts via \`prompts/list\` and \`prompts/get\`. Some MCP clients render them as slash commands; if your client does not, you (the agent) should invoke them directly via \`prompts/get\` when the user's request matches one of the triggers described below.`;
|
|
23231
24180
|
var PROMPT_CATALOG_BULLETS = {
|
|
24181
|
+
leadbay_build_campaign: `- \`leadbay_build_campaign\` (optional args: audience, campaign_name): Build a sales campaign from scratch in one guided flow: discover on the active lens, qualify and pick a cohort, enrich the contacts most likely to engage, save it via \`leadbay_create_campaign\`, then show a one-tap call/email view via \`leadbay_campaign_call_sheet\`. Trigger on "build me a campaign", "set up a new campaign", "create a campaign from scratch". WORK an existing campaign with \`leadbay_work_campaign\`; split leads across reps with \`leadbay_setup_team_prospecting\`.`,
|
|
23232
24182
|
leadbay_daily_check_in: `- \`leadbay_daily_check_in\`: Morning DISCOVERY workflow \u2014 new leads from the lens wishlist. Trigger on "show me leads", "what's new today", "let's prospect", "run my check-in", "my morning check-in", "I do this every day", "every morning". Recurrence language always means this prompt. Do NOT trigger on follow-up phrasings ("follow up", "before my trip") \u2014 those go to \`leadbay_followup_check_in\`.`,
|
|
23233
24183
|
leadbay_extend_my_lens: `- \`leadbay_extend_my_lens\` (optional args: extra_count): Add more leads to the current lens on demand \u2014 for users whose appetite exceeds the standard daily fill. The agent picks seeds silently from what's already on the lens, fires the extra refill, and surfaces the queue confirmation. The user never reviews the seed list.`,
|
|
23234
24184
|
leadbay_followup_check_in: `- \`leadbay_followup_check_in\`: Follow-up check-in: surface KNOWN leads from the Monitor view needing re-engagement. Trigger on "follow up", "already known leads", "what's overdue", "before my trip", "who should I re-engage". Do NOT trigger on "show me today's leads", "my morning check-in", "run my check-in", "I do this every day", "every morning" \u2014 those go to \`leadbay_daily_check_in\`.`,
|
|
@@ -23376,6 +24326,30 @@ var CATALOG = [
|
|
|
23376
24326
|
)
|
|
23377
24327
|
]
|
|
23378
24328
|
},
|
|
24329
|
+
{
|
|
24330
|
+
name: "leadbay_build_campaign",
|
|
24331
|
+
description: PROMPT_META.leadbay_build_campaign.short_description,
|
|
24332
|
+
arguments: [
|
|
24333
|
+
{
|
|
24334
|
+
name: "audience",
|
|
24335
|
+
description: "Optional: a fresh audience to target (e.g. 'dental clinics in Texas'). Omit to build from your ACTIVE lens \u2014 the default.",
|
|
24336
|
+
required: false
|
|
24337
|
+
},
|
|
24338
|
+
{
|
|
24339
|
+
name: "campaign_name",
|
|
24340
|
+
description: "Optional: a name for the campaign. Omit and one is derived from the lens/audience + date (or the backend AI-names it).",
|
|
24341
|
+
required: false
|
|
24342
|
+
}
|
|
24343
|
+
],
|
|
24344
|
+
render: (args) => [
|
|
24345
|
+
userMessage(
|
|
24346
|
+
substitutePlaceholders(leadbay_build_campaign, {
|
|
24347
|
+
audience_block: args.audience ? `Target audience: **${args.audience}** \u2014 if my active lens doesn't already cover it, set it up first (confirm before switching lenses).` : "Use my active Leadbay lens as the audience.",
|
|
24348
|
+
campaign_name_paren: args.campaign_name ? ` named **${args.campaign_name}**` : ""
|
|
24349
|
+
})
|
|
24350
|
+
)
|
|
24351
|
+
]
|
|
24352
|
+
},
|
|
23379
24353
|
{
|
|
23380
24354
|
name: "leadbay_setup_team_prospecting",
|
|
23381
24355
|
description: PROMPT_META.leadbay_setup_team_prospecting.short_description,
|
|
@@ -25301,11 +26275,11 @@ async function uninstallFromClaudeCode() {
|
|
|
25301
26275
|
// installer/install-json-config.ts
|
|
25302
26276
|
async function installInJsonConfig(configPath, token, region, includeWrite, telemetryEnabled, localBinPath) {
|
|
25303
26277
|
try {
|
|
25304
|
-
const { readFileSync: readFileSync3, writeFileSync, existsSync:
|
|
26278
|
+
const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync4, mkdirSync, statSync } = await import("fs");
|
|
25305
26279
|
const { dirname: dirname3 } = await import("path");
|
|
25306
26280
|
let parsed = {};
|
|
25307
26281
|
let preserved = {};
|
|
25308
|
-
const existed =
|
|
26282
|
+
const existed = existsSync4(configPath);
|
|
25309
26283
|
if (existed) {
|
|
25310
26284
|
const raw = readFileSync3(configPath, "utf8");
|
|
25311
26285
|
try {
|
|
@@ -25355,8 +26329,8 @@ function stripJsonMcpEntry(existing) {
|
|
|
25355
26329
|
}
|
|
25356
26330
|
async function uninstallFromJsonConfig(configPath) {
|
|
25357
26331
|
try {
|
|
25358
|
-
const { existsSync:
|
|
25359
|
-
if (!
|
|
26332
|
+
const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync } = await import("fs");
|
|
26333
|
+
if (!existsSync4(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
|
|
25360
26334
|
const existing = readFileSync3(configPath, "utf8");
|
|
25361
26335
|
const { content, changed } = stripJsonMcpEntry(existing);
|
|
25362
26336
|
if (!changed) return { ok: true, message: "leadbay entry not present" };
|
|
@@ -25439,10 +26413,10 @@ function stripShellExportBlock(existing) {
|
|
|
25439
26413
|
}
|
|
25440
26414
|
async function installInCodexConfig(configPath, includeWrite, telemetryEnabled, localBinPath) {
|
|
25441
26415
|
try {
|
|
25442
|
-
const { readFileSync: readFileSync3, writeFileSync, existsSync:
|
|
26416
|
+
const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync4, mkdirSync, statSync, renameSync, chmodSync } = await import("fs");
|
|
25443
26417
|
const { dirname: dirname3 } = await import("path");
|
|
25444
26418
|
let existing = "";
|
|
25445
|
-
const existed =
|
|
26419
|
+
const existed = existsSync4(configPath);
|
|
25446
26420
|
if (existed) {
|
|
25447
26421
|
existing = readFileSync3(configPath, "utf8");
|
|
25448
26422
|
} else {
|
|
@@ -25488,15 +26462,15 @@ async function appendShellExports(token, region, includeWrite, telemetryEnabled)
|
|
|
25488
26462
|
}
|
|
25489
26463
|
return { ok: true, message: "env exported with setx; restart Codex/terminal" };
|
|
25490
26464
|
}
|
|
25491
|
-
const { existsSync:
|
|
26465
|
+
const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
|
|
25492
26466
|
const os = await import("os");
|
|
25493
26467
|
const home = os.homedir();
|
|
25494
|
-
const preferred = [`${home}/.zshrc`, `${home}/.bashrc`].filter((path) =>
|
|
26468
|
+
const preferred = [`${home}/.zshrc`, `${home}/.bashrc`].filter((path) => existsSync4(path));
|
|
25495
26469
|
const paths = preferred.length ? preferred : [`${home}/.profile`];
|
|
25496
26470
|
const block = buildShellExportBlock(token, region, includeWrite, telemetryEnabled);
|
|
25497
26471
|
const updated = [];
|
|
25498
26472
|
for (const path of paths) {
|
|
25499
|
-
const existing =
|
|
26473
|
+
const existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
|
|
25500
26474
|
const merged = mergeShellExportBlock(existing, block);
|
|
25501
26475
|
if (!merged.changed) continue;
|
|
25502
26476
|
const tmp = `${path}.leadbay.tmp`;
|
|
@@ -25514,8 +26488,8 @@ async function appendShellExports(token, region, includeWrite, telemetryEnabled)
|
|
|
25514
26488
|
}
|
|
25515
26489
|
async function uninstallFromCodexConfig(configPath) {
|
|
25516
26490
|
try {
|
|
25517
|
-
const { existsSync:
|
|
25518
|
-
if (!
|
|
26491
|
+
const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync } = await import("fs");
|
|
26492
|
+
if (!existsSync4(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
|
|
25519
26493
|
const existing = readFileSync3(configPath, "utf8");
|
|
25520
26494
|
const { content, changed } = stripCodexBlock(existing);
|
|
25521
26495
|
if (!changed) return { ok: true, message: "leadbay block not present" };
|
|
@@ -25527,11 +26501,11 @@ async function uninstallFromCodexConfig(configPath) {
|
|
|
25527
26501
|
}
|
|
25528
26502
|
async function uninstallShellExports() {
|
|
25529
26503
|
try {
|
|
25530
|
-
const { existsSync:
|
|
26504
|
+
const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
|
|
25531
26505
|
const os = await import("os");
|
|
25532
26506
|
const home = os.homedir();
|
|
25533
26507
|
const candidates = [`${home}/.zshrc`, `${home}/.bashrc`, `${home}/.profile`].filter(
|
|
25534
|
-
(p) =>
|
|
26508
|
+
(p) => existsSync4(p)
|
|
25535
26509
|
);
|
|
25536
26510
|
const updated = [];
|
|
25537
26511
|
for (const p of candidates) {
|
|
@@ -25556,17 +26530,17 @@ async function uninstallShellExports() {
|
|
|
25556
26530
|
var DXT_EXTENSION_ID = "local.dxt.leadbay.leadbay";
|
|
25557
26531
|
async function removeDxtExtension(claudeSupportDir) {
|
|
25558
26532
|
try {
|
|
25559
|
-
const { existsSync:
|
|
26533
|
+
const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync, rmSync } = await import("fs");
|
|
25560
26534
|
const { join: join4 } = await import("path");
|
|
25561
26535
|
const extensionDir = join4(claudeSupportDir, "Claude Extensions", DXT_EXTENSION_ID);
|
|
25562
26536
|
const registryPath = join4(claudeSupportDir, "extensions-installations.json");
|
|
25563
26537
|
let removedDir = false;
|
|
25564
26538
|
let removedEntry = false;
|
|
25565
|
-
if (
|
|
26539
|
+
if (existsSync4(extensionDir)) {
|
|
25566
26540
|
rmSync(extensionDir, { recursive: true, force: true });
|
|
25567
26541
|
removedDir = true;
|
|
25568
26542
|
}
|
|
25569
|
-
if (
|
|
26543
|
+
if (existsSync4(registryPath)) {
|
|
25570
26544
|
try {
|
|
25571
26545
|
const raw = readFileSync3(registryPath, "utf8");
|
|
25572
26546
|
const parsed = JSON.parse(raw);
|
|
@@ -25962,7 +26936,7 @@ import { createHash as createHash5, randomBytes } from "crypto";
|
|
|
25962
26936
|
import { createServer } from "http";
|
|
25963
26937
|
import { request as httpsRequestRaw } from "https";
|
|
25964
26938
|
import { spawn as spawn3 } from "child_process";
|
|
25965
|
-
import { readdirSync as readdirSync2 } from "fs";
|
|
26939
|
+
import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
|
|
25966
26940
|
var LEADBAY_LOOPBACK_PORTS = [51789, 51790, 51791, 51792];
|
|
25967
26941
|
var BrowserOpenFailedError = class extends Error {
|
|
25968
26942
|
authorizeUrl;
|
|
@@ -26282,7 +27256,19 @@ function browserOpenCandidates(url) {
|
|
|
26282
27256
|
function browserLaunchEnv(debug) {
|
|
26283
27257
|
const env = { ...process.env };
|
|
26284
27258
|
if (process.platform !== "linux") return env;
|
|
26285
|
-
|
|
27259
|
+
let runtimeDir = env.XDG_RUNTIME_DIR;
|
|
27260
|
+
if (!runtimeDir || !existsSync3(runtimeDir)) {
|
|
27261
|
+
try {
|
|
27262
|
+
const uid = process.getuid?.();
|
|
27263
|
+
const candidate = uid !== void 0 ? `/run/user/${uid}` : void 0;
|
|
27264
|
+
if (candidate && existsSync3(candidate)) {
|
|
27265
|
+
runtimeDir = candidate;
|
|
27266
|
+
env.XDG_RUNTIME_DIR = candidate;
|
|
27267
|
+
debug?.(`browserLaunchEnv: injected XDG_RUNTIME_DIR=${candidate}`);
|
|
27268
|
+
}
|
|
27269
|
+
} catch {
|
|
27270
|
+
}
|
|
27271
|
+
}
|
|
26286
27272
|
if (!env.WAYLAND_DISPLAY && runtimeDir) {
|
|
26287
27273
|
try {
|
|
26288
27274
|
const sock = readdirSync2(runtimeDir).find((f) => /^wayland-\d+$/.test(f));
|
|
@@ -26293,6 +27279,13 @@ function browserLaunchEnv(debug) {
|
|
|
26293
27279
|
} catch {
|
|
26294
27280
|
}
|
|
26295
27281
|
}
|
|
27282
|
+
if (!env.DBUS_SESSION_BUS_ADDRESS && runtimeDir) {
|
|
27283
|
+
const busPath = `${runtimeDir}/bus`;
|
|
27284
|
+
if (existsSync3(busPath)) {
|
|
27285
|
+
env.DBUS_SESSION_BUS_ADDRESS = `unix:path=${busPath}`;
|
|
27286
|
+
debug?.(`browserLaunchEnv: injected DBUS_SESSION_BUS_ADDRESS=unix:path=${busPath}`);
|
|
27287
|
+
}
|
|
27288
|
+
}
|
|
26296
27289
|
if (!env.DISPLAY) {
|
|
26297
27290
|
try {
|
|
26298
27291
|
const x = readdirSync2("/tmp/.X11-unix").map((f) => f.match(/^X(\d+)$/)?.[1]).filter((n) => !!n).sort((a, b) => Number(a) - Number(b))[0];
|
|
@@ -26302,6 +27295,25 @@ function browserLaunchEnv(debug) {
|
|
|
26302
27295
|
}
|
|
26303
27296
|
debug?.(`browserLaunchEnv: injected DISPLAY=${env.DISPLAY}`);
|
|
26304
27297
|
}
|
|
27298
|
+
if (!env.XAUTHORITY) {
|
|
27299
|
+
try {
|
|
27300
|
+
let xauth;
|
|
27301
|
+
if (runtimeDir) {
|
|
27302
|
+
const cookie = readdirSync2(runtimeDir).find(
|
|
27303
|
+
(f) => /^\.mutter-Xwaylandauth\./.test(f)
|
|
27304
|
+
);
|
|
27305
|
+
if (cookie) xauth = `${runtimeDir}/${cookie}`;
|
|
27306
|
+
}
|
|
27307
|
+
if (!xauth && env.HOME && existsSync3(`${env.HOME}/.Xauthority`)) {
|
|
27308
|
+
xauth = `${env.HOME}/.Xauthority`;
|
|
27309
|
+
}
|
|
27310
|
+
if (xauth) {
|
|
27311
|
+
env.XAUTHORITY = xauth;
|
|
27312
|
+
debug?.(`browserLaunchEnv: injected XAUTHORITY=${xauth}`);
|
|
27313
|
+
}
|
|
27314
|
+
} catch {
|
|
27315
|
+
}
|
|
27316
|
+
}
|
|
26305
27317
|
return env;
|
|
26306
27318
|
}
|
|
26307
27319
|
async function openInBrowser(url, debug) {
|
|
@@ -26424,7 +27436,7 @@ var OAUTH_BASE_URLS = {
|
|
|
26424
27436
|
fr: "https://staging.api.leadbay.app"
|
|
26425
27437
|
}
|
|
26426
27438
|
};
|
|
26427
|
-
var VERSION = "0.
|
|
27439
|
+
var VERSION = "0.23.1";
|
|
26428
27440
|
var HELP = `
|
|
26429
27441
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
26430
27442
|
|
|
@@ -26524,9 +27536,9 @@ function exitWithTokenError() {
|
|
|
26524
27536
|
function hydrateEnvFromCredentialsFile() {
|
|
26525
27537
|
if (process.env.LEADBAY_TOKEN) return false;
|
|
26526
27538
|
try {
|
|
26527
|
-
const { existsSync:
|
|
27539
|
+
const { existsSync: existsSync4, readFileSync: readFileSync3 } = require_("node:fs");
|
|
26528
27540
|
const { path } = resolveOAuthBootstrapCredentialsPath();
|
|
26529
|
-
if (!
|
|
27541
|
+
if (!existsSync4(path)) return false;
|
|
26530
27542
|
const parsed = JSON.parse(readFileSync3(path, "utf8"));
|
|
26531
27543
|
const env = parsed?.mcpServers?.leadbay?.env;
|
|
26532
27544
|
if (!env || typeof env !== "object") return false;
|
|
@@ -27120,8 +28132,8 @@ Restart your MCP client to pick up the new server.
|
|
|
27120
28132
|
usingLegacyPath = resolved.legacy;
|
|
27121
28133
|
}
|
|
27122
28134
|
try {
|
|
27123
|
-
const { existsSync:
|
|
27124
|
-
if (
|
|
28135
|
+
const { existsSync: existsSync4, readFileSync: readFileSync3 } = await import("fs");
|
|
28136
|
+
if (existsSync4(targetPath) && !force) {
|
|
27125
28137
|
let existing;
|
|
27126
28138
|
try {
|
|
27127
28139
|
existing = JSON.parse(readFileSync3(targetPath, "utf8"));
|