@leadbay/mcp 0.18.2 → 0.19.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 +14 -0
- package/dist/bin.js +1320 -340
- package/dist/http-server.js +1259 -338
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -5134,6 +5134,7 @@ var init_composite_file_names = __esm({
|
|
|
5134
5134
|
"leadbay_research_lead_by_id",
|
|
5135
5135
|
"leadbay_research_lead_by_name_fuzzy",
|
|
5136
5136
|
"leadbay_resolve_import_rows",
|
|
5137
|
+
"leadbay_scan_portfolio_signals",
|
|
5137
5138
|
"leadbay_seed_candidates",
|
|
5138
5139
|
"leadbay_tour_plan"
|
|
5139
5140
|
]);
|
|
@@ -5439,7 +5440,7 @@ var init_notifications = __esm({
|
|
|
5439
5440
|
});
|
|
5440
5441
|
|
|
5441
5442
|
// ../core/dist/tool-descriptions.generated.js
|
|
5442
|
-
var leadbay_account_history, leadbay_account_status, leadbay_acknowledge_notification, 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_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_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_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_seed_candidates, leadbay_select_leads, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_user_prompt, leadbay_tour_plan, leadbay_update_lens, leadbay_update_lens_filter;
|
|
5443
|
+
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_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_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_user_prompt, leadbay_tour_plan, leadbay_unpin_contact, leadbay_update_contact, leadbay_update_lens, leadbay_update_lens_filter;
|
|
5443
5444
|
var init_tool_descriptions_generated = __esm({
|
|
5444
5445
|
"../core/dist/tool-descriptions.generated.js"() {
|
|
5445
5446
|
"use strict";
|
|
@@ -5673,6 +5674,45 @@ WHEN TO USE: immediately after you finish reviewing / revising in response to a
|
|
|
5673
5674
|
WHEN NOT TO USE: before doing the revision work; for general "mark all read" sweeps (call once per notification id you've actually consumed).
|
|
5674
5675
|
|
|
5675
5676
|
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\`.
|
|
5677
|
+
`;
|
|
5678
|
+
leadbay_add_contact = `## WHEN TO USE
|
|
5679
|
+
|
|
5680
|
+
Trigger phrases: "add a contact to this company", "add this person to <company>", "create a contact from this LinkedIn URL", "this company has no contacts \u2014 add one", "I found someone on LinkedIn, add them to <lead>".
|
|
5681
|
+
|
|
5682
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
5683
|
+
|
|
5684
|
+
Do NOT use for: "import these companies / a CSV of leads and qualify them" \u2192 \`leadbay_import_and_qualify\`; "get email/phone for a contact already on the company" \u2192 \`leadbay_enrich_titles\`; "remove / delete this contact" \u2192 \`leadbay_remove_contact\`.
|
|
5685
|
+
|
|
5686
|
+
Prefer when: user wants to attach ONE known person to an already-identified company \u2014 pass the company's \`lead_id\` plus the person's name (+ optional linkedin_page/title/email/phone)
|
|
5687
|
+
|
|
5688
|
+
Examples that SHOULD invoke this tool:
|
|
5689
|
+
- "Acme has no suggested contacts \u2014 add Jane Doe, VP Eng, here's her LinkedIn."
|
|
5690
|
+
- "Add this person I found on LinkedIn to that company."
|
|
5691
|
+
- "Create a contact for John Smith, CFO, on this lead."
|
|
5692
|
+
|
|
5693
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
5694
|
+
- "Import these 40 domains from my CRM and qualify them."
|
|
5695
|
+
- "Get me the email for the contact already on this company."
|
|
5696
|
+
- "Remove that contact, it's the wrong person."
|
|
5697
|
+
|
|
5698
|
+
## RENDER (quick)
|
|
5699
|
+
|
|
5700
|
+
One-line confirmation: the contact's name + title now sits on the company.
|
|
5701
|
+
No table. If the contact has no email/phone yet, note it can be enriched.
|
|
5702
|
+
|
|
5703
|
+
---
|
|
5704
|
+
|
|
5705
|
+
Add a single contact (a person) to a known company \u2014 the in-conversation "create a contact" path. Use it when a rep has found someone (often just a LinkedIn URL) and wants them on an already-identified Leadbay company without leaving the chat.
|
|
5706
|
+
|
|
5707
|
+
Pass the parent company's \`lead_id\` plus the person's \`first_name\` + \`last_name\`. Everything else is optional: \`job_title\`, \`linkedin_page\`, \`email\`, \`phone_number\`.
|
|
5708
|
+
|
|
5709
|
+
Backend: \`POST /leads/{lead_id}/contacts\` \u2192 returns the created contact with its new \`id\`. This is the same direct endpoint the Leadbay web UI uses \u2014 one call, no import/qualify quota. (Distinct from \`leadbay_import_and_qualify\`, which is for importing *lists of companies*, not attaching a single person.)
|
|
5710
|
+
|
|
5711
|
+
The created contact starts unenriched \u2014 if it has no email/phone, enrich it via \`leadbay_enrich_titles\`. The undo is \`leadbay_remove_contact\` (pass the returned \`contact.id\`).
|
|
5712
|
+
|
|
5713
|
+
Returns \`{ added: true, lead_id, contact: { id, first_name, last_name, job_title, linkedin_page, email, phone_number, \u2026 } }\`.
|
|
5714
|
+
|
|
5715
|
+
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw).
|
|
5676
5716
|
`;
|
|
5677
5717
|
leadbay_add_leads_to_campaign = `## WHEN TO USE
|
|
5678
5718
|
|
|
@@ -5847,29 +5887,23 @@ Do not enumerate the affected leads \u2014 that's the job of \`leadbay_pull_lead
|
|
|
5847
5887
|
|
|
5848
5888
|
---
|
|
5849
5889
|
|
|
5890
|
+
---
|
|
5891
|
+
|
|
5850
5892
|
## NEXT STEPS \u2014 after kicking off bulk qualification
|
|
5851
5893
|
|
|
5852
|
-
**
|
|
5894
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
5853
5895
|
|
|
5854
|
-
|
|
5896
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
5855
5897
|
|
|
5856
|
-
|
|
5857
|
-
|
|
5858
|
-
|
|
5859
|
-
question: "What next?",
|
|
5860
|
-
type: "single_select",
|
|
5861
|
-
options: [
|
|
5862
|
-
"<Suggest column from row 1>",
|
|
5863
|
-
"<Suggest column from row 2>",
|
|
5864
|
-
"<Suggest column from row 3>"
|
|
5865
|
-
]
|
|
5866
|
-
}]
|
|
5867
|
-
})
|
|
5868
|
-
\`\`\`
|
|
5898
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
5899
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
5900
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
5869
5901
|
|
|
5870
|
-
|
|
5902
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
5903
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
5904
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
5871
5905
|
|
|
5872
|
-
|
|
5906
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
5873
5907
|
|
|
5874
5908
|
---
|
|
5875
5909
|
|
|
@@ -6001,7 +6035,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
|
|
|
6001
6035
|
|---|---|---|
|
|
6002
6036
|
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
6003
6037
|
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
6004
|
-
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen |
|
|
6038
|
+
| \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
|
|
6005
6039
|
|
|
6006
6040
|
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
6007
6041
|
|
|
@@ -6093,6 +6127,8 @@ WHEN TO USE: after \`leadbay_list_campaigns\` (or when the user named a specific
|
|
|
6093
6127
|
WHEN NOT TO USE: for cross-campaign pulse (use \`leadbay_list_campaigns\`); to drill into a lead's full timeline (use \`leadbay_get_lead_activities\` or \`leadbay_research_lead_by_id\`); to log outreach (\`leadbay_report_outreach\`).
|
|
6094
6128
|
|
|
6095
6129
|
**Response**: \`{items, pagination, summary, _meta}\`. Use the \`summary\` for the one-line headline; use \`items\` for the per-lead table.
|
|
6130
|
+
|
|
6131
|
+
---
|
|
6096
6132
|
`;
|
|
6097
6133
|
leadbay_clear_selection = `Clear the user's transient selection.
|
|
6098
6134
|
|
|
@@ -6300,7 +6336,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
|
|
|
6300
6336
|
|---|---|---|
|
|
6301
6337
|
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
6302
6338
|
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
6303
|
-
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen |
|
|
6339
|
+
| \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
|
|
6304
6340
|
|
|
6305
6341
|
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
6306
6342
|
|
|
@@ -6337,7 +6373,7 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
|
6337
6373
|
|
|
6338
6374
|
\`queued\` \u2192 \u2705 "Queued <N> extra leads on lens <id>. Pull in ~30s." Do NOT
|
|
6339
6375
|
list \`accepted_seeds\`; they're internal.
|
|
6340
|
-
\`quota_exceeded\` \u2192 render three options via \`ask_user_input_v0\` (smaller
|
|
6376
|
+
\`quota_exceeded\` \u2192 render three options via your host's choice widget (\`ask_user_input_v0\` or \`AskUserQuestion\`) (smaller
|
|
6341
6377
|
count / wait until reset / upgrade).
|
|
6342
6378
|
\`refresh_in_progress\` \u2192 "lens is filling, retry in a minute".
|
|
6343
6379
|
\`no_valid_seeds\` \u2192 silently re-call \`leadbay_seed_candidates\`, retry once.
|
|
@@ -6355,7 +6391,7 @@ Queue an additive extra-refill on a lens \u2014 more leads on the same criteria,
|
|
|
6355
6391
|
**Status envelope (translated from raw API errors so the agent routes on \`status\`).**
|
|
6356
6392
|
|
|
6357
6393
|
- \`status: "queued"\` \u2014 fill is queued. \`accepted_seeds\` lists IDs that passed validation. NEXT STEP: call \`leadbay_pull_leads\` in ~30s.
|
|
6358
|
-
- \`status: "quota_exceeded"\` \u2014 daily LENS_EXTRA_REFILL hit. Response carries \`quota: {used_today, resets_at}\` + a \`message\` to surface. **Render three options via \`ask_user_input_v0
|
|
6394
|
+
- \`status: "quota_exceeded"\` \u2014 daily LENS_EXTRA_REFILL hit. Response carries \`quota: {used_today, resets_at}\` + a \`message\` to surface. **Render three options via your host's choice widget (\`ask_user_input_v0\` or \`AskUserQuestion\`)**: (1) smaller \`extra_count\`, (2) wait until \`resets_at\`, (3) upgrade plan (TIER1=150, TIER2=1000). Do NOT silently retry.
|
|
6359
6395
|
- \`status: "refresh_in_progress"\` \u2014 a refresh or extra-refill is already running. Tell the user to wait and call \`leadbay_pull_leads\` in ~30s.
|
|
6360
6396
|
- \`status: "no_valid_seeds"\` \u2014 seeds went stale. Silently re-call \`leadbay_seed_candidates\` and retry once; only surface to the user if the second attempt also fails.
|
|
6361
6397
|
|
|
@@ -6370,27 +6406,19 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
|
|
|
6370
6406
|
|
|
6371
6407
|
## NEXT STEPS \u2014 after \`leadbay_extend_lens\`
|
|
6372
6408
|
|
|
6373
|
-
**
|
|
6409
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
6374
6410
|
|
|
6375
|
-
|
|
6411
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
6376
6412
|
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
question: "What next?",
|
|
6381
|
-
type: "single_select",
|
|
6382
|
-
options: [
|
|
6383
|
-
"<Suggest column from row 1>",
|
|
6384
|
-
"<Suggest column from row 2>",
|
|
6385
|
-
"<Suggest column from row 3>"
|
|
6386
|
-
]
|
|
6387
|
-
}]
|
|
6388
|
-
})
|
|
6389
|
-
\`\`\`
|
|
6413
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
6414
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
6415
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
6390
6416
|
|
|
6391
|
-
|
|
6417
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
6418
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
6419
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
6392
6420
|
|
|
6393
|
-
|
|
6421
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
6394
6422
|
|
|
6395
6423
|
---
|
|
6396
6424
|
|
|
@@ -6533,7 +6561,7 @@ Never link a person's name to the company's LinkedIn page (and vice versa) \u201
|
|
|
6533
6561
|
|
|
6534
6562
|
Open with **one short intro sentence** in chat ("Five lead visits across NYC for your trip next week \u2014 three in Midtown, plus Long Island and one in NJ.") and then invoke the widget, then the chat-side list above. **No markdown table.**
|
|
6535
6563
|
|
|
6536
|
-
**After the widget renders, end the turn with the NEXT STEPS surface** \u2014 not with a prose question. See "GATE \u2014 PREFER BUILT-IN HOST WIDGETS" below: surface 2\u20134 mutually-exclusive moves via \`ask_user_input_v0\` if the host exposes it, else as a short bulleted list. "Want me to plot these on a map or jump to outreach for Atlas?" is exactly the prose pattern to AVOID \u2014 it's a \`single_select\` with two options.
|
|
6564
|
+
**After the widget renders, end the turn with the NEXT STEPS surface** \u2014 not with a prose question. See "GATE \u2014 PREFER BUILT-IN HOST WIDGETS" below: surface 2\u20134 mutually-exclusive moves via your host's choice widget (\`ask_user_input_v0\` or \`AskUserQuestion\`) if the host exposes it, else as a short bulleted list. "Want me to plot these on a map or jump to outreach for Atlas?" is exactly the prose pattern to AVOID \u2014 it's a \`single_select\` with two options.
|
|
6537
6565
|
|
|
6538
6566
|
## RENDER \u2014 fallback for hosts without \`places_map_display_v0\`
|
|
6539
6567
|
|
|
@@ -6564,7 +6592,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
|
|
|
6564
6592
|
|---|---|---|
|
|
6565
6593
|
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
6566
6594
|
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
6567
|
-
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen |
|
|
6595
|
+
| \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
|
|
6568
6596
|
|
|
6569
6597
|
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
6570
6598
|
|
|
@@ -6709,27 +6737,19 @@ Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_res
|
|
|
6709
6737
|
|
|
6710
6738
|
## NEXT STEPS \u2014 after an import
|
|
6711
6739
|
|
|
6712
|
-
**
|
|
6740
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
6713
6741
|
|
|
6714
|
-
|
|
6742
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
6715
6743
|
|
|
6716
|
-
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
question: "What next?",
|
|
6720
|
-
type: "single_select",
|
|
6721
|
-
options: [
|
|
6722
|
-
"<Suggest column from row 1>",
|
|
6723
|
-
"<Suggest column from row 2>",
|
|
6724
|
-
"<Suggest column from row 3>"
|
|
6725
|
-
]
|
|
6726
|
-
}]
|
|
6727
|
-
})
|
|
6728
|
-
\`\`\`
|
|
6744
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
6745
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
6746
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
6729
6747
|
|
|
6730
|
-
|
|
6748
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
6749
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
6750
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
6731
6751
|
|
|
6732
|
-
|
|
6752
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
6733
6753
|
|
|
6734
6754
|
---
|
|
6735
6755
|
|
|
@@ -6782,27 +6802,19 @@ Defer the full list of imported leads to \`leadbay_pull_leads\` or \`leadbay_res
|
|
|
6782
6802
|
|
|
6783
6803
|
## NEXT STEPS \u2014 after an import
|
|
6784
6804
|
|
|
6785
|
-
**
|
|
6805
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
6786
6806
|
|
|
6787
|
-
|
|
6807
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
6788
6808
|
|
|
6789
|
-
|
|
6790
|
-
|
|
6791
|
-
|
|
6792
|
-
question: "What next?",
|
|
6793
|
-
type: "single_select",
|
|
6794
|
-
options: [
|
|
6795
|
-
"<Suggest column from row 1>",
|
|
6796
|
-
"<Suggest column from row 2>",
|
|
6797
|
-
"<Suggest column from row 3>"
|
|
6798
|
-
]
|
|
6799
|
-
}]
|
|
6800
|
-
})
|
|
6801
|
-
\`\`\`
|
|
6809
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
6810
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
6811
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
6802
6812
|
|
|
6803
|
-
|
|
6813
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
6814
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
6815
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
6804
6816
|
|
|
6805
|
-
|
|
6817
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
6806
6818
|
|
|
6807
6819
|
---
|
|
6808
6820
|
|
|
@@ -6942,6 +6954,8 @@ WHEN TO USE: the user wants the cross-campaign pulse \u2014 what cohorts are in
|
|
|
6942
6954
|
WHEN NOT TO USE: for per-lead progression inside ONE campaign (use \`leadbay_campaign_progression\`); to create a campaign (\`leadbay_create_campaign\`); to add leads to one (\`leadbay_add_leads_to_campaign\`).
|
|
6943
6955
|
|
|
6944
6956
|
**Response**: \`{campaigns: CampaignWithStats[], _meta}\`. Sort by \`updated_at desc\` when rendering \u2014 recency is the manager's natural lens.
|
|
6957
|
+
|
|
6958
|
+
---
|
|
6945
6959
|
`;
|
|
6946
6960
|
leadbay_list_lenses = `List all available Leadbay lenses (saved lead-search configurations). Each lens defines a different target market or buyer segment. The lens with \`is_last_active=true\` is used by default for lead discovery.
|
|
6947
6961
|
|
|
@@ -7068,7 +7082,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
|
|
|
7068
7082
|
|---|---|---|
|
|
7069
7083
|
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
7070
7084
|
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
7071
|
-
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen |
|
|
7085
|
+
| \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
|
|
7072
7086
|
|
|
7073
7087
|
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
7074
7088
|
|
|
@@ -7107,27 +7121,19 @@ render an empty table.
|
|
|
7107
7121
|
|
|
7108
7122
|
## NEXT STEPS \u2014 after \`leadbay_my_lenses\`
|
|
7109
7123
|
|
|
7110
|
-
**
|
|
7124
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
7111
7125
|
|
|
7112
|
-
|
|
7126
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
7113
7127
|
|
|
7114
|
-
|
|
7115
|
-
|
|
7116
|
-
|
|
7117
|
-
question: "What next?",
|
|
7118
|
-
type: "single_select",
|
|
7119
|
-
options: [
|
|
7120
|
-
"<Suggest column from row 1>",
|
|
7121
|
-
"<Suggest column from row 2>",
|
|
7122
|
-
"<Suggest column from row 3>"
|
|
7123
|
-
]
|
|
7124
|
-
}]
|
|
7125
|
-
})
|
|
7126
|
-
\`\`\`
|
|
7128
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
7129
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
7130
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
7127
7131
|
|
|
7128
|
-
|
|
7132
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
7133
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
7134
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
7129
7135
|
|
|
7130
|
-
|
|
7136
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
7131
7137
|
|
|
7132
7138
|
---
|
|
7133
7139
|
|
|
@@ -7205,7 +7211,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
|
|
|
7205
7211
|
|---|---|---|
|
|
7206
7212
|
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
7207
7213
|
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
7208
|
-
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen |
|
|
7214
|
+
| \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
|
|
7209
7215
|
|
|
7210
7216
|
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
7211
7217
|
|
|
@@ -7220,27 +7226,19 @@ ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We
|
|
|
7220
7226
|
|
|
7221
7227
|
## NEXT STEPS \u2014 after \`leadbay_new_lens\`
|
|
7222
7228
|
|
|
7223
|
-
**
|
|
7229
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
7224
7230
|
|
|
7225
|
-
|
|
7231
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
7226
7232
|
|
|
7227
|
-
|
|
7228
|
-
|
|
7229
|
-
|
|
7230
|
-
question: "What next?",
|
|
7231
|
-
type: "single_select",
|
|
7232
|
-
options: [
|
|
7233
|
-
"<Suggest column from row 1>",
|
|
7234
|
-
"<Suggest column from row 2>",
|
|
7235
|
-
"<Suggest column from row 3>"
|
|
7236
|
-
]
|
|
7237
|
-
}]
|
|
7238
|
-
})
|
|
7239
|
-
\`\`\`
|
|
7233
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
7234
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
7235
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
7240
7236
|
|
|
7241
|
-
|
|
7237
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
7238
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
7239
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
7242
7240
|
|
|
7243
|
-
|
|
7241
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
7244
7242
|
|
|
7245
7243
|
---
|
|
7246
7244
|
|
|
@@ -7279,6 +7277,42 @@ WHEN TO USE: low-level.
|
|
|
7279
7277
|
WHEN NOT TO USE: from agent flow \u2014 use leadbay_answer_clarification.
|
|
7280
7278
|
|
|
7281
7279
|
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\`.
|
|
7280
|
+
`;
|
|
7281
|
+
leadbay_pin_contact = `## WHEN TO USE
|
|
7282
|
+
|
|
7283
|
+
Trigger phrases: "pin this contact", "mark this person as priority", "make this the main contact", "favourite this contact".
|
|
7284
|
+
|
|
7285
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
7286
|
+
|
|
7287
|
+
Do NOT use for: "unpin / remove the pin" \u2192 \`leadbay_unpin_contact\`; "add a contact to this company" \u2192 \`leadbay_add_contact\`; "remove / delete this contact" \u2192 \`leadbay_remove_contact\`.
|
|
7288
|
+
|
|
7289
|
+
Prefer when: user wants ONE person flagged as the priority on a company \u2014 pass that contact's own \`contact_id\`
|
|
7290
|
+
|
|
7291
|
+
Examples that SHOULD invoke this tool:
|
|
7292
|
+
- "Pin Jane Doe as the main contact on this company."
|
|
7293
|
+
- "Mark this person as the priority contact."
|
|
7294
|
+
- "Favourite that contact so it shows first."
|
|
7295
|
+
|
|
7296
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
7297
|
+
- "Add a contact to this company."
|
|
7298
|
+
- "Remove that contact, wrong person."
|
|
7299
|
+
- "Stop showing me this lead."
|
|
7300
|
+
|
|
7301
|
+
## RENDER (quick)
|
|
7302
|
+
|
|
7303
|
+
One-line confirmation that the named contact (or id) is now pinned. No table.
|
|
7304
|
+
|
|
7305
|
+
---
|
|
7306
|
+
|
|
7307
|
+
Pin a single contact on a company so it surfaces first as a priority / favourite. Use when the user wants to flag a specific person as the one to focus on.
|
|
7308
|
+
|
|
7309
|
+
Pass the contact's **own** \`contact_id\` (the \`id\` field on a contact object from \`leadbay_research_lead_by_id\` or a contacts list) \u2014 **not** the parent lead id.
|
|
7310
|
+
|
|
7311
|
+
Backend: \`POST /contacts/{contact_id}/pin\` \u2192 204. Idempotent. The inverse is \`leadbay_unpin_contact\`.
|
|
7312
|
+
|
|
7313
|
+
Returns \`{ pinned: true, contact_id, action: "pinned" }\`.
|
|
7314
|
+
|
|
7315
|
+
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw).
|
|
7282
7316
|
`;
|
|
7283
7317
|
leadbay_prepare_outreach = `## WHEN TO USE
|
|
7284
7318
|
|
|
@@ -7309,7 +7343,7 @@ email. Do NOT paste the email body into chat prose alongside.
|
|
|
7309
7343
|
|
|
7310
7344
|
---
|
|
7311
7345
|
|
|
7312
|
-
Prepare a single-lead outreach brief: the full \`lead\` block (score, \`split_ai_summary\`, \`location\`, \`size\`, \`phone_numbers\`, \`website\`, \`description\`, \`social_urls\`, \`social_presence\`), the \`recommended_contact\` (always in the post-enrichment shape \u2014 \`contact_id\`, \`first_name\`, \`last_name\`, \`job_title\`, \`email\`, \`phone_number\`, \`linkedin_page\`, \`is_org_contact\` \u2014 with nulls where data isn't yet enriched), \`additional_contacts_count
|
|
7346
|
+
Prepare a single-lead outreach brief: the full \`lead\` block (score, \`split_ai_summary\`, \`location\`, \`size\`, \`phone_numbers\`, \`website\`, \`description\`, \`social_urls\`, \`social_presence\`), the \`recommended_contact\` (always in the post-enrichment shape \u2014 \`contact_id\`, \`first_name\`, \`last_name\`, \`job_title\`, \`email\`, \`phone_number\`, \`linkedin_page\`, \`is_org_contact\` \u2014 with nulls where data isn't yet enriched), \`additional_contacts_count\`, and an \`enrichment\` block describing async state.
|
|
7313
7347
|
|
|
7314
7348
|
Optionally trigger contact enrichment in-flight with \`enrich:true\`. Enrichment is async (~60s). **Self-polling pattern (no separate tool needed):** re-call \`leadbay_prepare_outreach(leadId)\` without \`enrich\`; check \`enrichment.complete\`. When \`complete: true\`, the recommended contact now carries \`email\` and/or \`phone_number\`.
|
|
7315
7349
|
|
|
@@ -7326,46 +7360,9 @@ WHEN NOT TO USE: across many leads \u2014 use leadbay_enrich_titles for bulk; fo
|
|
|
7326
7360
|
|
|
7327
7361
|
## RENDER \u2014 host-native message composer is the PRIMARY surface
|
|
7328
7362
|
|
|
7329
|
-
\`message_compose_v1\`
|
|
7363
|
+
Route every draft through \`message_compose_v1\` (Claude's email composer). Above it, emit ONE short markdown context paragraph: score callout + sector fit + linked contact name + bare phone/email pills. Do NOT paste the email body into chat prose alongside \u2014 the composer IS the visual.
|
|
7330
7364
|
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
**Single draft:**
|
|
7334
|
-
|
|
7335
|
-
\`\`\`
|
|
7336
|
-
message_compose_v1({
|
|
7337
|
-
kind: "email",
|
|
7338
|
-
summary_title: "Outreach to <Contact Name> at <Company>",
|
|
7339
|
-
variants: [{
|
|
7340
|
-
label: "Lead with the M&A signal",
|
|
7341
|
-
subject: "<one-line subject \u2014 references the angle>",
|
|
7342
|
-
body: "<5-8 sentence email; salesperson voice; references signal + a clear next step>"
|
|
7343
|
-
}]
|
|
7344
|
-
})
|
|
7345
|
-
\`\`\`
|
|
7346
|
-
|
|
7347
|
-
**Strategic options (preferred when split_ai_summary surfaces multiple angles):**
|
|
7348
|
-
|
|
7349
|
-
\`\`\`
|
|
7350
|
-
message_compose_v1({
|
|
7351
|
-
kind: "email",
|
|
7352
|
-
summary_title: "Three angles for <Company> outreach",
|
|
7353
|
-
variants: [
|
|
7354
|
-
{ label: "Push for alignment", subject: "...", body: "..." },
|
|
7355
|
-
{ label: "Reference the M&A signal", subject: "...", body: "..." },
|
|
7356
|
-
{ label: "Soft intro \u2014 peer reference", subject: "...", body: "..." }
|
|
7357
|
-
]
|
|
7358
|
-
})
|
|
7359
|
-
\`\`\`
|
|
7360
|
-
|
|
7361
|
-
Constraints:
|
|
7362
|
-
- **Labels describe STRATEGY, not tone.** "Push for alignment", "Reference M&A signal", "Lead with peer reference" \u2014 not "Friendly" / "Formal" / "Aggressive".
|
|
7363
|
-
- **2\u20133 variants when strategic options are clearly distinct.** One variant when you have a single best-angle draft.
|
|
7364
|
-
- Subject required for \`kind: "email"\`. Phone/call openers use \`kind: "other"\` with the opener in \`body\`.
|
|
7365
|
-
|
|
7366
|
-
The composer becomes the single visual. **Don't also paste the email body into chat prose** \u2014 that's just noise next to the composer.
|
|
7367
|
-
|
|
7368
|
-
For phone-only contacts (no email enriched), use \`kind: "other"\` with a 60-second call opener.
|
|
7365
|
+
Variant shape: 1\u20133 entries. Labels describe **strategy** ("Push for alignment", "Reference the M&A signal", "Soft intro \u2014 peer reference"), not tone. \`kind: "email"\` requires \`subject\`; phone/call openers use \`kind: "other"\` with the opener in \`body\`.
|
|
7369
7366
|
|
|
7370
7367
|
## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
|
|
7371
7368
|
|
|
@@ -7377,7 +7374,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
|
|
|
7377
7374
|
|---|---|---|
|
|
7378
7375
|
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
7379
7376
|
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
7380
|
-
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen |
|
|
7377
|
+
| \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
|
|
7381
7378
|
|
|
7382
7379
|
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
7383
7380
|
|
|
@@ -7458,27 +7455,19 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
7458
7455
|
|
|
7459
7456
|
## NEXT STEPS \u2014 after the outreach brief
|
|
7460
7457
|
|
|
7461
|
-
**
|
|
7458
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
7462
7459
|
|
|
7463
|
-
|
|
7460
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
7464
7461
|
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7468
|
-
question: "What next?",
|
|
7469
|
-
type: "single_select",
|
|
7470
|
-
options: [
|
|
7471
|
-
"<Suggest column from row 1>",
|
|
7472
|
-
"<Suggest column from row 2>",
|
|
7473
|
-
"<Suggest column from row 3>"
|
|
7474
|
-
]
|
|
7475
|
-
}]
|
|
7476
|
-
})
|
|
7477
|
-
\`\`\`
|
|
7462
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
7463
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
7464
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
7478
7465
|
|
|
7479
|
-
|
|
7466
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
7467
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
7468
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
7480
7469
|
|
|
7481
|
-
|
|
7470
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
7482
7471
|
|
|
7483
7472
|
---
|
|
7484
7473
|
|
|
@@ -7498,7 +7487,6 @@ Offer 2\u20133 follow-ups. Choose based on enrichment state + available channels
|
|
|
7498
7487
|
| User reports they reached out | "Log this outreach \u2014 creates prospecting action + outcome" | leadbay_report_outreach(leadId, contact_id, ...) |
|
|
7499
7488
|
| User adds context for next time | "Save a note on the contact or company" | leadbay_add_note |
|
|
7500
7489
|
| After a successful exchange | "Update qualification answers based on what you learned" | leadbay_answer_clarification |
|
|
7501
|
-
|
|
7502
7490
|
The "log outreach" step is the most-important follow-up \u2014 it closes the loop and populates history for the next \`leadbay_prepare_outreach\` call. Detect intent from natural language: "I sent the email", "she didn't pick up", "left a voicemail", "they responded yes/no", etc.
|
|
7503
7491
|
`;
|
|
7504
7492
|
leadbay_preview_bulk_enrichment = `Preview a bulk-enrichment cost given a set of job titles applied to the current selection. Returns \`{selected_leads, enriched_contacts, enrichable_contacts, title_suggestions, auto_included_titles, previously_enriched_titles}\`. \`previously_enriched_titles\` is a newer field (in prod soon) \u2014 when present, the agent can recommend repeating those titles for new leads.
|
|
@@ -7549,9 +7537,9 @@ table. Detail + status priority below.
|
|
|
7549
7537
|
|
|
7550
7538
|
Pull KNOWN leads from the user's Monitor view \u2014 the re-engagement entry point. Use when the user asks "what should I follow up on", "leads I haven't contacted", "leads in [city]", "before my trip", or any phrasing implying pre-existing pipeline context. For NEW leads from Discover, use \`leadbay_pull_leads\`.
|
|
7551
7539
|
|
|
7552
|
-
Backend: wraps \`GET /1.5/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.5/monitor/filter
|
|
7540
|
+
Backend: wraps \`GET /1.5/monitor?personal=&liked=&filtered=&count=&page=\` plus, when \`set_filter\` is supplied, a preceding \`POST /1.5/monitor/filter\`. The Monitor filter is a single \`FilterItem\` per user \u2014 refreshing restores it.
|
|
7553
7541
|
|
|
7554
|
-
**Filter mechanism \u2014 store-then-apply.** Pass \`set_filter: { criteria: FilterCriterion[] }\` to overwrite the server-stored filter, then the composite re-fetches with \`filtered:true\`. \`FilterCriterion\` is the backend's \`anyOf\` over 10 typed criteria: \`size\`, \`keywords\`, \`sector_ids\`, \`location_ids\`, \`custom_field
|
|
7542
|
+
**Filter mechanism \u2014 store-then-apply.** Pass \`set_filter: { criteria: FilterCriterion[] }\` to overwrite the server-stored filter, then the composite re-fetches with \`filtered:true\`. \`FilterCriterion\` is the backend's \`anyOf\` over 10 typed criteria: \`size\`, \`keywords\`, \`sector_ids\`, \`location_ids\`, \`custom_field\`(\`_comparison\`), \`yc\`, \`liked\`, \`last_action\` (MonitorActionType enum), \`last_action_date\` (with \`last_days\`).
|
|
7555
7543
|
|
|
7556
7544
|
Practical mapping from user phrasing to criterion:
|
|
7557
7545
|
|
|
@@ -7564,11 +7552,11 @@ Practical mapping from user phrasing to criterion:
|
|
|
7564
7552
|
| "leads 50\u2013200 employees" | \`{type: "size", sizes: [{min: 50, max: 200}]}\` |
|
|
7565
7553
|
| "Y Combinator companies" | \`{type: "yc"}\` |
|
|
7566
7554
|
|
|
7567
|
-
Geo filtering needs \`admin_area_id\` resolution \u2014 backend rejects free-text in \`location_ids\`. Pass \`city: "<free-text>"\` and the composite calls \`/geo/search\` internally, picks the best match, merges its id into \`set_filter\`. Ambiguous matches return \`status: "ambiguous_locations"\` + \`location_ambiguities[]\` \u2014 pick an id and re-call with \`city_id\`.
|
|
7555
|
+
Geo filtering needs \`admin_area_id\` resolution \u2014 backend rejects free-text in \`location_ids\`. Pass \`city: "<free-text>"\` and the composite calls \`/geo/search\` internally, picks the best match, merges its id into \`set_filter\`. Ambiguous matches return \`status: "ambiguous_locations"\` + \`location_ambiguities[]\` \u2014 pick an id and re-call with \`city_id\`.
|
|
7568
7556
|
|
|
7569
|
-
**Place names go through \`city\`, NEVER \`keywords\`.**
|
|
7557
|
+
**Place names go through \`city\`, NEVER \`keywords\`.** Any geographic token the user names \u2014 cities (\`"Berlin"\`), states/regions (\`"Texas"\`, \`"Bavaria"\`), countries (\`"France"\`), neighborhoods (\`"Brooklyn"\`) \u2014 resolves via \`/geo/search\` (all admin levels). A place name in \`keywords\` becomes a TEXT-MATCH against company descriptions (\u22480 hits), not a real filter. If a place resolves ambiguously, surface the choices \u2014 never silently fall back to keyword search or the unfiltered view.
|
|
7570
7558
|
|
|
7571
|
-
**Pushback exclusion.** Leads with active pushback (\`pushback_status\` set
|
|
7559
|
+
**Pushback exclusion.** Leads with active pushback (\`pushback_status\` set, \`pushback_until > today\`) are excluded client-side; \`total_excluded_by_pushback\` reports how many rows were dropped.
|
|
7572
7560
|
|
|
7573
7561
|
WHEN TO USE: re-engaging pipeline ("what should I follow up on", "stale leads"), filtering monitored leads by city / sector / recency / action type / liked. The canonical orchestrator is the \`leadbay_followup_check_in\` prompt.
|
|
7574
7562
|
|
|
@@ -7576,6 +7564,16 @@ WHEN NOT TO USE: for NEW leads \u2014 that's \`leadbay_pull_leads\` (Discover).
|
|
|
7576
7564
|
|
|
7577
7565
|
**Anti-confusion guardrail.** Iterating \`pull_leads\` pages looking for \`prospecting_actions_count > 0\` or \`notes_count > 0\` rows is the wrong entry point \u2014 the two read different tables. Leads with follow-up history live in \`pull_followups\`.
|
|
7578
7566
|
|
|
7567
|
+
**SIGNAL HONESTY \u2014 never infer signals from freshness.** \`stale_at\`,
|
|
7568
|
+
\`web_fetch_in_progress\`, \`fetch_at\` are freshness markers, not signal
|
|
7569
|
+
indicators \u2014 signal presence is read ONLY from the actual \`signals[]\` /
|
|
7570
|
+
\`web_fetch.content\` entries. For "which of my leads have signal X" across a
|
|
7571
|
+
portfolio, call **\`leadbay_scan_portfolio_signals\`** (bulk-reads cached
|
|
7572
|
+
signals); don't loop \`leadbay_research_lead_by_id\` per lead or guess from
|
|
7573
|
+
freshness. A lead with no cached content is \`not_researched\`, not "no match";
|
|
7574
|
+
never report a signal verdict for a lead you never read.
|
|
7575
|
+
|
|
7576
|
+
|
|
7579
7577
|
---
|
|
7580
7578
|
|
|
7581
7579
|
## RENDERING \u2014 follow-ups table, status-badge driven
|
|
@@ -7662,27 +7660,19 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
7662
7660
|
|
|
7663
7661
|
## NEXT STEPS \u2014 after the follow-ups table
|
|
7664
7662
|
|
|
7665
|
-
**
|
|
7663
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
7666
7664
|
|
|
7667
|
-
|
|
7665
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
7668
7666
|
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
question: "What next?",
|
|
7673
|
-
type: "single_select",
|
|
7674
|
-
options: [
|
|
7675
|
-
"<Suggest column from row 1>",
|
|
7676
|
-
"<Suggest column from row 2>",
|
|
7677
|
-
"<Suggest column from row 3>"
|
|
7678
|
-
]
|
|
7679
|
-
}]
|
|
7680
|
-
})
|
|
7681
|
-
\`\`\`
|
|
7667
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
7668
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
7669
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
7682
7670
|
|
|
7683
|
-
|
|
7671
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
7672
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
7673
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
7684
7674
|
|
|
7685
|
-
|
|
7675
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
7686
7676
|
|
|
7687
7677
|
---
|
|
7688
7678
|
|
|
@@ -7703,7 +7693,6 @@ Always include at least one filter-modification offer (users think in filters: b
|
|
|
7703
7693
|
| User wants to defer a lead | "Snooze [Company] for 3 / 6 / 12 months" | leadbay_set_pushback({ lead_ids:[leadId], status:"3" }) |
|
|
7704
7694
|
| User completed outreach mid-flow | "Log the outreach + record the outcome" | leadbay_report_outreach |
|
|
7705
7695
|
| Discovery mode might fit better | "Looking for NEW leads instead? Switch to discovery." | leadbay_pull_leads |
|
|
7706
|
-
|
|
7707
7696
|
Always offer at least one of: prep outreach, refilter, pushback. Pushback is the canonical way to honor "not now" / "next quarter" \u2014 leads with active pushback are excluded from this view until expiry.
|
|
7708
7697
|
`;
|
|
7709
7698
|
leadbay_pull_leads = `## WHEN TO USE
|
|
@@ -7830,27 +7819,19 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
7830
7819
|
|
|
7831
7820
|
## NEXT STEPS \u2014 after rendering the pull_leads table
|
|
7832
7821
|
|
|
7833
|
-
**
|
|
7822
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
7834
7823
|
|
|
7835
|
-
|
|
7824
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
7836
7825
|
|
|
7837
|
-
|
|
7838
|
-
|
|
7839
|
-
|
|
7840
|
-
question: "What next?",
|
|
7841
|
-
type: "single_select",
|
|
7842
|
-
options: [
|
|
7843
|
-
"<Suggest column from row 1>",
|
|
7844
|
-
"<Suggest column from row 2>",
|
|
7845
|
-
"<Suggest column from row 3>"
|
|
7846
|
-
]
|
|
7847
|
-
}]
|
|
7848
|
-
})
|
|
7849
|
-
\`\`\`
|
|
7826
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
7827
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
7828
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
7850
7829
|
|
|
7851
|
-
|
|
7830
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
7831
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
7832
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
7852
7833
|
|
|
7853
|
-
|
|
7834
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
7854
7835
|
|
|
7855
7836
|
---
|
|
7856
7837
|
|
|
@@ -7860,6 +7841,7 @@ Pick 2\u20133 items below based on what was actually observed in the response. T
|
|
|
7860
7841
|
|
|
7861
7842
|
| Observation | Suggest | Calls |
|
|
7862
7843
|
|------------------------------------------------------------|--------------------------------------------------------------|--------------------------------------------------------|
|
|
7844
|
+
| \u2265 5 leads returned (any batch) | "Build an interactive lead triage board for this batch" | emit antArtifact from data in hand (do NOT re-call leadbay_pull_leads) |
|
|
7863
7845
|
| \`has_more == true\` | "Pull the next page (page N+1 of M)" | leadbay_pull_leads(page = current + 1, lensId = pinned)|
|
|
7864
7846
|
| \u2265 3 rows have \`qualification_summary.answered == 0\` | "Deepen AI qualification on the rows without \u2756 caps" | leadbay_bulk_qualify_leads(leadIds=[\u2026]) |
|
|
7865
7847
|
| User points at a single row | "Research [Company] in depth" | leadbay_research_lead_by_id(leadId) |
|
|
@@ -7870,7 +7852,7 @@ Pick 2\u20133 items below based on what was actually observed in the response. T
|
|
|
7870
7852
|
| Top row has contacts but no phone/email | "Order contact enrichment to surface email/phone first" | leadbay_enrich_titles(...) or leadbay_prepare_outreach(leadId, enrich:true) |
|
|
7871
7853
|
| \`computing_scores == true\` or \`computing_wishlist == true\` | "Scores are still being computed \u2014 re-pull in ~30s" | leadbay_pull_leads (retry with same lensId) |
|
|
7872
7854
|
| User wants a narrower / wider audience | "Adjust the lens filters (sector / size)" | leadbay_adjust_audience(...) |
|
|
7873
|
-
|
|
7855
|
+
| Phase 4 research was run (\`research_lead_by_id\` called) AND top contacts lack direct email/phone | "Enrich contacts on [Lead1], [Lead2] to get direct emails and phone numbers" | leadbay_enrich_contacts(leadId, contactId) \u2014 ONE call per contact (the tool takes a single leadId + contactId, never a list) |
|
|
7874
7856
|
If nothing in the menu applies cleanly, suggest only "pull next page" and "research a specific lead in depth" \u2014 never invent a tool that doesn't exist.
|
|
7875
7857
|
`;
|
|
7876
7858
|
leadbay_qualify_lead = `Trigger AI qualification for a single lead (web fetch + AI rescore). The operation is asynchronous \u2014 results take ~60s. \`forceFetch:true\` re-runs even if recent data exists.
|
|
@@ -7926,6 +7908,43 @@ WHEN TO USE: when audience filters (leadbay_adjust_audience) aren't enough.
|
|
|
7926
7908
|
WHEN NOT TO USE: to answer a pending clarification \u2014 that's leadbay_answer_clarification.
|
|
7927
7909
|
|
|
7928
7910
|
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\`.
|
|
7911
|
+
`;
|
|
7912
|
+
leadbay_remove_contact = `## WHEN TO USE
|
|
7913
|
+
|
|
7914
|
+
Trigger phrases: "remove this contact", "delete this contact", "take this person off the company", "that contact is wrong \u2014 get rid of it", "undo the contact I just added".
|
|
7915
|
+
|
|
7916
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
7917
|
+
|
|
7918
|
+
Do NOT use for: "add a contact to this company" \u2192 \`leadbay_add_contact\`; "stop showing me this lead / not interested" \u2192 \`leadbay_dislike_lead\`.
|
|
7919
|
+
|
|
7920
|
+
Prefer when: user wants a specific PERSON gone from a company \u2014 pass that contact's own \`contact_id\` (from a contacts list), not the lead id
|
|
7921
|
+
|
|
7922
|
+
Examples that SHOULD invoke this tool:
|
|
7923
|
+
- "Remove Jane Doe from that company \u2014 I added her by mistake."
|
|
7924
|
+
- "Delete this contact, it's the wrong person."
|
|
7925
|
+
- "Undo the contact I just added to Acme."
|
|
7926
|
+
|
|
7927
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
7928
|
+
- "Stop showing me this lead."
|
|
7929
|
+
- "Add a contact to this company."
|
|
7930
|
+
- "Show me today's leads."
|
|
7931
|
+
|
|
7932
|
+
## RENDER (quick)
|
|
7933
|
+
|
|
7934
|
+
One-line confirmation: name the contact (or id) and that it was removed
|
|
7935
|
+
from the company. No table.
|
|
7936
|
+
|
|
7937
|
+
---
|
|
7938
|
+
|
|
7939
|
+
Remove a single contact from a company by archiving it. This is the **undo** for the add-a-contact path (\`leadbay_add_contact\`) \u2014 when a rep adds the wrong person, or finds a stale contact, this takes them off the company.
|
|
7940
|
+
|
|
7941
|
+
Pass the contact's **own** \`contact_id\` \u2014 the \`id\` field on a contact object returned by \`leadbay_research_lead_by_id\` or the contacts list. **Not** the parent lead id; the archive endpoint is keyed by the contact directly.
|
|
7942
|
+
|
|
7943
|
+
Backend: \`POST /contacts/{contact_id}/archive\` \u2192 204. Archive is a **soft-delete** \u2014 the contact leaves the company's active contact list (the same action the Leadbay web UI's contact "delete" fires). Idempotent: archiving an already-archived contact is a no-op.
|
|
7944
|
+
|
|
7945
|
+
Returns \`{ archived: true, contact_id, action: "archived" }\`.
|
|
7946
|
+
|
|
7947
|
+
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw).
|
|
7929
7948
|
`;
|
|
7930
7949
|
leadbay_remove_epilogue = `Bulk-clear the epilogue status from a set of leads.
|
|
7931
7950
|
|
|
@@ -8055,7 +8074,7 @@ Trigger phrases: "tell me about this lead", "deep dive on the lead I just picked
|
|
|
8055
8074
|
|
|
8056
8075
|
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
8057
8076
|
|
|
8058
|
-
Do NOT use for: "company name without lead id" \u2192 \`leadbay_research_lead_by_name_fuzzy\`; "draft outreach for <Contact>" \u2192 \`leadbay_prepare_outreach\`.
|
|
8077
|
+
Do NOT use for: "company name without lead id" \u2192 \`leadbay_research_lead_by_name_fuzzy\`; "draft outreach for <Contact>" \u2192 \`leadbay_prepare_outreach\`; "add a contact to this company" \u2192 \`leadbay_add_contact\`.
|
|
8059
8078
|
|
|
8060
8079
|
Prefer when: user picked a row and you have its UUID; pass \`leadId\`
|
|
8061
8080
|
|
|
@@ -8070,42 +8089,46 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
|
8070
8089
|
---
|
|
8071
8090
|
|
|
8072
8091
|
Tell me everything decision-relevant about a single lead, identified by its
|
|
8073
|
-
Leadbay UUID. Bundles the lens-scoped
|
|
8074
|
-
|
|
8075
|
-
(
|
|
8076
|
-
|
|
8077
|
-
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
|
|
8084
|
-
|
|
8085
|
-
|
|
8086
|
-
|
|
8087
|
-
|
|
8088
|
-
|
|
8089
|
-
the case where the user names a company in prose without a UUID \u2014 it
|
|
8090
|
-
fuzzy-resolves the name against the active lens's wishlist, then delegates
|
|
8091
|
-
here. Both return the same shape; the fuzzy wrapper just adds
|
|
8092
|
-
\`_meta.resolved_from\` and \`_meta.match_candidates\` so you can offer
|
|
8093
|
-
disambiguation.
|
|
8092
|
+
Leadbay UUID. Bundles the lens-scoped profile, AI qualification answers,
|
|
8093
|
+
structured web-research signals (hot flags + sources), the two-tier contact set
|
|
8094
|
+
(\`enriched\` + \`org\`), the unified \`recent_activities\` timeline, engagement
|
|
8095
|
+
counts, and a \`_meta.has_reachable_contact\` hint that drives NEXT STEPS. Order
|
|
8096
|
+
is deliberate: qualification, signals, firmographics, contacts, recent activity.
|
|
8097
|
+
|
|
8098
|
+
Scoring has two layers: the basic \`score\` (firmographic, always present) and
|
|
8099
|
+
the AI qualification layer (\`ai_agent_lead_score\` + per-question answers +
|
|
8100
|
+
web_fetch signals). The AI layer is pre-populated for roughly the top 10 of
|
|
8101
|
+
each daily batch, and on-demand (via leadbay_bulk_qualify_leads) below that.
|
|
8102
|
+
Combine both when judging a lead.
|
|
8103
|
+
|
|
8104
|
+
The companion **leadbay_research_lead_by_name_fuzzy** wraps this one when the
|
|
8105
|
+
user names a company without a UUID: it fuzzy-resolves against the active
|
|
8106
|
+
lens's wishlist, then delegates here. Same shape, plus \`_meta.resolved_from\` /
|
|
8107
|
+
\`_meta.match_candidates\`.
|
|
8094
8108
|
|
|
8095
8109
|
WHEN TO USE: when picking up a single lead from
|
|
8096
8110
|
leadbay_pull_leads (or any list that exposed a leadId) to decide whether to
|
|
8097
8111
|
act on it.
|
|
8098
8112
|
|
|
8099
8113
|
WHEN NOT TO USE: across many leads at once \u2014 that's
|
|
8100
|
-
leadbay_pull_leads' job
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
**
|
|
8105
|
-
|
|
8106
|
-
|
|
8107
|
-
|
|
8108
|
-
|
|
8114
|
+
leadbay_pull_leads' job (portfolio-wide signal questions go to
|
|
8115
|
+
leadbay_scan_portfolio_signals; see below). This composite supersedes the
|
|
8116
|
+
lower-level leadbay_get_lead_profile.
|
|
8117
|
+
|
|
8118
|
+
**SIGNAL HONESTY \u2014 never infer signals from freshness.** \`stale_at\`,
|
|
8119
|
+
\`web_fetch_in_progress\`, \`fetch_at\` are freshness markers, not signal
|
|
8120
|
+
indicators \u2014 signal presence is read ONLY from the actual \`signals[]\` /
|
|
8121
|
+
\`web_fetch.content\` entries. For "which of my leads have signal X" across a
|
|
8122
|
+
portfolio, call **\`leadbay_scan_portfolio_signals\`** (bulk-reads cached
|
|
8123
|
+
signals); don't loop \`leadbay_research_lead_by_id\` per lead or guess from
|
|
8124
|
+
freshness. A lead with no cached content is \`not_researched\`, not "no match";
|
|
8125
|
+
never report a signal verdict for a lead you never read.
|
|
8126
|
+
|
|
8127
|
+
|
|
8128
|
+
**Concurrency note**: this composite reads many sub-resources per call. Call
|
|
8129
|
+
it **sequentially or in small batches (\u22643 parallel)**. Firing 10+ in parallel
|
|
8130
|
+
saturates the transport and produces misleading \`"Tool permission stream
|
|
8131
|
+
closed"\` errors \u2014 that's backpressure, not a permission failure. On a transient
|
|
8109
8132
|
stream/timeout failure, retry the same lead once before moving on.
|
|
8110
8133
|
|
|
8111
8134
|
---
|
|
@@ -8184,27 +8207,19 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
8184
8207
|
|
|
8185
8208
|
## NEXT STEPS \u2014 after the research card
|
|
8186
8209
|
|
|
8187
|
-
**
|
|
8210
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
8188
8211
|
|
|
8189
|
-
|
|
8212
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
8190
8213
|
|
|
8191
|
-
|
|
8192
|
-
|
|
8193
|
-
|
|
8194
|
-
question: "What next?",
|
|
8195
|
-
type: "single_select",
|
|
8196
|
-
options: [
|
|
8197
|
-
"<Suggest column from row 1>",
|
|
8198
|
-
"<Suggest column from row 2>",
|
|
8199
|
-
"<Suggest column from row 3>"
|
|
8200
|
-
]
|
|
8201
|
-
}]
|
|
8202
|
-
})
|
|
8203
|
-
\`\`\`
|
|
8214
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
8215
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
8216
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
8204
8217
|
|
|
8205
|
-
|
|
8218
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
8219
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
8220
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
8206
8221
|
|
|
8207
|
-
|
|
8222
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
8208
8223
|
|
|
8209
8224
|
---
|
|
8210
8225
|
|
|
@@ -8249,6 +8264,7 @@ out?"\`
|
|
|
8249
8264
|
|
|
8250
8265
|
| Observation | Suggest | Calls |
|
|
8251
8266
|
|--------------------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------|
|
|
8267
|
+
| Lead is clearly not a fit (wrong industry, too small) | "Dislike this lead" | leadbay_dislike_lead({ leadId }) |
|
|
8252
8268
|
| User is done with this lead | "Back to the inbox" | leadbay_pull_leads |
|
|
8253
8269
|
`;
|
|
8254
8270
|
leadbay_research_lead_by_name_fuzzy = `## WHEN TO USE
|
|
@@ -8372,27 +8388,19 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
8372
8388
|
|
|
8373
8389
|
## NEXT STEPS \u2014 after the research card
|
|
8374
8390
|
|
|
8375
|
-
**
|
|
8391
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
8376
8392
|
|
|
8377
|
-
|
|
8393
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
8378
8394
|
|
|
8379
|
-
|
|
8380
|
-
|
|
8381
|
-
|
|
8382
|
-
question: "What next?",
|
|
8383
|
-
type: "single_select",
|
|
8384
|
-
options: [
|
|
8385
|
-
"<Suggest column from row 1>",
|
|
8386
|
-
"<Suggest column from row 2>",
|
|
8387
|
-
"<Suggest column from row 3>"
|
|
8388
|
-
]
|
|
8389
|
-
}]
|
|
8390
|
-
})
|
|
8391
|
-
\`\`\`
|
|
8395
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
8396
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
8397
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
8392
8398
|
|
|
8393
|
-
|
|
8399
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
8400
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
8401
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
8394
8402
|
|
|
8395
|
-
|
|
8403
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
8396
8404
|
|
|
8397
8405
|
---
|
|
8398
8406
|
|
|
@@ -8437,6 +8445,7 @@ out?"\`
|
|
|
8437
8445
|
|
|
8438
8446
|
| Observation | Suggest | Calls |
|
|
8439
8447
|
|--------------------------------------------------------|----------------------------------------------------------|----------------------------------------------------------------|
|
|
8448
|
+
| Lead is clearly not a fit (wrong industry, too small) | "Dislike this lead" | leadbay_dislike_lead({ leadId }) |
|
|
8440
8449
|
| User is done with this lead | "Back to the inbox" | leadbay_pull_leads |
|
|
8441
8450
|
|
|
8442
8451
|
|
|
@@ -8483,6 +8492,178 @@ Below the table, a one-liner: \`"Ready: K rows \xB7 Ambiguous: A rows \xB7 Unmat
|
|
|
8483
8492
|
| Ambiguous rows present | "Inspect candidates for each ambiguous row" | (re-call with include_candidate_profiles=true) |
|
|
8484
8493
|
| Unmatched rows but websites present | "Import anyway \u2014 Leadbay will crawl and match later" | leadbay_import_leads (status check after) |
|
|
8485
8494
|
| User wants to skip rows they can't ID | "Drop unmatched rows and import the rest" | leadbay_import_leads (with filtered records) |
|
|
8495
|
+
`;
|
|
8496
|
+
leadbay_scan_portfolio_signals = `## WHEN TO USE
|
|
8497
|
+
|
|
8498
|
+
Trigger phrases: "which of my leads <did X>", "find leads that <raised / acquired / hired / moved / changed CEO>", "scan my portfolio for <signal>", "identify all the ones that <event> since <date>", "who in Monitor has a <funding / M&A / hiring> signal", "build a campaign from leads with <signal>".
|
|
8499
|
+
|
|
8500
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
8501
|
+
|
|
8502
|
+
Do NOT use for: "research one named company" \u2192 \`leadbay_research_lead_by_name_fuzzy\`; "everything about lead <UUID>" \u2192 \`leadbay_research_lead_by_id\`; "qualify my next N leads (they aren't researched yet)" \u2192 \`leadbay_bulk_qualify_leads\`; "just list my follow-ups" \u2192 \`leadbay_pull_followups\`.
|
|
8503
|
+
|
|
8504
|
+
Prefer when: user wants to FILTER a known portfolio by a web-research signal in bulk \u2014 pass \`query\`, optionally \`since\`, \`city\`/\`set_filter\`, or \`leadIds\`
|
|
8505
|
+
|
|
8506
|
+
Examples that SHOULD invoke this tool:
|
|
8507
|
+
- "Which of my leads acquired a company since 2025?"
|
|
8508
|
+
- "Scan my Lyon portfolio for funding signals."
|
|
8509
|
+
- "Find everyone in Monitor who changed CEO and build a campaign."
|
|
8510
|
+
|
|
8511
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
8512
|
+
- "Look up Acme Corp for me."
|
|
8513
|
+
- "Show me my follow-ups."
|
|
8514
|
+
- "Qualify my next 10 leads."
|
|
8515
|
+
|
|
8516
|
+
## RENDER (quick)
|
|
8517
|
+
|
|
8518
|
+
Cohort grouped by lead: one block per matched lead (name \xB7 location +
|
|
8519
|
+
its matched signal entries, hot first, source-linked). Open with
|
|
8520
|
+
"N match <query> (M scanned)"; ALWAYS close with an honesty footer \u2014
|
|
8521
|
+
"scanned N \xB7 matched M \xB7 K not yet researched". Never present
|
|
8522
|
+
not_researched leads as "no signal". Full layout below.
|
|
8523
|
+
|
|
8524
|
+
---
|
|
8525
|
+
|
|
8526
|
+
Scan a known portfolio for a specific web-research signal in one call. This is
|
|
8527
|
+
the bulk, read-only answer to "which of my leads have signal X" \u2014 the question
|
|
8528
|
+
that otherwise forces a per-lead \`leadbay_research_lead_by_id\` loop (one full
|
|
8529
|
+
profile call per lead, slow and quota-heavy).
|
|
8530
|
+
|
|
8531
|
+
**Reads CACHED signals only \u2014 does not trigger new research.** For each lead in
|
|
8532
|
+
scope it reads \`GET /leads/{id}/web_fetch\` (the already-computed web-research
|
|
8533
|
+
signals) and filters the entries against \`query\`. It issues NO web_fetch POST,
|
|
8534
|
+
so it does not consume AI qualification credits and does not re-crawl. Leads
|
|
8535
|
+
that have no cached content (never qualified, or still in progress) are
|
|
8536
|
+
reported in \`not_researched\` \u2014 they are **NOT** silently treated as "no
|
|
8537
|
+
match". Qualify them with \`leadbay_bulk_qualify_leads\`, then re-scan.
|
|
8538
|
+
|
|
8539
|
+
**Scope.** Pass \`leadIds\` for an explicit cohort, or omit it to scan the
|
|
8540
|
+
Monitor portfolio. Narrow the Monitor scope with \`city\` / \`set_filter\` exactly
|
|
8541
|
+
as \`leadbay_pull_followups\` does (store-then-apply server-side filter). The
|
|
8542
|
+
scan is bounded by \`max_leads\` (default 200, hard cap 300); when the portfolio
|
|
8543
|
+
is larger, \`truncated_at\` is set and coverage is partial \u2014 say so.
|
|
8544
|
+
|
|
8545
|
+
**Query.** \`query\` is matched case- and accent-insensitively against each
|
|
8546
|
+
signal entry's description, source, and section label. Comma- or
|
|
8547
|
+
space-separated terms are OR'd ("M&A, acquisition, rachet\xE9" matches any). Use
|
|
8548
|
+
\`since\` (ISO date) to keep only entries dated on/after it \u2014 entries with no
|
|
8549
|
+
date are kept (a missing date is not evidence the event is old).
|
|
8550
|
+
|
|
8551
|
+
**Result is campaign-ready.** \`matched[]\` carries \`lead_id\`, \`name\`,
|
|
8552
|
+
\`location\`, and the matching \`matched_signals[]\` (section + hot + source +
|
|
8553
|
+
date + description). Feed the matched \`lead_id\`s straight into
|
|
8554
|
+
\`leadbay_add_leads_to_campaign\` / \`leadbay_create_campaign\`.
|
|
8555
|
+
|
|
8556
|
+
On a 429 mid-scan, partial \`matched\` is returned with \`quota_exceeded: true\` \u2014
|
|
8557
|
+
offer the user wait-for-reset OR a top-up link (both unblock; a top-up clears
|
|
8558
|
+
the throttle immediately).
|
|
8559
|
+
|
|
8560
|
+
**SIGNAL HONESTY \u2014 never infer signals from freshness.** \`stale_at\`,
|
|
8561
|
+
\`web_fetch_in_progress\`, \`fetch_at\` are freshness markers, not signal
|
|
8562
|
+
indicators \u2014 signal presence is read ONLY from the actual \`signals[]\` /
|
|
8563
|
+
\`web_fetch.content\` entries. For "which of my leads have signal X" across a
|
|
8564
|
+
portfolio, call **\`leadbay_scan_portfolio_signals\`** (bulk-reads cached
|
|
8565
|
+
signals); don't loop \`leadbay_research_lead_by_id\` per lead or guess from
|
|
8566
|
+
freshness. A lead with no cached content is \`not_researched\`, not "no match";
|
|
8567
|
+
never report a signal verdict for a lead you never read.
|
|
8568
|
+
|
|
8569
|
+
|
|
8570
|
+
WHEN TO USE: when the user wants to filter a known
|
|
8571
|
+
portfolio by a web-research signal across many leads at once \u2014 discovering a
|
|
8572
|
+
cohort to act on, not inspecting a single lead.
|
|
8573
|
+
|
|
8574
|
+
WHEN NOT TO USE: for a single named company
|
|
8575
|
+
(leadbay_research_lead_by_name_fuzzy) or one lead by UUID
|
|
8576
|
+
(leadbay_research_lead_by_id); to qualify leads that have no signals yet
|
|
8577
|
+
(leadbay_bulk_qualify_leads); or to just list follow-ups with no signal filter
|
|
8578
|
+
(leadbay_pull_followups).
|
|
8579
|
+
|
|
8580
|
+
---
|
|
8581
|
+
|
|
8582
|
+
## RENDERING \u2014 bulk signal-scan results
|
|
8583
|
+
|
|
8584
|
+
The output is a cohort, grouped by lead. Lead with the matches, end with an
|
|
8585
|
+
honesty footer \u2014 never hide what wasn't scanned.
|
|
8586
|
+
|
|
8587
|
+
### Matched leads
|
|
8588
|
+
|
|
8589
|
+
Open with a one-line headline: \`**N leads match "<query>"** (M scanned).\`
|
|
8590
|
+
|
|
8591
|
+
Then one block per \`matched[]\` lead, ordered with \`hot\` matches first. Emit
|
|
8592
|
+
each as a host-parseable per-lead block so the chat host's place-card
|
|
8593
|
+
auto-detector can render it (per the repo "feed the address auto-detector"
|
|
8594
|
+
convention):
|
|
8595
|
+
|
|
8596
|
+
\`\`\`
|
|
8597
|
+
### <name> \xB7 <location>
|
|
8598
|
+
|
|
8599
|
+
<for each matched_signal, one bullet>
|
|
8600
|
+
- **<section_emoji> <section_label>** \u2014 <description> <\u{1F525} if hot> ([source](<source>), <date>)
|
|
8601
|
+
\`\`\`
|
|
8602
|
+
|
|
8603
|
+
- **Bold** the description of \`hot: true\` entries; leave cold entries plain.
|
|
8604
|
+
- Render \`source\` as a markdown link \`([source](url), date)\`; omit the date
|
|
8605
|
+
when null, omit the link when \`source\` is empty.
|
|
8606
|
+
- Cap to the 3 strongest signals per lead (hot first, then by date desc); if a
|
|
8607
|
+
lead has more, end its block with \`_+K more signals_\`.
|
|
8608
|
+
- When \`name\` is null (the scan was scoped by \`leadIds\` and the read failed to
|
|
8609
|
+
carry firmographics), fall back to \`### Lead <lead_id>\` \u2014 but prefer to enrich
|
|
8610
|
+
the name via the matched lead's own data when available.
|
|
8611
|
+
|
|
8612
|
+
### Honesty footer (ALWAYS print)
|
|
8613
|
+
|
|
8614
|
+
A single italic line summarising coverage:
|
|
8615
|
+
|
|
8616
|
+
\`_Scanned N \xB7 matched M \xB7 K had no cached signals (not yet researched)._\`
|
|
8617
|
+
|
|
8618
|
+
- When \`not_researched\` is non-empty, this is load-bearing: state plainly that
|
|
8619
|
+
those K leads were NOT searched and were NOT counted as "no match". Offer to
|
|
8620
|
+
qualify them and re-scan (see NEXT STEPS).
|
|
8621
|
+
- When \`truncated_at\` is set, add: \`_Coverage partial \u2014 only the first <truncated_at>
|
|
8622
|
+
leads were scanned; narrow the scope or raise max_leads._\`
|
|
8623
|
+
- When \`quota_exceeded\` is true, add the wait-or-top-up offer.
|
|
8624
|
+
|
|
8625
|
+
**Hide:** raw \`lead_id\` in prose (use it only for the campaign call), \`_meta\`,
|
|
8626
|
+
empty arrays, any freshness field. NEVER present \`not_researched\` leads as
|
|
8627
|
+
"no signal found".
|
|
8628
|
+
|
|
8629
|
+
|
|
8630
|
+
---
|
|
8631
|
+
|
|
8632
|
+
## NEXT STEPS \u2014 after the signal scan
|
|
8633
|
+
|
|
8634
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
8635
|
+
|
|
8636
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
8637
|
+
|
|
8638
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
8639
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
8640
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
8641
|
+
|
|
8642
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
8643
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
8644
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
8645
|
+
|
|
8646
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
8647
|
+
|
|
8648
|
+
---
|
|
8649
|
+
|
|
8650
|
+
|
|
8651
|
+
|
|
8652
|
+
The scan exists to BUILD A COHORT, not just to list. The default next move is
|
|
8653
|
+
almost always "turn the matched leads into a campaign."
|
|
8654
|
+
|
|
8655
|
+
| Observation | Suggest | Calls |
|
|
8656
|
+
|---------------------------------------------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------|
|
|
8657
|
+
| \`matched\` non-empty (top of menu) | "Build a campaign from the N matched leads" | leadbay_create_campaign / leadbay_add_leads_to_campaign(matched lead_ids) |
|
|
8658
|
+
| \`not_researched\` non-empty | "K leads aren't researched yet \u2014 qualify them, then re-scan" | leadbay_bulk_qualify_leads(not_researched lead_ids) \u2192 re-run leadbay_scan_portfolio_signals |
|
|
8659
|
+
| Zero matches but leads were researched | "Widen the query (synonyms) or relax \`since\`" | leadbay_scan_portfolio_signals(query: "<broader terms>", since: omit-or-earlier) |
|
|
8660
|
+
| \`truncated_at\` set | "Scan only covered N \u2014 narrow scope or raise the cap" | leadbay_scan_portfolio_signals({city / set_filter}) or raise \`max_leads\` |
|
|
8661
|
+
| One standout matched lead | "Open that lead's full brief" | leadbay_research_lead_by_id(leadId) |
|
|
8662
|
+
| \`quota_exceeded\` | "Wait for reset OR top up to finish the scan" | leadbay_create_topup_link |
|
|
8663
|
+
|
|
8664
|
+
NEVER report leads in \`not_researched\` as if they had no matching signal \u2014 they
|
|
8665
|
+
were never read. Distinguish "no signal X found" (researched, no match) from
|
|
8666
|
+
"not yet researched" (no data to search) every time.
|
|
8486
8667
|
`;
|
|
8487
8668
|
leadbay_seed_candidates = `## WHEN TO USE
|
|
8488
8669
|
|
|
@@ -8656,7 +8837,7 @@ Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can rou
|
|
|
8656
8837
|
|---|---|---|
|
|
8657
8838
|
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
8658
8839
|
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
8659
|
-
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen |
|
|
8840
|
+
| \`ask_user_input_v0\` (Claude chat / ChatGPT) **or** \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 whichever is in your tool set; their schemas differ, match the one you have | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | Per-tool schema in the server instructions + NEXT STEPS routing block. Max 3 questions. |
|
|
8660
8841
|
|
|
8661
8842
|
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
8662
8843
|
|
|
@@ -8674,6 +8855,83 @@ WHEN TO USE: the user signals a *mixed* tour-planning intent \u2014 they want bo
|
|
|
8674
8855
|
WHEN NOT TO USE: if the user only wants follow-ups (use \`leadbay_followups_map\`), only wants new leads (use \`leadbay_pull_leads\`), wants research on one specific account (\`leadbay_research_lead_by_id\`), or wants to persist the tour as a campaign artifact (chain into \`leadbay_create_campaign\` after this).
|
|
8675
8856
|
|
|
8676
8857
|
**Response envelope**: \`{city, city_id, monitor_leads, discover_leads, discover_filter_note, _meta}\` on happy path; \`{status: "ambiguous_locations", location_ambiguities, ...}\` when the passed \`city\` matched multiple admin areas.
|
|
8858
|
+
|
|
8859
|
+
---
|
|
8860
|
+
`;
|
|
8861
|
+
leadbay_unpin_contact = `## WHEN TO USE
|
|
8862
|
+
|
|
8863
|
+
Trigger phrases: "unpin this contact", "remove the pin from this contact", "this person isn't the priority anymore", "unfavourite this contact".
|
|
8864
|
+
|
|
8865
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
8866
|
+
|
|
8867
|
+
Do NOT use for: "pin / mark as priority" \u2192 \`leadbay_pin_contact\`; "remove / delete this contact" \u2192 \`leadbay_remove_contact\`.
|
|
8868
|
+
|
|
8869
|
+
Prefer when: user wants to clear the pinned flag on a contact (but keep the contact) \u2014 pass that contact's own \`contact_id\`
|
|
8870
|
+
|
|
8871
|
+
Examples that SHOULD invoke this tool:
|
|
8872
|
+
- "Unpin Jane Doe \u2014 she's not the priority anymore."
|
|
8873
|
+
- "Remove the pin from that contact."
|
|
8874
|
+
- "Unfavourite this person."
|
|
8875
|
+
|
|
8876
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
8877
|
+
- "Pin this contact as priority."
|
|
8878
|
+
- "Remove that contact entirely."
|
|
8879
|
+
- "Add a new contact to this company."
|
|
8880
|
+
|
|
8881
|
+
## RENDER (quick)
|
|
8882
|
+
|
|
8883
|
+
One-line confirmation that the named contact (or id) is no longer pinned.
|
|
8884
|
+
No table.
|
|
8885
|
+
|
|
8886
|
+
---
|
|
8887
|
+
|
|
8888
|
+
Unpin a single contact on a company \u2014 clears its priority / favourite flag. The contact stays on the company; only the pin is removed (to remove the contact entirely, use \`leadbay_remove_contact\`).
|
|
8889
|
+
|
|
8890
|
+
Pass the contact's **own** \`contact_id\` \u2014 not the parent lead id.
|
|
8891
|
+
|
|
8892
|
+
Backend: \`POST /contacts/{contact_id}/unpin\` \u2192 204. Idempotent. The inverse is \`leadbay_pin_contact\`.
|
|
8893
|
+
|
|
8894
|
+
Returns \`{ pinned: false, contact_id, action: "unpinned" }\`.
|
|
8895
|
+
|
|
8896
|
+
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw).
|
|
8897
|
+
`;
|
|
8898
|
+
leadbay_update_contact = `## WHEN TO USE
|
|
8899
|
+
|
|
8900
|
+
Trigger phrases: "update this contact", "fix this contact's title", "change their email / phone / LinkedIn", "edit this person's details", "correct the contact's name".
|
|
8901
|
+
|
|
8902
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
8903
|
+
|
|
8904
|
+
Do NOT use for: "add a new contact to this company" \u2192 \`leadbay_add_contact\`; "remove / delete this contact" \u2192 \`leadbay_remove_contact\`; "get email/phone for a contact (enrichment)" \u2192 \`leadbay_enrich_titles\`.
|
|
8905
|
+
|
|
8906
|
+
Prefer when: user wants to change details on an EXISTING contact \u2014 pass that contact's own \`contact_id\` plus first_name + last_name (required) and the fields to change
|
|
8907
|
+
|
|
8908
|
+
Examples that SHOULD invoke this tool:
|
|
8909
|
+
- "Update Jane's title to SVP Engineering."
|
|
8910
|
+
- "Fix this contact's LinkedIn URL."
|
|
8911
|
+
- "Change John's email to john@acme.com."
|
|
8912
|
+
|
|
8913
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
8914
|
+
- "Add a new contact to this company."
|
|
8915
|
+
- "Remove that contact, wrong person."
|
|
8916
|
+
- "Get me the email for this contact."
|
|
8917
|
+
|
|
8918
|
+
## RENDER (quick)
|
|
8919
|
+
|
|
8920
|
+
One-line confirmation naming the contact and what changed. No table.
|
|
8921
|
+
|
|
8922
|
+
---
|
|
8923
|
+
|
|
8924
|
+
Edit an existing contact in place \u2014 change their \`job_title\`, \`linkedin_page\`, \`email\`, \`phone_number\`, or name.
|
|
8925
|
+
|
|
8926
|
+
Pass the contact's **own** \`contact_id\` (the \`id\` field from \`leadbay_research_lead_by_id\` or a contacts list) \u2014 **not** the parent lead id.
|
|
8927
|
+
|
|
8928
|
+
**\`first_name\` + \`last_name\` are required even on an edit.** The backend validates the full contact identity and rejects a partial body (\`invalid contact\`). So pass the contact's *current* first/last name even when you're only changing the title \u2014 read the current values via \`leadbay_research_lead_by_id\` first if you don't have them.
|
|
8929
|
+
|
|
8930
|
+
Backend: \`POST /contacts/{contact_id}/update\` (snake_case body) \u2192 200 with the updated contact. Edits in place (same id). Camel-case bodies are rejected.
|
|
8931
|
+
|
|
8932
|
+
Returns \`{ updated: true, contact_id, contact: { id, first_name, last_name, job_title, linkedin_page, email, phone_number } }\`.
|
|
8933
|
+
|
|
8934
|
+
Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw).
|
|
8677
8935
|
`;
|
|
8678
8936
|
leadbay_update_lens = `Update lens metadata (name, description, mode flags). Does NOT change the audience filter \u2014 use leadbay_update_lens_filter for that.
|
|
8679
8937
|
|
|
@@ -13094,6 +13352,250 @@ var init_dislike_lead = __esm({
|
|
|
13094
13352
|
}
|
|
13095
13353
|
});
|
|
13096
13354
|
|
|
13355
|
+
// ../core/dist/tools/add-contact.js
|
|
13356
|
+
var addContact;
|
|
13357
|
+
var init_add_contact = __esm({
|
|
13358
|
+
"../core/dist/tools/add-contact.js"() {
|
|
13359
|
+
"use strict";
|
|
13360
|
+
init_tool_descriptions_generated();
|
|
13361
|
+
addContact = {
|
|
13362
|
+
name: "leadbay_add_contact",
|
|
13363
|
+
description: leadbay_add_contact,
|
|
13364
|
+
write: true,
|
|
13365
|
+
annotations: {
|
|
13366
|
+
title: "Add a contact",
|
|
13367
|
+
readOnlyHint: false,
|
|
13368
|
+
destructiveHint: false,
|
|
13369
|
+
idempotentHint: false,
|
|
13370
|
+
openWorldHint: true
|
|
13371
|
+
},
|
|
13372
|
+
inputSchema: {
|
|
13373
|
+
type: "object",
|
|
13374
|
+
properties: {
|
|
13375
|
+
lead_id: {
|
|
13376
|
+
type: "string",
|
|
13377
|
+
description: "UUID of the parent company (lead) to attach the contact to. The contact is created on this company."
|
|
13378
|
+
},
|
|
13379
|
+
first_name: { type: "string", description: "Contact first name." },
|
|
13380
|
+
last_name: { type: "string", description: "Contact last name." },
|
|
13381
|
+
job_title: { type: "string", description: "Contact job title (optional)." },
|
|
13382
|
+
linkedin_page: {
|
|
13383
|
+
type: "string",
|
|
13384
|
+
description: "Contact LinkedIn profile URL (optional)."
|
|
13385
|
+
},
|
|
13386
|
+
email: { type: "string", description: "Contact email (optional)." },
|
|
13387
|
+
phone_number: {
|
|
13388
|
+
type: "string",
|
|
13389
|
+
description: "Contact phone number (optional, free-form)."
|
|
13390
|
+
}
|
|
13391
|
+
},
|
|
13392
|
+
required: ["lead_id", "first_name", "last_name"],
|
|
13393
|
+
additionalProperties: false
|
|
13394
|
+
},
|
|
13395
|
+
execute: async (client, params, _ctx) => {
|
|
13396
|
+
const body = {
|
|
13397
|
+
first_name: params.first_name,
|
|
13398
|
+
last_name: params.last_name
|
|
13399
|
+
};
|
|
13400
|
+
if (params.job_title != null)
|
|
13401
|
+
body.job_title = params.job_title;
|
|
13402
|
+
if (params.linkedin_page != null)
|
|
13403
|
+
body.linkedin_page = params.linkedin_page;
|
|
13404
|
+
if (params.email != null)
|
|
13405
|
+
body.email = params.email;
|
|
13406
|
+
if (params.phone_number != null)
|
|
13407
|
+
body.phone_number = params.phone_number;
|
|
13408
|
+
const contact = await client.request("POST", `/leads/${params.lead_id}/contacts`, body);
|
|
13409
|
+
return { added: true, lead_id: params.lead_id, contact };
|
|
13410
|
+
}
|
|
13411
|
+
};
|
|
13412
|
+
}
|
|
13413
|
+
});
|
|
13414
|
+
|
|
13415
|
+
// ../core/dist/tools/remove-contact.js
|
|
13416
|
+
var removeContact;
|
|
13417
|
+
var init_remove_contact = __esm({
|
|
13418
|
+
"../core/dist/tools/remove-contact.js"() {
|
|
13419
|
+
"use strict";
|
|
13420
|
+
init_tool_descriptions_generated();
|
|
13421
|
+
removeContact = {
|
|
13422
|
+
name: "leadbay_remove_contact",
|
|
13423
|
+
description: leadbay_remove_contact,
|
|
13424
|
+
write: true,
|
|
13425
|
+
annotations: {
|
|
13426
|
+
title: "Remove a contact",
|
|
13427
|
+
readOnlyHint: false,
|
|
13428
|
+
// Soft-delete (archive), but it does remove the contact from the active
|
|
13429
|
+
// list, so flag it destructive so cautious clients can confirm.
|
|
13430
|
+
destructiveHint: true,
|
|
13431
|
+
idempotentHint: true,
|
|
13432
|
+
openWorldHint: true
|
|
13433
|
+
},
|
|
13434
|
+
inputSchema: {
|
|
13435
|
+
type: "object",
|
|
13436
|
+
properties: {
|
|
13437
|
+
contact_id: {
|
|
13438
|
+
type: "string",
|
|
13439
|
+
description: "UUID of the contact to remove (the contact's own `id`, e.g. from leadbay_research_lead_by_id's contacts list \u2014 NOT the parent lead id)."
|
|
13440
|
+
}
|
|
13441
|
+
},
|
|
13442
|
+
required: ["contact_id"],
|
|
13443
|
+
additionalProperties: false
|
|
13444
|
+
},
|
|
13445
|
+
execute: async (client, params, _ctx) => {
|
|
13446
|
+
await client.requestVoid("POST", `/contacts/${params.contact_id}/archive`);
|
|
13447
|
+
return { archived: true, contact_id: params.contact_id, action: "archived" };
|
|
13448
|
+
}
|
|
13449
|
+
};
|
|
13450
|
+
}
|
|
13451
|
+
});
|
|
13452
|
+
|
|
13453
|
+
// ../core/dist/tools/pin-contact.js
|
|
13454
|
+
var pinContact;
|
|
13455
|
+
var init_pin_contact = __esm({
|
|
13456
|
+
"../core/dist/tools/pin-contact.js"() {
|
|
13457
|
+
"use strict";
|
|
13458
|
+
init_tool_descriptions_generated();
|
|
13459
|
+
pinContact = {
|
|
13460
|
+
name: "leadbay_pin_contact",
|
|
13461
|
+
description: leadbay_pin_contact,
|
|
13462
|
+
write: true,
|
|
13463
|
+
annotations: {
|
|
13464
|
+
title: "Pin a contact",
|
|
13465
|
+
readOnlyHint: false,
|
|
13466
|
+
destructiveHint: false,
|
|
13467
|
+
idempotentHint: true,
|
|
13468
|
+
openWorldHint: true
|
|
13469
|
+
},
|
|
13470
|
+
inputSchema: {
|
|
13471
|
+
type: "object",
|
|
13472
|
+
properties: {
|
|
13473
|
+
contact_id: {
|
|
13474
|
+
type: "string",
|
|
13475
|
+
description: "UUID of the contact to pin (the contact's own `id` \u2014 NOT the parent lead id)."
|
|
13476
|
+
}
|
|
13477
|
+
},
|
|
13478
|
+
required: ["contact_id"],
|
|
13479
|
+
additionalProperties: false
|
|
13480
|
+
},
|
|
13481
|
+
execute: async (client, params, _ctx) => {
|
|
13482
|
+
await client.requestVoid("POST", `/contacts/${params.contact_id}/pin`);
|
|
13483
|
+
return { pinned: true, contact_id: params.contact_id, action: "pinned" };
|
|
13484
|
+
}
|
|
13485
|
+
};
|
|
13486
|
+
}
|
|
13487
|
+
});
|
|
13488
|
+
|
|
13489
|
+
// ../core/dist/tools/unpin-contact.js
|
|
13490
|
+
var unpinContact;
|
|
13491
|
+
var init_unpin_contact = __esm({
|
|
13492
|
+
"../core/dist/tools/unpin-contact.js"() {
|
|
13493
|
+
"use strict";
|
|
13494
|
+
init_tool_descriptions_generated();
|
|
13495
|
+
unpinContact = {
|
|
13496
|
+
name: "leadbay_unpin_contact",
|
|
13497
|
+
description: leadbay_unpin_contact,
|
|
13498
|
+
write: true,
|
|
13499
|
+
annotations: {
|
|
13500
|
+
title: "Unpin a contact",
|
|
13501
|
+
readOnlyHint: false,
|
|
13502
|
+
destructiveHint: false,
|
|
13503
|
+
idempotentHint: true,
|
|
13504
|
+
openWorldHint: true
|
|
13505
|
+
},
|
|
13506
|
+
inputSchema: {
|
|
13507
|
+
type: "object",
|
|
13508
|
+
properties: {
|
|
13509
|
+
contact_id: {
|
|
13510
|
+
type: "string",
|
|
13511
|
+
description: "UUID of the contact to unpin (the contact's own `id` \u2014 NOT the parent lead id)."
|
|
13512
|
+
}
|
|
13513
|
+
},
|
|
13514
|
+
required: ["contact_id"],
|
|
13515
|
+
additionalProperties: false
|
|
13516
|
+
},
|
|
13517
|
+
execute: async (client, params, _ctx) => {
|
|
13518
|
+
await client.requestVoid("POST", `/contacts/${params.contact_id}/unpin`);
|
|
13519
|
+
return { pinned: false, contact_id: params.contact_id, action: "unpinned" };
|
|
13520
|
+
}
|
|
13521
|
+
};
|
|
13522
|
+
}
|
|
13523
|
+
});
|
|
13524
|
+
|
|
13525
|
+
// ../core/dist/tools/update-contact.js
|
|
13526
|
+
var updateContact;
|
|
13527
|
+
var init_update_contact = __esm({
|
|
13528
|
+
"../core/dist/tools/update-contact.js"() {
|
|
13529
|
+
"use strict";
|
|
13530
|
+
init_tool_descriptions_generated();
|
|
13531
|
+
updateContact = {
|
|
13532
|
+
name: "leadbay_update_contact",
|
|
13533
|
+
description: leadbay_update_contact,
|
|
13534
|
+
write: true,
|
|
13535
|
+
annotations: {
|
|
13536
|
+
title: "Update a contact",
|
|
13537
|
+
readOnlyHint: false,
|
|
13538
|
+
destructiveHint: false,
|
|
13539
|
+
idempotentHint: true,
|
|
13540
|
+
openWorldHint: true
|
|
13541
|
+
},
|
|
13542
|
+
inputSchema: {
|
|
13543
|
+
type: "object",
|
|
13544
|
+
properties: {
|
|
13545
|
+
contact_id: {
|
|
13546
|
+
type: "string",
|
|
13547
|
+
description: "UUID of the contact to edit (the contact's own `id` \u2014 NOT the parent lead id)."
|
|
13548
|
+
},
|
|
13549
|
+
first_name: {
|
|
13550
|
+
type: "string",
|
|
13551
|
+
description: "Contact first name \u2014 REQUIRED even on an edit. Pass the current value if you're not changing it."
|
|
13552
|
+
},
|
|
13553
|
+
last_name: {
|
|
13554
|
+
type: "string",
|
|
13555
|
+
description: "Contact last name \u2014 REQUIRED even on an edit. Pass the current value if you're not changing it."
|
|
13556
|
+
},
|
|
13557
|
+
// Nullable so the agent can CLEAR a field (pass null) as well as set a
|
|
13558
|
+
// new value. execute forwards null verbatim; the backend accepts it.
|
|
13559
|
+
job_title: {
|
|
13560
|
+
type: ["string", "null"],
|
|
13561
|
+
description: "Contact job title. Pass null to clear it."
|
|
13562
|
+
},
|
|
13563
|
+
linkedin_page: {
|
|
13564
|
+
type: ["string", "null"],
|
|
13565
|
+
description: "Contact LinkedIn URL. Pass null to clear it."
|
|
13566
|
+
},
|
|
13567
|
+
email: {
|
|
13568
|
+
type: ["string", "null"],
|
|
13569
|
+
description: "Contact email. Pass null to clear it."
|
|
13570
|
+
},
|
|
13571
|
+
phone_number: {
|
|
13572
|
+
type: ["string", "null"],
|
|
13573
|
+
description: "Contact phone (free-form). Pass null to clear it."
|
|
13574
|
+
}
|
|
13575
|
+
},
|
|
13576
|
+
required: ["contact_id", "first_name", "last_name"],
|
|
13577
|
+
additionalProperties: false
|
|
13578
|
+
},
|
|
13579
|
+
execute: async (client, params, _ctx) => {
|
|
13580
|
+
const body = {
|
|
13581
|
+
first_name: params.first_name,
|
|
13582
|
+
last_name: params.last_name
|
|
13583
|
+
};
|
|
13584
|
+
if (params.job_title !== void 0)
|
|
13585
|
+
body.job_title = params.job_title;
|
|
13586
|
+
if (params.linkedin_page !== void 0)
|
|
13587
|
+
body.linkedin_page = params.linkedin_page;
|
|
13588
|
+
if (params.email !== void 0)
|
|
13589
|
+
body.email = params.email;
|
|
13590
|
+
if (params.phone_number !== void 0)
|
|
13591
|
+
body.phone_number = params.phone_number;
|
|
13592
|
+
const contact = await client.request("POST", `/contacts/${params.contact_id}/update`, body);
|
|
13593
|
+
return { updated: true, contact_id: params.contact_id, contact };
|
|
13594
|
+
}
|
|
13595
|
+
};
|
|
13596
|
+
}
|
|
13597
|
+
});
|
|
13598
|
+
|
|
13097
13599
|
// ../core/dist/composite/prepare-outreach.js
|
|
13098
13600
|
function normalizeLinkedinPage2(v) {
|
|
13099
13601
|
if (v == null)
|
|
@@ -13316,6 +13818,35 @@ function summarise(responses) {
|
|
|
13316
13818
|
}
|
|
13317
13819
|
return { answered, total, avg_qualification_boost: avg, best_response_excerpt: excerpt };
|
|
13318
13820
|
}
|
|
13821
|
+
function buildPullLeadsNextSteps(args) {
|
|
13822
|
+
const { leadCount, hasMore, nextPage } = args;
|
|
13823
|
+
if (leadCount <= 0)
|
|
13824
|
+
return null;
|
|
13825
|
+
const options = [];
|
|
13826
|
+
options.push({
|
|
13827
|
+
label: "Triage board",
|
|
13828
|
+
description: "Build an interactive lead triage board to sort and filter this batch.",
|
|
13829
|
+
kind: "build_artifact"
|
|
13830
|
+
});
|
|
13831
|
+
options.push({
|
|
13832
|
+
label: "Deepen qualification",
|
|
13833
|
+
description: "Run deeper AI qualification on these leads.",
|
|
13834
|
+
kind: "qualify_deeper"
|
|
13835
|
+
});
|
|
13836
|
+
if (hasMore && nextPage != null) {
|
|
13837
|
+
options.push({
|
|
13838
|
+
label: "Next page",
|
|
13839
|
+
description: `Pull page ${nextPage + 1} of this lens.`,
|
|
13840
|
+
kind: "pull_next_page"
|
|
13841
|
+
});
|
|
13842
|
+
}
|
|
13843
|
+
options.push({
|
|
13844
|
+
label: "Refine audience",
|
|
13845
|
+
description: "Adjust the lens audience / filters (sector, size, prompt).",
|
|
13846
|
+
kind: "refine_audience"
|
|
13847
|
+
});
|
|
13848
|
+
return { question: "What do you want to do next?", options: options.slice(0, 4) };
|
|
13849
|
+
}
|
|
13319
13850
|
var pullLeads;
|
|
13320
13851
|
var init_pull_leads = __esm({
|
|
13321
13852
|
"../core/dist/composite/pull-leads.js"() {
|
|
@@ -13386,6 +13917,24 @@ var init_pull_leads = __esm({
|
|
|
13386
13917
|
type: "boolean",
|
|
13387
13918
|
description: "True if scoring is still running."
|
|
13388
13919
|
},
|
|
13920
|
+
next_steps: {
|
|
13921
|
+
type: ["object", "null"],
|
|
13922
|
+
description: "Ready-made NEXT STEPS for the host's choice widget. Each option has a SHORT `label` (\u22645 words, fits AskUserQuestion's label cap on Claude cowork/Claude Code) and a full `description`. For AskUserQuestion (cowork/Claude Code) pass each option as {label, description}. For ask_user_input_v0 (Claude chat/ChatGPT, string-only options) use the `description` as the option string. Use these VERBATIM, in order \u2014 do NOT re-derive, reword, or render as prose when a widget tool exists. options[0] is the artifact offer (build the lead triage board) whenever the batch is non-empty. null only when the batch is empty.",
|
|
13923
|
+
properties: {
|
|
13924
|
+
question: { type: "string" },
|
|
13925
|
+
options: {
|
|
13926
|
+
type: "array",
|
|
13927
|
+
items: {
|
|
13928
|
+
type: "object",
|
|
13929
|
+
properties: {
|
|
13930
|
+
label: { type: "string" },
|
|
13931
|
+
description: { type: "string" },
|
|
13932
|
+
kind: { type: "string" }
|
|
13933
|
+
}
|
|
13934
|
+
}
|
|
13935
|
+
}
|
|
13936
|
+
}
|
|
13937
|
+
},
|
|
13389
13938
|
_meta: {
|
|
13390
13939
|
type: "object",
|
|
13391
13940
|
description: "Operator context: region + last-call latency.",
|
|
@@ -13460,6 +14009,8 @@ var init_pull_leads = __esm({
|
|
|
13460
14009
|
const currentPage = res.pagination?.page ?? page;
|
|
13461
14010
|
const hasMore = currentPage < totalPages - 1;
|
|
13462
14011
|
const nextPage = hasMore ? currentPage + 1 : null;
|
|
14012
|
+
const leadCount = res.items.length;
|
|
14013
|
+
const nextSteps = buildPullLeadsNextSteps({ leadCount, hasMore, nextPage });
|
|
13463
14014
|
return withAgentMemoryMeta(client, {
|
|
13464
14015
|
lens: { id: lensId },
|
|
13465
14016
|
leads: res.items.map((lead) => ({
|
|
@@ -13471,6 +14022,7 @@ var init_pull_leads = __esm({
|
|
|
13471
14022
|
next_page: nextPage,
|
|
13472
14023
|
computing_wishlist: res.computing_wishlist,
|
|
13473
14024
|
computing_scores: res.computing_scores,
|
|
14025
|
+
next_steps: nextSteps,
|
|
13474
14026
|
_meta: {
|
|
13475
14027
|
region: client.region,
|
|
13476
14028
|
latency_ms: client.lastMeta?.latency_ms ?? null
|
|
@@ -14587,6 +15139,46 @@ var init_campaign_call_sheet = __esm({
|
|
|
14587
15139
|
}
|
|
14588
15140
|
});
|
|
14589
15141
|
|
|
15142
|
+
// ../core/dist/composite/_web-fetch-helpers.js
|
|
15143
|
+
function splitEmojiSection(key) {
|
|
15144
|
+
const m = key.match(/^([^\p{L}\p{N}\s]+)\s+(.+)$/u);
|
|
15145
|
+
if (m)
|
|
15146
|
+
return { emoji: m[1], label: m[2] };
|
|
15147
|
+
return { emoji: null, label: key };
|
|
15148
|
+
}
|
|
15149
|
+
function reshapeWebFetchContent(content) {
|
|
15150
|
+
if (!content)
|
|
15151
|
+
return [];
|
|
15152
|
+
const sections = [];
|
|
15153
|
+
for (const [key, val] of Object.entries(content)) {
|
|
15154
|
+
if (!Array.isArray(val))
|
|
15155
|
+
continue;
|
|
15156
|
+
const { emoji, label } = splitEmojiSection(key);
|
|
15157
|
+
sections.push({
|
|
15158
|
+
section_label: label,
|
|
15159
|
+
section_emoji: emoji,
|
|
15160
|
+
entries: val
|
|
15161
|
+
});
|
|
15162
|
+
}
|
|
15163
|
+
sections.sort((a, b) => {
|
|
15164
|
+
const ai = SECTION_PRIORITY.findIndex((p) => a.section_label.toLowerCase().includes(p));
|
|
15165
|
+
const bi = SECTION_PRIORITY.findIndex((p) => b.section_label.toLowerCase().includes(p));
|
|
15166
|
+
const aN = ai < 0 ? SECTION_PRIORITY.length : ai;
|
|
15167
|
+
const bN = bi < 0 ? SECTION_PRIORITY.length : bi;
|
|
15168
|
+
if (aN !== bN)
|
|
15169
|
+
return aN - bN;
|
|
15170
|
+
return a.section_label.localeCompare(b.section_label);
|
|
15171
|
+
});
|
|
15172
|
+
return sections;
|
|
15173
|
+
}
|
|
15174
|
+
var SECTION_PRIORITY;
|
|
15175
|
+
var init_web_fetch_helpers = __esm({
|
|
15176
|
+
"../core/dist/composite/_web-fetch-helpers.js"() {
|
|
15177
|
+
"use strict";
|
|
15178
|
+
SECTION_PRIORITY = ["profile", "signals", "clues"];
|
|
15179
|
+
}
|
|
15180
|
+
});
|
|
15181
|
+
|
|
14590
15182
|
// ../core/dist/composite/research-lead-by-id.js
|
|
14591
15183
|
function normalizeLinkedinPage5(v) {
|
|
14592
15184
|
if (v == null)
|
|
@@ -14702,37 +15294,6 @@ _Truncated_: ${shape.truncation_hint ?? "response trimmed"}_`);
|
|
|
14702
15294
|
}
|
|
14703
15295
|
return out.join("\n");
|
|
14704
15296
|
}
|
|
14705
|
-
function splitEmojiSection(key) {
|
|
14706
|
-
const m = key.match(/^([^\p{L}\p{N}\s]+)\s+(.+)$/u);
|
|
14707
|
-
if (m)
|
|
14708
|
-
return { emoji: m[1], label: m[2] };
|
|
14709
|
-
return { emoji: null, label: key };
|
|
14710
|
-
}
|
|
14711
|
-
function reshapeWebFetchContent(content) {
|
|
14712
|
-
if (!content)
|
|
14713
|
-
return [];
|
|
14714
|
-
const sections = [];
|
|
14715
|
-
for (const [key, val] of Object.entries(content)) {
|
|
14716
|
-
if (!Array.isArray(val))
|
|
14717
|
-
continue;
|
|
14718
|
-
const { emoji, label } = splitEmojiSection(key);
|
|
14719
|
-
sections.push({
|
|
14720
|
-
section_label: label,
|
|
14721
|
-
section_emoji: emoji,
|
|
14722
|
-
entries: val
|
|
14723
|
-
});
|
|
14724
|
-
}
|
|
14725
|
-
sections.sort((a, b) => {
|
|
14726
|
-
const ai = SECTION_PRIORITY.findIndex((p) => a.section_label.toLowerCase().includes(p));
|
|
14727
|
-
const bi = SECTION_PRIORITY.findIndex((p) => b.section_label.toLowerCase().includes(p));
|
|
14728
|
-
const aN = ai < 0 ? SECTION_PRIORITY.length : ai;
|
|
14729
|
-
const bN = bi < 0 ? SECTION_PRIORITY.length : bi;
|
|
14730
|
-
if (aN !== bN)
|
|
14731
|
-
return aN - bN;
|
|
14732
|
-
return a.section_label.localeCompare(b.section_label);
|
|
14733
|
-
});
|
|
14734
|
-
return sections;
|
|
14735
|
-
}
|
|
14736
15297
|
function isReachable(c) {
|
|
14737
15298
|
if (!c)
|
|
14738
15299
|
return false;
|
|
@@ -14740,13 +15301,13 @@ function isReachable(c) {
|
|
|
14740
15301
|
const phone = typeof c.phone_number === "string" ? c.phone_number.trim() : "";
|
|
14741
15302
|
return email.length > 0 || phone.length > 0;
|
|
14742
15303
|
}
|
|
14743
|
-
var
|
|
15304
|
+
var researchLeadById, _innerExecute;
|
|
14744
15305
|
var init_research_lead_by_id = __esm({
|
|
14745
15306
|
"../core/dist/composite/research-lead-by-id.js"() {
|
|
14746
15307
|
"use strict";
|
|
14747
15308
|
init_agent_memory();
|
|
15309
|
+
init_web_fetch_helpers();
|
|
14748
15310
|
init_tool_descriptions_generated();
|
|
14749
|
-
SECTION_PRIORITY = ["profile", "signals", "clues"];
|
|
14750
15311
|
researchLeadById = {
|
|
14751
15312
|
name: "leadbay_research_lead_by_id",
|
|
14752
15313
|
annotations: {
|
|
@@ -15321,6 +15882,326 @@ var init_account_history = __esm({
|
|
|
15321
15882
|
}
|
|
15322
15883
|
});
|
|
15323
15884
|
|
|
15885
|
+
// ../core/dist/composite/scan-portfolio-signals.js
|
|
15886
|
+
function fold(s) {
|
|
15887
|
+
return s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase().trim();
|
|
15888
|
+
}
|
|
15889
|
+
function parseQueryTerms(query) {
|
|
15890
|
+
return query.split(/[,\s]+/).map((t) => fold(t)).filter((t) => t.length > 0);
|
|
15891
|
+
}
|
|
15892
|
+
function shortLocation(loc) {
|
|
15893
|
+
if (loc == null)
|
|
15894
|
+
return null;
|
|
15895
|
+
if (typeof loc === "string")
|
|
15896
|
+
return loc.trim() || null;
|
|
15897
|
+
if (typeof loc === "object") {
|
|
15898
|
+
const o = loc;
|
|
15899
|
+
const clean = (v) => {
|
|
15900
|
+
const s = typeof v === "string" ? v.trim() : "";
|
|
15901
|
+
return s && s.toUpperCase() !== "N/A" ? s : "";
|
|
15902
|
+
};
|
|
15903
|
+
const city = clean(o.city);
|
|
15904
|
+
const state = clean(o.state);
|
|
15905
|
+
if (city && state)
|
|
15906
|
+
return `${city}, ${state}`;
|
|
15907
|
+
if (city)
|
|
15908
|
+
return city;
|
|
15909
|
+
if (typeof o.full === "string" && o.full.trim())
|
|
15910
|
+
return o.full.trim();
|
|
15911
|
+
}
|
|
15912
|
+
return null;
|
|
15913
|
+
}
|
|
15914
|
+
function mergeLocationIds2(filter, ids) {
|
|
15915
|
+
const criteria = filter?.criteria ? [...filter.criteria] : [];
|
|
15916
|
+
const idx = criteria.findIndex((c) => c?.type === "location_ids" && c?.is_excluded === false);
|
|
15917
|
+
if (idx >= 0) {
|
|
15918
|
+
const cur = criteria[idx];
|
|
15919
|
+
const existing = Array.isArray(cur.locations) ? cur.locations : [];
|
|
15920
|
+
const merged = Array.from(/* @__PURE__ */ new Set([...existing, ...ids]));
|
|
15921
|
+
criteria[idx] = { ...cur, locations: merged };
|
|
15922
|
+
} else {
|
|
15923
|
+
criteria.push({ type: "location_ids", is_excluded: false, locations: ids });
|
|
15924
|
+
}
|
|
15925
|
+
return { criteria };
|
|
15926
|
+
}
|
|
15927
|
+
function entryMatches(entry, sectionLabel, terms) {
|
|
15928
|
+
if (terms.length === 0)
|
|
15929
|
+
return false;
|
|
15930
|
+
const haystack = fold([entry.description ?? "", entry.source ?? "", sectionLabel].join(" "));
|
|
15931
|
+
return terms.some((t) => haystack.includes(t));
|
|
15932
|
+
}
|
|
15933
|
+
function passesSince(entry, sinceMs) {
|
|
15934
|
+
if (sinceMs == null)
|
|
15935
|
+
return true;
|
|
15936
|
+
if (!entry.date)
|
|
15937
|
+
return true;
|
|
15938
|
+
const ts = Date.parse(entry.date);
|
|
15939
|
+
if (Number.isNaN(ts))
|
|
15940
|
+
return true;
|
|
15941
|
+
return ts >= sinceMs;
|
|
15942
|
+
}
|
|
15943
|
+
var DEFAULT_MAX_LEADS, HARD_MAX_LEADS, MONITOR_PAGE_SIZE, scanPortfolioSignals;
|
|
15944
|
+
var init_scan_portfolio_signals = __esm({
|
|
15945
|
+
"../core/dist/composite/scan-portfolio-signals.js"() {
|
|
15946
|
+
"use strict";
|
|
15947
|
+
init_agent_memory();
|
|
15948
|
+
init_web_fetch_helpers();
|
|
15949
|
+
init_geo_helpers();
|
|
15950
|
+
init_tool_descriptions_generated();
|
|
15951
|
+
DEFAULT_MAX_LEADS = 200;
|
|
15952
|
+
HARD_MAX_LEADS = 300;
|
|
15953
|
+
MONITOR_PAGE_SIZE = 200;
|
|
15954
|
+
scanPortfolioSignals = {
|
|
15955
|
+
name: "leadbay_scan_portfolio_signals",
|
|
15956
|
+
annotations: {
|
|
15957
|
+
title: "Scan a portfolio for a web-research signal in bulk",
|
|
15958
|
+
readOnlyHint: true,
|
|
15959
|
+
destructiveHint: false,
|
|
15960
|
+
idempotentHint: true,
|
|
15961
|
+
openWorldHint: true
|
|
15962
|
+
},
|
|
15963
|
+
description: leadbay_scan_portfolio_signals,
|
|
15964
|
+
inputSchema: {
|
|
15965
|
+
type: "object",
|
|
15966
|
+
properties: {
|
|
15967
|
+
query: {
|
|
15968
|
+
type: "string",
|
|
15969
|
+
description: "Signal terms to match (case- and accent-insensitive). Comma- or space-separated terms are OR'd, e.g. 'M&A, acquisition, rachet\xE9'. Matched against each signal entry's description, source, and section label."
|
|
15970
|
+
},
|
|
15971
|
+
leadIds: {
|
|
15972
|
+
type: "array",
|
|
15973
|
+
items: { type: "string" },
|
|
15974
|
+
description: "Explicit lead UUIDs to scan (skips Monitor pagination). Use when you already hold a cohort of ids."
|
|
15975
|
+
},
|
|
15976
|
+
city: {
|
|
15977
|
+
type: "string",
|
|
15978
|
+
description: "Free-text city / region to scope the Monitor portfolio before scanning (resolved via /geo/search, same as leadbay_pull_followups). Ignored when `leadIds` is given."
|
|
15979
|
+
},
|
|
15980
|
+
city_id: {
|
|
15981
|
+
type: "string",
|
|
15982
|
+
description: "Pre-resolved admin_area id (numeric string). Bypasses the resolver. Ignored when `leadIds` is given."
|
|
15983
|
+
},
|
|
15984
|
+
set_filter: {
|
|
15985
|
+
type: "object",
|
|
15986
|
+
description: "Optional Monitor FilterItem ({criteria: FilterCriterion[]}) to scope the portfolio before scanning. Persisted server-side then applied, mirroring leadbay_pull_followups. Ignored when `leadIds` is given.",
|
|
15987
|
+
properties: {
|
|
15988
|
+
criteria: { type: "array", items: { type: "object" } }
|
|
15989
|
+
}
|
|
15990
|
+
},
|
|
15991
|
+
since: {
|
|
15992
|
+
type: "string",
|
|
15993
|
+
description: "ISO date (e.g. '2025-01-01'). When set, only signal entries dated on/after it are returned. Entries with no date are kept (absence of a date is not evidence the event is old)."
|
|
15994
|
+
},
|
|
15995
|
+
max_leads: {
|
|
15996
|
+
type: "number",
|
|
15997
|
+
description: `Cap on leads scanned (default ${DEFAULT_MAX_LEADS}, hard max ${HARD_MAX_LEADS}). When the portfolio exceeds this, the scan is truncated and truncated_at is set.`
|
|
15998
|
+
}
|
|
15999
|
+
},
|
|
16000
|
+
required: ["query"],
|
|
16001
|
+
additionalProperties: false
|
|
16002
|
+
},
|
|
16003
|
+
outputSchema: {
|
|
16004
|
+
type: "object",
|
|
16005
|
+
properties: {
|
|
16006
|
+
matched: {
|
|
16007
|
+
type: "array",
|
|
16008
|
+
description: "Leads with \u22651 signal entry matching the query. Each: {lead_id, name, location, matched_signals:[{section_label, section_emoji, hot, source, date, description}]}. Campaign-ready \u2014 feed lead_ids straight into leadbay_add_leads_to_campaign.",
|
|
16009
|
+
items: { type: "object" }
|
|
16010
|
+
},
|
|
16011
|
+
not_researched: {
|
|
16012
|
+
type: "array",
|
|
16013
|
+
description: "Leads scanned that had NO cached signal content (web_fetch.content null or still in progress). These are NOT 'no match' \u2014 they were never researched. Qualify them (leadbay_bulk_qualify_leads) then re-scan. Each: {lead_id, name}.",
|
|
16014
|
+
items: { type: "object" }
|
|
16015
|
+
},
|
|
16016
|
+
scanned_count: {
|
|
16017
|
+
type: "number",
|
|
16018
|
+
description: "Total leads read in this scan (matched + non-matching + not_researched)."
|
|
16019
|
+
},
|
|
16020
|
+
matched_count: { type: "number", description: "Length of `matched`." },
|
|
16021
|
+
truncated_at: {
|
|
16022
|
+
type: "number",
|
|
16023
|
+
description: "Present only when the portfolio exceeded `max_leads`; equals the cap applied. Coverage is partial \u2014 narrow the scope (city / set_filter) or raise max_leads."
|
|
16024
|
+
},
|
|
16025
|
+
quota_exceeded: {
|
|
16026
|
+
type: "boolean",
|
|
16027
|
+
description: "True if a 429 was hit mid-scan. Partial `matched` is still returned. Offer wait-for-reset OR top-up."
|
|
16028
|
+
},
|
|
16029
|
+
status: {
|
|
16030
|
+
type: "string",
|
|
16031
|
+
description: "`ambiguous_locations` when a passed `city` matched multiple admin_areas; pick an id from `location_ambiguities` and re-call with `city_id`. Absent on the happy path."
|
|
16032
|
+
},
|
|
16033
|
+
location_ambiguities: {
|
|
16034
|
+
type: "array",
|
|
16035
|
+
description: "Only present when status === 'ambiguous_locations'.",
|
|
16036
|
+
items: { type: "object" }
|
|
16037
|
+
},
|
|
16038
|
+
_meta: {
|
|
16039
|
+
type: "object",
|
|
16040
|
+
properties: {
|
|
16041
|
+
region: { type: "string" },
|
|
16042
|
+
agent_memory: { type: "object" }
|
|
16043
|
+
}
|
|
16044
|
+
}
|
|
16045
|
+
},
|
|
16046
|
+
required: ["matched", "not_researched", "scanned_count", "matched_count", "quota_exceeded"]
|
|
16047
|
+
},
|
|
16048
|
+
execute: async (client, params, ctx) => {
|
|
16049
|
+
const terms = parseQueryTerms(params.query ?? "");
|
|
16050
|
+
const maxLeads = Math.min(params.max_leads ?? DEFAULT_MAX_LEADS, HARD_MAX_LEADS);
|
|
16051
|
+
const sinceParsed = params.since ? Date.parse(params.since) : NaN;
|
|
16052
|
+
const sinceValid = Number.isNaN(sinceParsed) ? null : sinceParsed;
|
|
16053
|
+
let portfolio;
|
|
16054
|
+
let truncatedAt;
|
|
16055
|
+
let quotaExceeded = false;
|
|
16056
|
+
if (params.leadIds && params.leadIds.length > 0) {
|
|
16057
|
+
const sliced = params.leadIds.slice(0, maxLeads);
|
|
16058
|
+
if (params.leadIds.length > maxLeads)
|
|
16059
|
+
truncatedAt = maxLeads;
|
|
16060
|
+
portfolio = sliced.map((id) => ({ id, name: null, location: null }));
|
|
16061
|
+
} else {
|
|
16062
|
+
let effectiveSetFilter = params.set_filter;
|
|
16063
|
+
const geoTexts = [];
|
|
16064
|
+
if (params.city)
|
|
16065
|
+
geoTexts.push(params.city);
|
|
16066
|
+
if (params.city_id)
|
|
16067
|
+
geoTexts.push(params.city_id);
|
|
16068
|
+
if (geoTexts.length > 0) {
|
|
16069
|
+
const { resolved, ambiguities } = await resolveLocations(client, geoTexts);
|
|
16070
|
+
if (ambiguities.length > 0) {
|
|
16071
|
+
return withAgentMemoryMeta(client, {
|
|
16072
|
+
status: "ambiguous_locations",
|
|
16073
|
+
location_ambiguities: ambiguities,
|
|
16074
|
+
matched: [],
|
|
16075
|
+
not_researched: [],
|
|
16076
|
+
scanned_count: 0,
|
|
16077
|
+
matched_count: 0,
|
|
16078
|
+
quota_exceeded: false,
|
|
16079
|
+
_meta: { region: client.region }
|
|
16080
|
+
}, ctx);
|
|
16081
|
+
}
|
|
16082
|
+
if (resolved.length > 0) {
|
|
16083
|
+
effectiveSetFilter = mergeLocationIds2(effectiveSetFilter, resolved);
|
|
16084
|
+
}
|
|
16085
|
+
}
|
|
16086
|
+
let filterStored = false;
|
|
16087
|
+
if (effectiveSetFilter) {
|
|
16088
|
+
try {
|
|
16089
|
+
await client.requestVoid("POST", "/monitor/filter", effectiveSetFilter);
|
|
16090
|
+
filterStored = true;
|
|
16091
|
+
} catch (err) {
|
|
16092
|
+
if (err?.code === "QUOTA_EXCEEDED")
|
|
16093
|
+
quotaExceeded = true;
|
|
16094
|
+
ctx?.logger?.warn?.(`scan_portfolio_signals: POST /monitor/filter failed (${err?.code ?? err?.message ?? err}); scanning UNfiltered to avoid trusting a stale server-side filter`);
|
|
16095
|
+
}
|
|
16096
|
+
}
|
|
16097
|
+
portfolio = [];
|
|
16098
|
+
let page = 0;
|
|
16099
|
+
while (portfolio.length < maxLeads) {
|
|
16100
|
+
const qs = new URLSearchParams({
|
|
16101
|
+
personal: "false",
|
|
16102
|
+
liked: "false",
|
|
16103
|
+
filtered: String(filterStored),
|
|
16104
|
+
count: String(MONITOR_PAGE_SIZE),
|
|
16105
|
+
page: String(page)
|
|
16106
|
+
}).toString();
|
|
16107
|
+
let monitor;
|
|
16108
|
+
try {
|
|
16109
|
+
monitor = await client.request("GET", `/monitor?${qs}`);
|
|
16110
|
+
} catch (err) {
|
|
16111
|
+
if (err?.code === "QUOTA_EXCEEDED") {
|
|
16112
|
+
quotaExceeded = true;
|
|
16113
|
+
break;
|
|
16114
|
+
}
|
|
16115
|
+
throw err;
|
|
16116
|
+
}
|
|
16117
|
+
const rawLeads = Array.isArray(monitor.items) ? monitor.items : Array.isArray(monitor.leads) ? monitor.leads : Array.isArray(monitor) ? monitor : [];
|
|
16118
|
+
if (rawLeads.length === 0)
|
|
16119
|
+
break;
|
|
16120
|
+
for (const lead of rawLeads) {
|
|
16121
|
+
if (portfolio.length >= maxLeads)
|
|
16122
|
+
break;
|
|
16123
|
+
portfolio.push({
|
|
16124
|
+
id: lead.id,
|
|
16125
|
+
name: lead.name ?? null,
|
|
16126
|
+
location: shortLocation(lead.location)
|
|
16127
|
+
});
|
|
16128
|
+
}
|
|
16129
|
+
const pages = monitor.pagination?.pages;
|
|
16130
|
+
if (typeof pages === "number" && page >= pages - 1)
|
|
16131
|
+
break;
|
|
16132
|
+
if (rawLeads.length < MONITOR_PAGE_SIZE)
|
|
16133
|
+
break;
|
|
16134
|
+
page += 1;
|
|
16135
|
+
}
|
|
16136
|
+
if (portfolio.length >= maxLeads)
|
|
16137
|
+
truncatedAt = maxLeads;
|
|
16138
|
+
}
|
|
16139
|
+
const matched = [];
|
|
16140
|
+
const notResearched = [];
|
|
16141
|
+
const reads = await Promise.all(portfolio.map(async (lead) => {
|
|
16142
|
+
try {
|
|
16143
|
+
const wf = await client.request("GET", `/leads/${lead.id}/web_fetch`);
|
|
16144
|
+
return { lead, wf, error: null };
|
|
16145
|
+
} catch (error) {
|
|
16146
|
+
return { lead, wf: null, error };
|
|
16147
|
+
}
|
|
16148
|
+
}));
|
|
16149
|
+
for (const r of reads) {
|
|
16150
|
+
const { lead, wf, error } = r;
|
|
16151
|
+
if (error) {
|
|
16152
|
+
if (error?.code === "QUOTA_EXCEEDED")
|
|
16153
|
+
quotaExceeded = true;
|
|
16154
|
+
notResearched.push({ lead_id: lead.id, name: lead.name });
|
|
16155
|
+
continue;
|
|
16156
|
+
}
|
|
16157
|
+
const hasContent = wf && wf.content != null && wf.in_progress !== true && Object.keys(wf.content).length > 0;
|
|
16158
|
+
if (!hasContent) {
|
|
16159
|
+
notResearched.push({ lead_id: lead.id, name: lead.name });
|
|
16160
|
+
continue;
|
|
16161
|
+
}
|
|
16162
|
+
const sections = reshapeWebFetchContent(wf.content);
|
|
16163
|
+
const matchedSignals = [];
|
|
16164
|
+
for (const sec of sections) {
|
|
16165
|
+
for (const entry of sec.entries) {
|
|
16166
|
+
if (!entryMatches(entry, sec.section_label, terms))
|
|
16167
|
+
continue;
|
|
16168
|
+
if (!passesSince(entry, sinceValid))
|
|
16169
|
+
continue;
|
|
16170
|
+
matchedSignals.push({
|
|
16171
|
+
section_label: sec.section_label,
|
|
16172
|
+
section_emoji: sec.section_emoji,
|
|
16173
|
+
hot: entry.hot === true,
|
|
16174
|
+
source: entry.source ?? "",
|
|
16175
|
+
date: entry.date ?? null,
|
|
16176
|
+
description: entry.description ?? ""
|
|
16177
|
+
});
|
|
16178
|
+
}
|
|
16179
|
+
}
|
|
16180
|
+
if (matchedSignals.length > 0) {
|
|
16181
|
+
matched.push({
|
|
16182
|
+
lead_id: lead.id,
|
|
16183
|
+
name: lead.name,
|
|
16184
|
+
location: lead.location,
|
|
16185
|
+
matched_signals: matchedSignals
|
|
16186
|
+
});
|
|
16187
|
+
}
|
|
16188
|
+
}
|
|
16189
|
+
const out = {
|
|
16190
|
+
matched,
|
|
16191
|
+
not_researched: notResearched,
|
|
16192
|
+
scanned_count: portfolio.length,
|
|
16193
|
+
matched_count: matched.length,
|
|
16194
|
+
quota_exceeded: quotaExceeded,
|
|
16195
|
+
_meta: { region: client.region }
|
|
16196
|
+
};
|
|
16197
|
+
if (truncatedAt !== void 0)
|
|
16198
|
+
out.truncated_at = truncatedAt;
|
|
16199
|
+
return withAgentMemoryMeta(client, out, ctx);
|
|
16200
|
+
}
|
|
16201
|
+
};
|
|
16202
|
+
}
|
|
16203
|
+
});
|
|
16204
|
+
|
|
15324
16205
|
// ../core/dist/composite/recall-ordered-titles.js
|
|
15325
16206
|
var recallOrderedTitles;
|
|
15326
16207
|
var init_recall_ordered_titles = __esm({
|
|
@@ -20486,6 +21367,7 @@ __export(dist_exports, {
|
|
|
20486
21367
|
resolveImportRows: () => resolveImportRows,
|
|
20487
21368
|
resolveRegion: () => resolveRegion,
|
|
20488
21369
|
reviseHintFor: () => reviseHintFor,
|
|
21370
|
+
scanPortfolioSignals: () => scanPortfolioSignals,
|
|
20489
21371
|
seedCandidates: () => seedCandidates,
|
|
20490
21372
|
selectLeads: () => selectLeads,
|
|
20491
21373
|
setActiveLens: () => setActiveLens,
|
|
@@ -20560,6 +21442,11 @@ var init_dist = __esm({
|
|
|
20560
21442
|
init_create_custom_field();
|
|
20561
21443
|
init_like_lead();
|
|
20562
21444
|
init_dislike_lead();
|
|
21445
|
+
init_add_contact();
|
|
21446
|
+
init_remove_contact();
|
|
21447
|
+
init_pin_contact();
|
|
21448
|
+
init_unpin_contact();
|
|
21449
|
+
init_update_contact();
|
|
20563
21450
|
init_prepare_outreach();
|
|
20564
21451
|
init_pull_leads();
|
|
20565
21452
|
init_pull_followups();
|
|
@@ -20574,6 +21461,7 @@ var init_dist = __esm({
|
|
|
20574
21461
|
init_research_lead_by_id();
|
|
20575
21462
|
init_research_lead_by_name_fuzzy();
|
|
20576
21463
|
init_account_history();
|
|
21464
|
+
init_scan_portfolio_signals();
|
|
20577
21465
|
init_recall_ordered_titles();
|
|
20578
21466
|
init_account_status();
|
|
20579
21467
|
init_bulk_qualify_leads();
|
|
@@ -20673,6 +21561,10 @@ var init_dist = __esm({
|
|
|
20673
21561
|
// reprioritize-a-neglected-account workflow (#3630 GAP C) must work in a
|
|
20674
21562
|
// default deployment without LEADBAY_MCP_ADVANCED=1.
|
|
20675
21563
|
accountHistory,
|
|
21564
|
+
// Bulk portfolio signal scan — read-only, no quota burn. The single-call
|
|
21565
|
+
// answer to "which of my leads have signal X" that previously forced a
|
|
21566
|
+
// per-lead research_lead_by_id loop (issue #3704).
|
|
21567
|
+
scanPortfolioSignals,
|
|
20676
21568
|
recallOrderedTitles,
|
|
20677
21569
|
accountStatus,
|
|
20678
21570
|
bulkEnrichStatus,
|
|
@@ -20722,6 +21614,18 @@ var init_dist = __esm({
|
|
|
20722
21614
|
reportOutreach,
|
|
20723
21615
|
importLeads,
|
|
20724
21616
|
importAndQualify,
|
|
21617
|
+
// Contact management (product#3703) — each is a single-call relay, so
|
|
21618
|
+
// granular-shaped and living in tools/; registered HERE (not granular-gated)
|
|
21619
|
+
// so reps can manage contacts in-conversation without LEADBAY_MCP_ADVANCED.
|
|
21620
|
+
// Same pattern as likeLead/dislikeLead below. Endpoints (all direct, the
|
|
21621
|
+
// ones the web UI uses — NOT the import pipeline, which 401s on some
|
|
21622
|
+
// accounts): add → POST /leads/{id}/contacts; remove → archive;
|
|
21623
|
+
// pin/unpin → /pin|/unpin; update → /update (snake_case, first/last required).
|
|
21624
|
+
addContact,
|
|
21625
|
+
removeContact,
|
|
21626
|
+
pinContact,
|
|
21627
|
+
unpinContact,
|
|
21628
|
+
updateContact,
|
|
20725
21629
|
// createCustomField is granular-shaped but file-import prompts depend on it
|
|
20726
21630
|
// to preserve source-system links without requiring advanced-tool exposure.
|
|
20727
21631
|
createCustomField,
|
|
@@ -20785,10 +21689,10 @@ import {
|
|
|
20785
21689
|
var leadbay_daily_check_in = `
|
|
20786
21690
|
## MEMORY
|
|
20787
21691
|
|
|
20788
|
-
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
|
|
21692
|
+
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.
|
|
20789
21693
|
|
|
20790
21694
|
|
|
20791
|
-
Run the Leadbay daily check-in for me. Treat this prompt the same way for any equivalent ask focused on NEW leads from the Discover wishlist: "get me leadbay leads", "best NEW leads to prospect today", "what's new today", "show me my batch", "let's prospect". For follow-up phrasings ("what should I follow up on", "leads I've already worked", "before my trip"), this is the wrong prompt \u2014 route to \`leadbay_followup_check_in\` instead. If the user's intent is ambiguous ("what should I work on?"), ASK once before picking an entry point.
|
|
21695
|
+
Run the Leadbay daily check-in for me. Treat this prompt the same way for any equivalent ask focused on NEW leads from the Discover wishlist: "get me leadbay leads", "best NEW leads to prospect today", "what's new today", "show me my batch", "let's prospect", "run my morning check-in", "my daily routine", "I do this every day", "every morning". For follow-up phrasings ("what should I follow up on", "leads I've already worked", "before my trip"), this is the wrong prompt \u2014 route to \`leadbay_followup_check_in\` instead. **Recurrence language ("I do this every day", "every morning", "my routine") always means this prompt \u2014 it is a daily batch check-in, not a follow-up.** If the user's intent is ambiguous ("what should I work on?"), ASK once before picking an entry point.
|
|
20792
21696
|
|
|
20793
21697
|
# Resilience rules for Leadbay long-running tools
|
|
20794
21698
|
|
|
@@ -20914,13 +21818,66 @@ If the batch returns fewer than 10 qualified leads, top it up: call \`leadbay_bu
|
|
|
20914
21818
|
|
|
20915
21819
|
# PHASE 4 \u2014 DEEP DIVE (every promising lead)
|
|
20916
21820
|
|
|
21821
|
+
**Skip Phase 4 if the user's request is primarily to view the batch** (e.g., "show me today's leads", "run my morning check-in", "what's in my inbox") \u2014 proceed directly to NEXT STEPS. Run Phase 4 when the user explicitly asks to research leads, names a specific company, or says "and then research" / "deep dive" / "tell me more about".
|
|
21822
|
+
|
|
20917
21823
|
Call \`leadbay_research_lead_by_id\` on **every** lead from your top 10 that the user might realistically prospect today (filter out clearly weak fits if any). Don't pick just one. **Call it sequentially** \u2014 one at a time, or batches of at most 3 in parallel. Do not fire 10 in parallel \u2014 it triggers transport backpressure that surfaces as \`"Tool permission stream closed"\` errors (see Rule 3 above). If a call fails, retry that single lead once; if the retry also fails, note the lead id and continue. Report Phase 4 results even if 1\u20132 leads were unresearchable.
|
|
20918
21824
|
|
|
20919
21825
|
For each researched lead surface:
|
|
20920
21826
|
- what makes it promising (1\u20132 sentences citing signals from the research)
|
|
20921
21827
|
- the **recommended contacts** the research returns \u2014 name, title, why they're the right starting point
|
|
20922
21828
|
|
|
20923
|
-
|
|
21829
|
+
Contact enrichment is offered in the NEXT STEPS widget below \u2014 do NOT emit a separate prose question here. The widget handles the enrichment offer as one of the selectable options. If the user selects enrichment, call \`leadbay_enrich_contacts({leadId, contactId})\` ONCE PER CONTACT \u2014 the tool takes a single leadId + contactId, never a list. (For bulk title/seniority enrichment across many leads at once, use \`leadbay_enrich_titles({leadIds: [...]})\` instead.) This consumes enrichment quota.
|
|
21830
|
+
|
|
21831
|
+
# NEXT STEPS
|
|
21832
|
+
|
|
21833
|
+
**Sequential request gate:** If the user's original message contained the literal phrase "and then" (e.g., "show me X and then do Y"), and all stated actions have been completed this turn, skip the NEXT STEPS widget entirely and emit STOP directly. The user stated their full plan; they do not need a "what next?" prompt.
|
|
21834
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, go directly to STOP without any widget.
|
|
21835
|
+
- Do NOT skip for: plain single-action requests ("show me today's leads"), recurring requests ("I do this every day"), or multi-step workflows the user didn't pre-specify.
|
|
21836
|
+
|
|
21837
|
+
**REQUIRED OPTIONS \u2014 triggers and position rules:**
|
|
21838
|
+
- **Recurring language** ("every day", "every morning", "I do this every", "remind me", "automate this", "recurring"): add "Schedule 'Daily prospecting check-in' as a recurring task" and place it **first**.
|
|
21839
|
+
- **\u22655 leads returned**: add "Build an interactive lead triage board for this batch" and place it **first** (or second if the scheduling offer above also applies).
|
|
21840
|
+
|
|
21841
|
+
## NEXT STEPS \u2014 after rendering the pull_leads table
|
|
21842
|
+
|
|
21843
|
+
**ALWAYS render NEXT STEPS via your host's next-step widget.** Use whichever is in your tool set \u2014 the NAME and SCHEMA differ: **\`ask_user_input_v0\`** (Claude chat / ChatGPT) takes plain-string options with \`type:"single_select"\`; **\`AskUserQuestion\`** (Claude cowork / Claude Code) takes object options \`{label, description}\` plus a required short \`header\` (\u226412 chars) and \`multiSelect\`, NO \`type\` field, and never add an "Other" option (the host adds it). Match the schema to the tool you actually have \u2014 the wrong schema fails silently and you fall back to prose. Prose bullets are the fallback ONLY when NEITHER widget exists. Any turn that would end with a choice must be the widget \u2014 the widget IS the question.
|
|
21844
|
+
|
|
21845
|
+
**If the tool result carries a \`next_steps\` object, that is the source of truth \u2014 use it directly.** Each option has a short \`.label\` (\u22645 words) and a full \`.description\`. Map \`next_steps.options[]\` into your host widget VERBATIM and in order: for \`AskUserQuestion\` (cowork / Claude Code) pass each as \`{label, description}\`; for \`ask_user_input_v0\` (Claude chat / ChatGPT, string options only) pass each option's \`.description\` as the string (it's the full sentence). Do NOT reword, reorder, drop, or prose-ify them \u2014 they're built deterministically by the server so the offer (incl. the artifact option at position 0) fires every time. Fall back to the table below only when there is NO \`next_steps\` field.
|
|
21846
|
+
|
|
21847
|
+
**One exception \u2014 skip the widget** when the user's original message contained a complete sequential instruction chain ("show me X and then do Y") AND all stated steps have been completed. In that case, end with STOP directly \u2014 the user stated their full plan and does not need a "what next?" prompt.
|
|
21848
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
21849
|
+
- Do NOT skip for: plain requests ("show me today's leads", "run my check-in"), recurring-language requests ("I do this every day"), or requests where only one action was stated.
|
|
21850
|
+
|
|
21851
|
+
Pick 2\u20134 rows from the (Observation, Suggest, Calls) table below most relevant to the response, then call your host's widget with ITS schema (per the schema rules above \u2014 wrong schema fails silently):
|
|
21852
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
21853
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
21854
|
+
|
|
21855
|
+
User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutually-exclusive options, AskUserQuestion labels \u22645 words (full text in \`description\`), max 3 questions. Table stays internal; never recite it.
|
|
21856
|
+
|
|
21857
|
+
---
|
|
21858
|
+
|
|
21859
|
+
|
|
21860
|
+
|
|
21861
|
+
Pick 2\u20133 items below based on what was actually observed in the response. The table is the source of truth for which moves are valid.
|
|
21862
|
+
|
|
21863
|
+
| Observation | Suggest | Calls |
|
|
21864
|
+
|------------------------------------------------------------|--------------------------------------------------------------|--------------------------------------------------------|
|
|
21865
|
+
| \u2265 5 leads returned (any batch) | "Build an interactive lead triage board for this batch" | emit antArtifact from data in hand (do NOT re-call leadbay_pull_leads) |
|
|
21866
|
+
| \`has_more == true\` | "Pull the next page (page N+1 of M)" | leadbay_pull_leads(page = current + 1, lensId = pinned)|
|
|
21867
|
+
| \u2265 3 rows have \`qualification_summary.answered == 0\` | "Deepen AI qualification on the rows without \u2756 caps" | leadbay_bulk_qualify_leads(leadIds=[\u2026]) |
|
|
21868
|
+
| User points at a single row | "Research [Company] in depth" | leadbay_research_lead_by_id(leadId) |
|
|
21869
|
+
| User only has a name (no leadId in context) | "Look up [Company] by name" | leadbay_research_lead_by_name_fuzzy(companyName) |
|
|
21870
|
+
| Top row has phone AND email | "Prepare an outreach for [Contact] \u2014 call + email" | leadbay_prepare_outreach(leadId) |
|
|
21871
|
+
| Top row has email but no phone | "Draft an outreach email for [Contact]" | leadbay_prepare_outreach(leadId) |
|
|
21872
|
+
| Top row has phone but no email | "Show [Contact]'s call details + a 60-second opener" | leadbay_prepare_outreach(leadId) |
|
|
21873
|
+
| Top row has contacts but no phone/email | "Order contact enrichment to surface email/phone first" | leadbay_enrich_titles(...) or leadbay_prepare_outreach(leadId, enrich:true) |
|
|
21874
|
+
| \`computing_scores == true\` or \`computing_wishlist == true\` | "Scores are still being computed \u2014 re-pull in ~30s" | leadbay_pull_leads (retry with same lensId) |
|
|
21875
|
+
| User wants a narrower / wider audience | "Adjust the lens filters (sector / size)" | leadbay_adjust_audience(...) |
|
|
21876
|
+
| Phase 4 research was run (\`research_lead_by_id\` called) AND top contacts lack direct email/phone | "Enrich contacts on [Lead1], [Lead2] to get direct emails and phone numbers" | leadbay_enrich_contacts(leadId, contactId) \u2014 ONE call per contact (the tool takes a single leadId + contactId, never a list) |
|
|
21877
|
+
If nothing in the menu applies cleanly, suggest only "pull next page" and "research a specific lead in depth" \u2014 never invent a tool that doesn't exist.
|
|
21878
|
+
|
|
21879
|
+
|
|
21880
|
+
**Final ordering check (do this before rendering):** Recurring offer \u2192 option 1; triage board \u2192 option 1 (or 2 if scheduling is also required). Swap if needed.
|
|
20924
21881
|
|
|
20925
21882
|
# GATE \u2014 STOP
|
|
20926
21883
|
|
|
@@ -21167,7 +22124,7 @@ Done. The map is the surface; the drafts are the action; the campaign is the per
|
|
|
21167
22124
|
var leadbay_prospecting_overview = `
|
|
21168
22125
|
## MEMORY
|
|
21169
22126
|
|
|
21170
|
-
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
|
|
22127
|
+
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.
|
|
21171
22128
|
|
|
21172
22129
|
|
|
21173
22130
|
# Leadbay Prospecting \u2014 Orientation
|
|
@@ -21441,7 +22398,7 @@ If the response status is \`applied\`, tell me Leadbay is regenerating intellige
|
|
|
21441
22398
|
var leadbay_research_a_domain = `
|
|
21442
22399
|
## MEMORY
|
|
21443
22400
|
|
|
21444
|
-
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
|
|
22401
|
+
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.
|
|
21445
22402
|
|
|
21446
22403
|
|
|
21447
22404
|
IRON LAW \u2014 NO FABRICATION. Every lead id, contact email, custom field id, mapping decision, and tool argument must trace to a value you read from the file the user attached or to an output from a leadbay_* tool call in this session. Do not invent values. Do not "fill in" a missing leadId with a name match. Do not synthesize a CRM id from a guess. If a value is missing, leave the field blank and say so.
|
|
@@ -21582,7 +22539,7 @@ Then ask me ONCE: "Which of these should we drop?" If I name leads to drop, excl
|
|
|
21582
22539
|
|
|
21583
22540
|
# PHASE 3 \u2014 DECIDE THE CAMPAIGN SHAPE
|
|
21584
22541
|
|
|
21585
|
-
If I provided a \`rep_split\` ("one campaign per rep: John gets Tulsa, Sarah gets OKC"), partition the validated leads accordingly. If I didn't, ask ONCE: "Create one campaign for the whole batch, or split per rep / region / sector?" \u2014 surface 2-4 options via \`ask_user_input_v0\` when available, else as a bulleted list.
|
|
22542
|
+
If I provided a \`rep_split\` ("one campaign per rep: John gets Tulsa, Sarah gets OKC"), partition the validated leads accordingly. If I didn't, ask ONCE: "Create one campaign for the whole batch, or split per rep / region / sector?" \u2014 surface 2-4 options via your host's choice widget (\`ask_user_input_v0\` or \`AskUserQuestion\`) when available, else as a bulleted list.
|
|
21586
22543
|
|
|
21587
22544
|
For each campaign-shape decision, derive a name. Templates:
|
|
21588
22545
|
- Whole batch: \`"<lens-name> \u2013 <YYYY-MM-DD>"\`
|
|
@@ -21599,7 +22556,7 @@ Once the campaigns are created, surface this caveat in plain prose:
|
|
|
21599
22556
|
|
|
21600
22557
|
> Campaign visibility is currently scoped to the user who CREATED the campaign \u2014 the reps won't see these in their own MCP \`leadbay_list_campaigns\` calls. They CAN see them in the web UI at app.leadbay.ai \u2192 Campaigns. Cross-user MCP visibility would need backend work; flag this as a #3630 US3 product gap if your reps work primarily through MCP.
|
|
21601
22558
|
|
|
21602
|
-
End with a NEXT STEPS chip via \`ask_user_input_v0
|
|
22559
|
+
End with a NEXT STEPS chip via your host's choice widget (\`ask_user_input_v0\` or \`AskUserQuestion\`): "View progression on one of these now?" \u2192 routes to \`leadbay_campaign_progression\`.
|
|
21603
22560
|
|
|
21604
22561
|
# PHASE 6 \u2014 STOP
|
|
21605
22562
|
|
|
@@ -21615,7 +22572,7 @@ If the prompt's body and the tool's RENDERING appear to conflict, the tool's REN
|
|
|
21615
22572
|
|
|
21616
22573
|
# PHASE 0 \u2014 PICK THE CAMPAIGN
|
|
21617
22574
|
|
|
21618
|
-
If I gave you a name or id, resolve it. Otherwise call \`leadbay_list_campaigns()\` and surface the active campaigns as a \`single_select\` via \`ask_user_input_v0\` (cap at 4 \u2014 sort by \`updated_at\` desc, archived hidden):
|
|
22575
|
+
If I gave you a name or id, resolve it. Otherwise call \`leadbay_list_campaigns()\` and surface the active campaigns as a \`single_select\` via your host's choice widget (\`ask_user_input_v0\` or \`AskUserQuestion\`) (cap at 4 \u2014 sort by \`updated_at\` desc, archived hidden):
|
|
21619
22576
|
|
|
21620
22577
|
> Which campaign do you want to work?
|
|
21621
22578
|
> - <Name 1> \xB7 <N leads> \xB7 last touched <date>
|
|
@@ -21648,7 +22605,7 @@ Call \`leadbay_campaign_call_sheet({campaign_id})\`. The response carries \`summ
|
|
|
21648
22605
|
\u{1F4CB} <total_leads> leads \xB7 \u{1F4DE} <leads_with_phone> with a phone \xB7 \u2709 <leads_with_email> with an email \xB7 \u{1F5FA} <leads_with_coords> with coords \xB7 \u{1F534} <leads_without_contacts> need enrichment \xB7 \u2705 <leads_already_contacted> already touched
|
|
21649
22606
|
\`\`\`
|
|
21650
22607
|
|
|
21651
|
-
**Then PROPOSE the right modes via \`ask_user_input_v0
|
|
22608
|
+
**Then PROPOSE the right modes via your host's choice widget (\`ask_user_input_v0\` or \`AskUserQuestion\`)** (2-4 options, sorted by what makes the most sense for THIS campaign's data):
|
|
21652
22609
|
|
|
21653
22610
|
- "\u{1F4DE} Start calling now" \u2014 IF \`ready_for_calling\`. Top option when phones are there.
|
|
21654
22611
|
- "\u2709 Email session instead" \u2014 IF \`ready_for_emailing\` AND \`email_ratio > phone_ratio\`. Don't surface this when calling is more obvious.
|
|
@@ -21710,23 +22667,9 @@ Optional: offer to review the \`leadbay_campaign_progression\` for the same camp
|
|
|
21710
22667
|
- If the user dictates an outcome that doesn't cleanly map to one of the four epilogue values, ASK ONCE before guessing.
|
|
21711
22668
|
`;
|
|
21712
22669
|
var PROMPT_META = {
|
|
21713
|
-
leadbay_daily_check_in: { "name": "leadbay_daily_check_in", "short_description": `
|
|
21714
|
-
|
|
21715
|
-
|
|
21716
|
-
on "leadbay leads", "best NEW leads", "what's new today", "show me the
|
|
21717
|
-
day's batch", "let's prospect". Do NOT trigger on follow-up phrasings
|
|
21718
|
-
("what should I follow up on", "before my trip") \u2014 those go to
|
|
21719
|
-
\`leadbay_followup_check_in\`.
|
|
21720
|
-
`, "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, not just one", "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'] },
|
|
21721
|
-
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 `ask_user_input_v0`", "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"] },
|
|
21722
|
-
leadbay_followup_check_in: { "name": "leadbay_followup_check_in", "short_description": `Run the canonical follow-up check-in: surface KNOWN leads from the
|
|
21723
|
-
Monitor view that need re-engagement today, ranked by AI urgency,
|
|
21724
|
-
with the canonical pull_followups table layout. Trigger when the
|
|
21725
|
-
user asks "follow up", "already known leads", "leads I haven't
|
|
21726
|
-
contacted", "leads in [city]", "before my trip", "this week",
|
|
21727
|
-
"this month", "what's overdue", "who should I re-engage", or
|
|
21728
|
-
anything that implies pre-existing pipeline context.
|
|
21729
|
-
`, "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)'] },
|
|
22670
|
+
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'] },
|
|
22671
|
+
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"] },
|
|
22672
|
+
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)'] },
|
|
21730
22673
|
leadbay_import_file: { "name": "leadbay_import_file", "short_description": "Import a user-supplied CSV/file into Leadbay through five phases with\nevidence gates \u2014 scan, derive, resolve identities, preserve & commit,\nthen optionally qualify and report. The job is to maximize how many\nrows the Leadbay system actually ingests and matches.\n", "arguments": [{ "name": "file", "description": "Path or user-visible name of the CSV/file to import. If omitted, use the file the user attached or referenced.", "required": false }, { "name": "instruction", "description": 'Additional user goal, e.g. "then qualify the leads", "preserve owner phone as a custom field", or "only import restaurants in Manhattan".', "required": false }], "expected_calls": ["leadbay_resolve_import_rows", "leadbay_list_mappable_fields", "leadbay_create_custom_field", "leadbay_import_leads", "leadbay_import_and_qualify", "leadbay_add_note", "leadbay_import_status"], "failure_modes": ["Picks LEADBAY_ID from score alone, name-only, fuzzy-name-only, root-domain-only, brand-only, postcode-only, or city-only evidence", "Drops meaningful business notes or CRM record links instead of preserving them as custom fields or lead notes", "Treats a consumer mailbox domain (gmail.com, hotmail.com, ...) as the company domain", "Skips deriving company_domain from a business email when no website column exists (this kills match rate)", "Skips the COLUMN PRESERVATION PLAN byproduct before importing", "Skips the DECISION LOG byproduct before writing LEADBAY_ID", "Returns the imported records WITHOUT writing LEADBAY_ID values back into the user's file (leaves the user no audit trail of what matched)", "Fabricates leadIds, contact emails, or mapping IDs not present in the file or a tool response"] },
|
|
21731
22674
|
leadbay_log_outreach: { "name": "leadbay_log_outreach", "short_description": "Log outreach (an email I sent, a call I made, a meeting I had) on a\nspecific lead. Captures verification so the SDR pipeline trusts the entry.\n", "arguments": [{ "name": "lead_id", "description": "The lead UUID. Get it from leadbay_pull_leads or leadbay_research_lead_by_id.", "required": true }, { "name": "summary", "description": "1-2 sentences describing what I did (e.g. 'Sent intro email to CTO citing recent Hornsea contract').", "required": true }], "expected_calls": ["leadbay_report_outreach"], "failure_modes": ["Calls leadbay_report_outreach without first collecting a verification source", "Fabricates a gmail_message_id or calendar_event_id (the human team treats verification as canonical)", "Records outreach to a different lead_id than the one the user supplied", "Skips the dry_run step when the user is unsure what would be sent"] },
|
|
21732
22675
|
leadbay_plan_tour_in_city: { "name": "leadbay_plan_tour_in_city", "short_description": "Plan a field sales tour: in one flow, surface follow-ups + fresh\nDiscover leads in the target city via `leadbay_tour_plan`, render\nto a map, draft in-area outreach via `leadbay_prepare_outreach`,\nand optionally persist the selected accounts as a named campaign\nvia `leadbay_create_campaign`. Closes #3630 US1 end-to-end.\n", "arguments": [{ "name": "city", "description": "City or region the user is visiting (e.g. 'Limoges', 'Bay Area'). Used as the geo filter for both Monitor and Discover lookups.", "required": true }, { "name": "date", "description": "When the visit is (e.g. 'May 24', 'next Thursday'). Surfaced in the outreach drafts as 'I'll be in <city> on <date>'.", "required": false }], "expected_calls": ["leadbay_tour_plan", "leadbay_research_lead_by_id", "leadbay_prepare_outreach", "leadbay_create_campaign"], "failure_modes": ["Calls leadbay_followups_map (Monitor-only) instead of leadbay_tour_plan \u2014 loses the Discover (fresh-lead) half that the user explicitly asked for", "Calls leadbay_pull_leads then drops the geo filter \u2014 returns the lens-wide wishlist instead of city-relevant fresh leads", 'Skips the campaign-persist step ("would you like to save these as a tour?") \u2014 leaves the rep with a one-shot map but no follow-up artifact', "Creates a campaign WITHOUT asking the user first \u2014 the persist step is high-intent; offer it, don't assume", "Fabricates lead_ids when seeding the campaign instead of using the ids returned by tour_plan"] },
|
|
@@ -21745,9 +22688,9 @@ should I follow up on" to "I'll send via lemlist".
|
|
|
21745
22688
|
};
|
|
21746
22689
|
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.`;
|
|
21747
22690
|
var PROMPT_CATALOG_BULLETS = {
|
|
21748
|
-
leadbay_daily_check_in: `- \`leadbay_daily_check_in\`:
|
|
22691
|
+
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\`.`,
|
|
21749
22692
|
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.`,
|
|
21750
|
-
leadbay_followup_check_in: `- \`leadbay_followup_check_in\`:
|
|
22693
|
+
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\`.`,
|
|
21751
22694
|
leadbay_import_file: `- \`leadbay_import_file\` (optional args: file, instruction): Import a user-supplied CSV/file into Leadbay through five phases with evidence gates \u2014 scan, derive, resolve identities, preserve & commit, then optionally qualify and report. The job is to maximize how many rows the Leadbay system actually ingests and matches.`,
|
|
21752
22695
|
leadbay_log_outreach: `- \`leadbay_log_outreach\` (required args: lead_id, summary): Log outreach (an email I sent, a call I made, a meeting I had) on a specific lead. Captures verification so the SDR pipeline trusts the entry.`,
|
|
21753
22696
|
leadbay_plan_tour_in_city: `- \`leadbay_plan_tour_in_city\` (required args: city; optional args: date): Plan a field sales tour: in one flow, surface follow-ups + fresh Discover leads in the target city via \`leadbay_tour_plan\`, render to a map, draft in-area outreach via \`leadbay_prepare_outreach\`, and optionally persist the selected accounts as a named campaign via \`leadbay_create_campaign\`. Closes #3630 US1 end-to-end.`,
|
|
@@ -22084,7 +23027,7 @@ async function readResource(uri, client) {
|
|
|
22084
23027
|
}
|
|
22085
23028
|
|
|
22086
23029
|
// src/host-widgets.ts
|
|
22087
|
-
var BUILTIN_WIDGETS_PARAGRAPH = 'Prefer host-native widgets over inline markdown when the data shape fits. Three to know: (1) `places_map_display_v0` \u2014 for \u22652 locations / map / travel intent. Pass `{name, address, latitude, longitude, notes}` per location; the host enriches via Google Places. (2) `message_compose_v1` \u2014 for any outreach draft (email / message / call opener). Pass 2\u20133 strategic variants with goal-oriented labels ("Push for alignment", "Reference M&A signal") \u2014 NOT tone labels. (3)
|
|
23030
|
+
var BUILTIN_WIDGETS_PARAGRAPH = 'Prefer host-native widgets over inline markdown when the data shape fits. Three to know: (1) `places_map_display_v0` \u2014 for \u22652 locations / map / travel intent. Pass `{name, address, latitude, longitude, notes}` per location; the host enriches via Google Places. (2) `message_compose_v1` \u2014 for any outreach draft (email / message / call opener). Pass 2\u20133 strategic variants with goal-oriented labels ("Push for alignment", "Reference M&A signal") \u2014 NOT tone labels. (3) The next-step / choice widget \u2014 for the NEXT STEPS questions every Leadbay tool emits. Its NAME AND SCHEMA differ by host; use whichever is in your tool set: (a) `ask_user_input_v0` (Claude chat / ChatGPT) \u2014 options are PLAIN STRINGS with `type: "single_select"`, e.g. `{questions:[{question:"What next?",type:"single_select",options:["Build a triage board","Pull next page"]}]}`. (b) `AskUserQuestion` (Claude cowork / Claude Code) \u2014 options are OBJECTS `{label, description}`, plus a required short `header` (\u226412 chars) and a `multiSelect` boolean, and NO `type` field; do not add an "Other" option (the host adds it). e.g. `{questions:[{question:"What next?",header:"Next step",multiSelect:false,options:[{label:"Triage board",description:"Build an interactive board to sort this batch."},{label:"Next page",description:"Pull page 2."}]}]}`. Match the schema to the tool you actually have \u2014 using the string-schema for AskUserQuestion (or vice-versa) makes the call fail and you silently fall back to prose. When the host exposes NEITHER widget, fall back to the per-tool markdown RENDERING block.\n\nWIDGET IS MANDATORY WHEN AVAILABLE: if EITHER `ask_user_input_v0` OR `AskUserQuestion` is present in your tool set, you MUST emit your NEXT STEPS / scheduling / artifact offer by CALLING that widget tool (with its correct schema above) \u2014 do NOT write the options out as a prose question ("Want me to run this every morning?", "Should I build a board?"). Prose for these offers is the FALLBACK reserved ONLY for hosts that expose neither widget. When the widget exists, presenting the same choices as prose instead of calling it is a defect: the user loses the click-to-select surface. So whenever you have a recurring-task offer, an artifact offer, or a 2\u20134 option next-step menu AND a widget tool is available \u2192 call it, every time.';
|
|
22088
23031
|
|
|
22089
23032
|
// src/server.ts
|
|
22090
23033
|
init_dist();
|
|
@@ -22730,6 +23673,41 @@ var QUOTA_TOPUP = `Quota & top-ups: when a tool returns QUOTA_EXCEEDED / 429, th
|
|
|
22730
23673
|
var VERIFICATION = `After every email, call, message, or meeting with a lead's contact, you MUST call leadbay_report_outreach with verification={source, ref} (gmail_message_id from the Gmail send, calendar_event_id from a booking, or user_confirmed='<the user's literal confirmation>'). Skipping or fabricating verification poisons the human team's pipeline.`;
|
|
22731
23674
|
|
|
22732
23675
|
// src/server.ts
|
|
23676
|
+
var ARTIFACT_PROPOSAL_PARAGRAPH = 'Artifact proposals: after any result the user would scan, compare, plan against, or return to \u2014 or any multi-item result \u2014 DEFAULT TO offering an artifact. Treat the offer as expected, not optional: a multi-item lead batch, a side-by-side comparison, or a research record the user will revisit each SHOULD get an artifact offer unless it clearly falls in the Skip list below. When in doubt, offer. WIDGET IS MANDATORY WHEN AVAILABLE: if your host\'s choice widget is in your tool set \u2014 `ask_user_input_v0` (Claude chat / ChatGPT) OR `AskUserQuestion` (Claude cowork / Claude Code) \u2014 you MUST make the offer by CALLING that widget tool \u2014 never as a prose line ("Want me to build an interactive lead triage board?"). Prose is the fallback ONLY for hosts that expose neither widget. Presenting the offer as prose when a widget exists is a defect \u2014 the user loses the click-to-select surface.\n\nTwo tracks:\n1. A NEXT STEPS menu is already firing (the choice widget with 2\u20134 options): include the artifact offer as one of the slots. It counts toward the 2\u20134 budget \u2014 replace the lowest-priority option when already at 4.\n2. No NEXT STEPS menu is firing: fire a standalone yes/no question by CALLING your host\'s choice widget with ITS schema (see the host-widget section: `ask_user_input_v0` takes string options; `AskUserQuestion` takes {label, description} objects with a `header` and `multiSelect`). The question is "Want me to build a [concrete artifact name] for this?" with yes/no options. Use the schema that matches the widget tool you actually have.\n\nName the artifact concretely from the result you just returned: "Build an interactive lead triage board", "Build a research one-pager for Acme Corp", "Build a side-by-side outreach variant comparison". Never use generic labels.\n\nOn selection of yes/build: build the artifact from data already in hand \u2014 do NOT re-call the tool. Emit with the antArtifact tag: type="text/html" for interactive boards/tables, type="application/vnd.ant.react" for React components, type="text/markdown" for one-pagers.\n\nSkip for: single-action confirmations (like/dislike, report_outreach), mid-flow steps, results a host widget already covers (places_map_display_v0, message_compose_v1).';
|
|
23677
|
+
var SCHEDULED_TASK_PARAGRAPH = `Scheduled task proposals: check the user's LITERAL words. NEVER fire based on the workflow or prompt type \u2014 only on what the user ACTUALLY TYPED.
|
|
23678
|
+
|
|
23679
|
+
NEVER fire for these examples (no recurring language in user message):
|
|
23680
|
+
- 'Show me today\\'s leads.' \u2192 NO
|
|
23681
|
+
- 'What are my leads today?' \u2192 NO
|
|
23682
|
+
- 'Pull my best new prospects.' \u2192 NO
|
|
23683
|
+
- 'Show me my batch.' \u2192 NO
|
|
23684
|
+
- 'Run my morning check-in.' (no recurring qualifier) \u2192 NO
|
|
23685
|
+
|
|
23686
|
+
ONLY fire when the user's message contains explicit recurring language:
|
|
23687
|
+
- 'I do this every day' \u2192 YES
|
|
23688
|
+
- 'every morning' \u2192 YES
|
|
23689
|
+
- 'remind me every week' \u2192 YES
|
|
23690
|
+
- 'automate this' \u2192 YES
|
|
23691
|
+
- 'set this up as recurring' \u2192 YES
|
|
23692
|
+
|
|
23693
|
+
WIDGET IS MANDATORY WHEN AVAILABLE: if your host's choice widget is in your tool set \u2014 \`ask_user_input_v0\` (Claude chat / ChatGPT) OR \`AskUserQuestion\` (Claude cowork / Claude Code) \u2014 you MUST make the scheduling offer by CALLING that widget tool \u2014 never as a prose line ("Want me to run this every morning?"). Prose is the fallback ONLY for hosts that expose neither widget; presenting the offer as prose when a widget exists is a defect.
|
|
23694
|
+
|
|
23695
|
+
Render EVERY question below by CALLING your host's choice widget with ITS schema (see the host-widget section: \`ask_user_input_v0\` takes string options; \`AskUserQuestion\` takes {label, description} objects with a \`header\` and \`multiSelect\`). Use the schema that matches the widget tool you actually have \u2014 never the other one.
|
|
23696
|
+
|
|
23697
|
+
Two tracks \u2014 same as artifact proposals:
|
|
23698
|
+
1. A NEXT STEPS menu is already firing: add "Schedule [Task Name] as a recurring task" as one slot (counts toward the 2\u20134 budget \u2014 replace the lowest-priority option when already at 4).
|
|
23699
|
+
2. No NEXT STEPS menu is firing: fire a standalone yes/no question "Want me to schedule this as a recurring task?" (options: yes / no thanks).
|
|
23700
|
+
|
|
23701
|
+
If the user selects yes, continue with a multi-step flow, one widget call per step:
|
|
23702
|
+
Step 1 \u2014 frequency: "How often?" \u2192 Every day / Every weekday / Weekly / Custom.
|
|
23703
|
+
Step 2a \u2014 if "Every day" or "Every weekday": "What time?" \u2192 Morning (8am) / Midday (12pm) / Afternoon (5pm) / Custom.
|
|
23704
|
+
Step 2b \u2014 if "Weekly": "Which day?" \u2192 Monday / Wednesday / Friday / Custom.
|
|
23705
|
+
Step 2c \u2014 if "Custom" at any step: ask for a free-text description and interpret it to determine the schedule.
|
|
23706
|
+
After the schedule is confirmed: judge whether the scheduled run should also produce an artifact. If yes, offer "Should each run also build an artifact (e.g. a fresh lead board)?" \u2192 yes / no.
|
|
23707
|
+
|
|
23708
|
+
Name the task concretely from context: "Daily prospecting check-in", "Weekly follow-up sweep", "Monday morning lead review". Never use generic labels.
|
|
23709
|
+
|
|
23710
|
+
Skip for: single-action confirmations, mid-flow steps, one-off lookups with no recurrence signal.`;
|
|
22733
23711
|
function buildScoringParagraph(has) {
|
|
22734
23712
|
const base = "Two scoring layers: every lead has a basic `score` (firmographic \u2014 already decent, usually correlates with AI). Roughly the top 10 of each batch are also AI-qualified (targeted web research + qualification questions \u2192 `ai_agent_lead_score`, surfaced as `qualification_summary` on leadbay_pull_leads). Leads past the top ~10 are not worse \u2014 the system is saving resources.";
|
|
22735
23713
|
const deepenTools = [];
|
|
@@ -22844,6 +23822,8 @@ function buildServerInstructions(exposed) {
|
|
|
22844
23822
|
if (has("leadbay_agent_memory_capture")) {
|
|
22845
23823
|
parts.push(AGENT_MEMORY);
|
|
22846
23824
|
}
|
|
23825
|
+
parts.push(ARTIFACT_PROPOSAL_PARAGRAPH);
|
|
23826
|
+
parts.push(SCHEDULED_TASK_PARAGRAPH);
|
|
22847
23827
|
parts.push(BUILTIN_WIDGETS_PARAGRAPH);
|
|
22848
23828
|
return parts.join("\n\n");
|
|
22849
23829
|
}
|
|
@@ -24638,7 +25618,7 @@ var OAUTH_BASE_URLS = {
|
|
|
24638
25618
|
fr: "https://staging.api.leadbay.app"
|
|
24639
25619
|
}
|
|
24640
25620
|
};
|
|
24641
|
-
var VERSION = "0.
|
|
25621
|
+
var VERSION = "0.19.1";
|
|
24642
25622
|
var HELP = `
|
|
24643
25623
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
24644
25624
|
|