@leadbay/mcp 0.17.3 → 0.18.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/bin.js +1054 -76
- package/dist/http-server.js +692 -67
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/dist/http-server.js
CHANGED
|
@@ -1756,6 +1756,23 @@ var LeadbayClient = class {
|
|
|
1756
1756
|
await this.resolveOrgId();
|
|
1757
1757
|
await this.resolveTasteProfile();
|
|
1758
1758
|
}
|
|
1759
|
+
// ─── Notifications helpers ────────────────────────────────────────────
|
|
1760
|
+
// Backend exposes `GET /notifications`, `POST /notifications/{id}/seen`,
|
|
1761
|
+
// `POST /notifications/{id}/archive`, plus `GET /ws/ticket?v=1.0` to mint
|
|
1762
|
+
// a one-shot WS URL. See backend/docs/adr/notifications.md for shape.
|
|
1763
|
+
async listNotifications(args = {}) {
|
|
1764
|
+
const params = new URLSearchParams();
|
|
1765
|
+
params.set("archived", String(args.archived ?? false));
|
|
1766
|
+
params.set("page", String(args.page ?? 0));
|
|
1767
|
+
params.set("count", String(args.count ?? 50));
|
|
1768
|
+
return this.request("GET", `/notifications?${params.toString()}`);
|
|
1769
|
+
}
|
|
1770
|
+
async acknowledgeNotification(notificationId, action = "seen") {
|
|
1771
|
+
await this.requestVoid("POST", `/notifications/${notificationId}/${action}`);
|
|
1772
|
+
}
|
|
1773
|
+
async getWsTicket() {
|
|
1774
|
+
return this.request("GET", "/auth/ws?v=1.0");
|
|
1775
|
+
}
|
|
1759
1776
|
makeError(code, message, hint, endpoint, retry_after, http_status) {
|
|
1760
1777
|
const out = { error: true, code, message, hint };
|
|
1761
1778
|
if (endpoint || this._region) {
|
|
@@ -6171,6 +6188,7 @@ function makeAgentMemoryTombstone(input) {
|
|
|
6171
6188
|
|
|
6172
6189
|
// ../core/dist/composite/_composite-file-names.js
|
|
6173
6190
|
var COMPOSITE_FILE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
6191
|
+
"leadbay_account_history",
|
|
6174
6192
|
"leadbay_account_status",
|
|
6175
6193
|
"leadbay_add_leads_to_campaign",
|
|
6176
6194
|
"leadbay_adjust_audience",
|
|
@@ -6205,7 +6223,156 @@ var COMPOSITE_FILE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
|
6205
6223
|
"leadbay_tour_plan"
|
|
6206
6224
|
]);
|
|
6207
6225
|
|
|
6226
|
+
// ../core/dist/notifications/inbox.js
|
|
6227
|
+
var DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
6228
|
+
|
|
6208
6229
|
// ../core/dist/tool-descriptions.generated.js
|
|
6230
|
+
var leadbay_account_history = `## WHEN TO USE
|
|
6231
|
+
|
|
6232
|
+
Trigger phrases: "what's the history on this account", "why should I revisit this account", "summarize everything we've done with <Company>", "has this account gone cold", "give me the back-story on lead <UUID>".
|
|
6233
|
+
|
|
6234
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
6235
|
+
|
|
6236
|
+
Do NOT use for: "live signals only, no history" \u2192 \`leadbay_research_lead_by_id\`; "which accounts should I follow up with" \u2192 \`leadbay_pull_followups\`.
|
|
6237
|
+
|
|
6238
|
+
Prefer when: user wants ONE account's full back-story \u2014 notes + past activity + current signals together; pass \`leadId\`
|
|
6239
|
+
|
|
6240
|
+
Examples that SHOULD invoke this tool:
|
|
6241
|
+
- "What's the full history on this account \u2014 why did it resurface?"
|
|
6242
|
+
- "Summarize everything we've logged on that lead and whether it's worth another visit."
|
|
6243
|
+
|
|
6244
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
6245
|
+
- "Tell me the current AI take on this lead."
|
|
6246
|
+
- "Which accounts should I follow up with this week?"
|
|
6247
|
+
|
|
6248
|
+
## RENDER (quick)
|
|
6249
|
+
|
|
6250
|
+
Single resurfaced-account card. Lead the card with the current signal/trigger
|
|
6251
|
+
line (from \`signals\`), then a "History" section: notes digest
|
|
6252
|
+
(chronological) + the activity timeline. Close with a one-line "why revisit
|
|
6253
|
+
now" synthesis and a suggested outreach angle tied to the freshest signal.
|
|
6254
|
+
|
|
6255
|
+
---
|
|
6256
|
+
|
|
6257
|
+
Give me one account's full back-story in a single call. This is the tool for
|
|
6258
|
+
the **reprioritize-a-neglected-account** workflow: the user has an account
|
|
6259
|
+
that was contacted or quoted long ago and wants to know, in one shot, whether
|
|
6260
|
+
a fresh signal makes it worth another visit.
|
|
6261
|
+
|
|
6262
|
+
It bundles three reads on one \`leadId\`:
|
|
6263
|
+
|
|
6264
|
+
1. **Current state** \u2014 passed through verbatim from \`leadbay_research_lead_by_id\`:
|
|
6265
|
+
\`signals\` (web-research signals with hot flags + sources), \`firmographics\`,
|
|
6266
|
+
\`qualification\` answers, \`contacts\`, and \`engagement\` counts. This is the
|
|
6267
|
+
"why is this account hot NOW" layer.
|
|
6268
|
+
2. **\`notes\`** \u2014 the FULL list of notes logged on the record (note body +
|
|
6269
|
+
\`created_at\`), chronological. \`leadbay_research_lead_by_id\` only returns a
|
|
6270
|
+
\`notes_count\`; this tool returns the bodies so you can summarize the
|
|
6271
|
+
historical context.
|
|
6272
|
+
3. **\`activities\`** \u2014 the interaction timeline (\`{type, date}\` entries, newest
|
|
6273
|
+
first) plus the total count. Drives the "no contact in N months" judgement.
|
|
6274
|
+
|
|
6275
|
+
\`notes\` and \`activities\` **degrade gracefully**: if either read fails the card
|
|
6276
|
+
still returns with that section empty (\`notes: []\` /
|
|
6277
|
+
\`activities.total: 0\`). The current-state block is load-bearing \u2014 if research
|
|
6278
|
+
itself errors the whole call fails, because there is nothing to narrate.
|
|
6279
|
+
|
|
6280
|
+
Params: \`leadId\` (required UUID) and \`activityCount\` (optional, default 50,
|
|
6281
|
+
max 100).
|
|
6282
|
+
|
|
6283
|
+
Companion tools: **leadbay_research_lead_by_id** when the user only wants the
|
|
6284
|
+
live AI take with no history; **leadbay_pull_followups** when the user wants a
|
|
6285
|
+
LIST of accounts to act on rather than one account's deep history.
|
|
6286
|
+
|
|
6287
|
+
## RENDERING \u2014 single-record research card, mode-adaptive
|
|
6288
|
+
|
|
6289
|
+
Present as a single-record card, not a table. This tool gets invoked in two distinct user contexts \u2014 detect which and adapt the body density accordingly.
|
|
6290
|
+
|
|
6291
|
+
**MODE A \u2014 Discovery.** The user is evaluating whether to pursue this company as a target. Signals: "tell me about", "what do they do", "is this a fit", "research [company]", arrival via a click-through from \`leadbay_pull_leads\`, no prior outreach context in the conversation. Next step is usually qualify, deep-dive via \`leadbay_research_lead_by_id\`, or decide whether to start outreach.
|
|
6292
|
+
|
|
6293
|
+
**MODE B \u2014 Contact preparation.** The user is about to call or email someone at this company and needs the talking points. Signals: "I'm calling them", "draft an email", "before my call", "outreach prep", "what should I say", or the conversation has already touched on a specific contact. Next step is usually \`leadbay_prepare_outreach\`.
|
|
6294
|
+
|
|
6295
|
+
Default to MODE A when uncertain. Always offer the cross-mode pivot at the end so the user can redirect if you guessed wrong.
|
|
6296
|
+
|
|
6297
|
+
### Common structure (both modes)
|
|
6298
|
+
|
|
6299
|
+
- **Header** (H4 or H5): \`<10-segment score bar>\` \`[Company name](website)\`. Use the score-bar algorithm; the bar lives in a single inline-code span. Prefix \`https://\` to website if it's a bare hostname.
|
|
6300
|
+
- **Pill row** (immediately below the header): short location \xB7 compact size \xB7 social pill chips iterated over \`social_urls\` (each non-null platform becomes \`[<platform-label>](<url>)\`) \xB7 \`[website-domain](website)\` \xB7 \`\u260E phone\` when \`phone_numbers[]\` is non-empty (use the first number). All \` \xB7 \`-separated.
|
|
6301
|
+
- **Blurb**: render \`description\` (preferred) or \`short_description\` as a single blockquoted paragraph.
|
|
6302
|
+
- **Staleness line**: italic, \`"Researched <relative time>"\` from \`web_insights_fetched_at\`. Use \`"today"\` / \`"yesterday"\` / \`"N days ago"\` up to 30 days, then absolute date. Prefix with \`\u26A0\` if older than 30 days.
|
|
6303
|
+
- **Contacts table** (always at the bottom):
|
|
6304
|
+
\`\`\`
|
|
6305
|
+
| | Name | Title | LinkedIn |
|
|
6306
|
+
\`\`\`
|
|
6307
|
+
Markers in column 1:
|
|
6308
|
+
- \`\u2605\` \u2014 \`recommended_contact\` match.
|
|
6309
|
+
- \`\u{1F48E}\` \u2014 name fuzzy-matches a \`hot: true\` entry in \`web_insights\` key_people. (Use \`\u{1F48E}\`, not \`\u{1F525}\`, to avoid glyph collision with the follow-up status badge.)
|
|
6310
|
+
Sort \`\u2605\` first, then \`\u{1F48E}\`-only rows, then API order. Link the name via \`linkedin_page\` first; fall back to LinkedIn people-search with \`<First>+<Last>+<Company>\`. Append \`\xB0\` only when the fallback is in use AND \`social_presence.linkedin == false\`. Cap to 6 rows; if \`contacts_count > shown\`, end with \`"+N more \u2014 ask to see the full list"\`.
|
|
6311
|
+
|
|
6312
|
+
### MODE A body (Discovery, fuller, scannable)
|
|
6313
|
+
|
|
6314
|
+
Render each non-empty \`web_insights\` section as H5 with the emoji + label intact. Section order: \`\u{1F3E2} company profile\` \u2192 \`\u{1F4C8} business signals\` \u2192 \`\u{1F4A1} prospecting clues\` \u2192 \`\u{1F9E9} strategic positioning\` \u2192 \`\u{1F50E} technologies & innovation\`. Inside each, bullet 3\u20135 items. Sort \`hot: true\` items first. **Bold** the description text of hot items; leave cold items plain. Render \`source\` as \`[source](url)\` at the end; include \`date\` when present. Omit empty sections. Skip \`\u{1F517} social links\` (already in the pill row) and \`\u{1F464} key people\` (already in the contacts table).
|
|
6315
|
+
|
|
6316
|
+
### MODE B body (Contact preparation, tighter)
|
|
6317
|
+
|
|
6318
|
+
Render exactly two H5 sections:
|
|
6319
|
+
|
|
6320
|
+
##### \u{1F3AF} Conversation hooks
|
|
6321
|
+
|
|
6322
|
+
Distill the 3 most recent / most hot signals from \`\u{1F4C8} business signals\` and \`\u{1F4A1} prospecting clues\` into one-sentence talking points in salesperson voice. Strip the academic framing. Cite the source inline.
|
|
6323
|
+
|
|
6324
|
+
##### \u{1F464} About the person *(only when recommended_contact is non-empty)*
|
|
6325
|
+
|
|
6326
|
+
2-line summary: their title + any context from \`web_insights\` key_people. If they appear in a hot signal ("X appointed CEO"), surface that prominently.
|
|
6327
|
+
|
|
6328
|
+
Skip \u{1F3E2} profile, \u{1F9E9} strategic positioning, \u{1F50E} technologies in MODE B \u2014 context the user doesn't need for the next 30 seconds.
|
|
6329
|
+
|
|
6330
|
+
If \`qualification[]\` is non-empty, append one collapsed line: \`"Qualification: N questions answered, avg boost X"\` and offer to expand in NEXT STEPS.
|
|
6331
|
+
|
|
6332
|
+
**Hide:** \`id\`, \`lead.id\`, \`contact.id\`, \`lead.location.pos\`, \`web_fetch_in_progress\`, \`enrichment_in_progress\`, \`recommended_contact_title\` (duplicates \`recommended_contact.job_title\`), empty arrays, fields whose value is the string \`"null"\`, \`contact.source\` (internal), insights whose \`source\` is empty.
|
|
6333
|
+
|
|
6334
|
+
**Legend (print once below the card):** \`\` \`\u25B0\` firmographic \xB7 \`\u2756\` AI booster \xB7 \`\u25B1\` unfilled \xB7 \u2605 recommended \xB7 \u{1F48E} hot in web_insights \xB7 \xB0 = no company LinkedIn (fallback link only) \`\`
|
|
6335
|
+
|
|
6336
|
+
## Linking a contact's name
|
|
6337
|
+
|
|
6338
|
+
**MANDATORY: every contact name in your output \u2014 table cells, prose, headers, "Reach <Name>" callouts \u2014 MUST be wrapped in markdown link syntax \`[Name](URL)\`. Never render a contact name as bare text. A plain-text name is a broken contact card; the underlined name is the user's primary affordance for "take me to this person's profile". No "no URL available" exception \u2014 the search URL below is always constructable from name + company.**
|
|
6339
|
+
|
|
6340
|
+
URL priority (first applicable wins):
|
|
6341
|
+
|
|
6342
|
+
1. **Real profile** \u2014 \`contact.linkedin_page\` when it's a string starting with \`https://\` (the MCP coerces the legacy literal \`"null"\` string to real null before you see it).
|
|
6343
|
+
2. **Constructed people-search** \u2014 \`https://www.linkedin.com/search/results/people/?keywords=<First>+<Last>+<Company>\`. URL-encode params. Strip Inc / LLC / Corp / Ltd / GmbH / Co / S.A. / S.L. / PLC / AG / SAS / SARL suffixes from the company. Append a trailing \` \xB0\` to the rendered name ONLY when this fallback is in use AND \`social_presence.linkedin == false\`. Never append \`\xB0\` when a real \`linkedin_page\` was used.
|
|
6344
|
+
|
|
6345
|
+
Never link a person's name to the company's LinkedIn page (and vice versa) \u2014 the two surfaces are different and conflating them quietly degrades the workflow.
|
|
6346
|
+
|
|
6347
|
+
## Linking the company
|
|
6348
|
+
|
|
6349
|
+
Use the lead's \`website\` as the company-name link target \u2014 prefix \`https://\` if the value is a bare hostname. (The MCP does NOT synthesize a Leadbay-app deep-link URL; the team has not standardized one. Linking to \`website\` is always real data.)
|
|
6350
|
+
|
|
6351
|
+
When the response carries \`social_urls\` (the post-fix multi-platform URL block on rich-lead responses), render every non-null platform as a pill chip in the company-info row. Iterate over \`social_urls\`'s keys \u2014 never hardcode a fixed list \u2014 and emit each as \`[<platform-label>](<url>)\`. Skip platforms whose URL is null.
|
|
6352
|
+
|
|
6353
|
+
\`social_presence\` carries booleans for the same 6 platforms (crunchbase, facebook, instagram, linkedin, tiktok, twitter) \u2014 useful when you only care that the company has a profile somewhere. Use it as the \xB0-flag signal in the contact people-search fallback (see linking/contact-linkedin).
|
|
6354
|
+
|
|
6355
|
+
|
|
6356
|
+
|
|
6357
|
+
### RENDERING \u2014 the history layer (on top of the card above)
|
|
6358
|
+
|
|
6359
|
+
After the research card, add a **History** section so the user sees why this
|
|
6360
|
+
account resurfaced:
|
|
6361
|
+
|
|
6362
|
+
- **##### \u{1F5D2} Notes** \u2014 render \`notes\` chronologically (oldest \u2192 newest). Each
|
|
6363
|
+
as a bullet: \`**<relative date from created_at>** \u2014 <note body>\`. Cap at 8;
|
|
6364
|
+
if \`_meta.notes_count > shown\`, end with \`"+N more notes"\`. Omit the section
|
|
6365
|
+
entirely when \`notes\` is empty.
|
|
6366
|
+
- **##### \u{1F553} Timeline** \u2014 render \`activities.activities\` newest-first as a
|
|
6367
|
+
compact bullet list: \`<relative date> \xB7 <type>\`. Cap at 10; if
|
|
6368
|
+
\`activities.total > shown\`, end with \`"+N earlier"\`. Omit when empty.
|
|
6369
|
+
- **##### \u21BB Why revisit now** \u2014 one or two sentences synthesizing the freshest
|
|
6370
|
+
HOT signal from \`signals\` against the gap in \`activities\` (e.g. "Won a public
|
|
6371
|
+
tender last month; no logged contact since the 2024 quote \u2014 strong re-open
|
|
6372
|
+
angle"). Then one suggested outreach angle tied to that signal. This
|
|
6373
|
+
synthesis is the payload of the whole tool \u2014 always include it when there is
|
|
6374
|
+
at least one hot signal.
|
|
6375
|
+
`;
|
|
6209
6376
|
var leadbay_account_status = `## WHEN TO USE
|
|
6210
6377
|
|
|
6211
6378
|
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".
|
|
@@ -6226,10 +6393,13 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
|
6226
6393
|
|
|
6227
6394
|
## RENDER (quick)
|
|
6228
6395
|
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6396
|
+
If \`quota_error\` is set the call FAILED \u2014 quota unreadable; on 401/403 tell
|
|
6397
|
+
the user to reconnect. NEVER report zero usage or "no limits". Else render
|
|
6398
|
+
\`quota.org.resources\` (usage lives there, NOT at quota.resources) as a
|
|
6399
|
+
table, never prose: rows = resources (llm_completion \xB7 ai_rescore \xB7
|
|
6400
|
+
web_fetch + others), cols = Daily/Weekly/Monthly used \`count\` (= amount
|
|
6401
|
+
USED; no cap field, \`plan\` may be null \u2014 never invent a denominator).
|
|
6402
|
+
Empty = a 0 table, not "unlimited". Above: org + admin, lens.
|
|
6233
6403
|
|
|
6234
6404
|
---
|
|
6235
6405
|
|
|
@@ -6241,10 +6411,53 @@ Show the user's account state \u2014 admin rights, language, last-active lens, c
|
|
|
6241
6411
|
|
|
6242
6412
|
**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. If the retry hits the wall again, only then re-offer top-up / wait. **A stale quota snapshot is never a reason to gate-keep a topped-up user.**
|
|
6243
6413
|
|
|
6414
|
+
**\`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.
|
|
6415
|
+
|
|
6416
|
+
## GATE \u2014 INSPECT \`_meta.notifications\` ON EVERY RESPONSE
|
|
6417
|
+
|
|
6418
|
+
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:
|
|
6419
|
+
|
|
6420
|
+
| Kind | What just finished |
|
|
6421
|
+
|---|---|
|
|
6422
|
+
| \`bulk_enrich\` | A bulk contact-enrichment job (emails / phone numbers attached to leads' contacts). |
|
|
6423
|
+
| \`bulk_qualify\` | A bulk lead-qualification job (web-fetch + AI rescore, producing \`ai_agent_lead_score\` and qualification answers). |
|
|
6424
|
+
| \`import\` | A CSV / CRM file import (rows resolved to leads in the user's pipeline). |
|
|
6425
|
+
|
|
6426
|
+
**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.
|
|
6427
|
+
|
|
6428
|
+
| Kind | Outputs you've produced that may now be stale \u2014 refresh them |
|
|
6429
|
+
|---|---|
|
|
6430
|
+
| \`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. |
|
|
6431
|
+
| \`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\`. |
|
|
6432
|
+
| \`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\`. |
|
|
6433
|
+
|
|
6434
|
+
**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.
|
|
6435
|
+
|
|
6436
|
+
**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.
|
|
6437
|
+
|
|
6438
|
+
**Don't poll.** When you launch a long operation (enrichment / qualification / import), simply continue the conversation \u2014 the next time you call any tool, the completed-work entry will appear in \`_meta.notifications\`. Status tools (\`leadbay_bulk_enrich_status\`, \`leadbay_qualify_status\`, \`leadbay_import_status\`) exist for the case where you want the answer NOW (mid-conversation), not for ambient polling.
|
|
6439
|
+
|
|
6440
|
+
Also surfaced as a top-level \`notifications\` array on \`leadbay_account_status\` \u2014 same shape, same handling.
|
|
6441
|
+
|
|
6442
|
+
|
|
6244
6443
|
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 to offer the top-up alternative), and after the user signals a top-up so the agent can resume the interrupted workflow.
|
|
6245
6444
|
|
|
6246
6445
|
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.
|
|
6247
6446
|
`;
|
|
6447
|
+
var leadbay_acknowledge_notification = `Acknowledge a Leadbay notification \u2014 i.e. tell the MCP and the backend "I've seen this and acted on it." Wraps \`POST /1.5/notifications/{id}/seen\` (default) or \`/archive\` (when \`archive:true\`) and drops the entry from the local inbox so subsequent \`_meta.notifications\` payloads stop carrying it.
|
|
6448
|
+
|
|
6449
|
+
**When to call.** After you read an entry from \`_meta.notifications\` or \`account_status.notifications\` and have revised whatever prior output the just-finished background work might have made stale (outreach drafts, lead lists, "available leads" claims, followup plans). Mark-seen tells the human team's pipeline you handled this and prevents the notification from re-surfacing on every subsequent tool response.
|
|
6450
|
+
|
|
6451
|
+
If nothing you produced for the user is affected, ack anyway with \`archive:false\` \u2014 the entry should still clear so the inbox stays focused on what's actually pending.
|
|
6452
|
+
|
|
6453
|
+
Use \`archive:true\` only when you want the row gone from the FE notification dropdown too (e.g. a non-actionable system notification that's already handled). Default behaviour is \`seen\` \u2014 same as the FE dropdown's "click to read" semantics.
|
|
6454
|
+
|
|
6455
|
+
WHEN TO USE: immediately after you finish reviewing / revising in response to a \`_meta.notifications[]\` entry. Idempotent \u2014 calling twice with the same id is safe.
|
|
6456
|
+
|
|
6457
|
+
WHEN NOT TO USE: before doing the revision work; for general "mark all read" sweeps (call once per notification id you've actually consumed).
|
|
6458
|
+
|
|
6459
|
+
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\`.
|
|
6460
|
+
`;
|
|
6248
6461
|
var leadbay_add_leads_to_campaign = `## WHEN TO USE
|
|
6249
6462
|
|
|
6250
6463
|
Trigger phrases: "add leads to <name> campaign", "attach these to <campaign>", "put these in Q2 Push", "add to existing campaign".
|
|
@@ -6373,6 +6586,10 @@ var leadbay_bulk_enrich_status = `Check status + per-lead contacts for a bulk en
|
|
|
6373
6586
|
WHEN TO USE: poll this after leadbay_enrich_titles returns a \`bulk_id\`. Default \`include_contacts=false\` for cheap status polls; set \`include_contacts=true\` once \`all_done\` flips for the final read.
|
|
6374
6587
|
|
|
6375
6588
|
WHEN NOT TO USE: as a substitute for leadbay_research_lead_by_id \u2014 that already includes enriched contacts for a single lead.
|
|
6589
|
+
|
|
6590
|
+
## CREDIT COST \u2014 show the balance, discreetly
|
|
6591
|
+
|
|
6592
|
+
Once \`all_done\` is true the result carries \`credits_remaining\` (the post-spend AI-credit balance). Don't make a fuss \u2014 no sentence, no callout. Just append ONE small italic line in parentheses at the very END of your reply: \`_(N credits remaining)_\`. If \`credits_remaining\` is null (billing unavailable), omit the line \u2014 don't print 0. Do NOT report a "credits used" figure for this run: the per-contact cost can't be scoped to this specific enrichment (a lead's contact list mixes in earlier runs), so any "X used" number would be misleading.
|
|
6376
6593
|
`;
|
|
6377
6594
|
var leadbay_bulk_qualify_leads = `Pick the next N unqualified leads in the active lens and qualify them (run AI rescore + web fetch). Pass \`wait_for_completion:false\` to return quickly with \`{status:'running', qualify_id}\`; poll leadbay_qualify_status with that id. With \`wait_for_completion\` omitted/true, the legacy behavior polls until the answers are populated or a budget is exhausted. Already-qualified leads (those with a non-null \`ai_agent_lead_score\`) are silently no-ops on the backend, so this composite paginates past them to find fresh candidates. On 429 mid-fanout, stops launching but keeps polling already-launched leads.
|
|
6378
6595
|
|
|
@@ -6837,6 +7054,10 @@ WHEN TO USE: when you have a specific \`contact_id\` (from leadbay_get_contacts)
|
|
|
6837
7054
|
|
|
6838
7055
|
WHEN NOT TO USE: for bulk enrichment by job title across many leads \u2014 use leadbay_enrich_titles, which handles the selection lifecycle and returns a clean preview/launch flow.
|
|
6839
7056
|
|
|
7057
|
+
## CREDIT COST \u2014 discreet
|
|
7058
|
+
|
|
7059
|
+
This is a paid call. The result returns \`credits_remaining\` (billing.ai_credits, read before the spend). Don't make a fuss about credits: only flag the balance if it's low (e.g. \u2264 a few credits) so the user can decide. Otherwise append it quietly as a small italic parenthetical at the END of your reply \u2014 \`_(N credits remaining)_\`. Don't quote an exact per-contact cost (the rate is backend-only). The actual per-contact cost (enrichment.credits_used) appears on the contact via leadbay_get_contacts after enrichment. If \`credits_remaining\` is null, omit the line \u2014 don't assume zero.
|
|
7060
|
+
|
|
6840
7061
|
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\`.
|
|
6841
7062
|
`;
|
|
6842
7063
|
var leadbay_enrich_titles = `Order contact enrichments by job title across many leads. Contacts are NOT returned by default with a lead (Leadbay keeps enrichment out-of-band to control cost); the agent requests them on demand via this tool when it's ready to actually reach out. Two modes: (A) NO \`titles\` param \u2014 returns the available titles + Leadbay's \`title_suggestions\` + \`auto_included_titles\` + a count of enrichable contacts, so the agent can ask the user which titles to enrich. (B) \`titles\` given \u2014 calls preview, then launches if there's anything enrichable. On 429 returns \`{status:'quota_exceeded'}\` cleanly. Selection lifecycle is wrapped in a try/finally so the user's selection is left clean even on error.
|
|
@@ -6845,6 +7066,35 @@ WHEN TO USE: as the agent's go-to enrichment entry point, immediately before pro
|
|
|
6845
7066
|
|
|
6846
7067
|
WHEN NOT TO USE: to enrich a single contact \u2014 that's leadbay_enrich_contacts (granular). Speculatively, before the user has committed to outreaching \u2014 enrichment spends credits.
|
|
6847
7068
|
|
|
7069
|
+
## CREDIT COST \u2014 make spend visible
|
|
7070
|
+
|
|
7071
|
+
Enrichment is the main PAID operation. Surface cost both before and after.
|
|
7072
|
+
|
|
7073
|
+
**BEFORE (confirm before launching).** The discover / preview_only / dry_run modes return \`credits_remaining\` (the balance) and \`enrichable_contacts\` (the volume that would be enriched). Tell the user plainly: **"You have {credits_remaining} credits. This will enrich {enrichable_contacts} contacts."** then ask them to confirm before you launch the paid run. Route that confirmation through \`ask_user_input_v0\` ("Enrich {enrichable_contacts} contacts now?" \u2192 ["Yes, enrich", "No, cancel"]). Do NOT state an exact estimated cost \u2014 the per-contact credit rate lives backend-side and is not in the preview; show the balance and the count, never a fabricated "will cost N credits". If \`credits_remaining\` is null, billing is unavailable \u2014 say the balance is unknown, don't assume zero or unlimited.
|
|
7074
|
+
|
|
7075
|
+
**AFTER (show the balance, discreetly).** Once the job finishes \u2014 poll \`leadbay_bulk_enrich_status\`, which returns \`credits_remaining\` (the post-spend balance). Don't make a fuss: append ONE small italic line in parentheses at the very END of your reply \u2014 \`_(N credits remaining)_\`. Omit it if \`credits_remaining\` is null. Do NOT report a "credits used" figure: per-run cost can't be scoped reliably (a lead's contacts mix earlier enrichments), so only the balance is shown.
|
|
7076
|
+
|
|
7077
|
+
## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
|
|
7078
|
+
|
|
7079
|
+
Modern chat hosts (Claude, ChatGPT) expose first-party widgets the agent can route into. These ALWAYS produce a better UX than markdown tables / inline prose for the data shapes they support \u2014 they're tappable on mobile, persistent across turns, and integrate with the host's quick-actions.
|
|
7080
|
+
|
|
7081
|
+
**The Big Three** \u2014 when a tool result fits, route there:
|
|
7082
|
+
|
|
7083
|
+
| Host widget | Use when | Field map (from Leadbay payload) |
|
|
7084
|
+
|---|---|---|
|
|
7085
|
+
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
7086
|
+
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
7087
|
+
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | \`{questions: [{question: "What next?", type: "single_select", options: [<2-4 short button labels>]}]}\`; max 3 questions per call |
|
|
7088
|
+
|
|
7089
|
+
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
7090
|
+
|
|
7091
|
+
**Rules:**
|
|
7092
|
+
- The widget IS the visual. Do NOT emit a markdown table or prose list of the same data alongside \u2014 that produces two competing UIs.
|
|
7093
|
+
- Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
|
|
7094
|
+
- When the host doesn't expose the named widget, the agent falls back to the prose/table rendering the per-tool description already specifies. The directive is host-conditional; the fallback is automatic.
|
|
7095
|
+
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
7096
|
+
|
|
7097
|
+
|
|
6848
7098
|
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\`.
|
|
6849
7099
|
`;
|
|
6850
7100
|
var leadbay_extend_lens = `## WHEN TO USE
|
|
@@ -9612,10 +9862,36 @@ var getQuota = {
|
|
|
9612
9862
|
outputSchema: {
|
|
9613
9863
|
type: "object",
|
|
9614
9864
|
properties: {
|
|
9615
|
-
plan: {
|
|
9865
|
+
plan: {
|
|
9866
|
+
type: ["string", "null"],
|
|
9867
|
+
description: "Org plan tier (e.g., FREE, TIER1, TIER2). May be null."
|
|
9868
|
+
},
|
|
9869
|
+
org: {
|
|
9870
|
+
type: "object",
|
|
9871
|
+
description: "Org-level quota state.",
|
|
9872
|
+
properties: {
|
|
9873
|
+
spend: { type: "array", description: "Reserved; empty in practice.", items: { type: "object" } },
|
|
9874
|
+
resources: {
|
|
9875
|
+
type: "array",
|
|
9876
|
+
description: "Per-resource per-window USAGE. Each: {resource_type, count, window_type, resets_at}. `count` is the amount USED in that window (not remaining, not a cap). No cap field is returned by the API.",
|
|
9877
|
+
items: { type: "object" }
|
|
9878
|
+
}
|
|
9879
|
+
}
|
|
9880
|
+
},
|
|
9881
|
+
user: {
|
|
9882
|
+
type: "object",
|
|
9883
|
+
description: "User-level quota state, same shape as `org`. May be absent.",
|
|
9884
|
+
properties: {
|
|
9885
|
+
spend: { type: "array", items: { type: "object" } },
|
|
9886
|
+
resources: { type: "array", items: { type: "object" } }
|
|
9887
|
+
}
|
|
9888
|
+
},
|
|
9889
|
+
// Legacy/compat: the live API does NOT return a top-level `windows`
|
|
9890
|
+
// array — usage lives in org/user.resources[]. Declared only so older
|
|
9891
|
+
// recorded fixtures still conform; do not rely on it.
|
|
9616
9892
|
windows: {
|
|
9617
9893
|
type: "array",
|
|
9618
|
-
description: "
|
|
9894
|
+
description: "Deprecated \u2014 not returned by the live API. Use org/user.resources[].",
|
|
9619
9895
|
items: { type: "object" }
|
|
9620
9896
|
}
|
|
9621
9897
|
}
|
|
@@ -11148,9 +11424,23 @@ async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseB
|
|
|
11148
11424
|
await pollPreprocess(client, importId, phaseBudget, ctx, signal);
|
|
11149
11425
|
ctx?.logger?.info?.(`import-leads: preprocess done for importId=${importId}`);
|
|
11150
11426
|
if (dryRun) {
|
|
11151
|
-
return { importId, records: [] };
|
|
11427
|
+
return { importId, records: [], notification_id: null };
|
|
11428
|
+
}
|
|
11429
|
+
let updateMappingsResp = null;
|
|
11430
|
+
try {
|
|
11431
|
+
updateMappingsResp = await client.request("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
11432
|
+
} catch (err) {
|
|
11433
|
+
if (err?.code === "API_ERROR" || err?.code === "NOT_FOUND") {
|
|
11434
|
+
ctx?.logger?.warn?.(`import-leads: update_mappings raw error (${err?.code}); retrying void`);
|
|
11435
|
+
await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
11436
|
+
} else {
|
|
11437
|
+
throw err;
|
|
11438
|
+
}
|
|
11439
|
+
}
|
|
11440
|
+
const importNotificationId = updateMappingsResp?.notification_id ?? null;
|
|
11441
|
+
if (importNotificationId) {
|
|
11442
|
+
ctx?.logger?.info?.(`import-leads: notification_id=${importNotificationId} importId=${importId}`);
|
|
11152
11443
|
}
|
|
11153
|
-
await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
11154
11444
|
ctx?.logger?.info?.(`import-leads: mappings committed for importId=${importId}`);
|
|
11155
11445
|
const phaseBudget2 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
11156
11446
|
await pollProcess(client, importId, phaseBudget2, ctx, signal);
|
|
@@ -11158,7 +11448,7 @@ async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseB
|
|
|
11158
11448
|
const phaseBudget3 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
11159
11449
|
const records = await pollRecordsToTerminal(client, importId, phaseBudget3, chunk.length, ctx, signal);
|
|
11160
11450
|
ctx?.logger?.info?.(`import-leads: ${records.length} records terminal for importId=${importId}`);
|
|
11161
|
-
return { importId, records };
|
|
11451
|
+
return { importId, records, notification_id: importNotificationId };
|
|
11162
11452
|
}
|
|
11163
11453
|
function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
11164
11454
|
const seenInputIndex = /* @__PURE__ */ new Set();
|
|
@@ -11214,7 +11504,7 @@ function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
|
11214
11504
|
}
|
|
11215
11505
|
}
|
|
11216
11506
|
}
|
|
11217
|
-
function buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled) {
|
|
11507
|
+
function buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds) {
|
|
11218
11508
|
const leads = [];
|
|
11219
11509
|
const not_imported = [];
|
|
11220
11510
|
if (dryRun) {
|
|
@@ -11279,6 +11569,7 @@ function buildImportLeadsResult(client, prep, importIds, matched, notImported, d
|
|
|
11279
11569
|
leads,
|
|
11280
11570
|
not_imported,
|
|
11281
11571
|
importIds,
|
|
11572
|
+
notification_ids: notificationIds,
|
|
11282
11573
|
region: client.region,
|
|
11283
11574
|
cancelled: cancelled || void 0,
|
|
11284
11575
|
dry_run: dryRun || void 0,
|
|
@@ -11466,6 +11757,7 @@ var importLeads = {
|
|
|
11466
11757
|
leads: [],
|
|
11467
11758
|
not_imported,
|
|
11468
11759
|
importIds: [],
|
|
11760
|
+
notification_ids: [],
|
|
11469
11761
|
region: client.region,
|
|
11470
11762
|
dry_run: dryRun || void 0,
|
|
11471
11763
|
_meta: client.lastMeta ?? {
|
|
@@ -11520,6 +11812,10 @@ var importLeads = {
|
|
|
11520
11812
|
status: "running",
|
|
11521
11813
|
handle_id: reservation.record.bulk_id,
|
|
11522
11814
|
importIds: importIds2,
|
|
11815
|
+
// Notifications fire from update_mappings, which the background
|
|
11816
|
+
// task hasn't called yet at this point. They surface via the WS
|
|
11817
|
+
// listener / catch-up REST on subsequent agent turns.
|
|
11818
|
+
notification_ids: [],
|
|
11523
11819
|
progress: {
|
|
11524
11820
|
phase: reservation.record.status === "complete" ? "complete" : importIds2.length > 0 ? "preprocess" : "queued",
|
|
11525
11821
|
records_processed: reservation.record.status === "complete" ? reservation.record.records_total : 0,
|
|
@@ -11540,6 +11836,7 @@ var importLeads = {
|
|
|
11540
11836
|
}
|
|
11541
11837
|
ctx?.logger?.info?.(`import-leads(${prep.mode}): ${prep.validInputs.length} rows \u2192 ${chunks.length} chunk(s); dry_run=${dryRun}, totalBudgetMs=${totalBudget}`);
|
|
11542
11838
|
const importIds = [];
|
|
11839
|
+
const notificationIds = [];
|
|
11543
11840
|
const matched = /* @__PURE__ */ new Map();
|
|
11544
11841
|
const notImported = /* @__PURE__ */ new Map();
|
|
11545
11842
|
let cancelled = false;
|
|
@@ -11551,6 +11848,9 @@ var importLeads = {
|
|
|
11551
11848
|
for (let i = 0; i < chunks.length; i++) {
|
|
11552
11849
|
const chunk = chunks[i];
|
|
11553
11850
|
const out = await runOneChunk(client, chunk, i, chunks.length, prep.header, prep.mappings, dryRun, perPhaseBudget, totalDeadline, ctx, signal, recordImportId);
|
|
11851
|
+
if (out.notification_id && !notificationIds.includes(out.notification_id)) {
|
|
11852
|
+
notificationIds.push(out.notification_id);
|
|
11853
|
+
}
|
|
11554
11854
|
if (!dryRun) {
|
|
11555
11855
|
reconcileOneChunk(prep, out, matched, notImported);
|
|
11556
11856
|
}
|
|
@@ -11571,7 +11871,7 @@ var importLeads = {
|
|
|
11571
11871
|
throw err;
|
|
11572
11872
|
}
|
|
11573
11873
|
}
|
|
11574
|
-
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled);
|
|
11874
|
+
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds);
|
|
11575
11875
|
}
|
|
11576
11876
|
};
|
|
11577
11877
|
async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, handleId) {
|
|
@@ -11588,17 +11888,21 @@ async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, ha
|
|
|
11588
11888
|
void (async () => {
|
|
11589
11889
|
const bgCtx = { logger: ctx.logger, bulkTracker: tracker };
|
|
11590
11890
|
const importIds = uploadedChunks.map((chunk) => chunk.importId);
|
|
11891
|
+
const notificationIds = [];
|
|
11591
11892
|
const matched = /* @__PURE__ */ new Map();
|
|
11592
11893
|
const notImported = /* @__PURE__ */ new Map();
|
|
11593
11894
|
try {
|
|
11594
11895
|
const totalDeadline = Date.now() + opts.totalBudget;
|
|
11595
11896
|
for (const upload of uploadedChunks) {
|
|
11596
11897
|
const out = await completeUploadedChunk(client, upload, prep.mappings, opts.dryRun, opts.perPhaseBudget, totalDeadline, bgCtx, void 0);
|
|
11898
|
+
if (out.notification_id && !notificationIds.includes(out.notification_id)) {
|
|
11899
|
+
notificationIds.push(out.notification_id);
|
|
11900
|
+
}
|
|
11597
11901
|
if (!opts.dryRun) {
|
|
11598
11902
|
reconcileOneChunk(prep, out, matched, notImported);
|
|
11599
11903
|
}
|
|
11600
11904
|
}
|
|
11601
|
-
const result = buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false);
|
|
11905
|
+
const result = buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false, notificationIds);
|
|
11602
11906
|
await tracker.markImportComplete(handleId, {
|
|
11603
11907
|
leads: result.leads,
|
|
11604
11908
|
not_imported: result.not_imported,
|
|
@@ -12266,6 +12570,63 @@ var agentMemoryReview = {
|
|
|
12266
12570
|
}
|
|
12267
12571
|
};
|
|
12268
12572
|
|
|
12573
|
+
// ../core/dist/tools/acknowledge-notification.js
|
|
12574
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12575
|
+
var acknowledgeNotification = {
|
|
12576
|
+
name: "leadbay_acknowledge_notification",
|
|
12577
|
+
annotations: {
|
|
12578
|
+
title: "Acknowledge a Leadbay notification",
|
|
12579
|
+
readOnlyHint: false,
|
|
12580
|
+
destructiveHint: false,
|
|
12581
|
+
idempotentHint: true,
|
|
12582
|
+
openWorldHint: true
|
|
12583
|
+
},
|
|
12584
|
+
description: leadbay_acknowledge_notification,
|
|
12585
|
+
write: true,
|
|
12586
|
+
inputSchema: {
|
|
12587
|
+
type: "object",
|
|
12588
|
+
properties: {
|
|
12589
|
+
notification_id: {
|
|
12590
|
+
type: "string",
|
|
12591
|
+
description: "UUID of the notification to acknowledge. Use the notification_id from `_meta.notifications[]` or `account_status.notifications[]`."
|
|
12592
|
+
},
|
|
12593
|
+
archive: {
|
|
12594
|
+
type: "boolean",
|
|
12595
|
+
description: "If true, archive the notification (won't appear in `archived=false` listings). If false / omitted, mark seen (resets firstSeenAt)."
|
|
12596
|
+
}
|
|
12597
|
+
},
|
|
12598
|
+
required: ["notification_id"],
|
|
12599
|
+
additionalProperties: false
|
|
12600
|
+
},
|
|
12601
|
+
outputSchema: {
|
|
12602
|
+
type: "object",
|
|
12603
|
+
properties: {
|
|
12604
|
+
acknowledged: { type: "boolean" },
|
|
12605
|
+
notification_id: { type: "string" },
|
|
12606
|
+
action: { type: "string", enum: ["seen", "archive"] }
|
|
12607
|
+
},
|
|
12608
|
+
required: ["acknowledged", "notification_id", "action"]
|
|
12609
|
+
},
|
|
12610
|
+
execute: async (client, params, ctx) => {
|
|
12611
|
+
if (!UUID_RE.test(params.notification_id)) {
|
|
12612
|
+
return {
|
|
12613
|
+
error: true,
|
|
12614
|
+
code: "BAD_INPUT",
|
|
12615
|
+
message: "notification_id must be a UUID",
|
|
12616
|
+
hint: "Pass the notification_id verbatim from _meta.notifications[].notification_id or account_status.notifications[].notification_id."
|
|
12617
|
+
};
|
|
12618
|
+
}
|
|
12619
|
+
const action = params.archive ? "archive" : "seen";
|
|
12620
|
+
await client.acknowledgeNotification(params.notification_id, action);
|
|
12621
|
+
ctx?.notificationsInbox?.markSeen(params.notification_id);
|
|
12622
|
+
return {
|
|
12623
|
+
acknowledged: true,
|
|
12624
|
+
notification_id: params.notification_id,
|
|
12625
|
+
action
|
|
12626
|
+
};
|
|
12627
|
+
}
|
|
12628
|
+
};
|
|
12629
|
+
|
|
12269
12630
|
// ../core/dist/tools/select-leads.js
|
|
12270
12631
|
var selectLeads = {
|
|
12271
12632
|
name: "leadbay_select_leads",
|
|
@@ -15169,6 +15530,78 @@ var researchLeadByNameFuzzy = {
|
|
|
15169
15530
|
}
|
|
15170
15531
|
};
|
|
15171
15532
|
|
|
15533
|
+
// ../core/dist/composite/account-history.js
|
|
15534
|
+
var accountHistory = {
|
|
15535
|
+
name: "leadbay_account_history",
|
|
15536
|
+
annotations: {
|
|
15537
|
+
title: "One account's full back-story",
|
|
15538
|
+
readOnlyHint: true,
|
|
15539
|
+
destructiveHint: false,
|
|
15540
|
+
idempotentHint: true,
|
|
15541
|
+
openWorldHint: true
|
|
15542
|
+
},
|
|
15543
|
+
description: leadbay_account_history,
|
|
15544
|
+
inputSchema: {
|
|
15545
|
+
type: "object",
|
|
15546
|
+
properties: {
|
|
15547
|
+
leadId: { type: "string", description: "Lead UUID (required)" },
|
|
15548
|
+
activityCount: {
|
|
15549
|
+
type: "number",
|
|
15550
|
+
description: "Number of activity-timeline entries to return, max 100 (default: 50)."
|
|
15551
|
+
},
|
|
15552
|
+
lensId: {
|
|
15553
|
+
type: "number",
|
|
15554
|
+
description: "Lens id the lead came from (escape hatch \u2014 normally omit; defaults to the active lens). Pass it when researching a lead from a lens other than the current default, so the underlying /lenses/{lensId}/leads/{leadId} fetch doesn't 404 after the active lens changed."
|
|
15555
|
+
}
|
|
15556
|
+
},
|
|
15557
|
+
required: ["leadId"],
|
|
15558
|
+
additionalProperties: false
|
|
15559
|
+
},
|
|
15560
|
+
execute: async (client, params, ctx) => {
|
|
15561
|
+
const leadId = params.leadId;
|
|
15562
|
+
const count = Math.max(1, Math.min(Math.floor(params.activityCount ?? 50), 100));
|
|
15563
|
+
const [research, notes, activities] = await Promise.all([
|
|
15564
|
+
researchLeadById.execute(client, { leadId, lensId: params.lensId, response_format: "json" }, ctx),
|
|
15565
|
+
client.request("GET", `/leads/${leadId}/notes`).catch(() => []),
|
|
15566
|
+
client.request("GET", `/leads/${leadId}/activities?count=${count}`).catch(() => ({ items: [], pagination: { total: 0 } }))
|
|
15567
|
+
]);
|
|
15568
|
+
const r = research;
|
|
15569
|
+
const noteList = Array.isArray(notes) ? notes : [];
|
|
15570
|
+
const activityItems = Array.isArray(activities?.items) ? activities.items : [];
|
|
15571
|
+
return {
|
|
15572
|
+
lead: {
|
|
15573
|
+
id: r.firmographics?.id ?? leadId,
|
|
15574
|
+
name: r.firmographics?.name ?? null
|
|
15575
|
+
},
|
|
15576
|
+
// Current state — signals, firmographics, qualification, contacts,
|
|
15577
|
+
// engagement: passed through verbatim from research_lead_by_id so the
|
|
15578
|
+
// agent gets the live "why is this account hot NOW" picture.
|
|
15579
|
+
signals: r.signals ?? null,
|
|
15580
|
+
firmographics: r.firmographics ?? null,
|
|
15581
|
+
qualification: r.qualification ?? [],
|
|
15582
|
+
contacts: r.contacts ?? null,
|
|
15583
|
+
engagement: r.engagement ?? null,
|
|
15584
|
+
// Historical context — the part research only counts/summarizes.
|
|
15585
|
+
notes: noteList,
|
|
15586
|
+
activities: {
|
|
15587
|
+
activities: activityItems.map((a) => ({ type: a.type, date: a.date })),
|
|
15588
|
+
total: activities?.pagination?.total ?? 0
|
|
15589
|
+
},
|
|
15590
|
+
// Preserve research's pass-through metadata (agent_memory summary,
|
|
15591
|
+
// lens_id, web_fetch_in_progress, has_reachable_contact, …) — the
|
|
15592
|
+
// generated description advertises the memory protocol, so dropping
|
|
15593
|
+
// _meta.agent_memory would make history narratives miss stored
|
|
15594
|
+
// preferences. Spread research _meta first, then layer our counts.
|
|
15595
|
+
_meta: {
|
|
15596
|
+
...r._meta ?? {},
|
|
15597
|
+
region: client.region,
|
|
15598
|
+
notes_count: noteList.length,
|
|
15599
|
+
activities_returned: activityItems.length
|
|
15600
|
+
}
|
|
15601
|
+
};
|
|
15602
|
+
}
|
|
15603
|
+
};
|
|
15604
|
+
|
|
15172
15605
|
// ../core/dist/composite/recall-ordered-titles.js
|
|
15173
15606
|
var recallOrderedTitles = {
|
|
15174
15607
|
name: "leadbay_recall_ordered_titles",
|
|
@@ -15336,7 +15769,21 @@ var accountStatus = {
|
|
|
15336
15769
|
},
|
|
15337
15770
|
quota: {
|
|
15338
15771
|
type: ["object", "null"],
|
|
15339
|
-
description: "Per-resource quota state (llm_completion, ai_rescore, web_fetch, LENS_EXTRA_REFILL) across daily/weekly/monthly windows. Null if /quota_status failed (
|
|
15772
|
+
description: "Per-resource quota state (llm_completion, ai_rescore, web_fetch, LENS_EXTRA_REFILL) across daily/weekly/monthly windows. Null if /quota_status failed (see quota_error) or genuinely returned nothing. Pre-check the LENS_EXTRA_REFILL entry before calling leadbay_extend_lens."
|
|
15773
|
+
},
|
|
15774
|
+
quota_error: {
|
|
15775
|
+
type: ["object", "null"],
|
|
15776
|
+
description: "Non-null ONLY when the quota_status call FAILED \u2014 {code, http_status, message}. A 401/403 means the token lacks quota scope: tell the user to reconnect / re-run OAuth. Treat as 'quota unreadable', NEVER as zero usage or 'no limits'.",
|
|
15777
|
+
properties: {
|
|
15778
|
+
code: { type: "string" },
|
|
15779
|
+
http_status: { type: ["number", "null"] },
|
|
15780
|
+
message: { type: "string" }
|
|
15781
|
+
}
|
|
15782
|
+
},
|
|
15783
|
+
notifications: {
|
|
15784
|
+
type: "array",
|
|
15785
|
+
description: "Terminal bulk-progress notifications the MCP knows about (background work the user or agent started that has since completed). Each entry carries notification_id, kind (bulk_enrich | bulk_qualify | import | other), bulk_progress counters, and a revise_hint pointing at prior agent outputs the just-finished work might have made stale. After revising affected outputs, call leadbay_acknowledge_notification(notification_id) to clear the entry. Empty array when nothing has completed.",
|
|
15786
|
+
items: { type: "object" }
|
|
15340
15787
|
},
|
|
15341
15788
|
_meta: {
|
|
15342
15789
|
type: "object",
|
|
@@ -15372,9 +15819,15 @@ var accountStatus = {
|
|
|
15372
15819
|
execute: async (client, _params, ctx) => {
|
|
15373
15820
|
const me = await client.resolveMe();
|
|
15374
15821
|
let quota = null;
|
|
15822
|
+
let quota_error = null;
|
|
15375
15823
|
try {
|
|
15376
15824
|
quota = await client.request("GET", `/organizations/${me.organization.id}/quota_status`);
|
|
15377
15825
|
} catch (err) {
|
|
15826
|
+
quota_error = {
|
|
15827
|
+
code: err?.code ?? "QUOTA_STATUS_FAILED",
|
|
15828
|
+
http_status: err?._meta?.http_status ?? null,
|
|
15829
|
+
message: err?.message ?? "quota_status request failed"
|
|
15830
|
+
};
|
|
15378
15831
|
ctx?.logger?.warn?.(`account_status: quota_status failed: ${err?.message ?? err?.code ?? err}`);
|
|
15379
15832
|
}
|
|
15380
15833
|
return withAgentMemoryMeta(client, {
|
|
@@ -15397,6 +15850,16 @@ var accountStatus = {
|
|
|
15397
15850
|
// on /me are intentionally NOT surfaced — they're defunct (see
|
|
15398
15851
|
// SHAPE-DRIFT.md probe round 4).
|
|
15399
15852
|
quota,
|
|
15853
|
+
// Inbox of terminal bulk-progress notifications. Same shape the MCP
|
|
15854
|
+
// server attaches to `_meta.notifications` on every tool response —
|
|
15855
|
+
// duplicated here as a top-level field so the agent's daily-rhythm
|
|
15856
|
+
// check-in (this composite) sees them without having to read _meta.
|
|
15857
|
+
// Empty array when the WS listener isn't wired (OpenClaw, tests) OR
|
|
15858
|
+
// when nothing has completed since the last ack.
|
|
15859
|
+
notifications: ctx?.notificationsInbox?.list() ?? [],
|
|
15860
|
+
// Non-null ONLY when the quota_status call failed. The agent must treat
|
|
15861
|
+
// this as "could not read quota" (reauth on 401/403) — NOT as zero usage.
|
|
15862
|
+
quota_error,
|
|
15400
15863
|
_meta: {
|
|
15401
15864
|
region: client.region
|
|
15402
15865
|
}
|
|
@@ -15410,6 +15873,33 @@ var DEFAULT_COUNT = 10;
|
|
|
15410
15873
|
var MAX_COUNT = 25;
|
|
15411
15874
|
var DEFAULT_PER_LEAD_BUDGET_MS = 9e4;
|
|
15412
15875
|
var DEFAULT_TOTAL_BUDGET_MS2 = 5 * 6e4;
|
|
15876
|
+
async function launchBulkQualify(client, leadIds, ctx) {
|
|
15877
|
+
await client.acquireSelectionLock();
|
|
15878
|
+
try {
|
|
15879
|
+
try {
|
|
15880
|
+
const qs = leadIds.map((id) => `leadIds=${encodeURIComponent(id)}`).join("&");
|
|
15881
|
+
await client.requestVoid("POST", `/leads/selection/select?${qs}`);
|
|
15882
|
+
try {
|
|
15883
|
+
const resp = await client.request("POST", "/leads/selection/web_fetch?force_fetch=false", {});
|
|
15884
|
+
return { resp, quotaExceeded: false };
|
|
15885
|
+
} catch (err) {
|
|
15886
|
+
if (err?.code === "QUOTA_EXCEEDED") {
|
|
15887
|
+
ctx?.logger?.warn?.("bulk_qualify_leads: 429 on bulk /leads/selection/web_fetch \u2014 no leads queued");
|
|
15888
|
+
return { resp: null, quotaExceeded: true };
|
|
15889
|
+
}
|
|
15890
|
+
throw err;
|
|
15891
|
+
}
|
|
15892
|
+
} finally {
|
|
15893
|
+
try {
|
|
15894
|
+
await client.requestVoid("POST", "/leads/selection/clear");
|
|
15895
|
+
} catch (e) {
|
|
15896
|
+
ctx?.logger?.warn?.(`bulk_qualify_leads: selection.clear failed: ${e?.message ?? e?.code}`);
|
|
15897
|
+
}
|
|
15898
|
+
}
|
|
15899
|
+
} finally {
|
|
15900
|
+
client.releaseSelectionLock();
|
|
15901
|
+
}
|
|
15902
|
+
}
|
|
15413
15903
|
var bulkQualifyLeads = {
|
|
15414
15904
|
name: "leadbay_bulk_qualify_leads",
|
|
15415
15905
|
annotations: {
|
|
@@ -15577,69 +16067,50 @@ var bulkQualifyLeads = {
|
|
|
15577
16067
|
per_lead_budget_ms: perLeadBudget,
|
|
15578
16068
|
total_budget_ms: totalBudget
|
|
15579
16069
|
});
|
|
15580
|
-
|
|
15581
|
-
|
|
16070
|
+
let launchedCount = 0;
|
|
16071
|
+
let notificationId = null;
|
|
15582
16072
|
let quotaExceeded2 = false;
|
|
16073
|
+
let failed2 = [];
|
|
15583
16074
|
if (!reservation.reused) {
|
|
15584
|
-
|
|
15585
|
-
|
|
15586
|
-
|
|
15587
|
-
|
|
15588
|
-
|
|
15589
|
-
|
|
15590
|
-
|
|
15591
|
-
|
|
15592
|
-
|
|
15593
|
-
|
|
15594
|
-
failed2.push({ lead_id: leadId, error: "lead not found" });
|
|
15595
|
-
} else {
|
|
15596
|
-
failed2.push({
|
|
15597
|
-
lead_id: leadId,
|
|
15598
|
-
error: err?.message ?? err?.code ?? "unknown"
|
|
15599
|
-
});
|
|
15600
|
-
}
|
|
15601
|
-
}
|
|
15602
|
-
}
|
|
15603
|
-
if (failed2.length === candidates.length || launched2.length > 0 || quotaExceeded2) {
|
|
15604
|
-
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id);
|
|
16075
|
+
const launch = await launchBulkQualify(client, candidates, ctx);
|
|
16076
|
+
quotaExceeded2 = launch.quotaExceeded;
|
|
16077
|
+
notificationId = launch.resp?.notification_id ?? null;
|
|
16078
|
+
const queuedIds = launch.resp?.queued_ids ?? [];
|
|
16079
|
+
const skippedIds = launch.resp?.skipped_ids ?? [];
|
|
16080
|
+
launchedCount = queuedIds.length;
|
|
16081
|
+
const seen = /* @__PURE__ */ new Set([...queuedIds, ...skippedIds]);
|
|
16082
|
+
failed2 = candidates.filter((id) => !seen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
|
|
16083
|
+
if (queuedIds.length > 0 || quotaExceeded2 || skippedIds.length > 0 || failed2.length === candidates.length) {
|
|
16084
|
+
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id, notificationId);
|
|
15605
16085
|
}
|
|
16086
|
+
} else {
|
|
16087
|
+
notificationId = reservation.record.notification_id ?? null;
|
|
16088
|
+
launchedCount = reservation.record.lead_ids.length;
|
|
15606
16089
|
}
|
|
15607
16090
|
const out = {
|
|
15608
16091
|
status: "running",
|
|
15609
16092
|
handle_id: reservation.record.bulk_id,
|
|
15610
16093
|
qualify_id: reservation.record.bulk_id,
|
|
15611
16094
|
lead_ids: candidates,
|
|
15612
|
-
launched_count:
|
|
16095
|
+
launched_count: launchedCount,
|
|
15613
16096
|
failed: failed2,
|
|
15614
16097
|
quota_exceeded: quotaExceeded2,
|
|
15615
16098
|
lens_id: lensId,
|
|
16099
|
+
notification_id: notificationId,
|
|
15616
16100
|
_meta: { region: client.region }
|
|
15617
16101
|
};
|
|
15618
16102
|
return out;
|
|
15619
16103
|
}
|
|
15620
|
-
const
|
|
15621
|
-
const
|
|
15622
|
-
|
|
15623
|
-
|
|
15624
|
-
|
|
15625
|
-
|
|
15626
|
-
|
|
15627
|
-
await client.requestVoid("POST", `/leads/${leadId}/web_fetch?force_fetch=false`);
|
|
15628
|
-
launched.push(leadId);
|
|
15629
|
-
} catch (err) {
|
|
15630
|
-
if (err?.code === "QUOTA_EXCEEDED") {
|
|
15631
|
-
quotaExceeded = true;
|
|
15632
|
-
ctx?.logger?.warn?.(`bulk_qualify_leads: 429 mid-fanout after launching ${launched.length}/${candidates.length} \u2014 stopping further launches but polling those already in flight`);
|
|
15633
|
-
} else if (err?.code === "NOT_FOUND") {
|
|
15634
|
-
failed.push({ lead_id: leadId, error: "lead not found" });
|
|
15635
|
-
} else {
|
|
15636
|
-
failed.push({
|
|
15637
|
-
lead_id: leadId,
|
|
15638
|
-
error: err?.message ?? err?.code ?? "unknown"
|
|
15639
|
-
});
|
|
15640
|
-
}
|
|
15641
|
-
}
|
|
16104
|
+
const inlineLaunch = await launchBulkQualify(client, candidates, ctx);
|
|
16105
|
+
const quotaExceeded = inlineLaunch.quotaExceeded;
|
|
16106
|
+
const launched = inlineLaunch.resp?.queued_ids ?? [];
|
|
16107
|
+
const inlineSkipped = inlineLaunch.resp?.skipped_ids ?? [];
|
|
16108
|
+
const inlineNotificationId = inlineLaunch.resp?.notification_id ?? null;
|
|
16109
|
+
if (inlineNotificationId) {
|
|
16110
|
+
ctx?.logger?.info?.(`bulk_qualify_leads: launched bulk progress_notification_id=${inlineNotificationId} queued=${launched.length} skipped=${inlineSkipped.length}`);
|
|
15642
16111
|
}
|
|
16112
|
+
const inlineFailedSeen = /* @__PURE__ */ new Set([...launched, ...inlineSkipped]);
|
|
16113
|
+
const failed = candidates.filter((id) => !inlineFailedSeen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
|
|
15643
16114
|
let progressDone = 0;
|
|
15644
16115
|
const progressTotal = launched.length;
|
|
15645
16116
|
if (progressTotal > 0) {
|
|
@@ -16352,6 +16823,7 @@ var importAndQualify = {
|
|
|
16352
16823
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
16353
16824
|
qualify_id: null,
|
|
16354
16825
|
import_ids: queued.importIds,
|
|
16826
|
+
notification_ids: queued.notification_ids ?? [],
|
|
16355
16827
|
imported: queued.leads.map((l) => ({
|
|
16356
16828
|
leadId: l.leadId,
|
|
16357
16829
|
...l.domain ? { domain: l.domain } : {},
|
|
@@ -16376,6 +16848,7 @@ var importAndQualify = {
|
|
|
16376
16848
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
16377
16849
|
qualify_id: null,
|
|
16378
16850
|
import_ids: queued.importIds,
|
|
16851
|
+
notification_ids: queued.notification_ids ?? [],
|
|
16379
16852
|
imported: [],
|
|
16380
16853
|
not_imported: [],
|
|
16381
16854
|
qualified: [],
|
|
@@ -16413,6 +16886,7 @@ var importAndQualify = {
|
|
|
16413
16886
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
16414
16887
|
qualify_id: null,
|
|
16415
16888
|
import_ids: importResult.importIds,
|
|
16889
|
+
notification_ids: importResult.notification_ids ?? [],
|
|
16416
16890
|
imported: [],
|
|
16417
16891
|
not_imported: importResult.not_imported.map(toNotImportedEntry),
|
|
16418
16892
|
qualified: [],
|
|
@@ -16450,6 +16924,7 @@ var importAndQualify = {
|
|
|
16450
16924
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
16451
16925
|
qualify_id: null,
|
|
16452
16926
|
import_ids: importResult.importIds,
|
|
16927
|
+
notification_ids: importResult.notification_ids ?? [],
|
|
16453
16928
|
imported,
|
|
16454
16929
|
not_imported,
|
|
16455
16930
|
qualified: [],
|
|
@@ -16560,6 +17035,7 @@ var importAndQualify = {
|
|
|
16560
17035
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
16561
17036
|
qualify_id: reservation.record.bulk_id,
|
|
16562
17037
|
import_ids: importResult.importIds,
|
|
17038
|
+
notification_ids: importResult.notification_ids ?? [],
|
|
16563
17039
|
imported,
|
|
16564
17040
|
not_imported,
|
|
16565
17041
|
qualified,
|
|
@@ -16901,6 +17377,14 @@ var importStatus = {
|
|
|
16901
17377
|
};
|
|
16902
17378
|
|
|
16903
17379
|
// ../core/dist/composite/qualify-status.js
|
|
17380
|
+
async function readNotification(client, notificationId) {
|
|
17381
|
+
try {
|
|
17382
|
+
const page = await client.listNotifications({ archived: false, count: 50 });
|
|
17383
|
+
return page.items.find((n) => n.id === notificationId) ?? null;
|
|
17384
|
+
} catch {
|
|
17385
|
+
return null;
|
|
17386
|
+
}
|
|
17387
|
+
}
|
|
16904
17388
|
var qualifyStatus = {
|
|
16905
17389
|
name: "leadbay_qualify_status",
|
|
16906
17390
|
annotations: {
|
|
@@ -17055,6 +17539,16 @@ var qualifyStatus = {
|
|
|
17055
17539
|
const { _stillRunning, _failedCode, ...rest } = r;
|
|
17056
17540
|
qualified.push(rest);
|
|
17057
17541
|
}
|
|
17542
|
+
let bulkProgress = null;
|
|
17543
|
+
let inProgressFlag = null;
|
|
17544
|
+
const notifId = record.notification_id ?? null;
|
|
17545
|
+
if (notifId) {
|
|
17546
|
+
const n = await readNotification(client, notifId);
|
|
17547
|
+
if (n) {
|
|
17548
|
+
bulkProgress = n.bulk_progress;
|
|
17549
|
+
inProgressFlag = n.in_progress;
|
|
17550
|
+
}
|
|
17551
|
+
}
|
|
17058
17552
|
const out = {
|
|
17059
17553
|
qualify_id: record.bulk_id,
|
|
17060
17554
|
launched_at: record.launched_at,
|
|
@@ -17066,6 +17560,9 @@ var qualifyStatus = {
|
|
|
17066
17560
|
still_running,
|
|
17067
17561
|
failed,
|
|
17068
17562
|
not_in_lens: [...notInLensSet],
|
|
17563
|
+
notification_id: notifId,
|
|
17564
|
+
bulk_progress: bulkProgress,
|
|
17565
|
+
in_progress: inProgressFlag,
|
|
17069
17566
|
region: client.region,
|
|
17070
17567
|
_meta: client.lastMeta ?? {
|
|
17071
17568
|
region: client.region,
|
|
@@ -17078,10 +17575,23 @@ var qualifyStatus = {
|
|
|
17078
17575
|
out.per_lead_budget_ms = record.per_lead_budget_ms;
|
|
17079
17576
|
if (record.total_budget_ms !== void 0)
|
|
17080
17577
|
out.total_budget_ms = record.total_budget_ms;
|
|
17578
|
+
if (bulkProgress && bulkProgress.quota_hit_count > 0) {
|
|
17579
|
+
out.quota_hit_hint = "Some leads hit the AI-credits quota during qualification. Top up via leadbay_create_topup_link to clear the throttle immediately, or wait until the daily/weekly window resets.";
|
|
17580
|
+
}
|
|
17081
17581
|
return out;
|
|
17082
17582
|
}
|
|
17083
17583
|
};
|
|
17084
17584
|
|
|
17585
|
+
// ../core/dist/composite/_credits-helpers.js
|
|
17586
|
+
async function readCreditsRemaining(client, force = false) {
|
|
17587
|
+
try {
|
|
17588
|
+
const me = await client.resolveMe(force);
|
|
17589
|
+
return me.organization.billing?.ai_credits ?? null;
|
|
17590
|
+
} catch {
|
|
17591
|
+
return null;
|
|
17592
|
+
}
|
|
17593
|
+
}
|
|
17594
|
+
|
|
17085
17595
|
// ../core/dist/composite/enrich-titles.js
|
|
17086
17596
|
var DEFAULT_CANDIDATE_COUNT = 25;
|
|
17087
17597
|
var enrichTitles = {
|
|
@@ -17165,6 +17675,10 @@ var enrichTitles = {
|
|
|
17165
17675
|
type: "number",
|
|
17166
17676
|
description: "Count of enrichable contacts at preview time."
|
|
17167
17677
|
},
|
|
17678
|
+
credits_remaining: {
|
|
17679
|
+
type: ["number", "null"],
|
|
17680
|
+
description: "AI-credit balance BEFORE launching (billing.ai_credits). Present in discover / preview_only / dry_run modes. Pair with enrichable_contacts to tell the user 'you have N credits, this will enrich M contacts' \u2014 do NOT estimate an exact cost (the per-contact rate is backend-only). Null = billing unavailable."
|
|
17681
|
+
},
|
|
17168
17682
|
selected_lead_count: {
|
|
17169
17683
|
type: "number",
|
|
17170
17684
|
description: "How many leads the selection covers."
|
|
@@ -17288,6 +17802,10 @@ var enrichTitles = {
|
|
|
17288
17802
|
previously_enriched: previouslyEnriched,
|
|
17289
17803
|
enrichable_contacts: enrichableContacts,
|
|
17290
17804
|
selected_lead_count: leadIds.length,
|
|
17805
|
+
// BEFORE: show balance + volume. We can't estimate exact cost
|
|
17806
|
+
// (the per-contact rate is backend-only), so surface the balance
|
|
17807
|
+
// and the count, not a fabricated "will cost N".
|
|
17808
|
+
credits_remaining: await readCreditsRemaining(client),
|
|
17291
17809
|
next_action: "Pick titles to enrich and call leadbay_enrich_titles again with titles=[...]"
|
|
17292
17810
|
};
|
|
17293
17811
|
}
|
|
@@ -17310,7 +17828,8 @@ var enrichTitles = {
|
|
|
17310
17828
|
preview,
|
|
17311
17829
|
launched: false,
|
|
17312
17830
|
message: "No enrichable contacts for the chosen titles. Try other titles from available_titles or recommendations.",
|
|
17313
|
-
available_titles: availableTitles
|
|
17831
|
+
available_titles: availableTitles,
|
|
17832
|
+
credits_remaining: await readCreditsRemaining(client)
|
|
17314
17833
|
};
|
|
17315
17834
|
}
|
|
17316
17835
|
if (params.dry_run) {
|
|
@@ -17318,7 +17837,12 @@ var enrichTitles = {
|
|
|
17318
17837
|
mode: "dry_run",
|
|
17319
17838
|
preview,
|
|
17320
17839
|
launched: false,
|
|
17321
|
-
would_launch: { titles: params.titles, email, phone }
|
|
17840
|
+
would_launch: { titles: params.titles, email, phone },
|
|
17841
|
+
// BEFORE confirmation gate: balance + how many contacts WOULD be
|
|
17842
|
+
// enriched. enrichable_contacts is the volume; credits_remaining
|
|
17843
|
+
// the balance. No estimated cost — that rate is backend-only.
|
|
17844
|
+
enrichable_contacts: preview.enrichable_contacts,
|
|
17845
|
+
credits_remaining: await readCreditsRemaining(client)
|
|
17322
17846
|
};
|
|
17323
17847
|
}
|
|
17324
17848
|
const tracker = ctx?.bulkTracker;
|
|
@@ -17348,6 +17872,7 @@ var enrichTitles = {
|
|
|
17348
17872
|
bulk_id: res.record.bulk_id,
|
|
17349
17873
|
launched_at: res.record.launched_at,
|
|
17350
17874
|
durability: res.record.durability,
|
|
17875
|
+
notification_id: res.record.notification_id ?? null,
|
|
17351
17876
|
seconds_since_original_launch: bulkSecondsSinceOriginal ?? 0,
|
|
17352
17877
|
titles: params.titles,
|
|
17353
17878
|
email,
|
|
@@ -17363,8 +17888,9 @@ var enrichTitles = {
|
|
|
17363
17888
|
total: 3,
|
|
17364
17889
|
message: `Launching enrichment for ${params.titles.length} title${params.titles.length === 1 ? "" : "s"}\u2026`
|
|
17365
17890
|
});
|
|
17891
|
+
let launchResp = null;
|
|
17366
17892
|
try {
|
|
17367
|
-
await client.
|
|
17893
|
+
launchResp = await client.request("POST", "/leads/selection/enrichment/launch", { titles: params.titles, email, phone });
|
|
17368
17894
|
} catch (err) {
|
|
17369
17895
|
const aborted = err?.name === "AbortError" || ctx?.signal?.aborted === true;
|
|
17370
17896
|
if (bulkRecord && tracker) {
|
|
@@ -17388,9 +17914,10 @@ var enrichTitles = {
|
|
|
17388
17914
|
}
|
|
17389
17915
|
throw err;
|
|
17390
17916
|
}
|
|
17917
|
+
const notificationId = launchResp?.notification_id ?? null;
|
|
17391
17918
|
if (bulkRecord && tracker) {
|
|
17392
17919
|
try {
|
|
17393
|
-
await tracker.markLaunched(bulkRecord.bulk_id);
|
|
17920
|
+
await tracker.markLaunched(bulkRecord.bulk_id, notificationId);
|
|
17394
17921
|
} catch (e) {
|
|
17395
17922
|
ctx?.logger?.warn?.(`enrich_titles: tracker.markLaunched failed: ${e?.message ?? e}`);
|
|
17396
17923
|
return {
|
|
@@ -17418,8 +17945,9 @@ var enrichTitles = {
|
|
|
17418
17945
|
bulk_id: bulkRecord?.bulk_id,
|
|
17419
17946
|
launched_at: bulkRecord?.launched_at,
|
|
17420
17947
|
durability: bulkRecord?.durability,
|
|
17421
|
-
|
|
17422
|
-
|
|
17948
|
+
notification_id: notificationId,
|
|
17949
|
+
message: notificationId ? "Enrichment job launched. The MCP is now listening for the backend notification \u2014 when enrichment finishes, a `_meta.notifications` entry will surface on your next tool response (also visible in `leadbay_account_status.notifications`)." : bulkRecord ? "Enrichment job launched. Backend did not return a notification id this time; poll via leadbay_bulk_enrich_status with the bulk_id." : "Enrichment job launched. No bulk_id tracker configured \u2014 poll leadbay_get_contacts per lead after ~60s; contact.enrichment.done flips to true.",
|
|
17950
|
+
next_action: notificationId ? "Wait for the next `_meta.notifications` entry (typically <2 min for a small batch). If you want progress sooner, call leadbay_bulk_enrich_status({bulk_id})." : bulkRecord ? "Call leadbay_bulk_enrich_status({bulk_id}) after ~60s; pass include_contacts=true for the final read." : "Wait ~60s, then call leadbay_research_lead_by_id or leadbay_get_contacts on the leads you care about."
|
|
17423
17951
|
};
|
|
17424
17952
|
} finally {
|
|
17425
17953
|
try {
|
|
@@ -17435,6 +17963,14 @@ var enrichTitles = {
|
|
|
17435
17963
|
};
|
|
17436
17964
|
|
|
17437
17965
|
// ../core/dist/composite/bulk-enrich-status.js
|
|
17966
|
+
async function readNotification2(client, notificationId) {
|
|
17967
|
+
try {
|
|
17968
|
+
const page = await client.listNotifications({ archived: false, count: 50 });
|
|
17969
|
+
return page.items.find((n) => n.id === notificationId) ?? null;
|
|
17970
|
+
} catch {
|
|
17971
|
+
return null;
|
|
17972
|
+
}
|
|
17973
|
+
}
|
|
17438
17974
|
var STATUS_FETCH_CONCURRENCY = 5;
|
|
17439
17975
|
async function pMap(items, fn, concurrency) {
|
|
17440
17976
|
const out = new Array(items.length);
|
|
@@ -17514,6 +18050,10 @@ var bulkEnrichStatus = {
|
|
|
17514
18050
|
type: "boolean",
|
|
17515
18051
|
description: "True when overall_progress.done === total AND no partial_failures."
|
|
17516
18052
|
},
|
|
18053
|
+
credits_remaining: {
|
|
18054
|
+
type: ["number", "null"],
|
|
18055
|
+
description: "AI-credit balance re-read after the spend (force-refreshed /users/me \u2192 billing.ai_credits). Present only when all_done. Null = billing unavailable (don't read as zero). NOTE: a per-run 'credits used' figure is intentionally NOT returned \u2014 getContacts can't scope cost to this bulk, so any sum would conflate historical enrichments."
|
|
18056
|
+
},
|
|
17517
18057
|
partial_failures: {
|
|
17518
18058
|
type: "array",
|
|
17519
18059
|
description: "Per-lead errors observed during contacts fan-out (omitted when no failures).",
|
|
@@ -17599,6 +18139,55 @@ var bulkEnrichStatus = {
|
|
|
17599
18139
|
launched_at: record.launched_at
|
|
17600
18140
|
};
|
|
17601
18141
|
}
|
|
18142
|
+
const notifId = record.notification_id ?? null;
|
|
18143
|
+
if (notifId) {
|
|
18144
|
+
const n = await readNotification2(client, notifId);
|
|
18145
|
+
if (n && n.bulk_progress) {
|
|
18146
|
+
const bp = n.bulk_progress;
|
|
18147
|
+
const inProgress = n.in_progress;
|
|
18148
|
+
let leads2 = [];
|
|
18149
|
+
if (!inProgress && includeContacts) {
|
|
18150
|
+
leads2 = await pMap(record.lead_ids, async (leadId) => {
|
|
18151
|
+
try {
|
|
18152
|
+
const out = await getContacts.execute(client, { leadId });
|
|
18153
|
+
const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
|
|
18154
|
+
return { lead_id: leadId, contacts };
|
|
18155
|
+
} catch {
|
|
18156
|
+
return { lead_id: leadId };
|
|
18157
|
+
}
|
|
18158
|
+
}, STATUS_FETCH_CONCURRENCY);
|
|
18159
|
+
} else {
|
|
18160
|
+
leads2 = record.lead_ids.map((id) => ({ lead_id: id }));
|
|
18161
|
+
}
|
|
18162
|
+
ctx?.logger?.info?.(`bulk.status_checked_via_notification bulk_id=${record.bulk_id} notification_id=${notifId} done=${bp.success_count}/${bp.total_count} in_progress=${inProgress} wall_ms=${Date.now() - startMs}`);
|
|
18163
|
+
const creditsRemaining2 = !inProgress ? await readCreditsRemaining(client, true) : null;
|
|
18164
|
+
return {
|
|
18165
|
+
bulk_id: record.bulk_id,
|
|
18166
|
+
notification_id: notifId,
|
|
18167
|
+
launched_at: record.launched_at,
|
|
18168
|
+
status: record.status,
|
|
18169
|
+
durability: record.durability,
|
|
18170
|
+
titles: record.titles,
|
|
18171
|
+
email: record.email,
|
|
18172
|
+
phone: record.phone,
|
|
18173
|
+
lens_id: record.lens_id,
|
|
18174
|
+
leads: leads2,
|
|
18175
|
+
overall_progress: {
|
|
18176
|
+
done: bp.success_count + bp.failure_count + bp.quota_hit_count,
|
|
18177
|
+
total: bp.total_count,
|
|
18178
|
+
done_ratio: bp.total_count === 0 ? 0 : (bp.success_count + bp.failure_count + bp.quota_hit_count) / bp.total_count
|
|
18179
|
+
},
|
|
18180
|
+
bulk_progress: bp,
|
|
18181
|
+
in_progress: inProgress,
|
|
18182
|
+
all_done: !inProgress,
|
|
18183
|
+
...!inProgress ? { credits_remaining: creditsRemaining2 } : {},
|
|
18184
|
+
...bp.quota_hit_count > 0 ? {
|
|
18185
|
+
quota_hit_hint: "Some contacts could not be enriched because the AI-credits quota was hit. Top up via leadbay_create_topup_link or wait for the window reset."
|
|
18186
|
+
} : {}
|
|
18187
|
+
};
|
|
18188
|
+
}
|
|
18189
|
+
ctx?.logger?.info?.(`bulk_enrich_status: notification ${notifId} not yet visible; falling back to per-lead fan-out`);
|
|
18190
|
+
}
|
|
17602
18191
|
let doneSoFar = 0;
|
|
17603
18192
|
const totalLeads = record.lead_ids.length;
|
|
17604
18193
|
const results = await pMap(record.lead_ids, async (leadId) => {
|
|
@@ -17664,6 +18253,10 @@ var bulkEnrichStatus = {
|
|
|
17664
18253
|
};
|
|
17665
18254
|
const allDone = totalAll > 0 && totalDone === totalAll && partialFailures.length === 0;
|
|
17666
18255
|
ctx?.logger?.info?.(`bulk.status_checked bulk_id=${record.bulk_id} done=${totalDone} total=${totalAll} wall_ms=${Date.now() - startMs}`);
|
|
18256
|
+
let creditsRemaining = null;
|
|
18257
|
+
if (allDone) {
|
|
18258
|
+
creditsRemaining = await readCreditsRemaining(client, true);
|
|
18259
|
+
}
|
|
17667
18260
|
return {
|
|
17668
18261
|
bulk_id: record.bulk_id,
|
|
17669
18262
|
launched_at: record.launched_at,
|
|
@@ -17676,6 +18269,7 @@ var bulkEnrichStatus = {
|
|
|
17676
18269
|
leads,
|
|
17677
18270
|
overall_progress: overallProgress,
|
|
17678
18271
|
all_done: allDone,
|
|
18272
|
+
...allDone ? { credits_remaining: creditsRemaining } : {},
|
|
17679
18273
|
...partialFailures.length > 0 ? { partial_failures: partialFailures } : {}
|
|
17680
18274
|
};
|
|
17681
18275
|
}
|
|
@@ -19360,6 +19954,13 @@ var compositeReadTools = [
|
|
|
19360
19954
|
campaignCallSheet,
|
|
19361
19955
|
researchLeadById,
|
|
19362
19956
|
researchLeadByNameFuzzy,
|
|
19957
|
+
// accountHistory layers FULL notes + activity timeline on top of research
|
|
19958
|
+
// so the agent can write the US4 "why has this dormant account resurfaced"
|
|
19959
|
+
// narrative in ONE call. ALWAYS exposed (compositeReadTools) — the underlying
|
|
19960
|
+
// get_lead_notes / get_lead_activities are ADVANCED-gated, but the
|
|
19961
|
+
// reprioritize-a-neglected-account workflow (#3630 GAP C) must work in a
|
|
19962
|
+
// default deployment without LEADBAY_MCP_ADVANCED=1.
|
|
19963
|
+
accountHistory,
|
|
19363
19964
|
recallOrderedTitles,
|
|
19364
19965
|
accountStatus,
|
|
19365
19966
|
bulkEnrichStatus,
|
|
@@ -19392,7 +19993,13 @@ var compositeReadTools = [
|
|
|
19392
19993
|
// didn't deliver"). Does not mutate Leadbay state; emits a PostHog
|
|
19393
19994
|
// event only. Companion to leadbay_report_outreach (which DOES write
|
|
19394
19995
|
// to the backend and stays gated behind LEADBAY_MCP_WRITE).
|
|
19395
|
-
reportFriction
|
|
19996
|
+
reportFriction,
|
|
19997
|
+
// Notification ack — ALWAYS exposed even though it POSTs to /seen.
|
|
19998
|
+
// _meta.notifications surfaces terminal bulk-progress notifications on
|
|
19999
|
+
// every tool response regardless of write gating; without ack the agent
|
|
20000
|
+
// sees the same entries on every call forever. Pairing the surfacing
|
|
20001
|
+
// channel with the clearing tool is non-optional.
|
|
20002
|
+
acknowledgeNotification
|
|
19396
20003
|
];
|
|
19397
20004
|
var compositeWriteTools = [
|
|
19398
20005
|
bulkQualifyLeads,
|
|
@@ -20211,6 +20818,22 @@ function buildServer(client, opts = {}) {
|
|
|
20211
20818
|
});
|
|
20212
20819
|
}
|
|
20213
20820
|
};
|
|
20821
|
+
const maybeAttachNotifications = (result) => {
|
|
20822
|
+
const inbox = opts.notificationsInbox;
|
|
20823
|
+
if (!inbox) return;
|
|
20824
|
+
if (result === null || typeof result !== "object" || Array.isArray(result)) {
|
|
20825
|
+
return;
|
|
20826
|
+
}
|
|
20827
|
+
const entries = inbox.list();
|
|
20828
|
+
if (entries.length === 0) return;
|
|
20829
|
+
const envelope = result;
|
|
20830
|
+
const target = envelope.__markdown_envelope === true && envelope.structured !== null && typeof envelope.structured === "object" && !Array.isArray(envelope.structured) ? envelope.structured : envelope;
|
|
20831
|
+
const existingMeta = target._meta && typeof target._meta === "object" && !Array.isArray(target._meta) ? target._meta : {};
|
|
20832
|
+
target._meta = {
|
|
20833
|
+
...existingMeta,
|
|
20834
|
+
notifications: entries
|
|
20835
|
+
};
|
|
20836
|
+
};
|
|
20214
20837
|
const isLeadbayBusinessError = (err) => err != null && typeof err === "object" && err.error === true && typeof err.code === "string";
|
|
20215
20838
|
const buildBusinessCtx = (toolName, envelope, triggered_by) => {
|
|
20216
20839
|
const meta = envelope._meta ?? {};
|
|
@@ -20330,11 +20953,13 @@ function buildServer(client, opts = {}) {
|
|
|
20330
20953
|
const result = await tool.execute(client, args, {
|
|
20331
20954
|
logger: opts.logger,
|
|
20332
20955
|
bulkTracker: opts.bulkTracker,
|
|
20956
|
+
notificationsInbox: opts.notificationsInbox,
|
|
20333
20957
|
signal: extra.signal,
|
|
20334
20958
|
progress,
|
|
20335
20959
|
elicit
|
|
20336
20960
|
});
|
|
20337
20961
|
maybeAttachUpdate(name, result);
|
|
20962
|
+
maybeAttachNotifications(result);
|
|
20338
20963
|
if (result && typeof result === "object" && result.error === true) {
|
|
20339
20964
|
const envText = formatErrorForLLM(result);
|
|
20340
20965
|
const envDur = Date.now() - callStart;
|
|
@@ -20629,7 +21254,7 @@ function parseWriteEnv(env = process.env) {
|
|
|
20629
21254
|
}
|
|
20630
21255
|
|
|
20631
21256
|
// src/http-server.ts
|
|
20632
|
-
var VERSION = true ? "0.
|
|
21257
|
+
var VERSION = true ? "0.18.1" : "0.0.0-dev";
|
|
20633
21258
|
var PORT = Number(process.env.PORT ?? 8080);
|
|
20634
21259
|
var HOST = process.env.HOST ?? "0.0.0.0";
|
|
20635
21260
|
var sseSessions = /* @__PURE__ */ new Map();
|