@leadbay/mcp 0.19.0 → 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 +6 -0
- package/dist/bin.js +1041 -71
- package/dist/http-server.js +980 -69
- 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
|
|
|
@@ -7237,6 +7277,42 @@ WHEN TO USE: low-level.
|
|
|
7237
7277
|
WHEN NOT TO USE: from agent flow \u2014 use leadbay_answer_clarification.
|
|
7238
7278
|
|
|
7239
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).
|
|
7240
7316
|
`;
|
|
7241
7317
|
leadbay_prepare_outreach = `## WHEN TO USE
|
|
7242
7318
|
|
|
@@ -7461,9 +7537,9 @@ table. Detail + status priority below.
|
|
|
7461
7537
|
|
|
7462
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\`.
|
|
7463
7539
|
|
|
7464
|
-
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.
|
|
7465
7541
|
|
|
7466
|
-
**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\`).
|
|
7467
7543
|
|
|
7468
7544
|
Practical mapping from user phrasing to criterion:
|
|
7469
7545
|
|
|
@@ -7476,11 +7552,11 @@ Practical mapping from user phrasing to criterion:
|
|
|
7476
7552
|
| "leads 50\u2013200 employees" | \`{type: "size", sizes: [{min: 50, max: 200}]}\` |
|
|
7477
7553
|
| "Y Combinator companies" | \`{type: "yc"}\` |
|
|
7478
7554
|
|
|
7479
|
-
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\`.
|
|
7480
7556
|
|
|
7481
|
-
**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.
|
|
7482
7558
|
|
|
7483
|
-
**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.
|
|
7484
7560
|
|
|
7485
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.
|
|
7486
7562
|
|
|
@@ -7488,6 +7564,16 @@ WHEN NOT TO USE: for NEW leads \u2014 that's \`leadbay_pull_leads\` (Discover).
|
|
|
7488
7564
|
|
|
7489
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\`.
|
|
7490
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
|
+
|
|
7491
7577
|
---
|
|
7492
7578
|
|
|
7493
7579
|
## RENDERING \u2014 follow-ups table, status-badge driven
|
|
@@ -7822,6 +7908,43 @@ WHEN TO USE: when audience filters (leadbay_adjust_audience) aren't enough.
|
|
|
7822
7908
|
WHEN NOT TO USE: to answer a pending clarification \u2014 that's leadbay_answer_clarification.
|
|
7823
7909
|
|
|
7824
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).
|
|
7825
7948
|
`;
|
|
7826
7949
|
leadbay_remove_epilogue = `Bulk-clear the epilogue status from a set of leads.
|
|
7827
7950
|
|
|
@@ -7951,7 +8074,7 @@ Trigger phrases: "tell me about this lead", "deep dive on the lead I just picked
|
|
|
7951
8074
|
|
|
7952
8075
|
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
7953
8076
|
|
|
7954
|
-
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\`.
|
|
7955
8078
|
|
|
7956
8079
|
Prefer when: user picked a row and you have its UUID; pass \`leadId\`
|
|
7957
8080
|
|
|
@@ -7966,42 +8089,46 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
|
7966
8089
|
---
|
|
7967
8090
|
|
|
7968
8091
|
Tell me everything decision-relevant about a single lead, identified by its
|
|
7969
|
-
Leadbay UUID. Bundles the lens-scoped
|
|
7970
|
-
|
|
7971
|
-
(
|
|
7972
|
-
|
|
7973
|
-
|
|
7974
|
-
|
|
7975
|
-
|
|
7976
|
-
|
|
7977
|
-
|
|
7978
|
-
|
|
7979
|
-
|
|
7980
|
-
|
|
7981
|
-
|
|
7982
|
-
|
|
7983
|
-
|
|
7984
|
-
|
|
7985
|
-
the case where the user names a company in prose without a UUID \u2014 it
|
|
7986
|
-
fuzzy-resolves the name against the active lens's wishlist, then delegates
|
|
7987
|
-
here. Both return the same shape; the fuzzy wrapper just adds
|
|
7988
|
-
\`_meta.resolved_from\` and \`_meta.match_candidates\` so you can offer
|
|
7989
|
-
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\`.
|
|
7990
8108
|
|
|
7991
8109
|
WHEN TO USE: when picking up a single lead from
|
|
7992
8110
|
leadbay_pull_leads (or any list that exposed a leadId) to decide whether to
|
|
7993
8111
|
act on it.
|
|
7994
8112
|
|
|
7995
8113
|
WHEN NOT TO USE: across many leads at once \u2014 that's
|
|
7996
|
-
leadbay_pull_leads' job
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
**
|
|
8001
|
-
|
|
8002
|
-
|
|
8003
|
-
|
|
8004
|
-
|
|
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
|
|
8005
8132
|
stream/timeout failure, retry the same lead once before moving on.
|
|
8006
8133
|
|
|
8007
8134
|
---
|
|
@@ -8365,6 +8492,178 @@ Below the table, a one-liner: \`"Ready: K rows \xB7 Ambiguous: A rows \xB7 Unmat
|
|
|
8365
8492
|
| Ambiguous rows present | "Inspect candidates for each ambiguous row" | (re-call with include_candidate_profiles=true) |
|
|
8366
8493
|
| Unmatched rows but websites present | "Import anyway \u2014 Leadbay will crawl and match later" | leadbay_import_leads (status check after) |
|
|
8367
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.
|
|
8368
8667
|
`;
|
|
8369
8668
|
leadbay_seed_candidates = `## WHEN TO USE
|
|
8370
8669
|
|
|
@@ -8558,6 +8857,81 @@ WHEN NOT TO USE: if the user only wants follow-ups (use \`leadbay_followups_map\
|
|
|
8558
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.
|
|
8559
8858
|
|
|
8560
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).
|
|
8561
8935
|
`;
|
|
8562
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.
|
|
8563
8937
|
|
|
@@ -12978,6 +13352,250 @@ var init_dislike_lead = __esm({
|
|
|
12978
13352
|
}
|
|
12979
13353
|
});
|
|
12980
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
|
+
|
|
12981
13599
|
// ../core/dist/composite/prepare-outreach.js
|
|
12982
13600
|
function normalizeLinkedinPage2(v) {
|
|
12983
13601
|
if (v == null)
|
|
@@ -14521,6 +15139,46 @@ var init_campaign_call_sheet = __esm({
|
|
|
14521
15139
|
}
|
|
14522
15140
|
});
|
|
14523
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
|
+
|
|
14524
15182
|
// ../core/dist/composite/research-lead-by-id.js
|
|
14525
15183
|
function normalizeLinkedinPage5(v) {
|
|
14526
15184
|
if (v == null)
|
|
@@ -14636,37 +15294,6 @@ _Truncated_: ${shape.truncation_hint ?? "response trimmed"}_`);
|
|
|
14636
15294
|
}
|
|
14637
15295
|
return out.join("\n");
|
|
14638
15296
|
}
|
|
14639
|
-
function splitEmojiSection(key) {
|
|
14640
|
-
const m = key.match(/^([^\p{L}\p{N}\s]+)\s+(.+)$/u);
|
|
14641
|
-
if (m)
|
|
14642
|
-
return { emoji: m[1], label: m[2] };
|
|
14643
|
-
return { emoji: null, label: key };
|
|
14644
|
-
}
|
|
14645
|
-
function reshapeWebFetchContent(content) {
|
|
14646
|
-
if (!content)
|
|
14647
|
-
return [];
|
|
14648
|
-
const sections = [];
|
|
14649
|
-
for (const [key, val] of Object.entries(content)) {
|
|
14650
|
-
if (!Array.isArray(val))
|
|
14651
|
-
continue;
|
|
14652
|
-
const { emoji, label } = splitEmojiSection(key);
|
|
14653
|
-
sections.push({
|
|
14654
|
-
section_label: label,
|
|
14655
|
-
section_emoji: emoji,
|
|
14656
|
-
entries: val
|
|
14657
|
-
});
|
|
14658
|
-
}
|
|
14659
|
-
sections.sort((a, b) => {
|
|
14660
|
-
const ai = SECTION_PRIORITY.findIndex((p) => a.section_label.toLowerCase().includes(p));
|
|
14661
|
-
const bi = SECTION_PRIORITY.findIndex((p) => b.section_label.toLowerCase().includes(p));
|
|
14662
|
-
const aN = ai < 0 ? SECTION_PRIORITY.length : ai;
|
|
14663
|
-
const bN = bi < 0 ? SECTION_PRIORITY.length : bi;
|
|
14664
|
-
if (aN !== bN)
|
|
14665
|
-
return aN - bN;
|
|
14666
|
-
return a.section_label.localeCompare(b.section_label);
|
|
14667
|
-
});
|
|
14668
|
-
return sections;
|
|
14669
|
-
}
|
|
14670
15297
|
function isReachable(c) {
|
|
14671
15298
|
if (!c)
|
|
14672
15299
|
return false;
|
|
@@ -14674,13 +15301,13 @@ function isReachable(c) {
|
|
|
14674
15301
|
const phone = typeof c.phone_number === "string" ? c.phone_number.trim() : "";
|
|
14675
15302
|
return email.length > 0 || phone.length > 0;
|
|
14676
15303
|
}
|
|
14677
|
-
var
|
|
15304
|
+
var researchLeadById, _innerExecute;
|
|
14678
15305
|
var init_research_lead_by_id = __esm({
|
|
14679
15306
|
"../core/dist/composite/research-lead-by-id.js"() {
|
|
14680
15307
|
"use strict";
|
|
14681
15308
|
init_agent_memory();
|
|
15309
|
+
init_web_fetch_helpers();
|
|
14682
15310
|
init_tool_descriptions_generated();
|
|
14683
|
-
SECTION_PRIORITY = ["profile", "signals", "clues"];
|
|
14684
15311
|
researchLeadById = {
|
|
14685
15312
|
name: "leadbay_research_lead_by_id",
|
|
14686
15313
|
annotations: {
|
|
@@ -15255,6 +15882,326 @@ var init_account_history = __esm({
|
|
|
15255
15882
|
}
|
|
15256
15883
|
});
|
|
15257
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
|
+
|
|
15258
16205
|
// ../core/dist/composite/recall-ordered-titles.js
|
|
15259
16206
|
var recallOrderedTitles;
|
|
15260
16207
|
var init_recall_ordered_titles = __esm({
|
|
@@ -20420,6 +21367,7 @@ __export(dist_exports, {
|
|
|
20420
21367
|
resolveImportRows: () => resolveImportRows,
|
|
20421
21368
|
resolveRegion: () => resolveRegion,
|
|
20422
21369
|
reviseHintFor: () => reviseHintFor,
|
|
21370
|
+
scanPortfolioSignals: () => scanPortfolioSignals,
|
|
20423
21371
|
seedCandidates: () => seedCandidates,
|
|
20424
21372
|
selectLeads: () => selectLeads,
|
|
20425
21373
|
setActiveLens: () => setActiveLens,
|
|
@@ -20494,6 +21442,11 @@ var init_dist = __esm({
|
|
|
20494
21442
|
init_create_custom_field();
|
|
20495
21443
|
init_like_lead();
|
|
20496
21444
|
init_dislike_lead();
|
|
21445
|
+
init_add_contact();
|
|
21446
|
+
init_remove_contact();
|
|
21447
|
+
init_pin_contact();
|
|
21448
|
+
init_unpin_contact();
|
|
21449
|
+
init_update_contact();
|
|
20497
21450
|
init_prepare_outreach();
|
|
20498
21451
|
init_pull_leads();
|
|
20499
21452
|
init_pull_followups();
|
|
@@ -20508,6 +21461,7 @@ var init_dist = __esm({
|
|
|
20508
21461
|
init_research_lead_by_id();
|
|
20509
21462
|
init_research_lead_by_name_fuzzy();
|
|
20510
21463
|
init_account_history();
|
|
21464
|
+
init_scan_portfolio_signals();
|
|
20511
21465
|
init_recall_ordered_titles();
|
|
20512
21466
|
init_account_status();
|
|
20513
21467
|
init_bulk_qualify_leads();
|
|
@@ -20607,6 +21561,10 @@ var init_dist = __esm({
|
|
|
20607
21561
|
// reprioritize-a-neglected-account workflow (#3630 GAP C) must work in a
|
|
20608
21562
|
// default deployment without LEADBAY_MCP_ADVANCED=1.
|
|
20609
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,
|
|
20610
21568
|
recallOrderedTitles,
|
|
20611
21569
|
accountStatus,
|
|
20612
21570
|
bulkEnrichStatus,
|
|
@@ -20656,6 +21614,18 @@ var init_dist = __esm({
|
|
|
20656
21614
|
reportOutreach,
|
|
20657
21615
|
importLeads,
|
|
20658
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,
|
|
20659
21629
|
// createCustomField is granular-shaped but file-import prompts depend on it
|
|
20660
21630
|
// to preserve source-system links without requiring advanced-tool exposure.
|
|
20661
21631
|
createCustomField,
|
|
@@ -24648,7 +25618,7 @@ var OAUTH_BASE_URLS = {
|
|
|
24648
25618
|
fr: "https://staging.api.leadbay.app"
|
|
24649
25619
|
}
|
|
24650
25620
|
};
|
|
24651
|
-
var VERSION = "0.19.
|
|
25621
|
+
var VERSION = "0.19.1";
|
|
24652
25622
|
var HELP = `
|
|
24653
25623
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
24654
25624
|
|