@leadbay/mcp 0.33.4 → 0.34.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/dist/bin.js +382 -18
- package/dist/http-server.js +386 -18
- package/dist/installer-electron.js +5 -4
- package/dist/installer-gui.js +4 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# Changelog — @leadbay/mcp
|
|
2
2
|
|
|
3
|
+
## 0.34.0 — 2026-09-02
|
|
4
|
+
|
|
5
|
+
The OpenAI app directory rejects an app that sells digital goods — "plugins may
|
|
6
|
+
conduct commerce only for physical goods. Selling digital products or services
|
|
7
|
+
— including subscriptions, digital content, tokens, or credits — is not
|
|
8
|
+
allowed" — and separately forbids promoting the purchase: a plugin "must not
|
|
9
|
+
display subscription plans, initiate new subscriptions, or promote upgrades" or
|
|
10
|
+
"link directly to a checkout". Signing in to an existing paid account and using
|
|
11
|
+
what it entitles you to IS allowed. Anthropic's Software Directory Policy has
|
|
12
|
+
no equivalent clause; its nearest rule bars software that "executes financial
|
|
13
|
+
transactions on behalf of users", which our Stripe-URL handoff does not do.
|
|
14
|
+
|
|
15
|
+
`POST /chatgpt/mcp` is the URL submitted to OpenAI. Same Hono app, same auth,
|
|
16
|
+
same handler, built with `includeCommerce: false`. Four things go away
|
|
17
|
+
together — the tools, and every text that would promote buying them, since
|
|
18
|
+
removing only the tools left the agent still pushing a top-up it could no
|
|
19
|
+
longer produce:
|
|
20
|
+
|
|
21
|
+
- `leadbay_create_topup_link` and `leadbay_open_billing_portal` are not
|
|
22
|
+
registered (filtered after the catalogue arrays merge — both appear in
|
|
23
|
+
`compositeReadTools` and `granularReadTools`).
|
|
24
|
+
- Tool descriptions lose their `{{commerce}}` blocks
|
|
25
|
+
(`NO_COMMERCE_TOOL_DESCRIPTIONS`, emitted by promptforge).
|
|
26
|
+
- The `QUOTA_TOPUP` instruction paragraph is not pushed. `quota-topup.md` was
|
|
27
|
+
split verbatim: the selling paragraph is gated, and the neutral one that says
|
|
28
|
+
when to re-render the quota gauge became `quota-refresh.md`, pushed on both.
|
|
29
|
+
- `LeadbayClient.commerce` drops the two selling sentences from the
|
|
30
|
+
QUOTA_EXCEEDED hint ("OR top up AI credits…", "…or direct them to
|
|
31
|
+
app.leadbay.ai → Billing"). One client per session on hosted, so the flag
|
|
32
|
+
cannot leak across tenants.
|
|
33
|
+
|
|
34
|
+
Nothing is reworded for ChatGPT. `{{commerce}}` only ever DELETES — there is no
|
|
35
|
+
second, softened wording anywhere to drift out of sync, and the Claude surface
|
|
36
|
+
keeps selling exactly as hard as before. Enforced two ways:
|
|
37
|
+
`commerce-gate.test.ts` asserts the gated description and instruction strings
|
|
38
|
+
are pure character-level *subsequences* of the Claude ones (any reworded
|
|
39
|
+
character fails), and the generated descriptions were diffed against `main` to
|
|
40
|
+
confirm the default rendering did not move by a byte.
|
|
41
|
+
|
|
42
|
+
A path rather than an `initialize` clientInfo sniff, and rather than a query
|
|
43
|
+
parameter: the OAuth protected-resource identifier IS the path
|
|
44
|
+
(`/.well-known/oauth-protected-resource/chatgpt/mcp` → `resource:
|
|
45
|
+
https://mcp.leadbay.app/chatgpt/mcp`). A `?commerce=off` flag would leave both
|
|
46
|
+
URLs advertising the same resource, so a client reconnecting to the audience it
|
|
47
|
+
registered would get the selling tools back. RFC 8707 also says a resource URI
|
|
48
|
+
should carry no query component.
|
|
49
|
+
|
|
50
|
+
`RESOURCE_PATHS` gains the path so OAuth discovery resolves for it, and the
|
|
51
|
+
installer's ChatGPT Desktop entry now hands out `HOSTED_MCP_URL_CHATGPT`.
|
|
52
|
+
|
|
3
53
|
## 0.33.4 — 2026-09-02
|
|
4
54
|
|
|
5
55
|
`leadbay_enrich_contacts` moves from `granularWriteTools` to
|
package/dist/bin.js
CHANGED
|
@@ -312,6 +312,15 @@ var init_client = __esm({
|
|
|
312
312
|
this._region = region ?? _LeadbayClient.regionFromBaseUrl(baseUrl);
|
|
313
313
|
}
|
|
314
314
|
}
|
|
315
|
+
/**
|
|
316
|
+
* Whether this client may compose text that promotes a purchase. Default
|
|
317
|
+
* true. The MCP server sets it false for a host whose directory forbids
|
|
318
|
+
* promoting upgrades (see BuildServerOptions.includeCommerce); the only
|
|
319
|
+
* effect is that the QUOTA_EXCEEDED hint drops its two selling sentences.
|
|
320
|
+
* Set per client, and the hosted server builds one client per session, so
|
|
321
|
+
* this never leaks across tenants.
|
|
322
|
+
*/
|
|
323
|
+
commerce = true;
|
|
315
324
|
get baseUrl() {
|
|
316
325
|
return this._baseUrl;
|
|
317
326
|
}
|
|
@@ -631,7 +640,12 @@ var init_client = __esm({
|
|
|
631
640
|
// agent can generate the URL itself instead of asking the user to
|
|
632
641
|
// navigate to a website. Once the user has topped up, the previous
|
|
633
642
|
// 429 is stale — retry the failed call.
|
|
634
|
-
|
|
643
|
+
//
|
|
644
|
+
// The two selling sentences are dropped when `commerce` is off — this
|
|
645
|
+
// hint is text the agent reads out, and a host may forbid promoting a
|
|
646
|
+
// purchase. Nothing is reworded; the rest of the hint is unchanged, and
|
|
647
|
+
// "the user topped up (elsewhere), so retry" survives either way.
|
|
648
|
+
`${hintBase}` + (this.commerce ? `, OR top up AI credits \u2014 top-ups clear the throttle immediately. Offer the user to generate a Stripe checkout URL via leadbay_create_topup_link, OR direct them to app.leadbay.ai \u2192 Billing. ` : `. `) + `Check leadbay_account_status / leadbay_get_quota to see which resource window (daily/weekly/monthly) was hit. Once the user has topped up, the previous QUOTA_EXCEEDED is stale \u2014 re-call leadbay_account_status to refresh, then RETRY the original operation.`,
|
|
635
649
|
endpoint,
|
|
636
650
|
retryAfter,
|
|
637
651
|
status
|
|
@@ -1283,7 +1297,7 @@ var init_notifications = __esm({
|
|
|
1283
1297
|
});
|
|
1284
1298
|
|
|
1285
1299
|
// ../core/dist/tool-descriptions.generated.js
|
|
1286
|
-
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_answer_clarification, leadbay_artifact_kit, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_delete_custom_field, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_custom_fields, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_qualification_questions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_getting_started, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_pin_contact, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_contact, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_scan_portfolio_signals, leadbay_seed_candidates, leadbay_select_leads, leadbay_send_feedback, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_lead_status, leadbay_set_pushback, leadbay_set_qualification_questions, leadbay_set_telemetry, leadbay_set_user_prompt, leadbay_team_activity, leadbay_tour_plan, leadbay_unpin_contact, leadbay_update_contact, leadbay_update_custom_field, leadbay_update_lens, leadbay_update_lens_filter;
|
|
1300
|
+
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_answer_clarification, leadbay_artifact_kit, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_delete_custom_field, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_custom_fields, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_qualification_questions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_getting_started, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_pin_contact, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_contact, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_scan_portfolio_signals, leadbay_seed_candidates, leadbay_select_leads, leadbay_send_feedback, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_lead_status, leadbay_set_pushback, leadbay_set_qualification_questions, leadbay_set_telemetry, leadbay_set_user_prompt, leadbay_team_activity, leadbay_tour_plan, leadbay_unpin_contact, leadbay_update_contact, leadbay_update_custom_field, leadbay_update_lens, leadbay_update_lens_filter, NO_COMMERCE_TOOL_DESCRIPTIONS;
|
|
1287
1301
|
var init_tool_descriptions_generated = __esm({
|
|
1288
1302
|
"../core/dist/tool-descriptions.generated.js"() {
|
|
1289
1303
|
"use strict";
|
|
@@ -5860,6 +5874,345 @@ WHEN NOT TO USE: from agent flow \u2014 use leadbay_adjust_audience, which handl
|
|
|
5860
5874
|
|
|
5861
5875
|
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\`.
|
|
5862
5876
|
`;
|
|
5877
|
+
NO_COMMERCE_TOOL_DESCRIPTIONS = {
|
|
5878
|
+
leadbay_account_status: `## WHEN TO USE
|
|
5879
|
+
|
|
5880
|
+
Trigger phrases: "what's my account status", "how much quota do I have", "what lens am I on", "I topped up / I bought credits / I added credits".
|
|
5881
|
+
|
|
5882
|
+
Do NOT use for: "show me leads" \u2192 \`leadbay_pull_leads\`.
|
|
5883
|
+
|
|
5884
|
+
Prefer when: meta question about account, quota, active lens, or top-up recovery
|
|
5885
|
+
|
|
5886
|
+
Examples that SHOULD invoke this tool:
|
|
5887
|
+
- "What's my account status?"
|
|
5888
|
+
- "How much quota do I have left this week?"
|
|
5889
|
+
|
|
5890
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
5891
|
+
- "Show me today's leads."
|
|
5892
|
+
- "What should I follow up on?"
|
|
5893
|
+
|
|
5894
|
+
## RENDER (quick)
|
|
5895
|
+
|
|
5896
|
+
Report user + org, AND quota whenever readable \u2014 include quota even on a plain
|
|
5897
|
+
"what account?" ask. NEVER mention the lens unless asked (use
|
|
5898
|
+
\`last_requested_lens_name\`, never the id). SILENT on quota ONLY when
|
|
5899
|
+
\`quota_error\` set, \`unlimited_credits\` true, or quota null. Else render
|
|
5900
|
+
Daily/Weekly/Monthly from \`quota.user\` (fall back to \`quota.org\` if \`user\`
|
|
5901
|
+
absent) as \`$used / $cap (N% used) \xB7 resets\` (or a resource-count table when
|
|
5902
|
+
\`spend[]\` empty). Never say raw "credits".
|
|
5903
|
+
|
|
5904
|
+
---
|
|
5905
|
+
|
|
5906
|
+
Show the user's account state \u2014 admin rights, language, last-active lens, quota usage across daily/weekly/monthly windows, and whether the org's intelligence is mid-regeneration. **Show quota the way the web app does \u2014 a percentage-used + dollar-spend gauge per window, never raw "credits".** Each window in \`quota.<group>.spend[]\` carries \`current_units\` / \`max_units\` in dollar_cents (% used = the ratio, $ = \`/100\`); the \`quota.<group>.resources[]\` list gives the per-resource usage breakdown (\`count\`, plus \`max_units\` when a per-resource cap exists). **Pre-check the \`LENS_EXTRA_REFILL\` resource here before calling \`leadbay_extend_lens\`** \u2014 look in **\`quota.org.resources[]\`** first (admins), and fall back to **\`quota.user.resources[]\`** when \`quota.org\` is absent (non-admin callers only get the \`user\` group), matching the resource type **case-insensitively** (it may arrive as \`LENS_EXTRA_REFILL\` or \`lens_extra_refill\`). Its full requested batch must fit into the remaining daily quota or the call is rejected outright. Quota windows also hint at the user's consumption pace: heavy recent activity (ai_rescore / web_fetch near their window limits) is a signal that Leadbay will deliver a larger fresh batch next time the user logs back in, since batch size is paced by real consumption.
|
|
5907
|
+
|
|
5908
|
+
**After a user tops up, do NOT keep refusing \u2014 RETRY.** If the user signals they topped up / bought credits / added credits, the previous QUOTA_EXCEEDED is invalidated the moment the Stripe webhook lands. RE-CALL \`leadbay_account_status\` to pick up the new state AND retry the originally failed call. The retry itself does not require a successful account_status check first \u2014 a topped-up user has cleared the throttle whether or not your cached snapshot reflects it yet. **A stale quota snapshot is never a reason to gate-keep a topped-up user.**
|
|
5909
|
+
|
|
5910
|
+
**\`notifications\` block.** The response now includes a top-level \`notifications\` array listing background work the user (or agent) initiated that has since completed (\`bulk_enrich\`, \`bulk_qualify\`, \`import\`). These are signals to revise prior agent outputs the just-finished work might have made stale \u2014 they're NOT a pending-task list for the user. After revising (or confirming nothing is affected), call \`leadbay_acknowledge_notification(notification_id)\`. Full handling protocol below.
|
|
5911
|
+
|
|
5912
|
+
## GATE \u2014 INSPECT \`_meta.notifications\` ON EVERY RESPONSE
|
|
5913
|
+
|
|
5914
|
+
Some Leadbay tool responses include a \`_meta.notifications\` array listing **background work the user (or you) initiated that has since completed**. The three relevant kinds:
|
|
5915
|
+
|
|
5916
|
+
| Kind | What just finished |
|
|
5917
|
+
|---|---|
|
|
5918
|
+
| \`bulk_enrich\` | A bulk contact-enrichment job (emails / phone numbers attached to leads' contacts). |
|
|
5919
|
+
| \`bulk_qualify\` | A bulk lead-qualification job (web-fetch + AI rescore, producing \`ai_agent_lead_score\` and qualification answers). |
|
|
5920
|
+
| \`import\` | A CSV / CRM file import (rows resolved to leads in the user's pipeline). |
|
|
5921
|
+
|
|
5922
|
+
**Your job when you see an entry: revise prior outputs the just-finished work might have made stale.** This is NOT a pending-task list. It's a "your earlier answer used data that has now changed" signal.
|
|
5923
|
+
|
|
5924
|
+
| Kind | Outputs you've produced that may now be stale \u2014 refresh them |
|
|
5925
|
+
|---|---|
|
|
5926
|
+
| \`bulk_enrich\` | Outreach drafts mentioning these leads' contacts; contact lists; recommended-lead lists citing \`contact_count\`; NEXT STEPS that asked the user to wait for emails / phones. Re-fetch via \`leadbay_get_contacts(leadId)\` for the affected leads. |
|
|
5927
|
+
| \`bulk_qualify\` | Lead rankings / shortlists you produced without \`ai_agent_lead_score\`; "today's leads"; followup maps; prepare-outreach picks. Re-pull via \`leadbay_pull_leads\` / \`leadbay_research_lead_by_id\`. |
|
|
5928
|
+
| \`import\` | "Available leads" claims; pulls from the affected lens that ran before the import landed; followup planning that needed the imported set. Re-pull via \`leadbay_pull_leads\` / \`leadbay_pull_followups\`. |
|
|
5929
|
+
|
|
5930
|
+
**After revising (or after confirming no prior output is affected):** call \`leadbay_acknowledge_notification(notification_id)\` so the entry stops resurfacing on every tool response. Ack-and-move-on is correct even when nothing was stale \u2014 that's how the inbox stays focused on what's actually pending.
|
|
5931
|
+
|
|
5932
|
+
**Do NOT** interpret these entries as "things waiting for the user." The user expects you to handle them silently. They are signals to YOU \u2014 agent \u2014 that prior outputs need a refresh.
|
|
5933
|
+
|
|
5934
|
+
**Poll a job you launched THIS turn; don't poll one from a PREVIOUS turn.** The rule splits by *when* the work was kicked off:
|
|
5935
|
+
|
|
5936
|
+
- **Previous turn / before an MCP restart, and the user has NOT asked about it** \u2014 don't poll for it in the background. Simply continue the conversation; the next time you call any tool, the completed-work entry appears in \`_meta.notifications\` (also on \`leadbay_account_status.notifications\`). This is the ambient push path \u2014 leave it to do its job. **But if the user explicitly asks for status or to "wait for it to finish"** (e.g. a multi-turn flow where a job was launched in a prior turn and this turn says "wait for enrichment to finish, then \u2026"), DO poll its status tool now until done, exactly as for a this-turn job below \u2014 the ambient push only surfaces *completed* work, so it can't answer a live "is it done / wait for it" request while the job is still running.
|
|
5937
|
+
- **This turn (you just launched it)** \u2014 the DEFAULT is: do NOT end your turn on the "launched" ack; stay active and poll the job's status tool in a loop until it reports done, then report the finished result yourself, rather than spinning forever or deferring the result to a later turn. (Two exceptions, detailed below: the user explicitly asked NOT to wait / to run it in the background; or it's a large qualification/import that's async by design \u2014 in those cases hand back the handle instead of looping.) Each status tool has its OWN terminal signal \u2014 poll until:
|
|
5938
|
+
- \`leadbay_bulk_enrich_status\` \u2192 \`all_done:true\` \u2014 OR \`overall_progress.done\` holds steady across several SPACED polls (~15\u201330s apart) over at least ~90s\u20132 min of elapsed time (some contacts are unresolvable, so \`all_done\` can stay false forever). Don't call a plateau from the first few back-to-back reads \u2014 early on \`done\` sits flat while the backend spins up. Once the plateau is real, report what resolved and name what didn't.
|
|
5939
|
+
- \`leadbay_qualify_status\` \u2192 \`still_running\` is empty: every launched lead has finished or failed. (\`in_progress\` also reads \`false\` on the fast path, but it can be \`null\` on the legacy/fallback read \u2014 so treat an empty \`still_running\` as terminal on its own; only require \`in_progress:false\` when that field is actually present.) LIKE imports, large qualification runs are async by design: \`leadbay_bulk_qualify_leads\` defaults to \`wait_for_completion:false\` for \`count > 5\` or chained workflows because blocking can time out, and \`leadbay_qualify_status\` may take minutes/hours. So don't force a long polling loop on a big run \u2014 return the handle/progress and let completion arrive via \`_meta.notifications\` \u2014 UNLESS the user explicitly asked to wait, or it's a small run that finishes quickly. A small \`wait_for_completion:true\` run you can poll to \`still_running\` empty inline.
|
|
5940
|
+
- \`leadbay_import_status\` \u2192 \`status:"complete"\` (or \`"failed"\`). BUT imports are the exception to the stay-active loop: a large \`leadbay_import_leads({wait_for_completion:false})\` is meant to return a handle and resolve over minutes, and the tool does ONE refresh pass per call. Don't block the conversation looping on it \u2014 surface the returned progress/handle and let the completion arrive via \`_meta.notifications\` \u2014 UNLESS the user explicitly asked you to wait for the import, or it's a small import that finishes quickly.
|
|
5941
|
+
|
|
5942
|
+
Enrichment polls to completion in-turn BY DEFAULT \u2014 the exception is when the user explicitly said to start it in the background / not wait ("kick it off, I'll check later"), in which case hand back the bulk_id and let completion arrive via \`_meta.notifications\` (only when a notification id exists; if none was returned, tell the user to ask again / that you'll poll later, since nothing will auto-surface). For qualification and imports, poll inline only for small/quick runs or when the user explicitly asked you to wait; otherwise return the handle and let \`_meta.notifications\` deliver it. Either way, the user should never have to ask "is it done yet?" for work you kicked off in the same turn \u2014 you either report it or hand back a clear in-progress handle.
|
|
5943
|
+
|
|
5944
|
+
Also surfaced as a top-level \`notifications\` array on \`leadbay_account_status\` \u2014 same shape, same handling.
|
|
5945
|
+
|
|
5946
|
+
|
|
5947
|
+
---
|
|
5948
|
+
|
|
5949
|
+
## RENDERING \u2014 quota windows (percentage + $, like the frontend)
|
|
5950
|
+
|
|
5951
|
+
Mirror the Leadbay web quota widget: three windows side by side \u2014 **Daily**,
|
|
5952
|
+
**Weekly**, **Monthly** \u2014 each headlined by a **% used** gauge and a **$ spend /
|
|
5953
|
+
$ cap** figure, with a per-resource usage breakdown underneath. **Never speak in
|
|
5954
|
+
raw "credits"** for quota \u2014 the unit is a percentage and a dollar spend.
|
|
5955
|
+
|
|
5956
|
+
**Include the quota whenever it is readable** \u2014 as part of the default account
|
|
5957
|
+
answer, even when the user only asked "what account am I connected to?". The
|
|
5958
|
+
sole reason to omit it is the silence gate below (unreadable quota, or an
|
|
5959
|
+
unlimited account); it is NOT gated on the user explicitly asking for quota.
|
|
5960
|
+
|
|
5961
|
+
**Silence gate (check FIRST).** Render NOTHING about quota when any of these
|
|
5962
|
+
holds \u2014 do not mention quota at all, do not say "unreadable", never tell the user
|
|
5963
|
+
to reconnect:
|
|
5964
|
+
- \`quota\` is null, OR \`quota_error\` is set (a 401/403 backend quirk for plan-less
|
|
5965
|
+
orgs \u2014 the same token read user/org fine), OR
|
|
5966
|
+
- \`organization.unlimited_credits\` is true (internal/unlimited account \u2014 stay
|
|
5967
|
+
silent on quota; never announce "unlimited").
|
|
5968
|
+
|
|
5969
|
+
**Pick the group (for DISPLAY only).** Prefer \`quota.user\` (present for every
|
|
5970
|
+
caller). Use \`quota.org\` only when \`quota.user\` is absent (admins receive both \u2014
|
|
5971
|
+
still show the caller's own \`user\` view). Call the chosen group \`<group>\` below.
|
|
5972
|
+
|
|
5973
|
+
**Exception \u2014 lens-refill pre-checks read the refill row, ORG-first.** This
|
|
5974
|
+
user-preference is for the display gauge ONLY. When you pre-check the
|
|
5975
|
+
\`LENS_EXTRA_REFILL\` resource before \`leadbay_extend_lens\`, look for the row in
|
|
5976
|
+
**\`quota.org.resources[]\` first** (admins get the org group, and the refill
|
|
5977
|
+
quota is org-scoped there); when \`quota.org\` is absent \u2014 non-admin callers only
|
|
5978
|
+
receive the \`user\` group \u2014 fall back to **\`quota.user.resources[]\`**. Match the
|
|
5979
|
+
resource type case-insensitively (\`LENS_EXTRA_REFILL\` / \`lens_extra_refill\`).
|
|
5980
|
+
Skipping the \`user\` fallback for non-admins would make the row invisible even
|
|
5981
|
+
when the quota data exists, so the agent burns the write and hits the very 429
|
|
5982
|
+
this pre-check exists to avoid.
|
|
5983
|
+
|
|
5984
|
+
**Per window (fixed order: daily \u2192 weekly \u2192 monthly).** Match entries by
|
|
5985
|
+
\`window_type\` (\`"daily"\` / \`"weekly"\` / \`"monthly"\`).
|
|
5986
|
+
|
|
5987
|
+
**Headline \u2014 when \`<group>.spend[]\` has an entry for the window (the % gauge):**
|
|
5988
|
+
- \`pct = round(current_units / max_units \xD7 100)\` (both are dollar_cents).
|
|
5989
|
+
- \`$used = (current_units / 100).toFixed(2)\`, \`$cap = (max_units / 100).toFixed(2)\`.
|
|
5990
|
+
- 10-segment bar in a SINGLE inline-code span (backticks give it contrast):
|
|
5991
|
+
\`filled = round(pct / 10)\` clamped 0..10; \`bar = "\u25B0"\xD7filled + "\u25B1"\xD7(10 \u2212 filled)\`.
|
|
5992
|
+
Use ONLY \`\u25B0\`/\`\u25B1\` \u2014 do NOT use the \`\u2756\` glyph (that identity belongs to lead
|
|
5993
|
+
discovery, not quota).
|
|
5994
|
+
- Line: **\`<Window>\`** \`\` \`\u25B0\u25B0\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\` \`\` \`<pct>% used \xB7 $<used> / $<cap> \xB7 resets <resets_at, relative>\`.
|
|
5995
|
+
e.g. \`**Daily** \` + \`\` \`\u25B0\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\` \`\` + \` 7% used \xB7 $0.84 / $12.00 \xB7 resets in ~7 h\`.
|
|
5996
|
+
|
|
5997
|
+
**Fallback \u2014 when \`<group>.spend[]\` is empty** (internal / free orgs have no
|
|
5998
|
+
OVERALL_SPEND quota): no gauge. Render the per-window resource breakdown as a
|
|
5999
|
+
compact table instead \u2014 one row per resource in \`<group>.resources[]\` for that
|
|
6000
|
+
window: the friendly label + \`count\` (append \`/ <max_units>\` only when
|
|
6001
|
+
\`max_units\` is a number). This is the pre-existing behavior, preserved.
|
|
6002
|
+
|
|
6003
|
+
**Resource labels (look up case-insensitively \u2014 lower-case \`resource_type\`
|
|
6004
|
+
first).** Localize to \`user.language\` (FR canonical shown; English in parens):
|
|
6005
|
+
- \`llm_completion\` \u2192 **G\xE9n\xE9rations par IA** (AI generations)
|
|
6006
|
+
- \`ai_rescore\` \u2192 **Leads qualifi\xE9s** (qualified leads)
|
|
6007
|
+
- \`web_fetch\` \u2192 **Informations web** (web insights)
|
|
6008
|
+
- \`contact_enrichment_phone\` \u2192 **T\xE9l\xE9phones enrichis** (phones enriched)
|
|
6009
|
+
- \`contact_enrichment_email\` \u2192 **E-mails enrichis** (emails enriched)
|
|
6010
|
+
|
|
6011
|
+
Skip any resource type not in this map silently \u2014 never dump the raw
|
|
6012
|
+
\`resource_type\` string at the user.
|
|
6013
|
+
|
|
6014
|
+
**\`resets_at\`.** Show as a relative countdown ("resets in ~7 h", "resets in 3
|
|
6015
|
+
days"), computed against now \u2014 mirroring the widget's "r\xE9initialis\xE9 dans X". The
|
|
6016
|
+
raw value is an ISO-8601 timestamp.
|
|
6017
|
+
|
|
6018
|
+
**Top-up (optional, subordinate).** When \`quota.topup\` is present, you MAY add one
|
|
6019
|
+
small line below the windows: \`Top-up: $<remaining_cents/100> of $<total_credit_cents/100> left\`.
|
|
6020
|
+
Keep it secondary \u2014 the three window gauges are the headline. Omit when null.
|
|
6021
|
+
|
|
6022
|
+
**Legend** (once, below): \`\` \`\u25B0\` used \xB7 \`\u25B1\` remaining \`\`.
|
|
6023
|
+
|
|
6024
|
+
|
|
6025
|
+
---
|
|
6026
|
+
|
|
6027
|
+
WHEN TO USE: at the start of a session to know what the agent can/can't do, after a 429 to explain to the user which resource window was exhausted and when it resets, and after the user signals a top-up so the agent can resume the interrupted workflow.
|
|
6028
|
+
|
|
6029
|
+
WHEN NOT TO USE: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating. And: a recent quota snapshot showing "exhausted" is NOT a reason to refuse a write call when the user has just topped up \u2014 re-call this tool first, then proceed.
|
|
6030
|
+
`,
|
|
6031
|
+
leadbay_scan_portfolio_signals: `## WHEN TO USE
|
|
6032
|
+
|
|
6033
|
+
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>".
|
|
6034
|
+
|
|
6035
|
+
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\`.
|
|
6036
|
+
|
|
6037
|
+
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\`; NEVER a country name in \`city\` \u2014 a whole-country ask means NO geo filter
|
|
6038
|
+
|
|
6039
|
+
Examples that SHOULD invoke this tool:
|
|
6040
|
+
- "Which of my leads acquired a company since 2025?"
|
|
6041
|
+
- "Scan my Lyon portfolio for funding signals."
|
|
6042
|
+
- "Find everyone in Monitor who changed CEO and build a campaign."
|
|
6043
|
+
|
|
6044
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
6045
|
+
- "Look up Acme Corp for me."
|
|
6046
|
+
- "Show me my follow-ups."
|
|
6047
|
+
- "Qualify my next 10 leads."
|
|
6048
|
+
|
|
6049
|
+
## RENDER (quick)
|
|
6050
|
+
|
|
6051
|
+
Cohort grouped by lead: one block per matched lead (name \xB7 location +
|
|
6052
|
+
its matched signal entries, hot first, source-linked). Open with
|
|
6053
|
+
"N match <query> (M scanned)"; ALWAYS close with an honesty footer \u2014
|
|
6054
|
+
"scanned N \xB7 matched M \xB7 K not yet researched". Never present
|
|
6055
|
+
not_researched leads as "no signal". Full layout below.
|
|
6056
|
+
|
|
6057
|
+
---
|
|
6058
|
+
|
|
6059
|
+
Scan a known portfolio for a specific web-research signal in one call. This is
|
|
6060
|
+
the bulk, read-only answer to "which of my leads have signal X" \u2014 the question
|
|
6061
|
+
that otherwise forces a per-lead \`leadbay_research_lead_by_id\` loop (one full
|
|
6062
|
+
profile call per lead, slow and quota-heavy).
|
|
6063
|
+
|
|
6064
|
+
**Reads CACHED signals only \u2014 does not trigger new research.** For each lead in
|
|
6065
|
+
scope it reads \`GET /leads/{id}/web_fetch\` (the already-computed web-research
|
|
6066
|
+
signals) and filters the entries against \`query\`. It issues NO web_fetch POST,
|
|
6067
|
+
so it does not consume AI qualification credits and does not re-crawl. Leads
|
|
6068
|
+
that have no cached content (never qualified, or still in progress) are
|
|
6069
|
+
reported in \`not_researched\` \u2014 they are **NOT** silently treated as "no
|
|
6070
|
+
match". Qualify them with \`leadbay_bulk_qualify_leads\`, then re-scan.
|
|
6071
|
+
|
|
6072
|
+
**Scope.** Pass \`leadIds\` for an explicit cohort, or omit it to scan the
|
|
6073
|
+
Monitor portfolio. Narrow the Monitor scope with \`city\` / \`set_filter\` exactly
|
|
6074
|
+
as \`leadbay_pull_followups\` does (store-then-apply server-side filter).
|
|
6075
|
+
|
|
6076
|
+
**One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
|
|
6077
|
+
|
|
6078
|
+
**On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
|
|
6079
|
+
|
|
6080
|
+
\`axis: "include"\`:
|
|
6081
|
+
|
|
6082
|
+
- \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
|
|
6083
|
+
- \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
|
|
6084
|
+
- \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
|
|
6085
|
+
- \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
|
|
6086
|
+
|
|
6087
|
+
\`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
|
|
6088
|
+
|
|
6089
|
+
On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
|
|
6090
|
+
|
|
6091
|
+
**Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
|
|
6092
|
+
|
|
6093
|
+
Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
|
|
6094
|
+
The
|
|
6095
|
+
scan is bounded by \`max_leads\` (default 200, hard cap 300); when the portfolio
|
|
6096
|
+
is larger, \`truncated_at\` is set and coverage is partial \u2014 say so.
|
|
6097
|
+
|
|
6098
|
+
**Query.** \`query\` is matched case- and accent-insensitively against each
|
|
6099
|
+
signal entry's description, source, and section label. Comma- or
|
|
6100
|
+
space-separated terms are OR'd ("M&A, acquisition, rachet\xE9" matches any). Use
|
|
6101
|
+
\`since\` (ISO date) to keep only entries dated on/after it \u2014 entries with no
|
|
6102
|
+
date are kept (a missing date is not evidence the event is old).
|
|
6103
|
+
|
|
6104
|
+
**Result is campaign-ready.** \`matched[]\` carries \`lead_id\`, \`name\`,
|
|
6105
|
+
\`location\`, and the matching \`matched_signals[]\` (section + hot + source +
|
|
6106
|
+
date + description). Feed the matched \`lead_id\`s straight into
|
|
6107
|
+
\`leadbay_add_leads_to_campaign\` / \`leadbay_create_campaign\`.
|
|
6108
|
+
|
|
6109
|
+
**SIGNAL HONESTY \u2014 never infer signals from freshness.** \`stale_at\`,
|
|
6110
|
+
\`web_fetch_in_progress\`, \`fetch_at\` are freshness markers, not signal
|
|
6111
|
+
indicators \u2014 signal presence is read ONLY from the actual \`signals[]\` /
|
|
6112
|
+
\`web_fetch.content\` entries. For "which of my leads have signal X" across a
|
|
6113
|
+
portfolio, call **\`leadbay_scan_portfolio_signals\`** (bulk-reads cached
|
|
6114
|
+
signals); don't loop \`leadbay_research_lead_by_id\` per lead or guess from
|
|
6115
|
+
freshness. A lead with no cached content is \`not_researched\`, not "no match";
|
|
6116
|
+
never report a signal verdict for a lead you never read.
|
|
6117
|
+
|
|
6118
|
+
|
|
6119
|
+
WHEN TO USE: when the user wants to filter a known
|
|
6120
|
+
portfolio by a web-research signal across many leads at once \u2014 discovering a
|
|
6121
|
+
cohort to act on, not inspecting a single lead.
|
|
6122
|
+
|
|
6123
|
+
WHEN NOT TO USE: for a single named company
|
|
6124
|
+
(leadbay_research_lead_by_name_fuzzy) or one lead by UUID
|
|
6125
|
+
(leadbay_research_lead_by_id); to qualify leads that have no signals yet
|
|
6126
|
+
(leadbay_bulk_qualify_leads); or to just list follow-ups with no signal filter
|
|
6127
|
+
(leadbay_pull_followups).
|
|
6128
|
+
|
|
6129
|
+
---
|
|
6130
|
+
|
|
6131
|
+
## RENDERING \u2014 bulk signal-scan results
|
|
6132
|
+
|
|
6133
|
+
The output is a cohort, grouped by lead. Lead with the matches, end with an
|
|
6134
|
+
honesty footer \u2014 never hide what wasn't scanned.
|
|
6135
|
+
|
|
6136
|
+
### Matched leads
|
|
6137
|
+
|
|
6138
|
+
Open with a one-line headline: \`**N leads match "<query>"** (M scanned).\`
|
|
6139
|
+
|
|
6140
|
+
Then one block per \`matched[]\` lead, ordered with \`hot\` matches first. Emit
|
|
6141
|
+
each as a host-parseable per-lead block so the chat host's place-card
|
|
6142
|
+
auto-detector can render it (per the repo "feed the address auto-detector"
|
|
6143
|
+
convention):
|
|
6144
|
+
|
|
6145
|
+
\`\`\`
|
|
6146
|
+
### <name> \xB7 <location>
|
|
6147
|
+
|
|
6148
|
+
<for each matched_signal, one bullet>
|
|
6149
|
+
- **<section_emoji> <section_label>** \u2014 <description> <\u{1F525} if hot> ([source](<source>), <date>)
|
|
6150
|
+
\`\`\`
|
|
6151
|
+
|
|
6152
|
+
- **Bold** the description of \`hot: true\` entries; leave cold entries plain.
|
|
6153
|
+
- Render \`source\` as a markdown link \`([source](url), date)\`; omit the date
|
|
6154
|
+
when null, omit the link when \`source\` is empty.
|
|
6155
|
+
- Cap to the 3 strongest signals per lead (hot first, then by date desc); if a
|
|
6156
|
+
lead has more, end its block with \`_+K more signals_\`.
|
|
6157
|
+
- When \`name\` is null (the scan was scoped by \`leadIds\` and the read failed to
|
|
6158
|
+
carry firmographics), fall back to \`### Lead <lead_id>\` \u2014 but prefer to enrich
|
|
6159
|
+
the name via the matched lead's own data when available.
|
|
6160
|
+
|
|
6161
|
+
### Honesty footer (ALWAYS print)
|
|
6162
|
+
|
|
6163
|
+
A single italic line summarising coverage:
|
|
6164
|
+
|
|
6165
|
+
\`_Scanned N \xB7 matched M \xB7 K had no cached signals (not yet researched)._\`
|
|
6166
|
+
|
|
6167
|
+
- When \`not_researched\` is non-empty, this is load-bearing: state plainly that
|
|
6168
|
+
those K leads were NOT searched and were NOT counted as "no match". Offer to
|
|
6169
|
+
qualify them and re-scan (see NEXT STEPS).
|
|
6170
|
+
- When \`truncated_at\` is set, add: \`_Coverage partial \u2014 only the first <truncated_at>
|
|
6171
|
+
leads were scanned; narrow the scope or raise max_leads._\`
|
|
6172
|
+
|
|
6173
|
+
**Hide:** raw \`lead_id\` in prose (use it only for the campaign call), \`_meta\`,
|
|
6174
|
+
empty arrays, any freshness field. NEVER present \`not_researched\` leads as
|
|
6175
|
+
"no signal found".
|
|
6176
|
+
|
|
6177
|
+
|
|
6178
|
+
---
|
|
6179
|
+
|
|
6180
|
+
## NEXT STEPS \u2014 after the signal scan
|
|
6181
|
+
|
|
6182
|
+
**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.
|
|
6183
|
+
|
|
6184
|
+
**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.
|
|
6185
|
+
|
|
6186
|
+
**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.
|
|
6187
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
6188
|
+
- 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.
|
|
6189
|
+
|
|
6190
|
+
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):
|
|
6191
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
6192
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
6193
|
+
|
|
6194
|
+
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.
|
|
6195
|
+
|
|
6196
|
+
---
|
|
6197
|
+
|
|
6198
|
+
|
|
6199
|
+
|
|
6200
|
+
The scan exists to BUILD A COHORT, not just to list. The default next move is
|
|
6201
|
+
almost always "turn the matched leads into a campaign."
|
|
6202
|
+
|
|
6203
|
+
| Observation | Suggest | Calls |
|
|
6204
|
+
|---------------------------------------------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------|
|
|
6205
|
+
| \`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) |
|
|
6206
|
+
| \`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 |
|
|
6207
|
+
| Zero matches but leads were researched | "Widen the query (synonyms) or relax \`since\`" | leadbay_scan_portfolio_signals(query: "<broader terms>", since: omit-or-earlier) |
|
|
6208
|
+
| \`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\` |
|
|
6209
|
+
| One standout matched lead | "Open that lead's full brief" | leadbay_research_lead_by_id(leadId) |
|
|
6210
|
+
|
|
6211
|
+
NEVER report leads in \`not_researched\` as if they had no matching signal \u2014 they
|
|
6212
|
+
were never read. Distinguish "no signal X found" (researched, no match) from
|
|
6213
|
+
"not yet researched" (no data to search) every time.
|
|
6214
|
+
`
|
|
6215
|
+
};
|
|
5863
6216
|
}
|
|
5864
6217
|
});
|
|
5865
6218
|
|
|
@@ -21692,6 +22045,7 @@ __export(dist_exports, {
|
|
|
21692
22045
|
InMemoryBulkStore: () => InMemoryBulkStore,
|
|
21693
22046
|
LeadbayClient: () => LeadbayClient,
|
|
21694
22047
|
LocalBulkStore: () => LocalBulkStore,
|
|
22048
|
+
NO_COMMERCE_TOOL_DESCRIPTIONS: () => NO_COMMERCE_TOOL_DESCRIPTIONS,
|
|
21695
22049
|
NotificationsInbox: () => NotificationsInbox,
|
|
21696
22050
|
NotificationsWsClient: () => NotificationsWsClient,
|
|
21697
22051
|
REGIONS: () => REGIONS,
|
|
@@ -21917,6 +22271,7 @@ var init_dist = __esm({
|
|
|
21917
22271
|
init_artifact_kit();
|
|
21918
22272
|
init_bulk_store();
|
|
21919
22273
|
init_getting_started();
|
|
22274
|
+
init_tool_descriptions_generated();
|
|
21920
22275
|
granularReadTools = [
|
|
21921
22276
|
listLenses,
|
|
21922
22277
|
discoverLeads,
|
|
@@ -25724,9 +26079,8 @@ function buildAcknowledgeUpdateTool(opts) {
|
|
|
25724
26079
|
var ENRICHMENT_TERMINAL = `A settled-empty enrichment is TERMINAL \u2014 do not re-attempt it on a later run. On any contact record, \`enrichment.done: true\` together with \`enrichment.credits_used: 0\` means the reveal completed and the provider returned nothing. Roughly 29% of all enrichments land there, so treat it as an ordinary outcome, not an anomaly. The answer will not change tomorrow: a scheduled task that keeps calling leadbay_enrich_titles on the same contacts every run is buying nothing and hiding its own lack of progress. Read the two fields TOGETHER \u2014 \`credits_used: 0\` is also what an IN-FLIGHT reservation reports (\`done: false\`), and an ABSENT \`credits_used\` means the cost is unknown, not zero; neither is a verdict on its own. \`enrichment: null\` is a different state again \u2014 that contact was never requested and IS enrichable. The one retryable exception is a launch that errored in this same session: its reservation settles as a zero-credit failure, so retry it once, then treat it as terminal. When you report to the user, say what actually happened \u2014 "we looked for a contact at <company> and there isn't one we can reach" \u2014 instead of presenting it as still pending or silently retrying it.`;
|
|
25725
26080
|
var FRICTION = `Problem reports: when the user asks you to report a Leadbay problem ("report this", "tell the team this didn't work"), call leadbay_report_friction with {category, message (the user's own words), tool_called?, severity?}. If they stated the problem in the same breath as the request, those words ARE the message \u2014 send it in that turn rather than asking them to confirm wording they just gave you, and never stall on optional fields (omit what you don't know). If you notice a problem worth reporting but the user hasn't asked, OFFER once \u2014 "Want me to report this to the Leadbay team?" \u2014 and call it only if they agree. Never call it unprompted. Always tell the user the outcome the tool returns: if \`reported\` is false the report was NOT delivered and you must say so rather than implying it was sent. Frustration alone is not a reason to call it: keep solving their ask.`;
|
|
25726
26081
|
var MENTAL_MODEL = `How Leadbay works (mental model): Leadbay is a sales inbox, not a queryable database. Each day the user logs back in, a fresh batch of leads is delivered. Batch size is paced by how many leads the user has actually acted on recently \u2014 some workflows produce a big stream of smaller prospects, others a narrow stream of bigger ones. Pulling more won't produce more; the user acting on leads (outreach, skips, saves) does.`;
|
|
25727
|
-
var
|
|
25728
|
-
|
|
25729
|
-
Show the refreshed quota AFTER a paid action has actually COMPLETED: when leadbay_bulk_enrich_status reports the job done \u2014 all_done, OR a plateau you've decided is terminal (overall_progress.done stopped climbing across spaced polls, so some contacts are unresolvable and all_done stays false) \u2014 OR a top-up the user confirmed landed, call leadbay_account_status once and render the refreshed quota \u2014 the per-window %/$ gauge (Daily/Weekly/Monthly) it returns \u2014 so the user sees where they now stand. Wait for genuine completion: leadbay_enrich_contacts only LAUNCHES an async reveal (it returns a hint to check back in ~60s), so do NOT refresh quota right after it \u2014 the usage isn't reflected yet. For that single-contact flow, refresh only once a re-read of the lead's contacts (leadbay_research_lead_by_id; leadbay_get_contacts where exposed) shows the REQUESTED channel actually landed \u2014 the requested email and/or phone_number present \u2014 NOT enrichment.done alone (that flag is already true for a contact enriched on the other channel earlier, so a phone reveal could otherwise trigger the refresh before phone_number arrives). This is the canonical quota surface; do NOT hand-roll a 'credits' line in its place. Skip it only when account_status reports unlimited_credits, quota_error, or a null quota (nothing to show), or when billing is genuinely unavailable. Do it ONCE per completed action \u2014 not after every poll while work is still in progress.`;
|
|
26082
|
+
var QUOTA_REFRESH = `Show the refreshed quota AFTER a paid action has actually COMPLETED: when leadbay_bulk_enrich_status reports the job done \u2014 all_done, OR a plateau you've decided is terminal (overall_progress.done stopped climbing across spaced polls, so some contacts are unresolvable and all_done stays false) \u2014 OR a top-up the user confirmed landed, call leadbay_account_status once and render the refreshed quota \u2014 the per-window %/$ gauge (Daily/Weekly/Monthly) it returns \u2014 so the user sees where they now stand. Wait for genuine completion: leadbay_enrich_contacts only LAUNCHES an async reveal (it returns a hint to check back in ~60s), so do NOT refresh quota right after it \u2014 the usage isn't reflected yet. For that single-contact flow, refresh only once a re-read of the lead's contacts (leadbay_research_lead_by_id; leadbay_get_contacts where exposed) shows the REQUESTED channel actually landed \u2014 the requested email and/or phone_number present \u2014 NOT enrichment.done alone (that flag is already true for a contact enriched on the other channel earlier, so a phone reveal could otherwise trigger the refresh before phone_number arrives). This is the canonical quota surface; do NOT hand-roll a 'credits' line in its place. Skip it only when account_status reports unlimited_credits, quota_error, or a null quota (nothing to show), or when billing is genuinely unavailable. Do it ONCE per completed action \u2014 not after every poll while work is still in progress.`;
|
|
26083
|
+
var QUOTA_TOPUP = `Quota & top-ups: when a tool returns QUOTA_EXCEEDED / 429, the user has TWO options \u2014 wait for the window reset (daily / weekly / monthly resets shown in leadbay_account_status), OR top up AI credits (top-ups clear the throttle IMMEDIATELY \u2014 they are not subject to the same window). Always offer BOTH options; default-recommending 'wait until tomorrow' is wrong when a 30-second top-up unblocks the same call. If the host exposes leadbay_create_topup_link, OFFER it on every quota wall: 'Want me to generate a top-up link?' \u2014 when the user says yes, call leadbay_create_topup_link and surface the returned Stripe URL as a clickable link for the user to open in their browser. (Sibling leadbay_open_billing_portal is for ongoing subscription changes, not one-shot top-ups.) AFTER the user has topped up: do NOT keep refusing operations. A top-up invalidates every prior 429 and every stale 'you're at your quota' snapshot. The moment the user signals they topped up / bought credits / added credits \u2014 even WITHOUT re-calling account_status \u2014 treat the previous quota state as void and RETRY the originally failed call. (Best practice: re-call leadbay_account_status to surface the fresh state to the user, then retry; but the retry itself does NOT require a successful account_status check first. If the retry hits the wall again, THEN you have evidence the top-up didn't land; only then re-offer top-up / wait.) The agent's job after a top-up is to RESUME the workflow the user was on, not gate-keep.`;
|
|
25730
26084
|
var TRANSIENT_401 = `Transient 401s (DO NOT narrate as an auth problem): Leadbay bearer tokens do NOT expire on a timer, and the client already auto-retried this call once before surfacing it. So a lone AUTH_EXPIRED / 401 from a tool is almost always a brief Leadbay-side hiccup, NOT a sign the user must sign in again. When you see ONE, just silently retry the operation once more and carry on \u2014 do NOT tell the user to log in again, reconnect the connector, reauthorize, or 're-load Leadbay', and do NOT surface the bare '401' to them at all. Only if calls keep failing across several attempts should you mention it \u2014 and then frame it as a possible Leadbay-side issue or a possible logout (you cannot tell which from here), and offer to report it to the team. Never turn a one-off 401 into a 'your connection needs re-authenticating' message; that is the wrong read on a connection that is actually working.`;
|
|
25731
26085
|
var TRIGGERED_BY = `Trigger provenance (MANDATORY): every Leadbay composite-tool call MUST carry a non-empty \`_triggered_by\` argument \u2014 the verbatim slice of the user's most recent message that this call is acting upon. Quote literally; do NOT paraphrase, summarize, or substitute a one-word label like "leads" or "request" (those are rejected). If you are acting WITHOUT a fresh user message (a memory recall, a scheduled run, a self-initiated retry), pass the actual instruction you are acting on \u2014 the recalled directive, the schedule's intent, or the original request being retried \u2014 so the value is always a real, auditable trace. Strip any secrets the user pasted (API keys, passwords, card numbers, full home addresses) \u2014 replace with [REDACTED]. A composite call missing or blanking this field is rejected with LAST_PROMPT_REQUIRED; just re-call with the field set. This is a protocol requirement on EVERY composite invocation (not just the first), independent of any telemetry setting.`;
|
|
25732
26086
|
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.`;
|
|
@@ -25870,7 +26224,10 @@ function buildServerInstructions(exposed) {
|
|
|
25870
26224
|
}
|
|
25871
26225
|
parts.push(TRIGGERED_BY);
|
|
25872
26226
|
parts.push(MENTAL_MODEL);
|
|
25873
|
-
|
|
26227
|
+
if (has("leadbay_create_topup_link")) {
|
|
26228
|
+
parts.push(QUOTA_TOPUP);
|
|
26229
|
+
}
|
|
26230
|
+
parts.push(QUOTA_REFRESH);
|
|
25874
26231
|
if (has("leadbay_enrich_titles")) {
|
|
25875
26232
|
parts.push(ENRICHMENT_TERMINAL);
|
|
25876
26233
|
}
|
|
@@ -25890,6 +26247,10 @@ function buildServerInstructions(exposed) {
|
|
|
25890
26247
|
parts.push(BUILTIN_WIDGETS_PARAGRAPH);
|
|
25891
26248
|
return parts.join("\n\n");
|
|
25892
26249
|
}
|
|
26250
|
+
var COMMERCE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
26251
|
+
"leadbay_create_topup_link",
|
|
26252
|
+
"leadbay_open_billing_portal"
|
|
26253
|
+
]);
|
|
25893
26254
|
function formatErrorForLLM(err) {
|
|
25894
26255
|
if (err && typeof err === "object" && err.error === true) {
|
|
25895
26256
|
const parts = [`${err.message}.`, err.hint];
|
|
@@ -25977,16 +26338,19 @@ function buildServer(client, opts = {}) {
|
|
|
25977
26338
|
if (opts.extraTools) {
|
|
25978
26339
|
exposedTools.push(...opts.extraTools);
|
|
25979
26340
|
}
|
|
26341
|
+
const includeCommerce = opts.includeCommerce !== false;
|
|
26342
|
+
client.commerce = includeCommerce;
|
|
25980
26343
|
const toolByName = /* @__PURE__ */ new Map();
|
|
25981
26344
|
for (const t of exposedTools) {
|
|
25982
|
-
if (
|
|
25983
|
-
|
|
25984
|
-
|
|
25985
|
-
|
|
25986
|
-
|
|
25987
|
-
|
|
25988
|
-
|
|
25989
|
-
|
|
26345
|
+
if (toolByName.has(t.name) || t.name === "leadbay_login") continue;
|
|
26346
|
+
if (!includeCommerce && COMMERCE_TOOL_NAMES.has(t.name)) continue;
|
|
26347
|
+
const noCommerce = includeCommerce ? void 0 : NO_COMMERCE_TOOL_DESCRIPTIONS[t.name];
|
|
26348
|
+
toolByName.set(
|
|
26349
|
+
t.name,
|
|
26350
|
+
withTriggeredByMeta(noCommerce ? { ...t, description: noCommerce } : t, {
|
|
26351
|
+
mandatory: COMPOSITE_FILE_TOOL_NAMES.has(t.name)
|
|
26352
|
+
})
|
|
26353
|
+
);
|
|
25990
26354
|
}
|
|
25991
26355
|
const exposedNames = new Set(toolByName.keys());
|
|
25992
26356
|
const server = new Server(
|
|
@@ -26573,7 +26937,7 @@ import { spawn } from "child_process";
|
|
|
26573
26937
|
import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
|
|
26574
26938
|
import { join as join2 } from "path";
|
|
26575
26939
|
import { homedir as homedir2 } from "os";
|
|
26576
|
-
var
|
|
26940
|
+
var HOSTED_MCP_URL_CHATGPT = "https://mcp.leadbay.app/chatgpt/mcp";
|
|
26577
26941
|
function formatInstallOsLabel(platform2 = process.platform, arch = process.arch) {
|
|
26578
26942
|
const name = platform2 === "darwin" ? "macOS" : platform2 === "win32" ? "Windows" : platform2 === "linux" ? "Linux" : platform2;
|
|
26579
26943
|
return `${name} (${arch})`;
|
|
@@ -26702,7 +27066,7 @@ async function detectClients() {
|
|
|
26702
27066
|
out.push({ id: "claude-desktop", label: "Claude Desktop", detail: claudeDesktopPath, configPath: claudeDesktopPath, mode, supportDir: claudeSupportDir });
|
|
26703
27067
|
}
|
|
26704
27068
|
if (await isChatGptDesktopInstalled(home)) {
|
|
26705
|
-
out.push({ id: "chatgpt-desktop", label: "ChatGPT Desktop", detail:
|
|
27069
|
+
out.push({ id: "chatgpt-desktop", label: "ChatGPT Desktop", detail: HOSTED_MCP_URL_CHATGPT });
|
|
26706
27070
|
}
|
|
26707
27071
|
const cursorPath = process.platform === "win32" ? `${home}\\.cursor\\mcp.json` : `${home}/.cursor/mcp.json`;
|
|
26708
27072
|
if (await isCursorInstalled(home)) {
|
|
@@ -28002,7 +28366,7 @@ var OAUTH_BASE_URLS = {
|
|
|
28002
28366
|
fr: "https://staging.api.leadbay.app"
|
|
28003
28367
|
}
|
|
28004
28368
|
};
|
|
28005
|
-
var VERSION = "0.
|
|
28369
|
+
var VERSION = "0.34.0";
|
|
28006
28370
|
var HELP = `
|
|
28007
28371
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
28008
28372
|
|
|
@@ -29019,7 +29383,7 @@ Installing Leadbay MCP into:
|
|
|
29019
29383
|
} else if (c.id === "chatgpt-desktop") {
|
|
29020
29384
|
res = {
|
|
29021
29385
|
ok: true,
|
|
29022
|
-
message: "manual setup required; add this MCP URL in ChatGPT Settings > Apps: " +
|
|
29386
|
+
message: "manual setup required; add this MCP URL in ChatGPT Settings > Apps: " + HOSTED_MCP_URL_CHATGPT
|
|
29023
29387
|
};
|
|
29024
29388
|
} else if (c.id === "claude-desktop" && c.mode?.dxt && c.supportDir) {
|
|
29025
29389
|
const dxtResult = await removeDxtExtension(c.supportDir);
|
package/dist/http-server.js
CHANGED
|
@@ -3150,6 +3150,15 @@ var LeadbayClient = class _LeadbayClient {
|
|
|
3150
3150
|
this._region = region ?? _LeadbayClient.regionFromBaseUrl(baseUrl);
|
|
3151
3151
|
}
|
|
3152
3152
|
}
|
|
3153
|
+
/**
|
|
3154
|
+
* Whether this client may compose text that promotes a purchase. Default
|
|
3155
|
+
* true. The MCP server sets it false for a host whose directory forbids
|
|
3156
|
+
* promoting upgrades (see BuildServerOptions.includeCommerce); the only
|
|
3157
|
+
* effect is that the QUOTA_EXCEEDED hint drops its two selling sentences.
|
|
3158
|
+
* Set per client, and the hosted server builds one client per session, so
|
|
3159
|
+
* this never leaks across tenants.
|
|
3160
|
+
*/
|
|
3161
|
+
commerce = true;
|
|
3153
3162
|
get baseUrl() {
|
|
3154
3163
|
return this._baseUrl;
|
|
3155
3164
|
}
|
|
@@ -3469,7 +3478,12 @@ var LeadbayClient = class _LeadbayClient {
|
|
|
3469
3478
|
// agent can generate the URL itself instead of asking the user to
|
|
3470
3479
|
// navigate to a website. Once the user has topped up, the previous
|
|
3471
3480
|
// 429 is stale — retry the failed call.
|
|
3472
|
-
|
|
3481
|
+
//
|
|
3482
|
+
// The two selling sentences are dropped when `commerce` is off — this
|
|
3483
|
+
// hint is text the agent reads out, and a host may forbid promoting a
|
|
3484
|
+
// purchase. Nothing is reworded; the rest of the hint is unchanged, and
|
|
3485
|
+
// "the user topped up (elsewhere), so retry" survives either way.
|
|
3486
|
+
`${hintBase}` + (this.commerce ? `, OR top up AI credits \u2014 top-ups clear the throttle immediately. Offer the user to generate a Stripe checkout URL via leadbay_create_topup_link, OR direct them to app.leadbay.ai \u2192 Billing. ` : `. `) + `Check leadbay_account_status / leadbay_get_quota to see which resource window (daily/weekly/monthly) was hit. Once the user has topped up, the previous QUOTA_EXCEEDED is stale \u2014 re-call leadbay_account_status to refresh, then RETRY the original operation.`,
|
|
3473
3487
|
endpoint,
|
|
3474
3488
|
retryAfter,
|
|
3475
3489
|
status
|
|
@@ -8451,6 +8465,345 @@ WHEN NOT TO USE: from agent flow \u2014 use leadbay_adjust_audience, which handl
|
|
|
8451
8465
|
|
|
8452
8466
|
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\`.
|
|
8453
8467
|
`;
|
|
8468
|
+
var NO_COMMERCE_TOOL_DESCRIPTIONS = {
|
|
8469
|
+
leadbay_account_status: `## WHEN TO USE
|
|
8470
|
+
|
|
8471
|
+
Trigger phrases: "what's my account status", "how much quota do I have", "what lens am I on", "I topped up / I bought credits / I added credits".
|
|
8472
|
+
|
|
8473
|
+
Do NOT use for: "show me leads" \u2192 \`leadbay_pull_leads\`.
|
|
8474
|
+
|
|
8475
|
+
Prefer when: meta question about account, quota, active lens, or top-up recovery
|
|
8476
|
+
|
|
8477
|
+
Examples that SHOULD invoke this tool:
|
|
8478
|
+
- "What's my account status?"
|
|
8479
|
+
- "How much quota do I have left this week?"
|
|
8480
|
+
|
|
8481
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
8482
|
+
- "Show me today's leads."
|
|
8483
|
+
- "What should I follow up on?"
|
|
8484
|
+
|
|
8485
|
+
## RENDER (quick)
|
|
8486
|
+
|
|
8487
|
+
Report user + org, AND quota whenever readable \u2014 include quota even on a plain
|
|
8488
|
+
"what account?" ask. NEVER mention the lens unless asked (use
|
|
8489
|
+
\`last_requested_lens_name\`, never the id). SILENT on quota ONLY when
|
|
8490
|
+
\`quota_error\` set, \`unlimited_credits\` true, or quota null. Else render
|
|
8491
|
+
Daily/Weekly/Monthly from \`quota.user\` (fall back to \`quota.org\` if \`user\`
|
|
8492
|
+
absent) as \`$used / $cap (N% used) \xB7 resets\` (or a resource-count table when
|
|
8493
|
+
\`spend[]\` empty). Never say raw "credits".
|
|
8494
|
+
|
|
8495
|
+
---
|
|
8496
|
+
|
|
8497
|
+
Show the user's account state \u2014 admin rights, language, last-active lens, quota usage across daily/weekly/monthly windows, and whether the org's intelligence is mid-regeneration. **Show quota the way the web app does \u2014 a percentage-used + dollar-spend gauge per window, never raw "credits".** Each window in \`quota.<group>.spend[]\` carries \`current_units\` / \`max_units\` in dollar_cents (% used = the ratio, $ = \`/100\`); the \`quota.<group>.resources[]\` list gives the per-resource usage breakdown (\`count\`, plus \`max_units\` when a per-resource cap exists). **Pre-check the \`LENS_EXTRA_REFILL\` resource here before calling \`leadbay_extend_lens\`** \u2014 look in **\`quota.org.resources[]\`** first (admins), and fall back to **\`quota.user.resources[]\`** when \`quota.org\` is absent (non-admin callers only get the \`user\` group), matching the resource type **case-insensitively** (it may arrive as \`LENS_EXTRA_REFILL\` or \`lens_extra_refill\`). Its full requested batch must fit into the remaining daily quota or the call is rejected outright. Quota windows also hint at the user's consumption pace: heavy recent activity (ai_rescore / web_fetch near their window limits) is a signal that Leadbay will deliver a larger fresh batch next time the user logs back in, since batch size is paced by real consumption.
|
|
8498
|
+
|
|
8499
|
+
**After a user tops up, do NOT keep refusing \u2014 RETRY.** If the user signals they topped up / bought credits / added credits, the previous QUOTA_EXCEEDED is invalidated the moment the Stripe webhook lands. RE-CALL \`leadbay_account_status\` to pick up the new state AND retry the originally failed call. The retry itself does not require a successful account_status check first \u2014 a topped-up user has cleared the throttle whether or not your cached snapshot reflects it yet. **A stale quota snapshot is never a reason to gate-keep a topped-up user.**
|
|
8500
|
+
|
|
8501
|
+
**\`notifications\` block.** The response now includes a top-level \`notifications\` array listing background work the user (or agent) initiated that has since completed (\`bulk_enrich\`, \`bulk_qualify\`, \`import\`). These are signals to revise prior agent outputs the just-finished work might have made stale \u2014 they're NOT a pending-task list for the user. After revising (or confirming nothing is affected), call \`leadbay_acknowledge_notification(notification_id)\`. Full handling protocol below.
|
|
8502
|
+
|
|
8503
|
+
## GATE \u2014 INSPECT \`_meta.notifications\` ON EVERY RESPONSE
|
|
8504
|
+
|
|
8505
|
+
Some Leadbay tool responses include a \`_meta.notifications\` array listing **background work the user (or you) initiated that has since completed**. The three relevant kinds:
|
|
8506
|
+
|
|
8507
|
+
| Kind | What just finished |
|
|
8508
|
+
|---|---|
|
|
8509
|
+
| \`bulk_enrich\` | A bulk contact-enrichment job (emails / phone numbers attached to leads' contacts). |
|
|
8510
|
+
| \`bulk_qualify\` | A bulk lead-qualification job (web-fetch + AI rescore, producing \`ai_agent_lead_score\` and qualification answers). |
|
|
8511
|
+
| \`import\` | A CSV / CRM file import (rows resolved to leads in the user's pipeline). |
|
|
8512
|
+
|
|
8513
|
+
**Your job when you see an entry: revise prior outputs the just-finished work might have made stale.** This is NOT a pending-task list. It's a "your earlier answer used data that has now changed" signal.
|
|
8514
|
+
|
|
8515
|
+
| Kind | Outputs you've produced that may now be stale \u2014 refresh them |
|
|
8516
|
+
|---|---|
|
|
8517
|
+
| \`bulk_enrich\` | Outreach drafts mentioning these leads' contacts; contact lists; recommended-lead lists citing \`contact_count\`; NEXT STEPS that asked the user to wait for emails / phones. Re-fetch via \`leadbay_get_contacts(leadId)\` for the affected leads. |
|
|
8518
|
+
| \`bulk_qualify\` | Lead rankings / shortlists you produced without \`ai_agent_lead_score\`; "today's leads"; followup maps; prepare-outreach picks. Re-pull via \`leadbay_pull_leads\` / \`leadbay_research_lead_by_id\`. |
|
|
8519
|
+
| \`import\` | "Available leads" claims; pulls from the affected lens that ran before the import landed; followup planning that needed the imported set. Re-pull via \`leadbay_pull_leads\` / \`leadbay_pull_followups\`. |
|
|
8520
|
+
|
|
8521
|
+
**After revising (or after confirming no prior output is affected):** call \`leadbay_acknowledge_notification(notification_id)\` so the entry stops resurfacing on every tool response. Ack-and-move-on is correct even when nothing was stale \u2014 that's how the inbox stays focused on what's actually pending.
|
|
8522
|
+
|
|
8523
|
+
**Do NOT** interpret these entries as "things waiting for the user." The user expects you to handle them silently. They are signals to YOU \u2014 agent \u2014 that prior outputs need a refresh.
|
|
8524
|
+
|
|
8525
|
+
**Poll a job you launched THIS turn; don't poll one from a PREVIOUS turn.** The rule splits by *when* the work was kicked off:
|
|
8526
|
+
|
|
8527
|
+
- **Previous turn / before an MCP restart, and the user has NOT asked about it** \u2014 don't poll for it in the background. Simply continue the conversation; the next time you call any tool, the completed-work entry appears in \`_meta.notifications\` (also on \`leadbay_account_status.notifications\`). This is the ambient push path \u2014 leave it to do its job. **But if the user explicitly asks for status or to "wait for it to finish"** (e.g. a multi-turn flow where a job was launched in a prior turn and this turn says "wait for enrichment to finish, then \u2026"), DO poll its status tool now until done, exactly as for a this-turn job below \u2014 the ambient push only surfaces *completed* work, so it can't answer a live "is it done / wait for it" request while the job is still running.
|
|
8528
|
+
- **This turn (you just launched it)** \u2014 the DEFAULT is: do NOT end your turn on the "launched" ack; stay active and poll the job's status tool in a loop until it reports done, then report the finished result yourself, rather than spinning forever or deferring the result to a later turn. (Two exceptions, detailed below: the user explicitly asked NOT to wait / to run it in the background; or it's a large qualification/import that's async by design \u2014 in those cases hand back the handle instead of looping.) Each status tool has its OWN terminal signal \u2014 poll until:
|
|
8529
|
+
- \`leadbay_bulk_enrich_status\` \u2192 \`all_done:true\` \u2014 OR \`overall_progress.done\` holds steady across several SPACED polls (~15\u201330s apart) over at least ~90s\u20132 min of elapsed time (some contacts are unresolvable, so \`all_done\` can stay false forever). Don't call a plateau from the first few back-to-back reads \u2014 early on \`done\` sits flat while the backend spins up. Once the plateau is real, report what resolved and name what didn't.
|
|
8530
|
+
- \`leadbay_qualify_status\` \u2192 \`still_running\` is empty: every launched lead has finished or failed. (\`in_progress\` also reads \`false\` on the fast path, but it can be \`null\` on the legacy/fallback read \u2014 so treat an empty \`still_running\` as terminal on its own; only require \`in_progress:false\` when that field is actually present.) LIKE imports, large qualification runs are async by design: \`leadbay_bulk_qualify_leads\` defaults to \`wait_for_completion:false\` for \`count > 5\` or chained workflows because blocking can time out, and \`leadbay_qualify_status\` may take minutes/hours. So don't force a long polling loop on a big run \u2014 return the handle/progress and let completion arrive via \`_meta.notifications\` \u2014 UNLESS the user explicitly asked to wait, or it's a small run that finishes quickly. A small \`wait_for_completion:true\` run you can poll to \`still_running\` empty inline.
|
|
8531
|
+
- \`leadbay_import_status\` \u2192 \`status:"complete"\` (or \`"failed"\`). BUT imports are the exception to the stay-active loop: a large \`leadbay_import_leads({wait_for_completion:false})\` is meant to return a handle and resolve over minutes, and the tool does ONE refresh pass per call. Don't block the conversation looping on it \u2014 surface the returned progress/handle and let the completion arrive via \`_meta.notifications\` \u2014 UNLESS the user explicitly asked you to wait for the import, or it's a small import that finishes quickly.
|
|
8532
|
+
|
|
8533
|
+
Enrichment polls to completion in-turn BY DEFAULT \u2014 the exception is when the user explicitly said to start it in the background / not wait ("kick it off, I'll check later"), in which case hand back the bulk_id and let completion arrive via \`_meta.notifications\` (only when a notification id exists; if none was returned, tell the user to ask again / that you'll poll later, since nothing will auto-surface). For qualification and imports, poll inline only for small/quick runs or when the user explicitly asked you to wait; otherwise return the handle and let \`_meta.notifications\` deliver it. Either way, the user should never have to ask "is it done yet?" for work you kicked off in the same turn \u2014 you either report it or hand back a clear in-progress handle.
|
|
8534
|
+
|
|
8535
|
+
Also surfaced as a top-level \`notifications\` array on \`leadbay_account_status\` \u2014 same shape, same handling.
|
|
8536
|
+
|
|
8537
|
+
|
|
8538
|
+
---
|
|
8539
|
+
|
|
8540
|
+
## RENDERING \u2014 quota windows (percentage + $, like the frontend)
|
|
8541
|
+
|
|
8542
|
+
Mirror the Leadbay web quota widget: three windows side by side \u2014 **Daily**,
|
|
8543
|
+
**Weekly**, **Monthly** \u2014 each headlined by a **% used** gauge and a **$ spend /
|
|
8544
|
+
$ cap** figure, with a per-resource usage breakdown underneath. **Never speak in
|
|
8545
|
+
raw "credits"** for quota \u2014 the unit is a percentage and a dollar spend.
|
|
8546
|
+
|
|
8547
|
+
**Include the quota whenever it is readable** \u2014 as part of the default account
|
|
8548
|
+
answer, even when the user only asked "what account am I connected to?". The
|
|
8549
|
+
sole reason to omit it is the silence gate below (unreadable quota, or an
|
|
8550
|
+
unlimited account); it is NOT gated on the user explicitly asking for quota.
|
|
8551
|
+
|
|
8552
|
+
**Silence gate (check FIRST).** Render NOTHING about quota when any of these
|
|
8553
|
+
holds \u2014 do not mention quota at all, do not say "unreadable", never tell the user
|
|
8554
|
+
to reconnect:
|
|
8555
|
+
- \`quota\` is null, OR \`quota_error\` is set (a 401/403 backend quirk for plan-less
|
|
8556
|
+
orgs \u2014 the same token read user/org fine), OR
|
|
8557
|
+
- \`organization.unlimited_credits\` is true (internal/unlimited account \u2014 stay
|
|
8558
|
+
silent on quota; never announce "unlimited").
|
|
8559
|
+
|
|
8560
|
+
**Pick the group (for DISPLAY only).** Prefer \`quota.user\` (present for every
|
|
8561
|
+
caller). Use \`quota.org\` only when \`quota.user\` is absent (admins receive both \u2014
|
|
8562
|
+
still show the caller's own \`user\` view). Call the chosen group \`<group>\` below.
|
|
8563
|
+
|
|
8564
|
+
**Exception \u2014 lens-refill pre-checks read the refill row, ORG-first.** This
|
|
8565
|
+
user-preference is for the display gauge ONLY. When you pre-check the
|
|
8566
|
+
\`LENS_EXTRA_REFILL\` resource before \`leadbay_extend_lens\`, look for the row in
|
|
8567
|
+
**\`quota.org.resources[]\` first** (admins get the org group, and the refill
|
|
8568
|
+
quota is org-scoped there); when \`quota.org\` is absent \u2014 non-admin callers only
|
|
8569
|
+
receive the \`user\` group \u2014 fall back to **\`quota.user.resources[]\`**. Match the
|
|
8570
|
+
resource type case-insensitively (\`LENS_EXTRA_REFILL\` / \`lens_extra_refill\`).
|
|
8571
|
+
Skipping the \`user\` fallback for non-admins would make the row invisible even
|
|
8572
|
+
when the quota data exists, so the agent burns the write and hits the very 429
|
|
8573
|
+
this pre-check exists to avoid.
|
|
8574
|
+
|
|
8575
|
+
**Per window (fixed order: daily \u2192 weekly \u2192 monthly).** Match entries by
|
|
8576
|
+
\`window_type\` (\`"daily"\` / \`"weekly"\` / \`"monthly"\`).
|
|
8577
|
+
|
|
8578
|
+
**Headline \u2014 when \`<group>.spend[]\` has an entry for the window (the % gauge):**
|
|
8579
|
+
- \`pct = round(current_units / max_units \xD7 100)\` (both are dollar_cents).
|
|
8580
|
+
- \`$used = (current_units / 100).toFixed(2)\`, \`$cap = (max_units / 100).toFixed(2)\`.
|
|
8581
|
+
- 10-segment bar in a SINGLE inline-code span (backticks give it contrast):
|
|
8582
|
+
\`filled = round(pct / 10)\` clamped 0..10; \`bar = "\u25B0"\xD7filled + "\u25B1"\xD7(10 \u2212 filled)\`.
|
|
8583
|
+
Use ONLY \`\u25B0\`/\`\u25B1\` \u2014 do NOT use the \`\u2756\` glyph (that identity belongs to lead
|
|
8584
|
+
discovery, not quota).
|
|
8585
|
+
- Line: **\`<Window>\`** \`\` \`\u25B0\u25B0\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\` \`\` \`<pct>% used \xB7 $<used> / $<cap> \xB7 resets <resets_at, relative>\`.
|
|
8586
|
+
e.g. \`**Daily** \` + \`\` \`\u25B0\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\u25B1\` \`\` + \` 7% used \xB7 $0.84 / $12.00 \xB7 resets in ~7 h\`.
|
|
8587
|
+
|
|
8588
|
+
**Fallback \u2014 when \`<group>.spend[]\` is empty** (internal / free orgs have no
|
|
8589
|
+
OVERALL_SPEND quota): no gauge. Render the per-window resource breakdown as a
|
|
8590
|
+
compact table instead \u2014 one row per resource in \`<group>.resources[]\` for that
|
|
8591
|
+
window: the friendly label + \`count\` (append \`/ <max_units>\` only when
|
|
8592
|
+
\`max_units\` is a number). This is the pre-existing behavior, preserved.
|
|
8593
|
+
|
|
8594
|
+
**Resource labels (look up case-insensitively \u2014 lower-case \`resource_type\`
|
|
8595
|
+
first).** Localize to \`user.language\` (FR canonical shown; English in parens):
|
|
8596
|
+
- \`llm_completion\` \u2192 **G\xE9n\xE9rations par IA** (AI generations)
|
|
8597
|
+
- \`ai_rescore\` \u2192 **Leads qualifi\xE9s** (qualified leads)
|
|
8598
|
+
- \`web_fetch\` \u2192 **Informations web** (web insights)
|
|
8599
|
+
- \`contact_enrichment_phone\` \u2192 **T\xE9l\xE9phones enrichis** (phones enriched)
|
|
8600
|
+
- \`contact_enrichment_email\` \u2192 **E-mails enrichis** (emails enriched)
|
|
8601
|
+
|
|
8602
|
+
Skip any resource type not in this map silently \u2014 never dump the raw
|
|
8603
|
+
\`resource_type\` string at the user.
|
|
8604
|
+
|
|
8605
|
+
**\`resets_at\`.** Show as a relative countdown ("resets in ~7 h", "resets in 3
|
|
8606
|
+
days"), computed against now \u2014 mirroring the widget's "r\xE9initialis\xE9 dans X". The
|
|
8607
|
+
raw value is an ISO-8601 timestamp.
|
|
8608
|
+
|
|
8609
|
+
**Top-up (optional, subordinate).** When \`quota.topup\` is present, you MAY add one
|
|
8610
|
+
small line below the windows: \`Top-up: $<remaining_cents/100> of $<total_credit_cents/100> left\`.
|
|
8611
|
+
Keep it secondary \u2014 the three window gauges are the headline. Omit when null.
|
|
8612
|
+
|
|
8613
|
+
**Legend** (once, below): \`\` \`\u25B0\` used \xB7 \`\u25B1\` remaining \`\`.
|
|
8614
|
+
|
|
8615
|
+
|
|
8616
|
+
---
|
|
8617
|
+
|
|
8618
|
+
WHEN TO USE: at the start of a session to know what the agent can/can't do, after a 429 to explain to the user which resource window was exhausted and when it resets, and after the user signals a top-up so the agent can resume the interrupted workflow.
|
|
8619
|
+
|
|
8620
|
+
WHEN NOT TO USE: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating. And: a recent quota snapshot showing "exhausted" is NOT a reason to refuse a write call when the user has just topped up \u2014 re-call this tool first, then proceed.
|
|
8621
|
+
`,
|
|
8622
|
+
leadbay_scan_portfolio_signals: `## WHEN TO USE
|
|
8623
|
+
|
|
8624
|
+
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>".
|
|
8625
|
+
|
|
8626
|
+
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\`.
|
|
8627
|
+
|
|
8628
|
+
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\`; NEVER a country name in \`city\` \u2014 a whole-country ask means NO geo filter
|
|
8629
|
+
|
|
8630
|
+
Examples that SHOULD invoke this tool:
|
|
8631
|
+
- "Which of my leads acquired a company since 2025?"
|
|
8632
|
+
- "Scan my Lyon portfolio for funding signals."
|
|
8633
|
+
- "Find everyone in Monitor who changed CEO and build a campaign."
|
|
8634
|
+
|
|
8635
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
8636
|
+
- "Look up Acme Corp for me."
|
|
8637
|
+
- "Show me my follow-ups."
|
|
8638
|
+
- "Qualify my next 10 leads."
|
|
8639
|
+
|
|
8640
|
+
## RENDER (quick)
|
|
8641
|
+
|
|
8642
|
+
Cohort grouped by lead: one block per matched lead (name \xB7 location +
|
|
8643
|
+
its matched signal entries, hot first, source-linked). Open with
|
|
8644
|
+
"N match <query> (M scanned)"; ALWAYS close with an honesty footer \u2014
|
|
8645
|
+
"scanned N \xB7 matched M \xB7 K not yet researched". Never present
|
|
8646
|
+
not_researched leads as "no signal". Full layout below.
|
|
8647
|
+
|
|
8648
|
+
---
|
|
8649
|
+
|
|
8650
|
+
Scan a known portfolio for a specific web-research signal in one call. This is
|
|
8651
|
+
the bulk, read-only answer to "which of my leads have signal X" \u2014 the question
|
|
8652
|
+
that otherwise forces a per-lead \`leadbay_research_lead_by_id\` loop (one full
|
|
8653
|
+
profile call per lead, slow and quota-heavy).
|
|
8654
|
+
|
|
8655
|
+
**Reads CACHED signals only \u2014 does not trigger new research.** For each lead in
|
|
8656
|
+
scope it reads \`GET /leads/{id}/web_fetch\` (the already-computed web-research
|
|
8657
|
+
signals) and filters the entries against \`query\`. It issues NO web_fetch POST,
|
|
8658
|
+
so it does not consume AI qualification credits and does not re-crawl. Leads
|
|
8659
|
+
that have no cached content (never qualified, or still in progress) are
|
|
8660
|
+
reported in \`not_researched\` \u2014 they are **NOT** silently treated as "no
|
|
8661
|
+
match". Qualify them with \`leadbay_bulk_qualify_leads\`, then re-scan.
|
|
8662
|
+
|
|
8663
|
+
**Scope.** Pass \`leadIds\` for an explicit cohort, or omit it to scan the
|
|
8664
|
+
Monitor portfolio. Narrow the Monitor scope with \`city\` / \`set_filter\` exactly
|
|
8665
|
+
as \`leadbay_pull_followups\` does (store-then-apply server-side filter).
|
|
8666
|
+
|
|
8667
|
+
**One workspace = one country \u2014 a country name is NEVER a location filter.** The admin-area index holds no country nodes, so \`"France"\` matches the *commune of Francs* and \`"United States"\` matches *Statesboro*: the call is silently fenced to one village and every conclusion from it is wrong. City AND country named? Keep the city, drop the country.
|
|
8668
|
+
|
|
8669
|
+
**On \`code: "COUNTRY_LEVEL_LOCATION"\` read \`country_locations[].axis\` and \`[].kind\` \u2014 the recovery differs per case and they are NOT interchangeable, and do NOT retry with another spelling or a nearby city.**
|
|
8670
|
+
|
|
8671
|
+
\`axis: "include"\`:
|
|
8672
|
+
|
|
8673
|
+
- \`home_country\`, or "nationwide" / "everywhere" \u2192 drop that ONE value. Omit the geo argument (\`city\` / \`locations\` / \`location_ids\`) only if nothing else was on it \u2014 then the result covers the whole workspace. If other values remain, keep them and describe the result as those places.
|
|
8674
|
+
- \`foreign_country\` ("leads in France" on a US workspace) \u2192 **unsupported, not unfiltered.** Do NOT re-run without the argument: whole-workspace results are US leads and answer nothing about France. Say the workspace holds only its own country's companies.
|
|
8675
|
+
- \`supranational\` ("EU", "EMEA") \u2192 name what the workspace covers, then offer the whole-workspace view as an explicit choice rather than assuming it.
|
|
8676
|
+
- \`country_indeterminate\` (custom/staging backend) \u2192 its country is unknown, so claim nothing about what it holds.
|
|
8677
|
+
|
|
8678
|
+
\`axis: "exclude"\` reverses all of that \u2014 **never "omit the argument"**, which returns the very companies the user asked to remove. Excluding this workspace's own country would empty it; excluding any other country is a harmless no-op. Either way drop the value and ask what to carve out instead.
|
|
8679
|
+
|
|
8680
|
+
On a lens-WRITING tool (\`new_lens\`, \`adjust_audience\`, \`update_lens_filter\`) write NOTHING, with no re-call in any form: when the country was the only scope; for ANY \`foreign_country\` or \`supranational\` INCLUDE however much else came with it \u2014 the sectors and sizes were QUALIFYING that territory, not a second request, so writing them alone saves a real audience for a territory nobody asked about; and for ANY non-\`foreign_country\` \`exclude\` hit, likewise \u2014 dropping it and writing the rest inverts the ask.
|
|
8681
|
+
|
|
8682
|
+
**Never infer WHICH country this workspace serves from the user's wording** \u2014 "the whole US" does not make it one. Read \`_meta.region\` on any tool result \u2014 it outranks any recalled memory; on \`custom\`, claim nothing.
|
|
8683
|
+
|
|
8684
|
+
Place names never go in \`keywords\`, \`sectors\` or \`refine_prompt\` \u2014 text matches, not geo filters.
|
|
8685
|
+
The
|
|
8686
|
+
scan is bounded by \`max_leads\` (default 200, hard cap 300); when the portfolio
|
|
8687
|
+
is larger, \`truncated_at\` is set and coverage is partial \u2014 say so.
|
|
8688
|
+
|
|
8689
|
+
**Query.** \`query\` is matched case- and accent-insensitively against each
|
|
8690
|
+
signal entry's description, source, and section label. Comma- or
|
|
8691
|
+
space-separated terms are OR'd ("M&A, acquisition, rachet\xE9" matches any). Use
|
|
8692
|
+
\`since\` (ISO date) to keep only entries dated on/after it \u2014 entries with no
|
|
8693
|
+
date are kept (a missing date is not evidence the event is old).
|
|
8694
|
+
|
|
8695
|
+
**Result is campaign-ready.** \`matched[]\` carries \`lead_id\`, \`name\`,
|
|
8696
|
+
\`location\`, and the matching \`matched_signals[]\` (section + hot + source +
|
|
8697
|
+
date + description). Feed the matched \`lead_id\`s straight into
|
|
8698
|
+
\`leadbay_add_leads_to_campaign\` / \`leadbay_create_campaign\`.
|
|
8699
|
+
|
|
8700
|
+
**SIGNAL HONESTY \u2014 never infer signals from freshness.** \`stale_at\`,
|
|
8701
|
+
\`web_fetch_in_progress\`, \`fetch_at\` are freshness markers, not signal
|
|
8702
|
+
indicators \u2014 signal presence is read ONLY from the actual \`signals[]\` /
|
|
8703
|
+
\`web_fetch.content\` entries. For "which of my leads have signal X" across a
|
|
8704
|
+
portfolio, call **\`leadbay_scan_portfolio_signals\`** (bulk-reads cached
|
|
8705
|
+
signals); don't loop \`leadbay_research_lead_by_id\` per lead or guess from
|
|
8706
|
+
freshness. A lead with no cached content is \`not_researched\`, not "no match";
|
|
8707
|
+
never report a signal verdict for a lead you never read.
|
|
8708
|
+
|
|
8709
|
+
|
|
8710
|
+
WHEN TO USE: when the user wants to filter a known
|
|
8711
|
+
portfolio by a web-research signal across many leads at once \u2014 discovering a
|
|
8712
|
+
cohort to act on, not inspecting a single lead.
|
|
8713
|
+
|
|
8714
|
+
WHEN NOT TO USE: for a single named company
|
|
8715
|
+
(leadbay_research_lead_by_name_fuzzy) or one lead by UUID
|
|
8716
|
+
(leadbay_research_lead_by_id); to qualify leads that have no signals yet
|
|
8717
|
+
(leadbay_bulk_qualify_leads); or to just list follow-ups with no signal filter
|
|
8718
|
+
(leadbay_pull_followups).
|
|
8719
|
+
|
|
8720
|
+
---
|
|
8721
|
+
|
|
8722
|
+
## RENDERING \u2014 bulk signal-scan results
|
|
8723
|
+
|
|
8724
|
+
The output is a cohort, grouped by lead. Lead with the matches, end with an
|
|
8725
|
+
honesty footer \u2014 never hide what wasn't scanned.
|
|
8726
|
+
|
|
8727
|
+
### Matched leads
|
|
8728
|
+
|
|
8729
|
+
Open with a one-line headline: \`**N leads match "<query>"** (M scanned).\`
|
|
8730
|
+
|
|
8731
|
+
Then one block per \`matched[]\` lead, ordered with \`hot\` matches first. Emit
|
|
8732
|
+
each as a host-parseable per-lead block so the chat host's place-card
|
|
8733
|
+
auto-detector can render it (per the repo "feed the address auto-detector"
|
|
8734
|
+
convention):
|
|
8735
|
+
|
|
8736
|
+
\`\`\`
|
|
8737
|
+
### <name> \xB7 <location>
|
|
8738
|
+
|
|
8739
|
+
<for each matched_signal, one bullet>
|
|
8740
|
+
- **<section_emoji> <section_label>** \u2014 <description> <\u{1F525} if hot> ([source](<source>), <date>)
|
|
8741
|
+
\`\`\`
|
|
8742
|
+
|
|
8743
|
+
- **Bold** the description of \`hot: true\` entries; leave cold entries plain.
|
|
8744
|
+
- Render \`source\` as a markdown link \`([source](url), date)\`; omit the date
|
|
8745
|
+
when null, omit the link when \`source\` is empty.
|
|
8746
|
+
- Cap to the 3 strongest signals per lead (hot first, then by date desc); if a
|
|
8747
|
+
lead has more, end its block with \`_+K more signals_\`.
|
|
8748
|
+
- When \`name\` is null (the scan was scoped by \`leadIds\` and the read failed to
|
|
8749
|
+
carry firmographics), fall back to \`### Lead <lead_id>\` \u2014 but prefer to enrich
|
|
8750
|
+
the name via the matched lead's own data when available.
|
|
8751
|
+
|
|
8752
|
+
### Honesty footer (ALWAYS print)
|
|
8753
|
+
|
|
8754
|
+
A single italic line summarising coverage:
|
|
8755
|
+
|
|
8756
|
+
\`_Scanned N \xB7 matched M \xB7 K had no cached signals (not yet researched)._\`
|
|
8757
|
+
|
|
8758
|
+
- When \`not_researched\` is non-empty, this is load-bearing: state plainly that
|
|
8759
|
+
those K leads were NOT searched and were NOT counted as "no match". Offer to
|
|
8760
|
+
qualify them and re-scan (see NEXT STEPS).
|
|
8761
|
+
- When \`truncated_at\` is set, add: \`_Coverage partial \u2014 only the first <truncated_at>
|
|
8762
|
+
leads were scanned; narrow the scope or raise max_leads._\`
|
|
8763
|
+
|
|
8764
|
+
**Hide:** raw \`lead_id\` in prose (use it only for the campaign call), \`_meta\`,
|
|
8765
|
+
empty arrays, any freshness field. NEVER present \`not_researched\` leads as
|
|
8766
|
+
"no signal found".
|
|
8767
|
+
|
|
8768
|
+
|
|
8769
|
+
---
|
|
8770
|
+
|
|
8771
|
+
## NEXT STEPS \u2014 after the signal scan
|
|
8772
|
+
|
|
8773
|
+
**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.
|
|
8774
|
+
|
|
8775
|
+
**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.
|
|
8776
|
+
|
|
8777
|
+
**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.
|
|
8778
|
+
- Skip example: "Show me today's leads and then research the top one for me." \u2192 after research completes, emit STOP without the widget.
|
|
8779
|
+
- 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.
|
|
8780
|
+
|
|
8781
|
+
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):
|
|
8782
|
+
- \`ask_user_input_v0\`: \`{questions:[{question,type:"single_select",options:["<Suggest 1>","<Suggest 2>"]}]}\`
|
|
8783
|
+
- \`AskUserQuestion\`: \`{questions:[{question,header:"Next step",multiSelect:false,options:[{label:"<\u22645 words>",description:"<Suggest 1>"}]}]}\`
|
|
8784
|
+
|
|
8785
|
+
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.
|
|
8786
|
+
|
|
8787
|
+
---
|
|
8788
|
+
|
|
8789
|
+
|
|
8790
|
+
|
|
8791
|
+
The scan exists to BUILD A COHORT, not just to list. The default next move is
|
|
8792
|
+
almost always "turn the matched leads into a campaign."
|
|
8793
|
+
|
|
8794
|
+
| Observation | Suggest | Calls |
|
|
8795
|
+
|---------------------------------------------------|--------------------------------------------------------------|----------------------------------------------------------------------------------------|
|
|
8796
|
+
| \`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) |
|
|
8797
|
+
| \`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 |
|
|
8798
|
+
| Zero matches but leads were researched | "Widen the query (synonyms) or relax \`since\`" | leadbay_scan_portfolio_signals(query: "<broader terms>", since: omit-or-earlier) |
|
|
8799
|
+
| \`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\` |
|
|
8800
|
+
| One standout matched lead | "Open that lead's full brief" | leadbay_research_lead_by_id(leadId) |
|
|
8801
|
+
|
|
8802
|
+
NEVER report leads in \`not_researched\` as if they had no matching signal \u2014 they
|
|
8803
|
+
were never read. Distinguish "no signal X found" (researched, no match) from
|
|
8804
|
+
"not yet researched" (no data to search) every time.
|
|
8805
|
+
`
|
|
8806
|
+
};
|
|
8454
8807
|
|
|
8455
8808
|
// ../core/dist/tools/login.js
|
|
8456
8809
|
var login = {
|
|
@@ -23745,9 +24098,8 @@ function buildAcknowledgeUpdateTool(opts) {
|
|
|
23745
24098
|
var ENRICHMENT_TERMINAL = `A settled-empty enrichment is TERMINAL \u2014 do not re-attempt it on a later run. On any contact record, \`enrichment.done: true\` together with \`enrichment.credits_used: 0\` means the reveal completed and the provider returned nothing. Roughly 29% of all enrichments land there, so treat it as an ordinary outcome, not an anomaly. The answer will not change tomorrow: a scheduled task that keeps calling leadbay_enrich_titles on the same contacts every run is buying nothing and hiding its own lack of progress. Read the two fields TOGETHER \u2014 \`credits_used: 0\` is also what an IN-FLIGHT reservation reports (\`done: false\`), and an ABSENT \`credits_used\` means the cost is unknown, not zero; neither is a verdict on its own. \`enrichment: null\` is a different state again \u2014 that contact was never requested and IS enrichable. The one retryable exception is a launch that errored in this same session: its reservation settles as a zero-credit failure, so retry it once, then treat it as terminal. When you report to the user, say what actually happened \u2014 "we looked for a contact at <company> and there isn't one we can reach" \u2014 instead of presenting it as still pending or silently retrying it.`;
|
|
23746
24099
|
var FRICTION = `Problem reports: when the user asks you to report a Leadbay problem ("report this", "tell the team this didn't work"), call leadbay_report_friction with {category, message (the user's own words), tool_called?, severity?}. If they stated the problem in the same breath as the request, those words ARE the message \u2014 send it in that turn rather than asking them to confirm wording they just gave you, and never stall on optional fields (omit what you don't know). If you notice a problem worth reporting but the user hasn't asked, OFFER once \u2014 "Want me to report this to the Leadbay team?" \u2014 and call it only if they agree. Never call it unprompted. Always tell the user the outcome the tool returns: if \`reported\` is false the report was NOT delivered and you must say so rather than implying it was sent. Frustration alone is not a reason to call it: keep solving their ask.`;
|
|
23747
24100
|
var MENTAL_MODEL = `How Leadbay works (mental model): Leadbay is a sales inbox, not a queryable database. Each day the user logs back in, a fresh batch of leads is delivered. Batch size is paced by how many leads the user has actually acted on recently \u2014 some workflows produce a big stream of smaller prospects, others a narrow stream of bigger ones. Pulling more won't produce more; the user acting on leads (outreach, skips, saves) does.`;
|
|
23748
|
-
var
|
|
23749
|
-
|
|
23750
|
-
Show the refreshed quota AFTER a paid action has actually COMPLETED: when leadbay_bulk_enrich_status reports the job done \u2014 all_done, OR a plateau you've decided is terminal (overall_progress.done stopped climbing across spaced polls, so some contacts are unresolvable and all_done stays false) \u2014 OR a top-up the user confirmed landed, call leadbay_account_status once and render the refreshed quota \u2014 the per-window %/$ gauge (Daily/Weekly/Monthly) it returns \u2014 so the user sees where they now stand. Wait for genuine completion: leadbay_enrich_contacts only LAUNCHES an async reveal (it returns a hint to check back in ~60s), so do NOT refresh quota right after it \u2014 the usage isn't reflected yet. For that single-contact flow, refresh only once a re-read of the lead's contacts (leadbay_research_lead_by_id; leadbay_get_contacts where exposed) shows the REQUESTED channel actually landed \u2014 the requested email and/or phone_number present \u2014 NOT enrichment.done alone (that flag is already true for a contact enriched on the other channel earlier, so a phone reveal could otherwise trigger the refresh before phone_number arrives). This is the canonical quota surface; do NOT hand-roll a 'credits' line in its place. Skip it only when account_status reports unlimited_credits, quota_error, or a null quota (nothing to show), or when billing is genuinely unavailable. Do it ONCE per completed action \u2014 not after every poll while work is still in progress.`;
|
|
24101
|
+
var QUOTA_REFRESH = `Show the refreshed quota AFTER a paid action has actually COMPLETED: when leadbay_bulk_enrich_status reports the job done \u2014 all_done, OR a plateau you've decided is terminal (overall_progress.done stopped climbing across spaced polls, so some contacts are unresolvable and all_done stays false) \u2014 OR a top-up the user confirmed landed, call leadbay_account_status once and render the refreshed quota \u2014 the per-window %/$ gauge (Daily/Weekly/Monthly) it returns \u2014 so the user sees where they now stand. Wait for genuine completion: leadbay_enrich_contacts only LAUNCHES an async reveal (it returns a hint to check back in ~60s), so do NOT refresh quota right after it \u2014 the usage isn't reflected yet. For that single-contact flow, refresh only once a re-read of the lead's contacts (leadbay_research_lead_by_id; leadbay_get_contacts where exposed) shows the REQUESTED channel actually landed \u2014 the requested email and/or phone_number present \u2014 NOT enrichment.done alone (that flag is already true for a contact enriched on the other channel earlier, so a phone reveal could otherwise trigger the refresh before phone_number arrives). This is the canonical quota surface; do NOT hand-roll a 'credits' line in its place. Skip it only when account_status reports unlimited_credits, quota_error, or a null quota (nothing to show), or when billing is genuinely unavailable. Do it ONCE per completed action \u2014 not after every poll while work is still in progress.`;
|
|
24102
|
+
var QUOTA_TOPUP = `Quota & top-ups: when a tool returns QUOTA_EXCEEDED / 429, the user has TWO options \u2014 wait for the window reset (daily / weekly / monthly resets shown in leadbay_account_status), OR top up AI credits (top-ups clear the throttle IMMEDIATELY \u2014 they are not subject to the same window). Always offer BOTH options; default-recommending 'wait until tomorrow' is wrong when a 30-second top-up unblocks the same call. If the host exposes leadbay_create_topup_link, OFFER it on every quota wall: 'Want me to generate a top-up link?' \u2014 when the user says yes, call leadbay_create_topup_link and surface the returned Stripe URL as a clickable link for the user to open in their browser. (Sibling leadbay_open_billing_portal is for ongoing subscription changes, not one-shot top-ups.) AFTER the user has topped up: do NOT keep refusing operations. A top-up invalidates every prior 429 and every stale 'you're at your quota' snapshot. The moment the user signals they topped up / bought credits / added credits \u2014 even WITHOUT re-calling account_status \u2014 treat the previous quota state as void and RETRY the originally failed call. (Best practice: re-call leadbay_account_status to surface the fresh state to the user, then retry; but the retry itself does NOT require a successful account_status check first. If the retry hits the wall again, THEN you have evidence the top-up didn't land; only then re-offer top-up / wait.) The agent's job after a top-up is to RESUME the workflow the user was on, not gate-keep.`;
|
|
23751
24103
|
var TRANSIENT_401 = `Transient 401s (DO NOT narrate as an auth problem): Leadbay bearer tokens do NOT expire on a timer, and the client already auto-retried this call once before surfacing it. So a lone AUTH_EXPIRED / 401 from a tool is almost always a brief Leadbay-side hiccup, NOT a sign the user must sign in again. When you see ONE, just silently retry the operation once more and carry on \u2014 do NOT tell the user to log in again, reconnect the connector, reauthorize, or 're-load Leadbay', and do NOT surface the bare '401' to them at all. Only if calls keep failing across several attempts should you mention it \u2014 and then frame it as a possible Leadbay-side issue or a possible logout (you cannot tell which from here), and offer to report it to the team. Never turn a one-off 401 into a 'your connection needs re-authenticating' message; that is the wrong read on a connection that is actually working.`;
|
|
23752
24104
|
var TRIGGERED_BY = `Trigger provenance (MANDATORY): every Leadbay composite-tool call MUST carry a non-empty \`_triggered_by\` argument \u2014 the verbatim slice of the user's most recent message that this call is acting upon. Quote literally; do NOT paraphrase, summarize, or substitute a one-word label like "leads" or "request" (those are rejected). If you are acting WITHOUT a fresh user message (a memory recall, a scheduled run, a self-initiated retry), pass the actual instruction you are acting on \u2014 the recalled directive, the schedule's intent, or the original request being retried \u2014 so the value is always a real, auditable trace. Strip any secrets the user pasted (API keys, passwords, card numbers, full home addresses) \u2014 replace with [REDACTED]. A composite call missing or blanking this field is rejected with LAST_PROMPT_REQUIRED; just re-call with the field set. This is a protocol requirement on EVERY composite invocation (not just the first), independent of any telemetry setting.`;
|
|
23753
24105
|
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.`;
|
|
@@ -23891,7 +24243,10 @@ function buildServerInstructions(exposed) {
|
|
|
23891
24243
|
}
|
|
23892
24244
|
parts.push(TRIGGERED_BY);
|
|
23893
24245
|
parts.push(MENTAL_MODEL);
|
|
23894
|
-
|
|
24246
|
+
if (has("leadbay_create_topup_link")) {
|
|
24247
|
+
parts.push(QUOTA_TOPUP);
|
|
24248
|
+
}
|
|
24249
|
+
parts.push(QUOTA_REFRESH);
|
|
23895
24250
|
if (has("leadbay_enrich_titles")) {
|
|
23896
24251
|
parts.push(ENRICHMENT_TERMINAL);
|
|
23897
24252
|
}
|
|
@@ -23911,6 +24266,10 @@ function buildServerInstructions(exposed) {
|
|
|
23911
24266
|
parts.push(BUILTIN_WIDGETS_PARAGRAPH);
|
|
23912
24267
|
return parts.join("\n\n");
|
|
23913
24268
|
}
|
|
24269
|
+
var COMMERCE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
24270
|
+
"leadbay_create_topup_link",
|
|
24271
|
+
"leadbay_open_billing_portal"
|
|
24272
|
+
]);
|
|
23914
24273
|
function formatErrorForLLM(err) {
|
|
23915
24274
|
if (err && typeof err === "object" && err.error === true) {
|
|
23916
24275
|
const parts = [`${err.message}.`, err.hint];
|
|
@@ -23998,16 +24357,19 @@ function buildServer(client, opts = {}) {
|
|
|
23998
24357
|
if (opts.extraTools) {
|
|
23999
24358
|
exposedTools.push(...opts.extraTools);
|
|
24000
24359
|
}
|
|
24360
|
+
const includeCommerce = opts.includeCommerce !== false;
|
|
24361
|
+
client.commerce = includeCommerce;
|
|
24001
24362
|
const toolByName = /* @__PURE__ */ new Map();
|
|
24002
24363
|
for (const t of exposedTools) {
|
|
24003
|
-
if (
|
|
24004
|
-
|
|
24005
|
-
|
|
24006
|
-
|
|
24007
|
-
|
|
24008
|
-
|
|
24009
|
-
|
|
24010
|
-
|
|
24364
|
+
if (toolByName.has(t.name) || t.name === "leadbay_login") continue;
|
|
24365
|
+
if (!includeCommerce && COMMERCE_TOOL_NAMES.has(t.name)) continue;
|
|
24366
|
+
const noCommerce = includeCommerce ? void 0 : NO_COMMERCE_TOOL_DESCRIPTIONS[t.name];
|
|
24367
|
+
toolByName.set(
|
|
24368
|
+
t.name,
|
|
24369
|
+
withTriggeredByMeta(noCommerce ? { ...t, description: noCommerce } : t, {
|
|
24370
|
+
mandatory: COMPOSITE_FILE_TOOL_NAMES.has(t.name)
|
|
24371
|
+
})
|
|
24372
|
+
);
|
|
24011
24373
|
}
|
|
24012
24374
|
const exposedNames = new Set(toolByName.keys());
|
|
24013
24375
|
const server = new Server(
|
|
@@ -24768,7 +25130,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
24768
25130
|
}
|
|
24769
25131
|
|
|
24770
25132
|
// src/http-server.ts
|
|
24771
|
-
var VERSION = true ? "0.
|
|
25133
|
+
var VERSION = true ? "0.34.0" : "0.0.0-dev";
|
|
24772
25134
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
24773
25135
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
24774
25136
|
var logger = {
|
|
@@ -24888,19 +25250,21 @@ function extractBearer(authHeader) {
|
|
|
24888
25250
|
const m = /^Bearer\s+(.+)$/i.exec(authHeader);
|
|
24889
25251
|
return m ? m[1].trim() : void 0;
|
|
24890
25252
|
}
|
|
24891
|
-
|
|
25253
|
+
var COMMERCE_FREE_PATHS = /* @__PURE__ */ new Set(["/chatgpt/mcp"]);
|
|
25254
|
+
function buildServerFromClient(client, requestTelemetry, resourcePath) {
|
|
24892
25255
|
const includeWrite = parseWriteEnv();
|
|
24893
25256
|
const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
|
|
24894
25257
|
return buildServer(client, {
|
|
24895
25258
|
version: VERSION,
|
|
24896
25259
|
includeWrite,
|
|
24897
25260
|
includeAdvanced,
|
|
25261
|
+
includeCommerce: !COMMERCE_FREE_PATHS.has(resourcePath),
|
|
24898
25262
|
logger,
|
|
24899
25263
|
telemetry: requestTelemetry
|
|
24900
25264
|
});
|
|
24901
25265
|
}
|
|
24902
25266
|
var PRM_PREFIX = "/.well-known/oauth-protected-resource";
|
|
24903
|
-
var RESOURCE_PATHS = ["/mcp", "/sse", "/fr/mcp", "/fr/sse"];
|
|
25267
|
+
var RESOURCE_PATHS = ["/mcp", "/sse", "/fr/mcp", "/fr/sse", "/chatgpt/mcp"];
|
|
24904
25268
|
function requestOrigin(c) {
|
|
24905
25269
|
const url = new URL(c.req.url);
|
|
24906
25270
|
const proto = c.req.header("x-forwarded-proto") ?? url.protocol.replace(/:$/, "");
|
|
@@ -24964,6 +25328,7 @@ app.options("*", (c) => {
|
|
|
24964
25328
|
var MCP_BODY_LIMIT = bodyLimit({ maxSize: 1 * 1024 * 1024 });
|
|
24965
25329
|
app.use("/mcp", MCP_BODY_LIMIT);
|
|
24966
25330
|
app.use("/fr/mcp", MCP_BODY_LIMIT);
|
|
25331
|
+
app.use("/chatgpt/mcp", MCP_BODY_LIMIT);
|
|
24967
25332
|
app.use("/messages", MCP_BODY_LIMIT);
|
|
24968
25333
|
async function handleStreamable(c, resourcePath) {
|
|
24969
25334
|
const foreign = rejectForeignOrigin(c);
|
|
@@ -24976,7 +25341,8 @@ async function handleStreamable(c, resourcePath) {
|
|
|
24976
25341
|
}
|
|
24977
25342
|
const server = buildServerFromClient(
|
|
24978
25343
|
resolved.client,
|
|
24979
|
-
await telemetryHandleForRequest(resolved.client)
|
|
25344
|
+
await telemetryHandleForRequest(resolved.client),
|
|
25345
|
+
resourcePath
|
|
24980
25346
|
);
|
|
24981
25347
|
const transport = new StreamableHTTPServerTransport({
|
|
24982
25348
|
sessionIdGenerator: void 0,
|
|
@@ -25016,6 +25382,7 @@ async function handleStreamable(c, resourcePath) {
|
|
|
25016
25382
|
}
|
|
25017
25383
|
app.all("/mcp", (c) => handleStreamable(c, "/mcp"));
|
|
25018
25384
|
app.all("/fr/mcp", (c) => handleStreamable(c, "/fr/mcp"));
|
|
25385
|
+
app.all("/chatgpt/mcp", (c) => handleStreamable(c, "/chatgpt/mcp"));
|
|
25019
25386
|
async function handleSse(c, resourcePath) {
|
|
25020
25387
|
const foreign = rejectForeignOrigin(c);
|
|
25021
25388
|
if (foreign) return foreign;
|
|
@@ -25063,7 +25430,8 @@ async function handleSse(c, resourcePath) {
|
|
|
25063
25430
|
sessionOptedOut: session.suppressed,
|
|
25064
25431
|
fallbackEnabled: !session.suppressed
|
|
25065
25432
|
})
|
|
25066
|
-
)
|
|
25433
|
+
),
|
|
25434
|
+
resourcePath
|
|
25067
25435
|
);
|
|
25068
25436
|
await server.connect(transport);
|
|
25069
25437
|
const sessionId = transport.sessionId;
|
|
@@ -162,7 +162,7 @@ async function detectClients() {
|
|
|
162
162
|
out.push({ id: "claude-desktop", label: "Claude Desktop", detail: claudeDesktopPath, configPath: claudeDesktopPath, mode, supportDir: claudeSupportDir });
|
|
163
163
|
}
|
|
164
164
|
if (await isChatGptDesktopInstalled(home)) {
|
|
165
|
-
out.push({ id: "chatgpt-desktop", label: "ChatGPT Desktop", detail:
|
|
165
|
+
out.push({ id: "chatgpt-desktop", label: "ChatGPT Desktop", detail: HOSTED_MCP_URL_CHATGPT });
|
|
166
166
|
}
|
|
167
167
|
const cursorPath = process.platform === "win32" ? `${home}\\.cursor\\mcp.json` : `${home}/.cursor/mcp.json`;
|
|
168
168
|
if (await isCursorInstalled(home)) {
|
|
@@ -181,11 +181,12 @@ async function detectClients() {
|
|
|
181
181
|
}
|
|
182
182
|
return out;
|
|
183
183
|
}
|
|
184
|
-
var HOSTED_MCP_URL;
|
|
184
|
+
var HOSTED_MCP_URL, HOSTED_MCP_URL_CHATGPT;
|
|
185
185
|
var init_install_shared = __esm({
|
|
186
186
|
"installer/install-shared.ts"() {
|
|
187
187
|
"use strict";
|
|
188
188
|
HOSTED_MCP_URL = "https://mcp.leadbay.app/mcp";
|
|
189
|
+
HOSTED_MCP_URL_CHATGPT = "https://mcp.leadbay.app/chatgpt/mcp";
|
|
189
190
|
}
|
|
190
191
|
});
|
|
191
192
|
|
|
@@ -1266,7 +1267,7 @@ async function installInto(client, session, includeWrite, telemetryEnabled) {
|
|
|
1266
1267
|
res = exportRes.ok ? { ok: true, message: `${configRes.message}; ${exportRes.message}` } : { ok: false, message: `config ${configRes.message}; ${exportRes.message}` };
|
|
1267
1268
|
}
|
|
1268
1269
|
} else if (client.id === "chatgpt-desktop") {
|
|
1269
|
-
res = { ok: true, message: "manual setup required; add this MCP URL in ChatGPT Settings > Apps: " +
|
|
1270
|
+
res = { ok: true, message: "manual setup required; add this MCP URL in ChatGPT Settings > Apps: " + HOSTED_MCP_URL_CHATGPT };
|
|
1270
1271
|
} else if (client.id === "claude-desktop" && client.mode?.dxt && client.supportDir) {
|
|
1271
1272
|
const dxtResult = await removeDxtExtension(client.supportDir);
|
|
1272
1273
|
const jsonResult = await installInJsonConfig(client.configPath, session.token, session.region, includeWrite, telemetryEnabled, LOCAL_BIN_PATH);
|
|
@@ -1804,7 +1805,7 @@ var init_installer_gui = __esm({
|
|
|
1804
1805
|
init_install_dxt();
|
|
1805
1806
|
init_install_shared();
|
|
1806
1807
|
init_oauth();
|
|
1807
|
-
VERSION = true ? "0.
|
|
1808
|
+
VERSION = true ? "0.34.0" : "0.0.0-dev";
|
|
1808
1809
|
MESSAGES = {
|
|
1809
1810
|
en: {
|
|
1810
1811
|
installer: {
|
package/dist/installer-gui.js
CHANGED
|
@@ -382,6 +382,7 @@ import { existsSync, readFileSync, readdirSync } from "fs";
|
|
|
382
382
|
import { join } from "path";
|
|
383
383
|
import { homedir } from "os";
|
|
384
384
|
var HOSTED_MCP_URL = "https://mcp.leadbay.app/mcp";
|
|
385
|
+
var HOSTED_MCP_URL_CHATGPT = "https://mcp.leadbay.app/chatgpt/mcp";
|
|
385
386
|
function formatInstallOsLabel(platform = process.platform, arch = process.arch) {
|
|
386
387
|
const name = platform === "darwin" ? "macOS" : platform === "win32" ? "Windows" : platform === "linux" ? "Linux" : platform;
|
|
387
388
|
return `${name} (${arch})`;
|
|
@@ -510,7 +511,7 @@ async function detectClients() {
|
|
|
510
511
|
out.push({ id: "claude-desktop", label: "Claude Desktop", detail: claudeDesktopPath, configPath: claudeDesktopPath, mode, supportDir: claudeSupportDir });
|
|
511
512
|
}
|
|
512
513
|
if (await isChatGptDesktopInstalled(home)) {
|
|
513
|
-
out.push({ id: "chatgpt-desktop", label: "ChatGPT Desktop", detail:
|
|
514
|
+
out.push({ id: "chatgpt-desktop", label: "ChatGPT Desktop", detail: HOSTED_MCP_URL_CHATGPT });
|
|
514
515
|
}
|
|
515
516
|
const cursorPath = process.platform === "win32" ? `${home}\\.cursor\\mcp.json` : `${home}/.cursor/mcp.json`;
|
|
516
517
|
if (await isCursorInstalled(home)) {
|
|
@@ -1067,7 +1068,7 @@ async function oauthLogin(opts) {
|
|
|
1067
1068
|
}
|
|
1068
1069
|
|
|
1069
1070
|
// installer/installer-gui.ts
|
|
1070
|
-
var VERSION = true ? "0.
|
|
1071
|
+
var VERSION = true ? "0.34.0" : "0.0.0-dev";
|
|
1071
1072
|
var MESSAGES = {
|
|
1072
1073
|
en: {
|
|
1073
1074
|
installer: {
|
|
@@ -1358,7 +1359,7 @@ async function installInto(client, session, includeWrite, telemetryEnabled) {
|
|
|
1358
1359
|
res = exportRes.ok ? { ok: true, message: `${configRes.message}; ${exportRes.message}` } : { ok: false, message: `config ${configRes.message}; ${exportRes.message}` };
|
|
1359
1360
|
}
|
|
1360
1361
|
} else if (client.id === "chatgpt-desktop") {
|
|
1361
|
-
res = { ok: true, message: "manual setup required; add this MCP URL in ChatGPT Settings > Apps: " +
|
|
1362
|
+
res = { ok: true, message: "manual setup required; add this MCP URL in ChatGPT Settings > Apps: " + HOSTED_MCP_URL_CHATGPT };
|
|
1362
1363
|
} else if (client.id === "claude-desktop" && client.mode?.dxt && client.supportDir) {
|
|
1363
1364
|
const dxtResult = await removeDxtExtension(client.supportDir);
|
|
1364
1365
|
const jsonResult = await installInJsonConfig(client.configPath, session.token, session.region, includeWrite, telemetryEnabled, LOCAL_BIN_PATH);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leadbay/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"mcpName": "io.github.leadbay/leadbay-mcp",
|
|
5
5
|
"description": "Model Context Protocol (MCP) server for Leadbay — AI lead discovery, qualification, and enrichment for Claude Desktop, Cursor, and Claude Code.",
|
|
6
6
|
"type": "module",
|