@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/bin.js
CHANGED
|
@@ -553,6 +553,23 @@ var init_client = __esm({
|
|
|
553
553
|
await this.resolveOrgId();
|
|
554
554
|
await this.resolveTasteProfile();
|
|
555
555
|
}
|
|
556
|
+
// ─── Notifications helpers ────────────────────────────────────────────
|
|
557
|
+
// Backend exposes `GET /notifications`, `POST /notifications/{id}/seen`,
|
|
558
|
+
// `POST /notifications/{id}/archive`, plus `GET /ws/ticket?v=1.0` to mint
|
|
559
|
+
// a one-shot WS URL. See backend/docs/adr/notifications.md for shape.
|
|
560
|
+
async listNotifications(args = {}) {
|
|
561
|
+
const params = new URLSearchParams();
|
|
562
|
+
params.set("archived", String(args.archived ?? false));
|
|
563
|
+
params.set("page", String(args.page ?? 0));
|
|
564
|
+
params.set("count", String(args.count ?? 50));
|
|
565
|
+
return this.request("GET", `/notifications?${params.toString()}`);
|
|
566
|
+
}
|
|
567
|
+
async acknowledgeNotification(notificationId, action = "seen") {
|
|
568
|
+
await this.requestVoid("POST", `/notifications/${notificationId}/${action}`);
|
|
569
|
+
}
|
|
570
|
+
async getWsTicket() {
|
|
571
|
+
return this.request("GET", "/auth/ws?v=1.0");
|
|
572
|
+
}
|
|
556
573
|
makeError(code, message, hint, endpoint, retry_after, http_status) {
|
|
557
574
|
const out = { error: true, code, message, hint };
|
|
558
575
|
if (endpoint || this._region) {
|
|
@@ -5086,6 +5103,7 @@ var init_composite_file_names = __esm({
|
|
|
5086
5103
|
"../core/dist/composite/_composite-file-names.js"() {
|
|
5087
5104
|
"use strict";
|
|
5088
5105
|
COMPOSITE_FILE_TOOL_NAMES = /* @__PURE__ */ new Set([
|
|
5106
|
+
"leadbay_account_history",
|
|
5089
5107
|
"leadbay_account_status",
|
|
5090
5108
|
"leadbay_add_leads_to_campaign",
|
|
5091
5109
|
"leadbay_adjust_audience",
|
|
@@ -5122,11 +5140,455 @@ var init_composite_file_names = __esm({
|
|
|
5122
5140
|
}
|
|
5123
5141
|
});
|
|
5124
5142
|
|
|
5143
|
+
// ../core/dist/notifications/revise-hint.js
|
|
5144
|
+
function reviseHintFor(kind) {
|
|
5145
|
+
switch (kind) {
|
|
5146
|
+
case "bulk_enrich":
|
|
5147
|
+
return HINT_BULK_ENRICH;
|
|
5148
|
+
case "bulk_qualify":
|
|
5149
|
+
return HINT_BULK_QUALIFY;
|
|
5150
|
+
case "import":
|
|
5151
|
+
return HINT_IMPORT;
|
|
5152
|
+
default:
|
|
5153
|
+
return HINT_OTHER;
|
|
5154
|
+
}
|
|
5155
|
+
}
|
|
5156
|
+
function inferKind(n) {
|
|
5157
|
+
if (n.links.some((l) => l.type === "bulk_enrichment"))
|
|
5158
|
+
return "bulk_enrich";
|
|
5159
|
+
if (n.file_import_id)
|
|
5160
|
+
return "import";
|
|
5161
|
+
if (n.bulk_progress)
|
|
5162
|
+
return "bulk_qualify";
|
|
5163
|
+
return "other";
|
|
5164
|
+
}
|
|
5165
|
+
function anchorIdFor(n, kind) {
|
|
5166
|
+
if (kind === "bulk_enrich") {
|
|
5167
|
+
const link = n.links.find((l) => l.type === "bulk_enrichment");
|
|
5168
|
+
return link ? String(link.id) : null;
|
|
5169
|
+
}
|
|
5170
|
+
if (kind === "import")
|
|
5171
|
+
return n.file_import_id;
|
|
5172
|
+
return null;
|
|
5173
|
+
}
|
|
5174
|
+
function toInboxEntry(n) {
|
|
5175
|
+
const kind = inferKind(n);
|
|
5176
|
+
return {
|
|
5177
|
+
notification_id: n.id,
|
|
5178
|
+
kind,
|
|
5179
|
+
anchor_id: anchorIdFor(n, kind),
|
|
5180
|
+
title: n.title,
|
|
5181
|
+
bulk_progress: n.bulk_progress,
|
|
5182
|
+
completed_at: n.updated_at,
|
|
5183
|
+
revise_hint: reviseHintFor(kind)
|
|
5184
|
+
};
|
|
5185
|
+
}
|
|
5186
|
+
var HINT_BULK_ENRICH, HINT_BULK_QUALIFY, HINT_IMPORT, HINT_OTHER;
|
|
5187
|
+
var init_revise_hint = __esm({
|
|
5188
|
+
"../core/dist/notifications/revise-hint.js"() {
|
|
5189
|
+
"use strict";
|
|
5190
|
+
HINT_BULK_ENRICH = "Contact enrichment just finished. Revise any prior output that named these leads' contacts (outreach drafts, contact lists, recommended-lead lists with contact_count, NEXT STEPS asking the user to wait for emails / phones). Re-fetch contacts via leadbay_get_contacts for the affected leads.";
|
|
5191
|
+
HINT_BULK_QUALIFY = "Lead qualification just finished. Revise any prior lead list / ranking / outreach shortlist that depended on ai_agent_lead_score for these leads \u2014 today's leads, top-of-inbox, followups maps, prepare-outreach shortlists. Re-pull qualification answers via leadbay_research_lead_by_id or re-rank via leadbay_pull_leads.";
|
|
5192
|
+
HINT_IMPORT = "CSV / CRM import just finished. Revise any prior output that referenced 'leads available' before the import landed \u2014 lead lists pulled from the affected lens, 'what's new today', followup planning. Re-pull the affected lens via leadbay_pull_leads / leadbay_pull_followups.";
|
|
5193
|
+
HINT_OTHER = "Background work just completed. If you referenced its subject in prior output, re-fetch the affected data and revise.";
|
|
5194
|
+
}
|
|
5195
|
+
});
|
|
5196
|
+
|
|
5197
|
+
// ../core/dist/notifications/inbox.js
|
|
5198
|
+
var DEFAULT_TTL_MS, NotificationsInbox;
|
|
5199
|
+
var init_inbox = __esm({
|
|
5200
|
+
"../core/dist/notifications/inbox.js"() {
|
|
5201
|
+
"use strict";
|
|
5202
|
+
init_revise_hint();
|
|
5203
|
+
DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5204
|
+
NotificationsInbox = class {
|
|
5205
|
+
entries = /* @__PURE__ */ new Map();
|
|
5206
|
+
ttl_ms;
|
|
5207
|
+
now;
|
|
5208
|
+
constructor(opts = {}) {
|
|
5209
|
+
this.ttl_ms = opts.ttl_ms ?? DEFAULT_TTL_MS;
|
|
5210
|
+
this.now = opts.now ?? Date.now;
|
|
5211
|
+
}
|
|
5212
|
+
// Upsert by notification id. Latest write wins so duplicate arrivals
|
|
5213
|
+
// (WS event + REST catch-up landing the same row) collapse cleanly.
|
|
5214
|
+
record(n) {
|
|
5215
|
+
if (!n.bulk_progress)
|
|
5216
|
+
return;
|
|
5217
|
+
if (n.in_progress)
|
|
5218
|
+
return;
|
|
5219
|
+
const entry = toInboxEntry(n);
|
|
5220
|
+
this.entries.set(entry.notification_id, {
|
|
5221
|
+
entry,
|
|
5222
|
+
recordedAt: this.now()
|
|
5223
|
+
});
|
|
5224
|
+
}
|
|
5225
|
+
list() {
|
|
5226
|
+
this.expireStale();
|
|
5227
|
+
return [...this.entries.values()].map((e) => e.entry);
|
|
5228
|
+
}
|
|
5229
|
+
markSeen(notification_id) {
|
|
5230
|
+
this.entries.delete(notification_id);
|
|
5231
|
+
}
|
|
5232
|
+
size() {
|
|
5233
|
+
this.expireStale();
|
|
5234
|
+
return this.entries.size;
|
|
5235
|
+
}
|
|
5236
|
+
expireStale() {
|
|
5237
|
+
const cutoff = this.now() - this.ttl_ms;
|
|
5238
|
+
for (const [id, e] of this.entries) {
|
|
5239
|
+
if (e.recordedAt < cutoff)
|
|
5240
|
+
this.entries.delete(id);
|
|
5241
|
+
}
|
|
5242
|
+
}
|
|
5243
|
+
};
|
|
5244
|
+
}
|
|
5245
|
+
});
|
|
5246
|
+
|
|
5247
|
+
// ../core/dist/notifications/catch-up.js
|
|
5248
|
+
async function catchUpNotifications(client, inbox, opts = {}) {
|
|
5249
|
+
const count = opts.count ?? DEFAULT_COUNT;
|
|
5250
|
+
let added = 0;
|
|
5251
|
+
try {
|
|
5252
|
+
const page = await client.listNotifications({
|
|
5253
|
+
archived: false,
|
|
5254
|
+
page: 0,
|
|
5255
|
+
count
|
|
5256
|
+
});
|
|
5257
|
+
for (const n of page.items) {
|
|
5258
|
+
if (!n.bulk_progress)
|
|
5259
|
+
continue;
|
|
5260
|
+
if (n.in_progress)
|
|
5261
|
+
continue;
|
|
5262
|
+
if (n.first_seen_at)
|
|
5263
|
+
continue;
|
|
5264
|
+
const sizeBefore = inbox.size();
|
|
5265
|
+
inbox.record(n);
|
|
5266
|
+
if (inbox.size() > sizeBefore)
|
|
5267
|
+
added += 1;
|
|
5268
|
+
}
|
|
5269
|
+
opts.logger?.info?.(`notifications.catch_up scanned=${page.items.length} seeded=${added}`);
|
|
5270
|
+
} catch (err) {
|
|
5271
|
+
opts.logger?.warn?.(`notifications.catch_up failed: ${err?.message ?? err?.code ?? err}`);
|
|
5272
|
+
}
|
|
5273
|
+
return added;
|
|
5274
|
+
}
|
|
5275
|
+
var DEFAULT_COUNT;
|
|
5276
|
+
var init_catch_up = __esm({
|
|
5277
|
+
"../core/dist/notifications/catch-up.js"() {
|
|
5278
|
+
"use strict";
|
|
5279
|
+
DEFAULT_COUNT = 50;
|
|
5280
|
+
}
|
|
5281
|
+
});
|
|
5282
|
+
|
|
5283
|
+
// ../core/dist/notifications/ws-client.js
|
|
5284
|
+
var PING_INTERVAL_MS, RECONNECT_INITIAL_MS, RECONNECT_MAX_MS, NotificationsWsClient;
|
|
5285
|
+
var init_ws_client = __esm({
|
|
5286
|
+
"../core/dist/notifications/ws-client.js"() {
|
|
5287
|
+
"use strict";
|
|
5288
|
+
init_catch_up();
|
|
5289
|
+
PING_INTERVAL_MS = 3e4;
|
|
5290
|
+
RECONNECT_INITIAL_MS = 1e3;
|
|
5291
|
+
RECONNECT_MAX_MS = 3e4;
|
|
5292
|
+
NotificationsWsClient = class {
|
|
5293
|
+
client;
|
|
5294
|
+
inbox;
|
|
5295
|
+
logger;
|
|
5296
|
+
ws = null;
|
|
5297
|
+
pingTimer = null;
|
|
5298
|
+
reconnectTimer = null;
|
|
5299
|
+
reconnectDelay = RECONNECT_INITIAL_MS;
|
|
5300
|
+
stopped = false;
|
|
5301
|
+
constructor(opts) {
|
|
5302
|
+
this.client = opts.client;
|
|
5303
|
+
this.inbox = opts.inbox;
|
|
5304
|
+
this.logger = opts.logger;
|
|
5305
|
+
}
|
|
5306
|
+
async start() {
|
|
5307
|
+
this.stopped = false;
|
|
5308
|
+
await catchUpNotifications(this.client, this.inbox, { logger: this.logger });
|
|
5309
|
+
void this.connect();
|
|
5310
|
+
}
|
|
5311
|
+
stop() {
|
|
5312
|
+
this.stopped = true;
|
|
5313
|
+
if (this.pingTimer)
|
|
5314
|
+
clearInterval(this.pingTimer);
|
|
5315
|
+
if (this.reconnectTimer)
|
|
5316
|
+
clearTimeout(this.reconnectTimer);
|
|
5317
|
+
this.pingTimer = null;
|
|
5318
|
+
this.reconnectTimer = null;
|
|
5319
|
+
if (this.ws) {
|
|
5320
|
+
try {
|
|
5321
|
+
this.ws.close(1e3, "shutdown");
|
|
5322
|
+
} catch {
|
|
5323
|
+
}
|
|
5324
|
+
this.ws = null;
|
|
5325
|
+
}
|
|
5326
|
+
}
|
|
5327
|
+
async connect() {
|
|
5328
|
+
if (this.stopped)
|
|
5329
|
+
return;
|
|
5330
|
+
let url;
|
|
5331
|
+
try {
|
|
5332
|
+
const ticket = await this.client.getWsTicket();
|
|
5333
|
+
url = ticket.url;
|
|
5334
|
+
} catch (err) {
|
|
5335
|
+
this.logger?.warn?.(`notifications.ws ticket_fetch_failed: ${err?.message ?? err?.code ?? err}`);
|
|
5336
|
+
this.scheduleReconnect();
|
|
5337
|
+
return;
|
|
5338
|
+
}
|
|
5339
|
+
let ws;
|
|
5340
|
+
try {
|
|
5341
|
+
ws = new WebSocket(url);
|
|
5342
|
+
} catch (err) {
|
|
5343
|
+
this.logger?.warn?.(`notifications.ws construct_failed: ${err?.message ?? err}`);
|
|
5344
|
+
this.scheduleReconnect();
|
|
5345
|
+
return;
|
|
5346
|
+
}
|
|
5347
|
+
this.ws = ws;
|
|
5348
|
+
ws.addEventListener("open", () => {
|
|
5349
|
+
this.logger?.info?.("notifications.ws connected");
|
|
5350
|
+
this.reconnectDelay = RECONNECT_INITIAL_MS;
|
|
5351
|
+
void catchUpNotifications(this.client, this.inbox, {
|
|
5352
|
+
logger: this.logger
|
|
5353
|
+
});
|
|
5354
|
+
this.pingTimer = setInterval(() => this.sendPing(), PING_INTERVAL_MS);
|
|
5355
|
+
});
|
|
5356
|
+
ws.addEventListener("message", (ev) => {
|
|
5357
|
+
const text = typeof ev.data === "string" ? ev.data : String(ev.data ?? "");
|
|
5358
|
+
if (!text)
|
|
5359
|
+
return;
|
|
5360
|
+
let msg;
|
|
5361
|
+
try {
|
|
5362
|
+
msg = JSON.parse(text);
|
|
5363
|
+
} catch {
|
|
5364
|
+
this.logger?.warn?.("notifications.ws non_json_frame");
|
|
5365
|
+
return;
|
|
5366
|
+
}
|
|
5367
|
+
this.handleMessage(msg);
|
|
5368
|
+
});
|
|
5369
|
+
ws.addEventListener("error", (ev) => {
|
|
5370
|
+
this.logger?.warn?.(`notifications.ws error: ${ev?.message ?? "(no detail)"}`);
|
|
5371
|
+
});
|
|
5372
|
+
ws.addEventListener("close", (ev) => {
|
|
5373
|
+
this.logger?.info?.(`notifications.ws closed code=${ev.code} reason=${ev.reason || "(none)"}`);
|
|
5374
|
+
if (this.pingTimer)
|
|
5375
|
+
clearInterval(this.pingTimer);
|
|
5376
|
+
this.pingTimer = null;
|
|
5377
|
+
this.ws = null;
|
|
5378
|
+
this.scheduleReconnect();
|
|
5379
|
+
});
|
|
5380
|
+
}
|
|
5381
|
+
handleMessage(msg) {
|
|
5382
|
+
if (msg.type === "pong")
|
|
5383
|
+
return;
|
|
5384
|
+
if (msg.type === "ping") {
|
|
5385
|
+
this.sendRaw({ type: "pong" });
|
|
5386
|
+
return;
|
|
5387
|
+
}
|
|
5388
|
+
if (msg.type !== "notification")
|
|
5389
|
+
return;
|
|
5390
|
+
const { type: _t, ...rest } = msg;
|
|
5391
|
+
void _t;
|
|
5392
|
+
const n = rest;
|
|
5393
|
+
if (n.bulk_progress == null || n.in_progress) {
|
|
5394
|
+
return;
|
|
5395
|
+
}
|
|
5396
|
+
this.inbox.record(n);
|
|
5397
|
+
this.logger?.info?.(`notifications.ws terminal id=${n.id} kind=${n.file_import_id ? "import" : n.links.some((l) => l.type === "bulk_enrichment") ? "bulk_enrich" : "bulk_qualify"}`);
|
|
5398
|
+
}
|
|
5399
|
+
sendPing() {
|
|
5400
|
+
this.sendRaw({ type: "ping" });
|
|
5401
|
+
}
|
|
5402
|
+
sendRaw(obj) {
|
|
5403
|
+
if (!this.ws)
|
|
5404
|
+
return;
|
|
5405
|
+
if (this.ws.readyState !== WebSocket.OPEN)
|
|
5406
|
+
return;
|
|
5407
|
+
try {
|
|
5408
|
+
this.ws.send(JSON.stringify(obj));
|
|
5409
|
+
} catch (err) {
|
|
5410
|
+
this.logger?.warn?.(`notifications.ws send_failed: ${err?.message ?? err}`);
|
|
5411
|
+
}
|
|
5412
|
+
}
|
|
5413
|
+
scheduleReconnect() {
|
|
5414
|
+
if (this.stopped)
|
|
5415
|
+
return;
|
|
5416
|
+
if (this.reconnectTimer)
|
|
5417
|
+
return;
|
|
5418
|
+
const delay = this.reconnectDelay;
|
|
5419
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, RECONNECT_MAX_MS);
|
|
5420
|
+
this.reconnectTimer = setTimeout(() => {
|
|
5421
|
+
this.reconnectTimer = null;
|
|
5422
|
+
void this.connect();
|
|
5423
|
+
}, delay);
|
|
5424
|
+
this.logger?.info?.(`notifications.ws reconnect_in_${delay}ms`);
|
|
5425
|
+
}
|
|
5426
|
+
};
|
|
5427
|
+
}
|
|
5428
|
+
});
|
|
5429
|
+
|
|
5430
|
+
// ../core/dist/notifications/index.js
|
|
5431
|
+
var init_notifications = __esm({
|
|
5432
|
+
"../core/dist/notifications/index.js"() {
|
|
5433
|
+
"use strict";
|
|
5434
|
+
init_inbox();
|
|
5435
|
+
init_ws_client();
|
|
5436
|
+
init_catch_up();
|
|
5437
|
+
init_revise_hint();
|
|
5438
|
+
}
|
|
5439
|
+
});
|
|
5440
|
+
|
|
5125
5441
|
// ../core/dist/tool-descriptions.generated.js
|
|
5126
|
-
var leadbay_account_status, leadbay_add_leads_to_campaign, leadbay_add_note, leadbay_adjust_audience, leadbay_agent_memory_capture, leadbay_agent_memory_recall, leadbay_agent_memory_review, leadbay_answer_clarification, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_seed_candidates, leadbay_select_leads, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_user_prompt, leadbay_tour_plan, leadbay_update_lens, leadbay_update_lens_filter;
|
|
5442
|
+
var leadbay_account_history, leadbay_account_status, leadbay_acknowledge_notification, leadbay_add_leads_to_campaign, leadbay_add_note, leadbay_adjust_audience, leadbay_agent_memory_capture, leadbay_agent_memory_recall, leadbay_agent_memory_review, leadbay_answer_clarification, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_seed_candidates, leadbay_select_leads, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_user_prompt, leadbay_tour_plan, leadbay_update_lens, leadbay_update_lens_filter;
|
|
5127
5443
|
var init_tool_descriptions_generated = __esm({
|
|
5128
5444
|
"../core/dist/tool-descriptions.generated.js"() {
|
|
5129
5445
|
"use strict";
|
|
5446
|
+
leadbay_account_history = `## WHEN TO USE
|
|
5447
|
+
|
|
5448
|
+
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>".
|
|
5449
|
+
|
|
5450
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
5451
|
+
|
|
5452
|
+
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\`.
|
|
5453
|
+
|
|
5454
|
+
Prefer when: user wants ONE account's full back-story \u2014 notes + past activity + current signals together; pass \`leadId\`
|
|
5455
|
+
|
|
5456
|
+
Examples that SHOULD invoke this tool:
|
|
5457
|
+
- "What's the full history on this account \u2014 why did it resurface?"
|
|
5458
|
+
- "Summarize everything we've logged on that lead and whether it's worth another visit."
|
|
5459
|
+
|
|
5460
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
5461
|
+
- "Tell me the current AI take on this lead."
|
|
5462
|
+
- "Which accounts should I follow up with this week?"
|
|
5463
|
+
|
|
5464
|
+
## RENDER (quick)
|
|
5465
|
+
|
|
5466
|
+
Single resurfaced-account card. Lead the card with the current signal/trigger
|
|
5467
|
+
line (from \`signals\`), then a "History" section: notes digest
|
|
5468
|
+
(chronological) + the activity timeline. Close with a one-line "why revisit
|
|
5469
|
+
now" synthesis and a suggested outreach angle tied to the freshest signal.
|
|
5470
|
+
|
|
5471
|
+
---
|
|
5472
|
+
|
|
5473
|
+
Give me one account's full back-story in a single call. This is the tool for
|
|
5474
|
+
the **reprioritize-a-neglected-account** workflow: the user has an account
|
|
5475
|
+
that was contacted or quoted long ago and wants to know, in one shot, whether
|
|
5476
|
+
a fresh signal makes it worth another visit.
|
|
5477
|
+
|
|
5478
|
+
It bundles three reads on one \`leadId\`:
|
|
5479
|
+
|
|
5480
|
+
1. **Current state** \u2014 passed through verbatim from \`leadbay_research_lead_by_id\`:
|
|
5481
|
+
\`signals\` (web-research signals with hot flags + sources), \`firmographics\`,
|
|
5482
|
+
\`qualification\` answers, \`contacts\`, and \`engagement\` counts. This is the
|
|
5483
|
+
"why is this account hot NOW" layer.
|
|
5484
|
+
2. **\`notes\`** \u2014 the FULL list of notes logged on the record (note body +
|
|
5485
|
+
\`created_at\`), chronological. \`leadbay_research_lead_by_id\` only returns a
|
|
5486
|
+
\`notes_count\`; this tool returns the bodies so you can summarize the
|
|
5487
|
+
historical context.
|
|
5488
|
+
3. **\`activities\`** \u2014 the interaction timeline (\`{type, date}\` entries, newest
|
|
5489
|
+
first) plus the total count. Drives the "no contact in N months" judgement.
|
|
5490
|
+
|
|
5491
|
+
\`notes\` and \`activities\` **degrade gracefully**: if either read fails the card
|
|
5492
|
+
still returns with that section empty (\`notes: []\` /
|
|
5493
|
+
\`activities.total: 0\`). The current-state block is load-bearing \u2014 if research
|
|
5494
|
+
itself errors the whole call fails, because there is nothing to narrate.
|
|
5495
|
+
|
|
5496
|
+
Params: \`leadId\` (required UUID) and \`activityCount\` (optional, default 50,
|
|
5497
|
+
max 100).
|
|
5498
|
+
|
|
5499
|
+
Companion tools: **leadbay_research_lead_by_id** when the user only wants the
|
|
5500
|
+
live AI take with no history; **leadbay_pull_followups** when the user wants a
|
|
5501
|
+
LIST of accounts to act on rather than one account's deep history.
|
|
5502
|
+
|
|
5503
|
+
## RENDERING \u2014 single-record research card, mode-adaptive
|
|
5504
|
+
|
|
5505
|
+
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.
|
|
5506
|
+
|
|
5507
|
+
**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.
|
|
5508
|
+
|
|
5509
|
+
**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\`.
|
|
5510
|
+
|
|
5511
|
+
Default to MODE A when uncertain. Always offer the cross-mode pivot at the end so the user can redirect if you guessed wrong.
|
|
5512
|
+
|
|
5513
|
+
### Common structure (both modes)
|
|
5514
|
+
|
|
5515
|
+
- **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.
|
|
5516
|
+
- **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.
|
|
5517
|
+
- **Blurb**: render \`description\` (preferred) or \`short_description\` as a single blockquoted paragraph.
|
|
5518
|
+
- **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.
|
|
5519
|
+
- **Contacts table** (always at the bottom):
|
|
5520
|
+
\`\`\`
|
|
5521
|
+
| | Name | Title | LinkedIn |
|
|
5522
|
+
\`\`\`
|
|
5523
|
+
Markers in column 1:
|
|
5524
|
+
- \`\u2605\` \u2014 \`recommended_contact\` match.
|
|
5525
|
+
- \`\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.)
|
|
5526
|
+
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"\`.
|
|
5527
|
+
|
|
5528
|
+
### MODE A body (Discovery, fuller, scannable)
|
|
5529
|
+
|
|
5530
|
+
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).
|
|
5531
|
+
|
|
5532
|
+
### MODE B body (Contact preparation, tighter)
|
|
5533
|
+
|
|
5534
|
+
Render exactly two H5 sections:
|
|
5535
|
+
|
|
5536
|
+
##### \u{1F3AF} Conversation hooks
|
|
5537
|
+
|
|
5538
|
+
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.
|
|
5539
|
+
|
|
5540
|
+
##### \u{1F464} About the person *(only when recommended_contact is non-empty)*
|
|
5541
|
+
|
|
5542
|
+
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.
|
|
5543
|
+
|
|
5544
|
+
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.
|
|
5545
|
+
|
|
5546
|
+
If \`qualification[]\` is non-empty, append one collapsed line: \`"Qualification: N questions answered, avg boost X"\` and offer to expand in NEXT STEPS.
|
|
5547
|
+
|
|
5548
|
+
**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.
|
|
5549
|
+
|
|
5550
|
+
**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) \`\`
|
|
5551
|
+
|
|
5552
|
+
## Linking a contact's name
|
|
5553
|
+
|
|
5554
|
+
**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.**
|
|
5555
|
+
|
|
5556
|
+
URL priority (first applicable wins):
|
|
5557
|
+
|
|
5558
|
+
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).
|
|
5559
|
+
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.
|
|
5560
|
+
|
|
5561
|
+
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.
|
|
5562
|
+
|
|
5563
|
+
## Linking the company
|
|
5564
|
+
|
|
5565
|
+
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.)
|
|
5566
|
+
|
|
5567
|
+
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.
|
|
5568
|
+
|
|
5569
|
+
\`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).
|
|
5570
|
+
|
|
5571
|
+
|
|
5572
|
+
|
|
5573
|
+
### RENDERING \u2014 the history layer (on top of the card above)
|
|
5574
|
+
|
|
5575
|
+
After the research card, add a **History** section so the user sees why this
|
|
5576
|
+
account resurfaced:
|
|
5577
|
+
|
|
5578
|
+
- **##### \u{1F5D2} Notes** \u2014 render \`notes\` chronologically (oldest \u2192 newest). Each
|
|
5579
|
+
as a bullet: \`**<relative date from created_at>** \u2014 <note body>\`. Cap at 8;
|
|
5580
|
+
if \`_meta.notes_count > shown\`, end with \`"+N more notes"\`. Omit the section
|
|
5581
|
+
entirely when \`notes\` is empty.
|
|
5582
|
+
- **##### \u{1F553} Timeline** \u2014 render \`activities.activities\` newest-first as a
|
|
5583
|
+
compact bullet list: \`<relative date> \xB7 <type>\`. Cap at 10; if
|
|
5584
|
+
\`activities.total > shown\`, end with \`"+N earlier"\`. Omit when empty.
|
|
5585
|
+
- **##### \u21BB Why revisit now** \u2014 one or two sentences synthesizing the freshest
|
|
5586
|
+
HOT signal from \`signals\` against the gap in \`activities\` (e.g. "Won a public
|
|
5587
|
+
tender last month; no logged contact since the 2024 quote \u2014 strong re-open
|
|
5588
|
+
angle"). Then one suggested outreach angle tied to that signal. This
|
|
5589
|
+
synthesis is the payload of the whole tool \u2014 always include it when there is
|
|
5590
|
+
at least one hot signal.
|
|
5591
|
+
`;
|
|
5130
5592
|
leadbay_account_status = `## WHEN TO USE
|
|
5131
5593
|
|
|
5132
5594
|
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".
|
|
@@ -5147,10 +5609,13 @@ Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
|
5147
5609
|
|
|
5148
5610
|
## RENDER (quick)
|
|
5149
5611
|
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
5153
|
-
|
|
5612
|
+
If \`quota_error\` is set the call FAILED \u2014 quota unreadable; on 401/403 tell
|
|
5613
|
+
the user to reconnect. NEVER report zero usage or "no limits". Else render
|
|
5614
|
+
\`quota.org.resources\` (usage lives there, NOT at quota.resources) as a
|
|
5615
|
+
table, never prose: rows = resources (llm_completion \xB7 ai_rescore \xB7
|
|
5616
|
+
web_fetch + others), cols = Daily/Weekly/Monthly used \`count\` (= amount
|
|
5617
|
+
USED; no cap field, \`plan\` may be null \u2014 never invent a denominator).
|
|
5618
|
+
Empty = a 0 table, not "unlimited". Above: org + admin, lens.
|
|
5154
5619
|
|
|
5155
5620
|
---
|
|
5156
5621
|
|
|
@@ -5162,9 +5627,52 @@ Show the user's account state \u2014 admin rights, language, last-active lens, c
|
|
|
5162
5627
|
|
|
5163
5628
|
**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.**
|
|
5164
5629
|
|
|
5630
|
+
**\`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.
|
|
5631
|
+
|
|
5632
|
+
## GATE \u2014 INSPECT \`_meta.notifications\` ON EVERY RESPONSE
|
|
5633
|
+
|
|
5634
|
+
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:
|
|
5635
|
+
|
|
5636
|
+
| Kind | What just finished |
|
|
5637
|
+
|---|---|
|
|
5638
|
+
| \`bulk_enrich\` | A bulk contact-enrichment job (emails / phone numbers attached to leads' contacts). |
|
|
5639
|
+
| \`bulk_qualify\` | A bulk lead-qualification job (web-fetch + AI rescore, producing \`ai_agent_lead_score\` and qualification answers). |
|
|
5640
|
+
| \`import\` | A CSV / CRM file import (rows resolved to leads in the user's pipeline). |
|
|
5641
|
+
|
|
5642
|
+
**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.
|
|
5643
|
+
|
|
5644
|
+
| Kind | Outputs you've produced that may now be stale \u2014 refresh them |
|
|
5645
|
+
|---|---|
|
|
5646
|
+
| \`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. |
|
|
5647
|
+
| \`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\`. |
|
|
5648
|
+
| \`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\`. |
|
|
5649
|
+
|
|
5650
|
+
**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.
|
|
5651
|
+
|
|
5652
|
+
**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.
|
|
5653
|
+
|
|
5654
|
+
**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.
|
|
5655
|
+
|
|
5656
|
+
Also surfaced as a top-level \`notifications\` array on \`leadbay_account_status\` \u2014 same shape, same handling.
|
|
5657
|
+
|
|
5658
|
+
|
|
5165
5659
|
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.
|
|
5166
5660
|
|
|
5167
5661
|
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.
|
|
5662
|
+
`;
|
|
5663
|
+
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.
|
|
5664
|
+
|
|
5665
|
+
**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.
|
|
5666
|
+
|
|
5667
|
+
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.
|
|
5668
|
+
|
|
5669
|
+
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.
|
|
5670
|
+
|
|
5671
|
+
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.
|
|
5672
|
+
|
|
5673
|
+
WHEN NOT TO USE: before doing the revision work; for general "mark all read" sweeps (call once per notification id you've actually consumed).
|
|
5674
|
+
|
|
5675
|
+
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\`.
|
|
5168
5676
|
`;
|
|
5169
5677
|
leadbay_add_leads_to_campaign = `## WHEN TO USE
|
|
5170
5678
|
|
|
@@ -5294,6 +5802,10 @@ This tool MUTATES state. The caller (agent or human-in-the-loop) is responsible
|
|
|
5294
5802
|
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.
|
|
5295
5803
|
|
|
5296
5804
|
WHEN NOT TO USE: as a substitute for leadbay_research_lead_by_id \u2014 that already includes enriched contacts for a single lead.
|
|
5805
|
+
|
|
5806
|
+
## CREDIT COST \u2014 show the balance, discreetly
|
|
5807
|
+
|
|
5808
|
+
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.
|
|
5297
5809
|
`;
|
|
5298
5810
|
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.
|
|
5299
5811
|
|
|
@@ -5758,6 +6270,10 @@ WHEN TO USE: when you have a specific \`contact_id\` (from leadbay_get_contacts)
|
|
|
5758
6270
|
|
|
5759
6271
|
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.
|
|
5760
6272
|
|
|
6273
|
+
## CREDIT COST \u2014 discreet
|
|
6274
|
+
|
|
6275
|
+
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.
|
|
6276
|
+
|
|
5761
6277
|
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\`.
|
|
5762
6278
|
`;
|
|
5763
6279
|
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.
|
|
@@ -5766,6 +6282,35 @@ WHEN TO USE: as the agent's go-to enrichment entry point, immediately before pro
|
|
|
5766
6282
|
|
|
5767
6283
|
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.
|
|
5768
6284
|
|
|
6285
|
+
## CREDIT COST \u2014 make spend visible
|
|
6286
|
+
|
|
6287
|
+
Enrichment is the main PAID operation. Surface cost both before and after.
|
|
6288
|
+
|
|
6289
|
+
**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.
|
|
6290
|
+
|
|
6291
|
+
**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.
|
|
6292
|
+
|
|
6293
|
+
## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
|
|
6294
|
+
|
|
6295
|
+
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.
|
|
6296
|
+
|
|
6297
|
+
**The Big Three** \u2014 when a tool result fits, route there:
|
|
6298
|
+
|
|
6299
|
+
| Host widget | Use when | Field map (from Leadbay payload) |
|
|
6300
|
+
|---|---|---|
|
|
6301
|
+
| \`places_map_display_v0\` (Claude) | Result has \u22652 leads with \`location.city\` set, and the user's intent is geographic / "in person" / travel | \`{name: lead.company_name, address: "<city>, <country>", place_id: lead.location.place_id ?? omit, notes: <one-sentence pitch>}\` per location |
|
|
6302
|
+
| \`message_compose_v1\` (Claude) | You're about to draft outreach (email / message / call opener) | \`{kind: "email", summary_title, variants: [{label, body, subject}]}\` \u2014 2\u20133 variants, labels describe STRATEGY ("Push for alignment", "Reference the M&A signal"), not tone ("Friendly", "Formal") |
|
|
6303
|
+
| \`ask_user_input_v0\` (Claude) | The tool's NEXT STEPS block has 2\u20134 mutually-exclusive next moves and the user hasn't already chosen | \`{questions: [{question: "What next?", type: "single_select", options: [<2-4 short button labels>]}]}\`; max 3 questions per call |
|
|
6304
|
+
|
|
6305
|
+
ChatGPT exposes the same routing pattern via \`_meta.openai/outputTemplate\`. We don't ship any custom widgets ourselves \u2014 this gate is exclusively about routing into the host's first-party widgets when the data shape fits.
|
|
6306
|
+
|
|
6307
|
+
**Rules:**
|
|
6308
|
+
- 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.
|
|
6309
|
+
- Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
|
|
6310
|
+
- 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.
|
|
6311
|
+
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
6312
|
+
|
|
6313
|
+
|
|
5769
6314
|
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\`.
|
|
5770
6315
|
`;
|
|
5771
6316
|
leadbay_extend_lens = `## WHEN TO USE
|
|
@@ -8576,10 +9121,36 @@ var init_get_quota = __esm({
|
|
|
8576
9121
|
outputSchema: {
|
|
8577
9122
|
type: "object",
|
|
8578
9123
|
properties: {
|
|
8579
|
-
plan: {
|
|
9124
|
+
plan: {
|
|
9125
|
+
type: ["string", "null"],
|
|
9126
|
+
description: "Org plan tier (e.g., FREE, TIER1, TIER2). May be null."
|
|
9127
|
+
},
|
|
9128
|
+
org: {
|
|
9129
|
+
type: "object",
|
|
9130
|
+
description: "Org-level quota state.",
|
|
9131
|
+
properties: {
|
|
9132
|
+
spend: { type: "array", description: "Reserved; empty in practice.", items: { type: "object" } },
|
|
9133
|
+
resources: {
|
|
9134
|
+
type: "array",
|
|
9135
|
+
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.",
|
|
9136
|
+
items: { type: "object" }
|
|
9137
|
+
}
|
|
9138
|
+
}
|
|
9139
|
+
},
|
|
9140
|
+
user: {
|
|
9141
|
+
type: "object",
|
|
9142
|
+
description: "User-level quota state, same shape as `org`. May be absent.",
|
|
9143
|
+
properties: {
|
|
9144
|
+
spend: { type: "array", items: { type: "object" } },
|
|
9145
|
+
resources: { type: "array", items: { type: "object" } }
|
|
9146
|
+
}
|
|
9147
|
+
},
|
|
9148
|
+
// Legacy/compat: the live API does NOT return a top-level `windows`
|
|
9149
|
+
// array — usage lives in org/user.resources[]. Declared only so older
|
|
9150
|
+
// recorded fixtures still conform; do not rely on it.
|
|
8580
9151
|
windows: {
|
|
8581
9152
|
type: "array",
|
|
8582
|
-
description: "
|
|
9153
|
+
description: "Deprecated \u2014 not returned by the live API. Use org/user.resources[].",
|
|
8583
9154
|
items: { type: "object" }
|
|
8584
9155
|
}
|
|
8585
9156
|
}
|
|
@@ -10199,9 +10770,23 @@ async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseB
|
|
|
10199
10770
|
await pollPreprocess(client, importId, phaseBudget, ctx, signal);
|
|
10200
10771
|
ctx?.logger?.info?.(`import-leads: preprocess done for importId=${importId}`);
|
|
10201
10772
|
if (dryRun) {
|
|
10202
|
-
return { importId, records: [] };
|
|
10773
|
+
return { importId, records: [], notification_id: null };
|
|
10774
|
+
}
|
|
10775
|
+
let updateMappingsResp = null;
|
|
10776
|
+
try {
|
|
10777
|
+
updateMappingsResp = await client.request("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
10778
|
+
} catch (err) {
|
|
10779
|
+
if (err?.code === "API_ERROR" || err?.code === "NOT_FOUND") {
|
|
10780
|
+
ctx?.logger?.warn?.(`import-leads: update_mappings raw error (${err?.code}); retrying void`);
|
|
10781
|
+
await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
10782
|
+
} else {
|
|
10783
|
+
throw err;
|
|
10784
|
+
}
|
|
10785
|
+
}
|
|
10786
|
+
const importNotificationId = updateMappingsResp?.notification_id ?? null;
|
|
10787
|
+
if (importNotificationId) {
|
|
10788
|
+
ctx?.logger?.info?.(`import-leads: notification_id=${importNotificationId} importId=${importId}`);
|
|
10203
10789
|
}
|
|
10204
|
-
await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
10205
10790
|
ctx?.logger?.info?.(`import-leads: mappings committed for importId=${importId}`);
|
|
10206
10791
|
const phaseBudget2 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
10207
10792
|
await pollProcess(client, importId, phaseBudget2, ctx, signal);
|
|
@@ -10209,7 +10794,7 @@ async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseB
|
|
|
10209
10794
|
const phaseBudget3 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
10210
10795
|
const records = await pollRecordsToTerminal(client, importId, phaseBudget3, chunk.length, ctx, signal);
|
|
10211
10796
|
ctx?.logger?.info?.(`import-leads: ${records.length} records terminal for importId=${importId}`);
|
|
10212
|
-
return { importId, records };
|
|
10797
|
+
return { importId, records, notification_id: importNotificationId };
|
|
10213
10798
|
}
|
|
10214
10799
|
function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
10215
10800
|
const seenInputIndex = /* @__PURE__ */ new Set();
|
|
@@ -10265,7 +10850,7 @@ function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
|
10265
10850
|
}
|
|
10266
10851
|
}
|
|
10267
10852
|
}
|
|
10268
|
-
function buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled) {
|
|
10853
|
+
function buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds) {
|
|
10269
10854
|
const leads = [];
|
|
10270
10855
|
const not_imported = [];
|
|
10271
10856
|
if (dryRun) {
|
|
@@ -10330,6 +10915,7 @@ function buildImportLeadsResult(client, prep, importIds, matched, notImported, d
|
|
|
10330
10915
|
leads,
|
|
10331
10916
|
not_imported,
|
|
10332
10917
|
importIds,
|
|
10918
|
+
notification_ids: notificationIds,
|
|
10333
10919
|
region: client.region,
|
|
10334
10920
|
cancelled: cancelled || void 0,
|
|
10335
10921
|
dry_run: dryRun || void 0,
|
|
@@ -10355,17 +10941,21 @@ async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, ha
|
|
|
10355
10941
|
void (async () => {
|
|
10356
10942
|
const bgCtx = { logger: ctx.logger, bulkTracker: tracker };
|
|
10357
10943
|
const importIds = uploadedChunks.map((chunk) => chunk.importId);
|
|
10944
|
+
const notificationIds = [];
|
|
10358
10945
|
const matched = /* @__PURE__ */ new Map();
|
|
10359
10946
|
const notImported = /* @__PURE__ */ new Map();
|
|
10360
10947
|
try {
|
|
10361
10948
|
const totalDeadline = Date.now() + opts.totalBudget;
|
|
10362
10949
|
for (const upload of uploadedChunks) {
|
|
10363
10950
|
const out = await completeUploadedChunk(client, upload, prep.mappings, opts.dryRun, opts.perPhaseBudget, totalDeadline, bgCtx, void 0);
|
|
10951
|
+
if (out.notification_id && !notificationIds.includes(out.notification_id)) {
|
|
10952
|
+
notificationIds.push(out.notification_id);
|
|
10953
|
+
}
|
|
10364
10954
|
if (!opts.dryRun) {
|
|
10365
10955
|
reconcileOneChunk(prep, out, matched, notImported);
|
|
10366
10956
|
}
|
|
10367
10957
|
}
|
|
10368
|
-
const result = buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false);
|
|
10958
|
+
const result = buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false, notificationIds);
|
|
10369
10959
|
await tracker.markImportComplete(handleId, {
|
|
10370
10960
|
leads: result.leads,
|
|
10371
10961
|
not_imported: result.not_imported,
|
|
@@ -10598,6 +11188,7 @@ var init_import_leads = __esm({
|
|
|
10598
11188
|
leads: [],
|
|
10599
11189
|
not_imported,
|
|
10600
11190
|
importIds: [],
|
|
11191
|
+
notification_ids: [],
|
|
10601
11192
|
region: client.region,
|
|
10602
11193
|
dry_run: dryRun || void 0,
|
|
10603
11194
|
_meta: client.lastMeta ?? {
|
|
@@ -10652,6 +11243,10 @@ var init_import_leads = __esm({
|
|
|
10652
11243
|
status: "running",
|
|
10653
11244
|
handle_id: reservation.record.bulk_id,
|
|
10654
11245
|
importIds: importIds2,
|
|
11246
|
+
// Notifications fire from update_mappings, which the background
|
|
11247
|
+
// task hasn't called yet at this point. They surface via the WS
|
|
11248
|
+
// listener / catch-up REST on subsequent agent turns.
|
|
11249
|
+
notification_ids: [],
|
|
10655
11250
|
progress: {
|
|
10656
11251
|
phase: reservation.record.status === "complete" ? "complete" : importIds2.length > 0 ? "preprocess" : "queued",
|
|
10657
11252
|
records_processed: reservation.record.status === "complete" ? reservation.record.records_total : 0,
|
|
@@ -10672,6 +11267,7 @@ var init_import_leads = __esm({
|
|
|
10672
11267
|
}
|
|
10673
11268
|
ctx?.logger?.info?.(`import-leads(${prep.mode}): ${prep.validInputs.length} rows \u2192 ${chunks.length} chunk(s); dry_run=${dryRun}, totalBudgetMs=${totalBudget}`);
|
|
10674
11269
|
const importIds = [];
|
|
11270
|
+
const notificationIds = [];
|
|
10675
11271
|
const matched = /* @__PURE__ */ new Map();
|
|
10676
11272
|
const notImported = /* @__PURE__ */ new Map();
|
|
10677
11273
|
let cancelled = false;
|
|
@@ -10683,6 +11279,9 @@ var init_import_leads = __esm({
|
|
|
10683
11279
|
for (let i = 0; i < chunks.length; i++) {
|
|
10684
11280
|
const chunk = chunks[i];
|
|
10685
11281
|
const out = await runOneChunk(client, chunk, i, chunks.length, prep.header, prep.mappings, dryRun, perPhaseBudget, totalDeadline, ctx, signal, recordImportId);
|
|
11282
|
+
if (out.notification_id && !notificationIds.includes(out.notification_id)) {
|
|
11283
|
+
notificationIds.push(out.notification_id);
|
|
11284
|
+
}
|
|
10686
11285
|
if (!dryRun) {
|
|
10687
11286
|
reconcileOneChunk(prep, out, matched, notImported);
|
|
10688
11287
|
}
|
|
@@ -10703,7 +11302,7 @@ var init_import_leads = __esm({
|
|
|
10703
11302
|
throw err;
|
|
10704
11303
|
}
|
|
10705
11304
|
}
|
|
10706
|
-
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled);
|
|
11305
|
+
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds);
|
|
10707
11306
|
}
|
|
10708
11307
|
};
|
|
10709
11308
|
}
|
|
@@ -11411,6 +12010,70 @@ var init_agent_memory_review = __esm({
|
|
|
11411
12010
|
}
|
|
11412
12011
|
});
|
|
11413
12012
|
|
|
12013
|
+
// ../core/dist/tools/acknowledge-notification.js
|
|
12014
|
+
var UUID_RE, acknowledgeNotification;
|
|
12015
|
+
var init_acknowledge_notification = __esm({
|
|
12016
|
+
"../core/dist/tools/acknowledge-notification.js"() {
|
|
12017
|
+
"use strict";
|
|
12018
|
+
init_tool_descriptions_generated();
|
|
12019
|
+
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
12020
|
+
acknowledgeNotification = {
|
|
12021
|
+
name: "leadbay_acknowledge_notification",
|
|
12022
|
+
annotations: {
|
|
12023
|
+
title: "Acknowledge a Leadbay notification",
|
|
12024
|
+
readOnlyHint: false,
|
|
12025
|
+
destructiveHint: false,
|
|
12026
|
+
idempotentHint: true,
|
|
12027
|
+
openWorldHint: true
|
|
12028
|
+
},
|
|
12029
|
+
description: leadbay_acknowledge_notification,
|
|
12030
|
+
write: true,
|
|
12031
|
+
inputSchema: {
|
|
12032
|
+
type: "object",
|
|
12033
|
+
properties: {
|
|
12034
|
+
notification_id: {
|
|
12035
|
+
type: "string",
|
|
12036
|
+
description: "UUID of the notification to acknowledge. Use the notification_id from `_meta.notifications[]` or `account_status.notifications[]`."
|
|
12037
|
+
},
|
|
12038
|
+
archive: {
|
|
12039
|
+
type: "boolean",
|
|
12040
|
+
description: "If true, archive the notification (won't appear in `archived=false` listings). If false / omitted, mark seen (resets firstSeenAt)."
|
|
12041
|
+
}
|
|
12042
|
+
},
|
|
12043
|
+
required: ["notification_id"],
|
|
12044
|
+
additionalProperties: false
|
|
12045
|
+
},
|
|
12046
|
+
outputSchema: {
|
|
12047
|
+
type: "object",
|
|
12048
|
+
properties: {
|
|
12049
|
+
acknowledged: { type: "boolean" },
|
|
12050
|
+
notification_id: { type: "string" },
|
|
12051
|
+
action: { type: "string", enum: ["seen", "archive"] }
|
|
12052
|
+
},
|
|
12053
|
+
required: ["acknowledged", "notification_id", "action"]
|
|
12054
|
+
},
|
|
12055
|
+
execute: async (client, params, ctx) => {
|
|
12056
|
+
if (!UUID_RE.test(params.notification_id)) {
|
|
12057
|
+
return {
|
|
12058
|
+
error: true,
|
|
12059
|
+
code: "BAD_INPUT",
|
|
12060
|
+
message: "notification_id must be a UUID",
|
|
12061
|
+
hint: "Pass the notification_id verbatim from _meta.notifications[].notification_id or account_status.notifications[].notification_id."
|
|
12062
|
+
};
|
|
12063
|
+
}
|
|
12064
|
+
const action = params.archive ? "archive" : "seen";
|
|
12065
|
+
await client.acknowledgeNotification(params.notification_id, action);
|
|
12066
|
+
ctx?.notificationsInbox?.markSeen(params.notification_id);
|
|
12067
|
+
return {
|
|
12068
|
+
acknowledged: true,
|
|
12069
|
+
notification_id: params.notification_id,
|
|
12070
|
+
action
|
|
12071
|
+
};
|
|
12072
|
+
}
|
|
12073
|
+
};
|
|
12074
|
+
}
|
|
12075
|
+
});
|
|
12076
|
+
|
|
11414
12077
|
// ../core/dist/tools/select-leads.js
|
|
11415
12078
|
var selectLeads;
|
|
11416
12079
|
var init_select_leads = __esm({
|
|
@@ -14578,6 +15241,86 @@ var init_research_lead_by_name_fuzzy = __esm({
|
|
|
14578
15241
|
}
|
|
14579
15242
|
});
|
|
14580
15243
|
|
|
15244
|
+
// ../core/dist/composite/account-history.js
|
|
15245
|
+
var accountHistory;
|
|
15246
|
+
var init_account_history = __esm({
|
|
15247
|
+
"../core/dist/composite/account-history.js"() {
|
|
15248
|
+
"use strict";
|
|
15249
|
+
init_research_lead_by_id();
|
|
15250
|
+
init_tool_descriptions_generated();
|
|
15251
|
+
accountHistory = {
|
|
15252
|
+
name: "leadbay_account_history",
|
|
15253
|
+
annotations: {
|
|
15254
|
+
title: "One account's full back-story",
|
|
15255
|
+
readOnlyHint: true,
|
|
15256
|
+
destructiveHint: false,
|
|
15257
|
+
idempotentHint: true,
|
|
15258
|
+
openWorldHint: true
|
|
15259
|
+
},
|
|
15260
|
+
description: leadbay_account_history,
|
|
15261
|
+
inputSchema: {
|
|
15262
|
+
type: "object",
|
|
15263
|
+
properties: {
|
|
15264
|
+
leadId: { type: "string", description: "Lead UUID (required)" },
|
|
15265
|
+
activityCount: {
|
|
15266
|
+
type: "number",
|
|
15267
|
+
description: "Number of activity-timeline entries to return, max 100 (default: 50)."
|
|
15268
|
+
},
|
|
15269
|
+
lensId: {
|
|
15270
|
+
type: "number",
|
|
15271
|
+
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."
|
|
15272
|
+
}
|
|
15273
|
+
},
|
|
15274
|
+
required: ["leadId"],
|
|
15275
|
+
additionalProperties: false
|
|
15276
|
+
},
|
|
15277
|
+
execute: async (client, params, ctx) => {
|
|
15278
|
+
const leadId = params.leadId;
|
|
15279
|
+
const count = Math.max(1, Math.min(Math.floor(params.activityCount ?? 50), 100));
|
|
15280
|
+
const [research, notes, activities] = await Promise.all([
|
|
15281
|
+
researchLeadById.execute(client, { leadId, lensId: params.lensId, response_format: "json" }, ctx),
|
|
15282
|
+
client.request("GET", `/leads/${leadId}/notes`).catch(() => []),
|
|
15283
|
+
client.request("GET", `/leads/${leadId}/activities?count=${count}`).catch(() => ({ items: [], pagination: { total: 0 } }))
|
|
15284
|
+
]);
|
|
15285
|
+
const r = research;
|
|
15286
|
+
const noteList = Array.isArray(notes) ? notes : [];
|
|
15287
|
+
const activityItems = Array.isArray(activities?.items) ? activities.items : [];
|
|
15288
|
+
return {
|
|
15289
|
+
lead: {
|
|
15290
|
+
id: r.firmographics?.id ?? leadId,
|
|
15291
|
+
name: r.firmographics?.name ?? null
|
|
15292
|
+
},
|
|
15293
|
+
// Current state — signals, firmographics, qualification, contacts,
|
|
15294
|
+
// engagement: passed through verbatim from research_lead_by_id so the
|
|
15295
|
+
// agent gets the live "why is this account hot NOW" picture.
|
|
15296
|
+
signals: r.signals ?? null,
|
|
15297
|
+
firmographics: r.firmographics ?? null,
|
|
15298
|
+
qualification: r.qualification ?? [],
|
|
15299
|
+
contacts: r.contacts ?? null,
|
|
15300
|
+
engagement: r.engagement ?? null,
|
|
15301
|
+
// Historical context — the part research only counts/summarizes.
|
|
15302
|
+
notes: noteList,
|
|
15303
|
+
activities: {
|
|
15304
|
+
activities: activityItems.map((a) => ({ type: a.type, date: a.date })),
|
|
15305
|
+
total: activities?.pagination?.total ?? 0
|
|
15306
|
+
},
|
|
15307
|
+
// Preserve research's pass-through metadata (agent_memory summary,
|
|
15308
|
+
// lens_id, web_fetch_in_progress, has_reachable_contact, …) — the
|
|
15309
|
+
// generated description advertises the memory protocol, so dropping
|
|
15310
|
+
// _meta.agent_memory would make history narratives miss stored
|
|
15311
|
+
// preferences. Spread research _meta first, then layer our counts.
|
|
15312
|
+
_meta: {
|
|
15313
|
+
...r._meta ?? {},
|
|
15314
|
+
region: client.region,
|
|
15315
|
+
notes_count: noteList.length,
|
|
15316
|
+
activities_returned: activityItems.length
|
|
15317
|
+
}
|
|
15318
|
+
};
|
|
15319
|
+
}
|
|
15320
|
+
};
|
|
15321
|
+
}
|
|
15322
|
+
});
|
|
15323
|
+
|
|
14581
15324
|
// ../core/dist/composite/recall-ordered-titles.js
|
|
14582
15325
|
var recallOrderedTitles;
|
|
14583
15326
|
var init_recall_ordered_titles = __esm({
|
|
@@ -14758,7 +15501,21 @@ var init_account_status = __esm({
|
|
|
14758
15501
|
},
|
|
14759
15502
|
quota: {
|
|
14760
15503
|
type: ["object", "null"],
|
|
14761
|
-
description: "Per-resource quota state (llm_completion, ai_rescore, web_fetch, LENS_EXTRA_REFILL) across daily/weekly/monthly windows. Null if /quota_status failed (
|
|
15504
|
+
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."
|
|
15505
|
+
},
|
|
15506
|
+
quota_error: {
|
|
15507
|
+
type: ["object", "null"],
|
|
15508
|
+
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'.",
|
|
15509
|
+
properties: {
|
|
15510
|
+
code: { type: "string" },
|
|
15511
|
+
http_status: { type: ["number", "null"] },
|
|
15512
|
+
message: { type: "string" }
|
|
15513
|
+
}
|
|
15514
|
+
},
|
|
15515
|
+
notifications: {
|
|
15516
|
+
type: "array",
|
|
15517
|
+
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.",
|
|
15518
|
+
items: { type: "object" }
|
|
14762
15519
|
},
|
|
14763
15520
|
_meta: {
|
|
14764
15521
|
type: "object",
|
|
@@ -14794,9 +15551,15 @@ var init_account_status = __esm({
|
|
|
14794
15551
|
execute: async (client, _params, ctx) => {
|
|
14795
15552
|
const me = await client.resolveMe();
|
|
14796
15553
|
let quota = null;
|
|
15554
|
+
let quota_error = null;
|
|
14797
15555
|
try {
|
|
14798
15556
|
quota = await client.request("GET", `/organizations/${me.organization.id}/quota_status`);
|
|
14799
15557
|
} catch (err) {
|
|
15558
|
+
quota_error = {
|
|
15559
|
+
code: err?.code ?? "QUOTA_STATUS_FAILED",
|
|
15560
|
+
http_status: err?._meta?.http_status ?? null,
|
|
15561
|
+
message: err?.message ?? "quota_status request failed"
|
|
15562
|
+
};
|
|
14800
15563
|
ctx?.logger?.warn?.(`account_status: quota_status failed: ${err?.message ?? err?.code ?? err}`);
|
|
14801
15564
|
}
|
|
14802
15565
|
return withAgentMemoryMeta(client, {
|
|
@@ -14819,6 +15582,16 @@ var init_account_status = __esm({
|
|
|
14819
15582
|
// on /me are intentionally NOT surfaced — they're defunct (see
|
|
14820
15583
|
// SHAPE-DRIFT.md probe round 4).
|
|
14821
15584
|
quota,
|
|
15585
|
+
// Inbox of terminal bulk-progress notifications. Same shape the MCP
|
|
15586
|
+
// server attaches to `_meta.notifications` on every tool response —
|
|
15587
|
+
// duplicated here as a top-level field so the agent's daily-rhythm
|
|
15588
|
+
// check-in (this composite) sees them without having to read _meta.
|
|
15589
|
+
// Empty array when the WS listener isn't wired (OpenClaw, tests) OR
|
|
15590
|
+
// when nothing has completed since the last ack.
|
|
15591
|
+
notifications: ctx?.notificationsInbox?.list() ?? [],
|
|
15592
|
+
// Non-null ONLY when the quota_status call failed. The agent must treat
|
|
15593
|
+
// this as "could not read quota" (reauth on 401/403) — NOT as zero usage.
|
|
15594
|
+
quota_error,
|
|
14822
15595
|
_meta: {
|
|
14823
15596
|
region: client.region
|
|
14824
15597
|
}
|
|
@@ -14829,13 +15602,40 @@ var init_account_status = __esm({
|
|
|
14829
15602
|
});
|
|
14830
15603
|
|
|
14831
15604
|
// ../core/dist/composite/bulk-qualify-leads.js
|
|
14832
|
-
|
|
15605
|
+
async function launchBulkQualify(client, leadIds, ctx) {
|
|
15606
|
+
await client.acquireSelectionLock();
|
|
15607
|
+
try {
|
|
15608
|
+
try {
|
|
15609
|
+
const qs = leadIds.map((id) => `leadIds=${encodeURIComponent(id)}`).join("&");
|
|
15610
|
+
await client.requestVoid("POST", `/leads/selection/select?${qs}`);
|
|
15611
|
+
try {
|
|
15612
|
+
const resp = await client.request("POST", "/leads/selection/web_fetch?force_fetch=false", {});
|
|
15613
|
+
return { resp, quotaExceeded: false };
|
|
15614
|
+
} catch (err) {
|
|
15615
|
+
if (err?.code === "QUOTA_EXCEEDED") {
|
|
15616
|
+
ctx?.logger?.warn?.("bulk_qualify_leads: 429 on bulk /leads/selection/web_fetch \u2014 no leads queued");
|
|
15617
|
+
return { resp: null, quotaExceeded: true };
|
|
15618
|
+
}
|
|
15619
|
+
throw err;
|
|
15620
|
+
}
|
|
15621
|
+
} finally {
|
|
15622
|
+
try {
|
|
15623
|
+
await client.requestVoid("POST", "/leads/selection/clear");
|
|
15624
|
+
} catch (e) {
|
|
15625
|
+
ctx?.logger?.warn?.(`bulk_qualify_leads: selection.clear failed: ${e?.message ?? e?.code}`);
|
|
15626
|
+
}
|
|
15627
|
+
}
|
|
15628
|
+
} finally {
|
|
15629
|
+
client.releaseSelectionLock();
|
|
15630
|
+
}
|
|
15631
|
+
}
|
|
15632
|
+
var PAGE_SIZE, DEFAULT_COUNT2, MAX_COUNT, DEFAULT_PER_LEAD_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS2, bulkQualifyLeads;
|
|
14833
15633
|
var init_bulk_qualify_leads = __esm({
|
|
14834
15634
|
"../core/dist/composite/bulk-qualify-leads.js"() {
|
|
14835
15635
|
"use strict";
|
|
14836
15636
|
init_tool_descriptions_generated();
|
|
14837
15637
|
PAGE_SIZE = 50;
|
|
14838
|
-
|
|
15638
|
+
DEFAULT_COUNT2 = 10;
|
|
14839
15639
|
MAX_COUNT = 25;
|
|
14840
15640
|
DEFAULT_PER_LEAD_BUDGET_MS = 9e4;
|
|
14841
15641
|
DEFAULT_TOTAL_BUDGET_MS2 = 5 * 6e4;
|
|
@@ -14856,7 +15656,7 @@ var init_bulk_qualify_leads = __esm({
|
|
|
14856
15656
|
properties: {
|
|
14857
15657
|
count: {
|
|
14858
15658
|
type: "number",
|
|
14859
|
-
description: `How many fresh leads to qualify (default ${
|
|
15659
|
+
description: `How many fresh leads to qualify (default ${DEFAULT_COUNT2}, max ${MAX_COUNT})`
|
|
14860
15660
|
},
|
|
14861
15661
|
leadIds: {
|
|
14862
15662
|
type: "array",
|
|
@@ -14947,7 +15747,7 @@ var init_bulk_qualify_leads = __esm({
|
|
|
14947
15747
|
]
|
|
14948
15748
|
},
|
|
14949
15749
|
execute: async (client, params, ctx) => {
|
|
14950
|
-
const wantCount = Math.min(params.count ??
|
|
15750
|
+
const wantCount = Math.min(params.count ?? DEFAULT_COUNT2, MAX_COUNT);
|
|
14951
15751
|
const perLeadBudget = params.per_lead_budget_ms ?? DEFAULT_PER_LEAD_BUDGET_MS;
|
|
14952
15752
|
const totalBudget = params.total_budget_ms ?? DEFAULT_TOTAL_BUDGET_MS2;
|
|
14953
15753
|
const totalDeadline = Date.now() + totalBudget;
|
|
@@ -15006,69 +15806,50 @@ var init_bulk_qualify_leads = __esm({
|
|
|
15006
15806
|
per_lead_budget_ms: perLeadBudget,
|
|
15007
15807
|
total_budget_ms: totalBudget
|
|
15008
15808
|
});
|
|
15009
|
-
|
|
15010
|
-
|
|
15809
|
+
let launchedCount = 0;
|
|
15810
|
+
let notificationId = null;
|
|
15011
15811
|
let quotaExceeded2 = false;
|
|
15812
|
+
let failed2 = [];
|
|
15012
15813
|
if (!reservation.reused) {
|
|
15013
|
-
|
|
15014
|
-
|
|
15015
|
-
|
|
15016
|
-
|
|
15017
|
-
|
|
15018
|
-
|
|
15019
|
-
|
|
15020
|
-
|
|
15021
|
-
|
|
15022
|
-
|
|
15023
|
-
failed2.push({ lead_id: leadId, error: "lead not found" });
|
|
15024
|
-
} else {
|
|
15025
|
-
failed2.push({
|
|
15026
|
-
lead_id: leadId,
|
|
15027
|
-
error: err?.message ?? err?.code ?? "unknown"
|
|
15028
|
-
});
|
|
15029
|
-
}
|
|
15030
|
-
}
|
|
15031
|
-
}
|
|
15032
|
-
if (failed2.length === candidates.length || launched2.length > 0 || quotaExceeded2) {
|
|
15033
|
-
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id);
|
|
15814
|
+
const launch = await launchBulkQualify(client, candidates, ctx);
|
|
15815
|
+
quotaExceeded2 = launch.quotaExceeded;
|
|
15816
|
+
notificationId = launch.resp?.notification_id ?? null;
|
|
15817
|
+
const queuedIds = launch.resp?.queued_ids ?? [];
|
|
15818
|
+
const skippedIds = launch.resp?.skipped_ids ?? [];
|
|
15819
|
+
launchedCount = queuedIds.length;
|
|
15820
|
+
const seen = /* @__PURE__ */ new Set([...queuedIds, ...skippedIds]);
|
|
15821
|
+
failed2 = candidates.filter((id) => !seen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
|
|
15822
|
+
if (queuedIds.length > 0 || quotaExceeded2 || skippedIds.length > 0 || failed2.length === candidates.length) {
|
|
15823
|
+
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id, notificationId);
|
|
15034
15824
|
}
|
|
15825
|
+
} else {
|
|
15826
|
+
notificationId = reservation.record.notification_id ?? null;
|
|
15827
|
+
launchedCount = reservation.record.lead_ids.length;
|
|
15035
15828
|
}
|
|
15036
15829
|
const out = {
|
|
15037
15830
|
status: "running",
|
|
15038
15831
|
handle_id: reservation.record.bulk_id,
|
|
15039
15832
|
qualify_id: reservation.record.bulk_id,
|
|
15040
15833
|
lead_ids: candidates,
|
|
15041
|
-
launched_count:
|
|
15834
|
+
launched_count: launchedCount,
|
|
15042
15835
|
failed: failed2,
|
|
15043
15836
|
quota_exceeded: quotaExceeded2,
|
|
15044
15837
|
lens_id: lensId,
|
|
15838
|
+
notification_id: notificationId,
|
|
15045
15839
|
_meta: { region: client.region }
|
|
15046
15840
|
};
|
|
15047
15841
|
return out;
|
|
15048
15842
|
}
|
|
15049
|
-
const
|
|
15050
|
-
const
|
|
15051
|
-
|
|
15052
|
-
|
|
15053
|
-
|
|
15054
|
-
|
|
15055
|
-
|
|
15056
|
-
await client.requestVoid("POST", `/leads/${leadId}/web_fetch?force_fetch=false`);
|
|
15057
|
-
launched.push(leadId);
|
|
15058
|
-
} catch (err) {
|
|
15059
|
-
if (err?.code === "QUOTA_EXCEEDED") {
|
|
15060
|
-
quotaExceeded = true;
|
|
15061
|
-
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`);
|
|
15062
|
-
} else if (err?.code === "NOT_FOUND") {
|
|
15063
|
-
failed.push({ lead_id: leadId, error: "lead not found" });
|
|
15064
|
-
} else {
|
|
15065
|
-
failed.push({
|
|
15066
|
-
lead_id: leadId,
|
|
15067
|
-
error: err?.message ?? err?.code ?? "unknown"
|
|
15068
|
-
});
|
|
15069
|
-
}
|
|
15070
|
-
}
|
|
15843
|
+
const inlineLaunch = await launchBulkQualify(client, candidates, ctx);
|
|
15844
|
+
const quotaExceeded = inlineLaunch.quotaExceeded;
|
|
15845
|
+
const launched = inlineLaunch.resp?.queued_ids ?? [];
|
|
15846
|
+
const inlineSkipped = inlineLaunch.resp?.skipped_ids ?? [];
|
|
15847
|
+
const inlineNotificationId = inlineLaunch.resp?.notification_id ?? null;
|
|
15848
|
+
if (inlineNotificationId) {
|
|
15849
|
+
ctx?.logger?.info?.(`bulk_qualify_leads: launched bulk progress_notification_id=${inlineNotificationId} queued=${launched.length} skipped=${inlineSkipped.length}`);
|
|
15071
15850
|
}
|
|
15851
|
+
const inlineFailedSeen = /* @__PURE__ */ new Set([...launched, ...inlineSkipped]);
|
|
15852
|
+
const failed = candidates.filter((id) => !inlineFailedSeen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
|
|
15072
15853
|
let progressDone = 0;
|
|
15073
15854
|
const progressTotal = launched.length;
|
|
15074
15855
|
if (progressTotal > 0) {
|
|
@@ -15904,6 +16685,7 @@ var init_import_and_qualify = __esm({
|
|
|
15904
16685
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
15905
16686
|
qualify_id: null,
|
|
15906
16687
|
import_ids: queued.importIds,
|
|
16688
|
+
notification_ids: queued.notification_ids ?? [],
|
|
15907
16689
|
imported: queued.leads.map((l) => ({
|
|
15908
16690
|
leadId: l.leadId,
|
|
15909
16691
|
...l.domain ? { domain: l.domain } : {},
|
|
@@ -15928,6 +16710,7 @@ var init_import_and_qualify = __esm({
|
|
|
15928
16710
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
15929
16711
|
qualify_id: null,
|
|
15930
16712
|
import_ids: queued.importIds,
|
|
16713
|
+
notification_ids: queued.notification_ids ?? [],
|
|
15931
16714
|
imported: [],
|
|
15932
16715
|
not_imported: [],
|
|
15933
16716
|
qualified: [],
|
|
@@ -15965,6 +16748,7 @@ var init_import_and_qualify = __esm({
|
|
|
15965
16748
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
15966
16749
|
qualify_id: null,
|
|
15967
16750
|
import_ids: importResult.importIds,
|
|
16751
|
+
notification_ids: importResult.notification_ids ?? [],
|
|
15968
16752
|
imported: [],
|
|
15969
16753
|
not_imported: importResult.not_imported.map(toNotImportedEntry),
|
|
15970
16754
|
qualified: [],
|
|
@@ -16002,6 +16786,7 @@ var init_import_and_qualify = __esm({
|
|
|
16002
16786
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
16003
16787
|
qualify_id: null,
|
|
16004
16788
|
import_ids: importResult.importIds,
|
|
16789
|
+
notification_ids: importResult.notification_ids ?? [],
|
|
16005
16790
|
imported,
|
|
16006
16791
|
not_imported,
|
|
16007
16792
|
qualified: [],
|
|
@@ -16112,6 +16897,7 @@ var init_import_and_qualify = __esm({
|
|
|
16112
16897
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
16113
16898
|
qualify_id: reservation.record.bulk_id,
|
|
16114
16899
|
import_ids: importResult.importIds,
|
|
16900
|
+
notification_ids: importResult.notification_ids ?? [],
|
|
16115
16901
|
imported,
|
|
16116
16902
|
not_imported,
|
|
16117
16903
|
qualified,
|
|
@@ -16707,16 +17493,20 @@ var init_bulk_store = __esm({
|
|
|
16707
17493
|
this.logger?.info?.(`bulk.import_failed bulk_id=${bulk_id}`);
|
|
16708
17494
|
});
|
|
16709
17495
|
}
|
|
16710
|
-
async markLaunched(bulk_id) {
|
|
17496
|
+
async markLaunched(bulk_id, notification_id) {
|
|
16711
17497
|
return this.mutex.run(async () => {
|
|
16712
17498
|
const all = this.prune(await this.readAll());
|
|
16713
17499
|
const idx = all.findIndex((r) => r.bulk_id === bulk_id);
|
|
16714
17500
|
if (idx < 0) {
|
|
16715
17501
|
throw new Error(`bulk_id not found: ${bulk_id}`);
|
|
16716
17502
|
}
|
|
16717
|
-
all[idx] = {
|
|
17503
|
+
all[idx] = {
|
|
17504
|
+
...all[idx],
|
|
17505
|
+
status: "launched",
|
|
17506
|
+
...notification_id ? { notification_id } : {}
|
|
17507
|
+
};
|
|
16718
17508
|
await this.writeAll(all);
|
|
16719
|
-
this.logger?.info?.(`bulk.launched bulk_id=${bulk_id}`);
|
|
17509
|
+
this.logger?.info?.(`bulk.launched bulk_id=${bulk_id}${notification_id ? ` notification_id=${notification_id}` : ""}`);
|
|
16720
17510
|
return all[idx];
|
|
16721
17511
|
});
|
|
16722
17512
|
}
|
|
@@ -16968,6 +17758,14 @@ var init_import_status = __esm({
|
|
|
16968
17758
|
});
|
|
16969
17759
|
|
|
16970
17760
|
// ../core/dist/composite/qualify-status.js
|
|
17761
|
+
async function readNotification(client, notificationId) {
|
|
17762
|
+
try {
|
|
17763
|
+
const page = await client.listNotifications({ archived: false, count: 50 });
|
|
17764
|
+
return page.items.find((n) => n.id === notificationId) ?? null;
|
|
17765
|
+
} catch {
|
|
17766
|
+
return null;
|
|
17767
|
+
}
|
|
17768
|
+
}
|
|
16971
17769
|
var qualifyStatus;
|
|
16972
17770
|
var init_qualify_status = __esm({
|
|
16973
17771
|
"../core/dist/composite/qualify-status.js"() {
|
|
@@ -17129,6 +17927,16 @@ var init_qualify_status = __esm({
|
|
|
17129
17927
|
const { _stillRunning, _failedCode, ...rest } = r;
|
|
17130
17928
|
qualified.push(rest);
|
|
17131
17929
|
}
|
|
17930
|
+
let bulkProgress = null;
|
|
17931
|
+
let inProgressFlag = null;
|
|
17932
|
+
const notifId = record.notification_id ?? null;
|
|
17933
|
+
if (notifId) {
|
|
17934
|
+
const n = await readNotification(client, notifId);
|
|
17935
|
+
if (n) {
|
|
17936
|
+
bulkProgress = n.bulk_progress;
|
|
17937
|
+
inProgressFlag = n.in_progress;
|
|
17938
|
+
}
|
|
17939
|
+
}
|
|
17132
17940
|
const out = {
|
|
17133
17941
|
qualify_id: record.bulk_id,
|
|
17134
17942
|
launched_at: record.launched_at,
|
|
@@ -17140,6 +17948,9 @@ var init_qualify_status = __esm({
|
|
|
17140
17948
|
still_running,
|
|
17141
17949
|
failed,
|
|
17142
17950
|
not_in_lens: [...notInLensSet],
|
|
17951
|
+
notification_id: notifId,
|
|
17952
|
+
bulk_progress: bulkProgress,
|
|
17953
|
+
in_progress: inProgressFlag,
|
|
17143
17954
|
region: client.region,
|
|
17144
17955
|
_meta: client.lastMeta ?? {
|
|
17145
17956
|
region: client.region,
|
|
@@ -17152,17 +17963,36 @@ var init_qualify_status = __esm({
|
|
|
17152
17963
|
out.per_lead_budget_ms = record.per_lead_budget_ms;
|
|
17153
17964
|
if (record.total_budget_ms !== void 0)
|
|
17154
17965
|
out.total_budget_ms = record.total_budget_ms;
|
|
17966
|
+
if (bulkProgress && bulkProgress.quota_hit_count > 0) {
|
|
17967
|
+
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.";
|
|
17968
|
+
}
|
|
17155
17969
|
return out;
|
|
17156
17970
|
}
|
|
17157
17971
|
};
|
|
17158
17972
|
}
|
|
17159
17973
|
});
|
|
17160
17974
|
|
|
17975
|
+
// ../core/dist/composite/_credits-helpers.js
|
|
17976
|
+
async function readCreditsRemaining(client, force = false) {
|
|
17977
|
+
try {
|
|
17978
|
+
const me = await client.resolveMe(force);
|
|
17979
|
+
return me.organization.billing?.ai_credits ?? null;
|
|
17980
|
+
} catch {
|
|
17981
|
+
return null;
|
|
17982
|
+
}
|
|
17983
|
+
}
|
|
17984
|
+
var init_credits_helpers = __esm({
|
|
17985
|
+
"../core/dist/composite/_credits-helpers.js"() {
|
|
17986
|
+
"use strict";
|
|
17987
|
+
}
|
|
17988
|
+
});
|
|
17989
|
+
|
|
17161
17990
|
// ../core/dist/composite/enrich-titles.js
|
|
17162
17991
|
var DEFAULT_CANDIDATE_COUNT, enrichTitles;
|
|
17163
17992
|
var init_enrich_titles = __esm({
|
|
17164
17993
|
"../core/dist/composite/enrich-titles.js"() {
|
|
17165
17994
|
"use strict";
|
|
17995
|
+
init_credits_helpers();
|
|
17166
17996
|
init_tool_descriptions_generated();
|
|
17167
17997
|
DEFAULT_CANDIDATE_COUNT = 25;
|
|
17168
17998
|
enrichTitles = {
|
|
@@ -17246,6 +18076,10 @@ var init_enrich_titles = __esm({
|
|
|
17246
18076
|
type: "number",
|
|
17247
18077
|
description: "Count of enrichable contacts at preview time."
|
|
17248
18078
|
},
|
|
18079
|
+
credits_remaining: {
|
|
18080
|
+
type: ["number", "null"],
|
|
18081
|
+
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."
|
|
18082
|
+
},
|
|
17249
18083
|
selected_lead_count: {
|
|
17250
18084
|
type: "number",
|
|
17251
18085
|
description: "How many leads the selection covers."
|
|
@@ -17369,6 +18203,10 @@ var init_enrich_titles = __esm({
|
|
|
17369
18203
|
previously_enriched: previouslyEnriched,
|
|
17370
18204
|
enrichable_contacts: enrichableContacts,
|
|
17371
18205
|
selected_lead_count: leadIds.length,
|
|
18206
|
+
// BEFORE: show balance + volume. We can't estimate exact cost
|
|
18207
|
+
// (the per-contact rate is backend-only), so surface the balance
|
|
18208
|
+
// and the count, not a fabricated "will cost N".
|
|
18209
|
+
credits_remaining: await readCreditsRemaining(client),
|
|
17372
18210
|
next_action: "Pick titles to enrich and call leadbay_enrich_titles again with titles=[...]"
|
|
17373
18211
|
};
|
|
17374
18212
|
}
|
|
@@ -17391,7 +18229,8 @@ var init_enrich_titles = __esm({
|
|
|
17391
18229
|
preview,
|
|
17392
18230
|
launched: false,
|
|
17393
18231
|
message: "No enrichable contacts for the chosen titles. Try other titles from available_titles or recommendations.",
|
|
17394
|
-
available_titles: availableTitles
|
|
18232
|
+
available_titles: availableTitles,
|
|
18233
|
+
credits_remaining: await readCreditsRemaining(client)
|
|
17395
18234
|
};
|
|
17396
18235
|
}
|
|
17397
18236
|
if (params.dry_run) {
|
|
@@ -17399,7 +18238,12 @@ var init_enrich_titles = __esm({
|
|
|
17399
18238
|
mode: "dry_run",
|
|
17400
18239
|
preview,
|
|
17401
18240
|
launched: false,
|
|
17402
|
-
would_launch: { titles: params.titles, email, phone }
|
|
18241
|
+
would_launch: { titles: params.titles, email, phone },
|
|
18242
|
+
// BEFORE confirmation gate: balance + how many contacts WOULD be
|
|
18243
|
+
// enriched. enrichable_contacts is the volume; credits_remaining
|
|
18244
|
+
// the balance. No estimated cost — that rate is backend-only.
|
|
18245
|
+
enrichable_contacts: preview.enrichable_contacts,
|
|
18246
|
+
credits_remaining: await readCreditsRemaining(client)
|
|
17403
18247
|
};
|
|
17404
18248
|
}
|
|
17405
18249
|
const tracker = ctx?.bulkTracker;
|
|
@@ -17429,6 +18273,7 @@ var init_enrich_titles = __esm({
|
|
|
17429
18273
|
bulk_id: res.record.bulk_id,
|
|
17430
18274
|
launched_at: res.record.launched_at,
|
|
17431
18275
|
durability: res.record.durability,
|
|
18276
|
+
notification_id: res.record.notification_id ?? null,
|
|
17432
18277
|
seconds_since_original_launch: bulkSecondsSinceOriginal ?? 0,
|
|
17433
18278
|
titles: params.titles,
|
|
17434
18279
|
email,
|
|
@@ -17444,8 +18289,9 @@ var init_enrich_titles = __esm({
|
|
|
17444
18289
|
total: 3,
|
|
17445
18290
|
message: `Launching enrichment for ${params.titles.length} title${params.titles.length === 1 ? "" : "s"}\u2026`
|
|
17446
18291
|
});
|
|
18292
|
+
let launchResp = null;
|
|
17447
18293
|
try {
|
|
17448
|
-
await client.
|
|
18294
|
+
launchResp = await client.request("POST", "/leads/selection/enrichment/launch", { titles: params.titles, email, phone });
|
|
17449
18295
|
} catch (err) {
|
|
17450
18296
|
const aborted = err?.name === "AbortError" || ctx?.signal?.aborted === true;
|
|
17451
18297
|
if (bulkRecord && tracker) {
|
|
@@ -17469,9 +18315,10 @@ var init_enrich_titles = __esm({
|
|
|
17469
18315
|
}
|
|
17470
18316
|
throw err;
|
|
17471
18317
|
}
|
|
18318
|
+
const notificationId = launchResp?.notification_id ?? null;
|
|
17472
18319
|
if (bulkRecord && tracker) {
|
|
17473
18320
|
try {
|
|
17474
|
-
await tracker.markLaunched(bulkRecord.bulk_id);
|
|
18321
|
+
await tracker.markLaunched(bulkRecord.bulk_id, notificationId);
|
|
17475
18322
|
} catch (e) {
|
|
17476
18323
|
ctx?.logger?.warn?.(`enrich_titles: tracker.markLaunched failed: ${e?.message ?? e}`);
|
|
17477
18324
|
return {
|
|
@@ -17499,8 +18346,9 @@ var init_enrich_titles = __esm({
|
|
|
17499
18346
|
bulk_id: bulkRecord?.bulk_id,
|
|
17500
18347
|
launched_at: bulkRecord?.launched_at,
|
|
17501
18348
|
durability: bulkRecord?.durability,
|
|
17502
|
-
|
|
17503
|
-
|
|
18349
|
+
notification_id: notificationId,
|
|
18350
|
+
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.",
|
|
18351
|
+
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."
|
|
17504
18352
|
};
|
|
17505
18353
|
} finally {
|
|
17506
18354
|
try {
|
|
@@ -17518,6 +18366,14 @@ var init_enrich_titles = __esm({
|
|
|
17518
18366
|
});
|
|
17519
18367
|
|
|
17520
18368
|
// ../core/dist/composite/bulk-enrich-status.js
|
|
18369
|
+
async function readNotification2(client, notificationId) {
|
|
18370
|
+
try {
|
|
18371
|
+
const page = await client.listNotifications({ archived: false, count: 50 });
|
|
18372
|
+
return page.items.find((n) => n.id === notificationId) ?? null;
|
|
18373
|
+
} catch {
|
|
18374
|
+
return null;
|
|
18375
|
+
}
|
|
18376
|
+
}
|
|
17521
18377
|
async function pMap(items, fn, concurrency) {
|
|
17522
18378
|
const out = new Array(items.length);
|
|
17523
18379
|
let next = 0;
|
|
@@ -17538,6 +18394,7 @@ var init_bulk_enrich_status = __esm({
|
|
|
17538
18394
|
"use strict";
|
|
17539
18395
|
init_get_contacts();
|
|
17540
18396
|
init_bulk_store();
|
|
18397
|
+
init_credits_helpers();
|
|
17541
18398
|
init_tool_descriptions_generated();
|
|
17542
18399
|
STATUS_FETCH_CONCURRENCY = 5;
|
|
17543
18400
|
bulkEnrichStatus = {
|
|
@@ -17604,6 +18461,10 @@ var init_bulk_enrich_status = __esm({
|
|
|
17604
18461
|
type: "boolean",
|
|
17605
18462
|
description: "True when overall_progress.done === total AND no partial_failures."
|
|
17606
18463
|
},
|
|
18464
|
+
credits_remaining: {
|
|
18465
|
+
type: ["number", "null"],
|
|
18466
|
+
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."
|
|
18467
|
+
},
|
|
17607
18468
|
partial_failures: {
|
|
17608
18469
|
type: "array",
|
|
17609
18470
|
description: "Per-lead errors observed during contacts fan-out (omitted when no failures).",
|
|
@@ -17689,6 +18550,55 @@ var init_bulk_enrich_status = __esm({
|
|
|
17689
18550
|
launched_at: record.launched_at
|
|
17690
18551
|
};
|
|
17691
18552
|
}
|
|
18553
|
+
const notifId = record.notification_id ?? null;
|
|
18554
|
+
if (notifId) {
|
|
18555
|
+
const n = await readNotification2(client, notifId);
|
|
18556
|
+
if (n && n.bulk_progress) {
|
|
18557
|
+
const bp = n.bulk_progress;
|
|
18558
|
+
const inProgress = n.in_progress;
|
|
18559
|
+
let leads2 = [];
|
|
18560
|
+
if (!inProgress && includeContacts) {
|
|
18561
|
+
leads2 = await pMap(record.lead_ids, async (leadId) => {
|
|
18562
|
+
try {
|
|
18563
|
+
const out = await getContacts.execute(client, { leadId });
|
|
18564
|
+
const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
|
|
18565
|
+
return { lead_id: leadId, contacts };
|
|
18566
|
+
} catch {
|
|
18567
|
+
return { lead_id: leadId };
|
|
18568
|
+
}
|
|
18569
|
+
}, STATUS_FETCH_CONCURRENCY);
|
|
18570
|
+
} else {
|
|
18571
|
+
leads2 = record.lead_ids.map((id) => ({ lead_id: id }));
|
|
18572
|
+
}
|
|
18573
|
+
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}`);
|
|
18574
|
+
const creditsRemaining2 = !inProgress ? await readCreditsRemaining(client, true) : null;
|
|
18575
|
+
return {
|
|
18576
|
+
bulk_id: record.bulk_id,
|
|
18577
|
+
notification_id: notifId,
|
|
18578
|
+
launched_at: record.launched_at,
|
|
18579
|
+
status: record.status,
|
|
18580
|
+
durability: record.durability,
|
|
18581
|
+
titles: record.titles,
|
|
18582
|
+
email: record.email,
|
|
18583
|
+
phone: record.phone,
|
|
18584
|
+
lens_id: record.lens_id,
|
|
18585
|
+
leads: leads2,
|
|
18586
|
+
overall_progress: {
|
|
18587
|
+
done: bp.success_count + bp.failure_count + bp.quota_hit_count,
|
|
18588
|
+
total: bp.total_count,
|
|
18589
|
+
done_ratio: bp.total_count === 0 ? 0 : (bp.success_count + bp.failure_count + bp.quota_hit_count) / bp.total_count
|
|
18590
|
+
},
|
|
18591
|
+
bulk_progress: bp,
|
|
18592
|
+
in_progress: inProgress,
|
|
18593
|
+
all_done: !inProgress,
|
|
18594
|
+
...!inProgress ? { credits_remaining: creditsRemaining2 } : {},
|
|
18595
|
+
...bp.quota_hit_count > 0 ? {
|
|
18596
|
+
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."
|
|
18597
|
+
} : {}
|
|
18598
|
+
};
|
|
18599
|
+
}
|
|
18600
|
+
ctx?.logger?.info?.(`bulk_enrich_status: notification ${notifId} not yet visible; falling back to per-lead fan-out`);
|
|
18601
|
+
}
|
|
17692
18602
|
let doneSoFar = 0;
|
|
17693
18603
|
const totalLeads = record.lead_ids.length;
|
|
17694
18604
|
const results = await pMap(record.lead_ids, async (leadId) => {
|
|
@@ -17754,6 +18664,10 @@ var init_bulk_enrich_status = __esm({
|
|
|
17754
18664
|
};
|
|
17755
18665
|
const allDone = totalAll > 0 && totalDone === totalAll && partialFailures.length === 0;
|
|
17756
18666
|
ctx?.logger?.info?.(`bulk.status_checked bulk_id=${record.bulk_id} done=${totalDone} total=${totalAll} wall_ms=${Date.now() - startMs}`);
|
|
18667
|
+
let creditsRemaining = null;
|
|
18668
|
+
if (allDone) {
|
|
18669
|
+
creditsRemaining = await readCreditsRemaining(client, true);
|
|
18670
|
+
}
|
|
17757
18671
|
return {
|
|
17758
18672
|
bulk_id: record.bulk_id,
|
|
17759
18673
|
launched_at: record.launched_at,
|
|
@@ -17766,6 +18680,7 @@ var init_bulk_enrich_status = __esm({
|
|
|
17766
18680
|
leads,
|
|
17767
18681
|
overall_progress: overallProgress,
|
|
17768
18682
|
all_done: allDone,
|
|
18683
|
+
...allDone ? { credits_remaining: creditsRemaining } : {},
|
|
17769
18684
|
...partialFailures.length > 0 ? { partial_failures: partialFailures } : {}
|
|
17770
18685
|
};
|
|
17771
18686
|
}
|
|
@@ -19457,8 +20372,12 @@ __export(dist_exports, {
|
|
|
19457
20372
|
InMemoryBulkStore: () => InMemoryBulkStore,
|
|
19458
20373
|
LeadbayClient: () => LeadbayClient,
|
|
19459
20374
|
LocalBulkStore: () => LocalBulkStore,
|
|
20375
|
+
NotificationsInbox: () => NotificationsInbox,
|
|
20376
|
+
NotificationsWsClient: () => NotificationsWsClient,
|
|
19460
20377
|
REGIONS: () => REGIONS,
|
|
20378
|
+
accountHistory: () => accountHistory,
|
|
19461
20379
|
accountStatus: () => accountStatus,
|
|
20380
|
+
acknowledgeNotification: () => acknowledgeNotification,
|
|
19462
20381
|
addLeadsToCampaign: () => addLeadsToCampaign,
|
|
19463
20382
|
addNote: () => addNote,
|
|
19464
20383
|
adjustAudience: () => adjustAudience,
|
|
@@ -19466,6 +20385,7 @@ __export(dist_exports, {
|
|
|
19466
20385
|
agentMemoryRecall: () => agentMemoryRecall,
|
|
19467
20386
|
agentMemoryReview: () => agentMemoryReview,
|
|
19468
20387
|
agentMemoryTools: () => agentMemoryTools,
|
|
20388
|
+
anchorIdFor: () => anchorIdFor,
|
|
19469
20389
|
answerClarification: () => answerClarification,
|
|
19470
20390
|
appendEntry: () => appendEntry,
|
|
19471
20391
|
appendTombstone: () => appendTombstone,
|
|
@@ -19474,6 +20394,7 @@ __export(dist_exports, {
|
|
|
19474
20394
|
bulkQualifyLeads: () => bulkQualifyLeads,
|
|
19475
20395
|
campaignCallSheet: () => campaignCallSheet,
|
|
19476
20396
|
campaignProgression: () => campaignProgression,
|
|
20397
|
+
catchUpNotifications: () => catchUpNotifications,
|
|
19477
20398
|
clearAgentMemoryCache: () => clearAgentMemoryCache,
|
|
19478
20399
|
clearMockJournal: () => clearMockJournal,
|
|
19479
20400
|
clearSelection: () => clearSelection,
|
|
@@ -19523,6 +20444,7 @@ __export(dist_exports, {
|
|
|
19523
20444
|
importAndQualify: () => importAndQualify,
|
|
19524
20445
|
importLeads: () => importLeads,
|
|
19525
20446
|
importStatus: () => importStatus,
|
|
20447
|
+
inferKind: () => inferKind,
|
|
19526
20448
|
invalidateAgentMemoryCache: () => invalidateAgentMemoryCache,
|
|
19527
20449
|
isAgentMemoryEnabled: () => isAgentMemoryEnabled,
|
|
19528
20450
|
isValidBulkId: () => isValidBulkId,
|
|
@@ -19563,12 +20485,14 @@ __export(dist_exports, {
|
|
|
19563
20485
|
resolveAgentMemorySummary: () => resolveAgentMemorySummary,
|
|
19564
20486
|
resolveImportRows: () => resolveImportRows,
|
|
19565
20487
|
resolveRegion: () => resolveRegion,
|
|
20488
|
+
reviseHintFor: () => reviseHintFor,
|
|
19566
20489
|
seedCandidates: () => seedCandidates,
|
|
19567
20490
|
selectLeads: () => selectLeads,
|
|
19568
20491
|
setActiveLens: () => setActiveLens,
|
|
19569
20492
|
setEpilogueStatus: () => setEpilogueStatus,
|
|
19570
20493
|
setPushback: () => setPushback,
|
|
19571
20494
|
setUserPrompt: () => setUserPrompt,
|
|
20495
|
+
toInboxEntry: () => toInboxEntry,
|
|
19572
20496
|
tools: () => tools,
|
|
19573
20497
|
tourPlan: () => tourPlan,
|
|
19574
20498
|
updateLens: () => updateLens,
|
|
@@ -19583,6 +20507,7 @@ var init_dist = __esm({
|
|
|
19583
20507
|
init_types();
|
|
19584
20508
|
init_agent_memory();
|
|
19585
20509
|
init_composite_file_names();
|
|
20510
|
+
init_notifications();
|
|
19586
20511
|
init_login();
|
|
19587
20512
|
init_list_lenses();
|
|
19588
20513
|
init_discover_leads();
|
|
@@ -19612,6 +20537,7 @@ var init_dist = __esm({
|
|
|
19612
20537
|
init_agent_memory_recall();
|
|
19613
20538
|
init_agent_memory_capture();
|
|
19614
20539
|
init_agent_memory_review();
|
|
20540
|
+
init_acknowledge_notification();
|
|
19615
20541
|
init_select_leads();
|
|
19616
20542
|
init_deselect_leads();
|
|
19617
20543
|
init_clear_selection();
|
|
@@ -19647,6 +20573,7 @@ var init_dist = __esm({
|
|
|
19647
20573
|
init_campaign_call_sheet();
|
|
19648
20574
|
init_research_lead_by_id();
|
|
19649
20575
|
init_research_lead_by_name_fuzzy();
|
|
20576
|
+
init_account_history();
|
|
19650
20577
|
init_recall_ordered_titles();
|
|
19651
20578
|
init_account_status();
|
|
19652
20579
|
init_bulk_qualify_leads();
|
|
@@ -19739,6 +20666,13 @@ var init_dist = __esm({
|
|
|
19739
20666
|
campaignCallSheet,
|
|
19740
20667
|
researchLeadById,
|
|
19741
20668
|
researchLeadByNameFuzzy,
|
|
20669
|
+
// accountHistory layers FULL notes + activity timeline on top of research
|
|
20670
|
+
// so the agent can write the US4 "why has this dormant account resurfaced"
|
|
20671
|
+
// narrative in ONE call. ALWAYS exposed (compositeReadTools) — the underlying
|
|
20672
|
+
// get_lead_notes / get_lead_activities are ADVANCED-gated, but the
|
|
20673
|
+
// reprioritize-a-neglected-account workflow (#3630 GAP C) must work in a
|
|
20674
|
+
// default deployment without LEADBAY_MCP_ADVANCED=1.
|
|
20675
|
+
accountHistory,
|
|
19742
20676
|
recallOrderedTitles,
|
|
19743
20677
|
accountStatus,
|
|
19744
20678
|
bulkEnrichStatus,
|
|
@@ -19771,7 +20705,13 @@ var init_dist = __esm({
|
|
|
19771
20705
|
// didn't deliver"). Does not mutate Leadbay state; emits a PostHog
|
|
19772
20706
|
// event only. Companion to leadbay_report_outreach (which DOES write
|
|
19773
20707
|
// to the backend and stays gated behind LEADBAY_MCP_WRITE).
|
|
19774
|
-
reportFriction
|
|
20708
|
+
reportFriction,
|
|
20709
|
+
// Notification ack — ALWAYS exposed even though it POSTs to /seen.
|
|
20710
|
+
// _meta.notifications surfaces terminal bulk-progress notifications on
|
|
20711
|
+
// every tool response regardless of write gating; without ack the agent
|
|
20712
|
+
// sees the same entries on every call forever. Pairing the surfacing
|
|
20713
|
+
// channel with the clearing tool is non-optional.
|
|
20714
|
+
acknowledgeNotification
|
|
19775
20715
|
];
|
|
19776
20716
|
compositeWriteTools = [
|
|
19777
20717
|
bulkQualifyLeads,
|
|
@@ -22119,6 +23059,22 @@ function buildServer(client, opts = {}) {
|
|
|
22119
23059
|
});
|
|
22120
23060
|
}
|
|
22121
23061
|
};
|
|
23062
|
+
const maybeAttachNotifications = (result) => {
|
|
23063
|
+
const inbox = opts.notificationsInbox;
|
|
23064
|
+
if (!inbox) return;
|
|
23065
|
+
if (result === null || typeof result !== "object" || Array.isArray(result)) {
|
|
23066
|
+
return;
|
|
23067
|
+
}
|
|
23068
|
+
const entries = inbox.list();
|
|
23069
|
+
if (entries.length === 0) return;
|
|
23070
|
+
const envelope = result;
|
|
23071
|
+
const target = envelope.__markdown_envelope === true && envelope.structured !== null && typeof envelope.structured === "object" && !Array.isArray(envelope.structured) ? envelope.structured : envelope;
|
|
23072
|
+
const existingMeta = target._meta && typeof target._meta === "object" && !Array.isArray(target._meta) ? target._meta : {};
|
|
23073
|
+
target._meta = {
|
|
23074
|
+
...existingMeta,
|
|
23075
|
+
notifications: entries
|
|
23076
|
+
};
|
|
23077
|
+
};
|
|
22122
23078
|
const isLeadbayBusinessError = (err) => err != null && typeof err === "object" && err.error === true && typeof err.code === "string";
|
|
22123
23079
|
const buildBusinessCtx = (toolName, envelope, triggered_by) => {
|
|
22124
23080
|
const meta = envelope._meta ?? {};
|
|
@@ -22238,11 +23194,13 @@ function buildServer(client, opts = {}) {
|
|
|
22238
23194
|
const result = await tool.execute(client, args, {
|
|
22239
23195
|
logger: opts.logger,
|
|
22240
23196
|
bulkTracker: opts.bulkTracker,
|
|
23197
|
+
notificationsInbox: opts.notificationsInbox,
|
|
22241
23198
|
signal: extra.signal,
|
|
22242
23199
|
progress,
|
|
22243
23200
|
elicit
|
|
22244
23201
|
});
|
|
22245
23202
|
maybeAttachUpdate(name, result);
|
|
23203
|
+
maybeAttachNotifications(result);
|
|
22246
23204
|
if (result && typeof result === "object" && result.error === true) {
|
|
22247
23205
|
const envText = formatErrorForLLM(result);
|
|
22248
23206
|
const envDur = Date.now() - callStart;
|
|
@@ -23680,7 +24638,7 @@ var OAUTH_BASE_URLS = {
|
|
|
23680
24638
|
fr: "https://staging.api.leadbay.app"
|
|
23681
24639
|
}
|
|
23682
24640
|
};
|
|
23683
|
-
var VERSION = "0.
|
|
24641
|
+
var VERSION = "0.18.1";
|
|
23684
24642
|
var HELP = `
|
|
23685
24643
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
23686
24644
|
|
|
@@ -24777,21 +25735,41 @@ async function main() {
|
|
|
24777
25735
|
logger.warn?.(`update_check.unexpected ${err?.message ?? err}`);
|
|
24778
25736
|
});
|
|
24779
25737
|
}
|
|
25738
|
+
const notificationsInbox = new NotificationsInbox();
|
|
25739
|
+
let notificationsWs = null;
|
|
25740
|
+
const WS_DISABLED = process.env.LEADBAY_NOTIFICATIONS_WS_DISABLED === "1" || authState !== "ok";
|
|
25741
|
+
if (!WS_DISABLED) {
|
|
25742
|
+
notificationsWs = new NotificationsWsClient({
|
|
25743
|
+
client,
|
|
25744
|
+
inbox: notificationsInbox,
|
|
25745
|
+
logger
|
|
25746
|
+
});
|
|
25747
|
+
void notificationsWs.start().catch((err) => {
|
|
25748
|
+
logger.warn?.(
|
|
25749
|
+
`notifications.ws start_failed: ${err?.message ?? err}`
|
|
25750
|
+
);
|
|
25751
|
+
});
|
|
25752
|
+
}
|
|
24780
25753
|
const server = buildServer(client, {
|
|
24781
25754
|
includeAdvanced,
|
|
24782
25755
|
includeWrite,
|
|
24783
25756
|
logger,
|
|
24784
25757
|
bulkTracker,
|
|
25758
|
+
notificationsInbox,
|
|
24785
25759
|
version: VERSION,
|
|
24786
25760
|
telemetry,
|
|
24787
25761
|
updateStateStore
|
|
24788
25762
|
});
|
|
24789
25763
|
const transport = new StdioServerTransport();
|
|
24790
25764
|
logger.info?.(
|
|
24791
|
-
`Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability}, auth_state=${authState})`
|
|
25765
|
+
`Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability}, notifications_ws=${WS_DISABLED ? "disabled" : "enabled"}, auth_state=${authState})`
|
|
24792
25766
|
);
|
|
24793
25767
|
await server.connect(transport);
|
|
24794
25768
|
const shutdown = async (code) => {
|
|
25769
|
+
try {
|
|
25770
|
+
notificationsWs?.stop();
|
|
25771
|
+
} catch {
|
|
25772
|
+
}
|
|
24795
25773
|
try {
|
|
24796
25774
|
await telemetry.shutdown();
|
|
24797
25775
|
} finally {
|