@leadbay/mcp 0.23.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/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/tools/create-custom-field.js
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/tools/create-custom-field.js"() {
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 ?? config?.urlTemplate;
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({
@@ -16032,6 +16434,326 @@ var init_research_lead_by_name_fuzzy = __esm({
16032
16434
  }
16033
16435
  });
16034
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();
16667
+ init_tool_descriptions_generated();
16668
+ getLeadCustomFields = {
16669
+ name: "leadbay_get_lead_custom_fields",
16670
+ annotations: {
16671
+ title: "Read a lead's custom-field values",
16672
+ readOnlyHint: true,
16673
+ destructiveHint: false,
16674
+ idempotentHint: true,
16675
+ openWorldHint: true
16676
+ },
16677
+ description: leadbay_get_lead_custom_fields,
16678
+ inputSchema: {
16679
+ type: "object",
16680
+ properties: {
16681
+ leadId: { type: "string", description: "Lead UUID (required)" },
16682
+ lensId: {
16683
+ type: "number",
16684
+ description: "Lens id (escape hatch \u2014 normally omit; auto-resolves to the active lens)"
16685
+ }
16686
+ },
16687
+ required: ["leadId"],
16688
+ additionalProperties: false
16689
+ },
16690
+ outputSchema: {
16691
+ type: "object",
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"]
16708
+ },
16709
+ execute: async (client, params, ctx) => {
16710
+ const lensId = params.lensId ?? await client.resolveDefaultLens();
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}`);
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 } : {}
16751
+ }, ctx);
16752
+ }
16753
+ };
16754
+ }
16755
+ });
16756
+
16035
16757
  // ../core/dist/composite/account-history.js
16036
16758
  var accountHistory;
16037
16759
  var init_account_history = __esm({
@@ -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,
@@ -25525,11 +26275,11 @@ async function uninstallFromClaudeCode() {
25525
26275
  // installer/install-json-config.ts
25526
26276
  async function installInJsonConfig(configPath, token, region, includeWrite, telemetryEnabled, localBinPath) {
25527
26277
  try {
25528
- const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync3, mkdirSync, statSync } = await import("fs");
26278
+ const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync4, mkdirSync, statSync } = await import("fs");
25529
26279
  const { dirname: dirname3 } = await import("path");
25530
26280
  let parsed = {};
25531
26281
  let preserved = {};
25532
- const existed = existsSync3(configPath);
26282
+ const existed = existsSync4(configPath);
25533
26283
  if (existed) {
25534
26284
  const raw = readFileSync3(configPath, "utf8");
25535
26285
  try {
@@ -25579,8 +26329,8 @@ function stripJsonMcpEntry(existing) {
25579
26329
  }
25580
26330
  async function uninstallFromJsonConfig(configPath) {
25581
26331
  try {
25582
- const { existsSync: existsSync3, readFileSync: readFileSync3, writeFileSync } = await import("fs");
25583
- if (!existsSync3(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
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" };
25584
26334
  const existing = readFileSync3(configPath, "utf8");
25585
26335
  const { content, changed } = stripJsonMcpEntry(existing);
25586
26336
  if (!changed) return { ok: true, message: "leadbay entry not present" };
@@ -25663,10 +26413,10 @@ function stripShellExportBlock(existing) {
25663
26413
  }
25664
26414
  async function installInCodexConfig(configPath, includeWrite, telemetryEnabled, localBinPath) {
25665
26415
  try {
25666
- const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync3, mkdirSync, statSync, renameSync, chmodSync } = await import("fs");
26416
+ const { readFileSync: readFileSync3, writeFileSync, existsSync: existsSync4, mkdirSync, statSync, renameSync, chmodSync } = await import("fs");
25667
26417
  const { dirname: dirname3 } = await import("path");
25668
26418
  let existing = "";
25669
- const existed = existsSync3(configPath);
26419
+ const existed = existsSync4(configPath);
25670
26420
  if (existed) {
25671
26421
  existing = readFileSync3(configPath, "utf8");
25672
26422
  } else {
@@ -25712,15 +26462,15 @@ async function appendShellExports(token, region, includeWrite, telemetryEnabled)
25712
26462
  }
25713
26463
  return { ok: true, message: "env exported with setx; restart Codex/terminal" };
25714
26464
  }
25715
- const { existsSync: existsSync3, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
26465
+ const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
25716
26466
  const os = await import("os");
25717
26467
  const home = os.homedir();
25718
- const preferred = [`${home}/.zshrc`, `${home}/.bashrc`].filter((path) => existsSync3(path));
26468
+ const preferred = [`${home}/.zshrc`, `${home}/.bashrc`].filter((path) => existsSync4(path));
25719
26469
  const paths = preferred.length ? preferred : [`${home}/.profile`];
25720
26470
  const block = buildShellExportBlock(token, region, includeWrite, telemetryEnabled);
25721
26471
  const updated = [];
25722
26472
  for (const path of paths) {
25723
- const existing = existsSync3(path) ? readFileSync3(path, "utf8") : "";
26473
+ const existing = existsSync4(path) ? readFileSync3(path, "utf8") : "";
25724
26474
  const merged = mergeShellExportBlock(existing, block);
25725
26475
  if (!merged.changed) continue;
25726
26476
  const tmp = `${path}.leadbay.tmp`;
@@ -25738,8 +26488,8 @@ async function appendShellExports(token, region, includeWrite, telemetryEnabled)
25738
26488
  }
25739
26489
  async function uninstallFromCodexConfig(configPath) {
25740
26490
  try {
25741
- const { existsSync: existsSync3, readFileSync: readFileSync3, writeFileSync } = await import("fs");
25742
- if (!existsSync3(configPath)) return { ok: true, message: "config not found \u2014 nothing to do" };
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" };
25743
26493
  const existing = readFileSync3(configPath, "utf8");
25744
26494
  const { content, changed } = stripCodexBlock(existing);
25745
26495
  if (!changed) return { ok: true, message: "leadbay block not present" };
@@ -25751,11 +26501,11 @@ async function uninstallFromCodexConfig(configPath) {
25751
26501
  }
25752
26502
  async function uninstallShellExports() {
25753
26503
  try {
25754
- const { existsSync: existsSync3, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
26504
+ const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync, renameSync } = await import("fs");
25755
26505
  const os = await import("os");
25756
26506
  const home = os.homedir();
25757
26507
  const candidates = [`${home}/.zshrc`, `${home}/.bashrc`, `${home}/.profile`].filter(
25758
- (p) => existsSync3(p)
26508
+ (p) => existsSync4(p)
25759
26509
  );
25760
26510
  const updated = [];
25761
26511
  for (const p of candidates) {
@@ -25780,17 +26530,17 @@ async function uninstallShellExports() {
25780
26530
  var DXT_EXTENSION_ID = "local.dxt.leadbay.leadbay";
25781
26531
  async function removeDxtExtension(claudeSupportDir) {
25782
26532
  try {
25783
- const { existsSync: existsSync3, readFileSync: readFileSync3, writeFileSync, rmSync } = await import("fs");
26533
+ const { existsSync: existsSync4, readFileSync: readFileSync3, writeFileSync, rmSync } = await import("fs");
25784
26534
  const { join: join4 } = await import("path");
25785
26535
  const extensionDir = join4(claudeSupportDir, "Claude Extensions", DXT_EXTENSION_ID);
25786
26536
  const registryPath = join4(claudeSupportDir, "extensions-installations.json");
25787
26537
  let removedDir = false;
25788
26538
  let removedEntry = false;
25789
- if (existsSync3(extensionDir)) {
26539
+ if (existsSync4(extensionDir)) {
25790
26540
  rmSync(extensionDir, { recursive: true, force: true });
25791
26541
  removedDir = true;
25792
26542
  }
25793
- if (existsSync3(registryPath)) {
26543
+ if (existsSync4(registryPath)) {
25794
26544
  try {
25795
26545
  const raw = readFileSync3(registryPath, "utf8");
25796
26546
  const parsed = JSON.parse(raw);
@@ -26186,7 +26936,7 @@ import { createHash as createHash5, randomBytes } from "crypto";
26186
26936
  import { createServer } from "http";
26187
26937
  import { request as httpsRequestRaw } from "https";
26188
26938
  import { spawn as spawn3 } from "child_process";
26189
- import { readdirSync as readdirSync2 } from "fs";
26939
+ import { readdirSync as readdirSync2, existsSync as existsSync3 } from "fs";
26190
26940
  var LEADBAY_LOOPBACK_PORTS = [51789, 51790, 51791, 51792];
26191
26941
  var BrowserOpenFailedError = class extends Error {
26192
26942
  authorizeUrl;
@@ -26506,7 +27256,19 @@ function browserOpenCandidates(url) {
26506
27256
  function browserLaunchEnv(debug) {
26507
27257
  const env = { ...process.env };
26508
27258
  if (process.platform !== "linux") return env;
26509
- const runtimeDir = env.XDG_RUNTIME_DIR;
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
+ }
26510
27272
  if (!env.WAYLAND_DISPLAY && runtimeDir) {
26511
27273
  try {
26512
27274
  const sock = readdirSync2(runtimeDir).find((f) => /^wayland-\d+$/.test(f));
@@ -26517,6 +27279,13 @@ function browserLaunchEnv(debug) {
26517
27279
  } catch {
26518
27280
  }
26519
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
+ }
26520
27289
  if (!env.DISPLAY) {
26521
27290
  try {
26522
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];
@@ -26526,6 +27295,25 @@ function browserLaunchEnv(debug) {
26526
27295
  }
26527
27296
  debug?.(`browserLaunchEnv: injected DISPLAY=${env.DISPLAY}`);
26528
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
+ }
26529
27317
  return env;
26530
27318
  }
26531
27319
  async function openInBrowser(url, debug) {
@@ -26648,7 +27436,7 @@ var OAUTH_BASE_URLS = {
26648
27436
  fr: "https://staging.api.leadbay.app"
26649
27437
  }
26650
27438
  };
26651
- var VERSION = "0.23.0";
27439
+ var VERSION = "0.23.1";
26652
27440
  var HELP = `
26653
27441
  leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
26654
27442
 
@@ -26748,9 +27536,9 @@ function exitWithTokenError() {
26748
27536
  function hydrateEnvFromCredentialsFile() {
26749
27537
  if (process.env.LEADBAY_TOKEN) return false;
26750
27538
  try {
26751
- const { existsSync: existsSync3, readFileSync: readFileSync3 } = require_("node:fs");
27539
+ const { existsSync: existsSync4, readFileSync: readFileSync3 } = require_("node:fs");
26752
27540
  const { path } = resolveOAuthBootstrapCredentialsPath();
26753
- if (!existsSync3(path)) return false;
27541
+ if (!existsSync4(path)) return false;
26754
27542
  const parsed = JSON.parse(readFileSync3(path, "utf8"));
26755
27543
  const env = parsed?.mcpServers?.leadbay?.env;
26756
27544
  if (!env || typeof env !== "object") return false;
@@ -27344,8 +28132,8 @@ Restart your MCP client to pick up the new server.
27344
28132
  usingLegacyPath = resolved.legacy;
27345
28133
  }
27346
28134
  try {
27347
- const { existsSync: existsSync3, readFileSync: readFileSync3 } = await import("fs");
27348
- if (existsSync3(targetPath) && !force) {
28135
+ const { existsSync: existsSync4, readFileSync: readFileSync3 } = await import("fs");
28136
+ if (existsSync4(targetPath) && !force) {
27349
28137
  let existing;
27350
28138
  try {
27351
28139
  existing = JSON.parse(readFileSync3(targetPath, "utf8"));