@leadbay/mcp 0.17.2 → 0.18.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +38 -1
- package/dist/bin.js +1524 -99
- package/dist/http-server.js +1185 -113
- package/dist/installer-electron.js +145 -200
- package/dist/installer-gui.js +77 -109
- package/package.json +3 -4
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) {
|
|
@@ -5102,6 +5119,8 @@ var init_composite_file_names = __esm({
|
|
|
5102
5119
|
"leadbay_import_leads",
|
|
5103
5120
|
"leadbay_import_status",
|
|
5104
5121
|
"leadbay_list_campaigns",
|
|
5122
|
+
"leadbay_my_lenses",
|
|
5123
|
+
"leadbay_new_lens",
|
|
5105
5124
|
"leadbay_prepare_outreach",
|
|
5106
5125
|
"leadbay_pull_followups",
|
|
5107
5126
|
"leadbay_pull_leads",
|
|
@@ -5120,8 +5139,306 @@ var init_composite_file_names = __esm({
|
|
|
5120
5139
|
}
|
|
5121
5140
|
});
|
|
5122
5141
|
|
|
5142
|
+
// ../core/dist/notifications/revise-hint.js
|
|
5143
|
+
function reviseHintFor(kind) {
|
|
5144
|
+
switch (kind) {
|
|
5145
|
+
case "bulk_enrich":
|
|
5146
|
+
return HINT_BULK_ENRICH;
|
|
5147
|
+
case "bulk_qualify":
|
|
5148
|
+
return HINT_BULK_QUALIFY;
|
|
5149
|
+
case "import":
|
|
5150
|
+
return HINT_IMPORT;
|
|
5151
|
+
default:
|
|
5152
|
+
return HINT_OTHER;
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
function inferKind(n) {
|
|
5156
|
+
if (n.links.some((l) => l.type === "bulk_enrichment"))
|
|
5157
|
+
return "bulk_enrich";
|
|
5158
|
+
if (n.file_import_id)
|
|
5159
|
+
return "import";
|
|
5160
|
+
if (n.bulk_progress)
|
|
5161
|
+
return "bulk_qualify";
|
|
5162
|
+
return "other";
|
|
5163
|
+
}
|
|
5164
|
+
function anchorIdFor(n, kind) {
|
|
5165
|
+
if (kind === "bulk_enrich") {
|
|
5166
|
+
const link = n.links.find((l) => l.type === "bulk_enrichment");
|
|
5167
|
+
return link ? String(link.id) : null;
|
|
5168
|
+
}
|
|
5169
|
+
if (kind === "import")
|
|
5170
|
+
return n.file_import_id;
|
|
5171
|
+
return null;
|
|
5172
|
+
}
|
|
5173
|
+
function toInboxEntry(n) {
|
|
5174
|
+
const kind = inferKind(n);
|
|
5175
|
+
return {
|
|
5176
|
+
notification_id: n.id,
|
|
5177
|
+
kind,
|
|
5178
|
+
anchor_id: anchorIdFor(n, kind),
|
|
5179
|
+
title: n.title,
|
|
5180
|
+
bulk_progress: n.bulk_progress,
|
|
5181
|
+
completed_at: n.updated_at,
|
|
5182
|
+
revise_hint: reviseHintFor(kind)
|
|
5183
|
+
};
|
|
5184
|
+
}
|
|
5185
|
+
var HINT_BULK_ENRICH, HINT_BULK_QUALIFY, HINT_IMPORT, HINT_OTHER;
|
|
5186
|
+
var init_revise_hint = __esm({
|
|
5187
|
+
"../core/dist/notifications/revise-hint.js"() {
|
|
5188
|
+
"use strict";
|
|
5189
|
+
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.";
|
|
5190
|
+
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.";
|
|
5191
|
+
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.";
|
|
5192
|
+
HINT_OTHER = "Background work just completed. If you referenced its subject in prior output, re-fetch the affected data and revise.";
|
|
5193
|
+
}
|
|
5194
|
+
});
|
|
5195
|
+
|
|
5196
|
+
// ../core/dist/notifications/inbox.js
|
|
5197
|
+
var DEFAULT_TTL_MS, NotificationsInbox;
|
|
5198
|
+
var init_inbox = __esm({
|
|
5199
|
+
"../core/dist/notifications/inbox.js"() {
|
|
5200
|
+
"use strict";
|
|
5201
|
+
init_revise_hint();
|
|
5202
|
+
DEFAULT_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
5203
|
+
NotificationsInbox = class {
|
|
5204
|
+
entries = /* @__PURE__ */ new Map();
|
|
5205
|
+
ttl_ms;
|
|
5206
|
+
now;
|
|
5207
|
+
constructor(opts = {}) {
|
|
5208
|
+
this.ttl_ms = opts.ttl_ms ?? DEFAULT_TTL_MS;
|
|
5209
|
+
this.now = opts.now ?? Date.now;
|
|
5210
|
+
}
|
|
5211
|
+
// Upsert by notification id. Latest write wins so duplicate arrivals
|
|
5212
|
+
// (WS event + REST catch-up landing the same row) collapse cleanly.
|
|
5213
|
+
record(n) {
|
|
5214
|
+
if (!n.bulk_progress)
|
|
5215
|
+
return;
|
|
5216
|
+
if (n.in_progress)
|
|
5217
|
+
return;
|
|
5218
|
+
const entry = toInboxEntry(n);
|
|
5219
|
+
this.entries.set(entry.notification_id, {
|
|
5220
|
+
entry,
|
|
5221
|
+
recordedAt: this.now()
|
|
5222
|
+
});
|
|
5223
|
+
}
|
|
5224
|
+
list() {
|
|
5225
|
+
this.expireStale();
|
|
5226
|
+
return [...this.entries.values()].map((e) => e.entry);
|
|
5227
|
+
}
|
|
5228
|
+
markSeen(notification_id) {
|
|
5229
|
+
this.entries.delete(notification_id);
|
|
5230
|
+
}
|
|
5231
|
+
size() {
|
|
5232
|
+
this.expireStale();
|
|
5233
|
+
return this.entries.size;
|
|
5234
|
+
}
|
|
5235
|
+
expireStale() {
|
|
5236
|
+
const cutoff = this.now() - this.ttl_ms;
|
|
5237
|
+
for (const [id, e] of this.entries) {
|
|
5238
|
+
if (e.recordedAt < cutoff)
|
|
5239
|
+
this.entries.delete(id);
|
|
5240
|
+
}
|
|
5241
|
+
}
|
|
5242
|
+
};
|
|
5243
|
+
}
|
|
5244
|
+
});
|
|
5245
|
+
|
|
5246
|
+
// ../core/dist/notifications/catch-up.js
|
|
5247
|
+
async function catchUpNotifications(client, inbox, opts = {}) {
|
|
5248
|
+
const count = opts.count ?? DEFAULT_COUNT;
|
|
5249
|
+
let added = 0;
|
|
5250
|
+
try {
|
|
5251
|
+
const page = await client.listNotifications({
|
|
5252
|
+
archived: false,
|
|
5253
|
+
page: 0,
|
|
5254
|
+
count
|
|
5255
|
+
});
|
|
5256
|
+
for (const n of page.items) {
|
|
5257
|
+
if (!n.bulk_progress)
|
|
5258
|
+
continue;
|
|
5259
|
+
if (n.in_progress)
|
|
5260
|
+
continue;
|
|
5261
|
+
if (n.first_seen_at)
|
|
5262
|
+
continue;
|
|
5263
|
+
const sizeBefore = inbox.size();
|
|
5264
|
+
inbox.record(n);
|
|
5265
|
+
if (inbox.size() > sizeBefore)
|
|
5266
|
+
added += 1;
|
|
5267
|
+
}
|
|
5268
|
+
opts.logger?.info?.(`notifications.catch_up scanned=${page.items.length} seeded=${added}`);
|
|
5269
|
+
} catch (err) {
|
|
5270
|
+
opts.logger?.warn?.(`notifications.catch_up failed: ${err?.message ?? err?.code ?? err}`);
|
|
5271
|
+
}
|
|
5272
|
+
return added;
|
|
5273
|
+
}
|
|
5274
|
+
var DEFAULT_COUNT;
|
|
5275
|
+
var init_catch_up = __esm({
|
|
5276
|
+
"../core/dist/notifications/catch-up.js"() {
|
|
5277
|
+
"use strict";
|
|
5278
|
+
DEFAULT_COUNT = 50;
|
|
5279
|
+
}
|
|
5280
|
+
});
|
|
5281
|
+
|
|
5282
|
+
// ../core/dist/notifications/ws-client.js
|
|
5283
|
+
var PING_INTERVAL_MS, RECONNECT_INITIAL_MS, RECONNECT_MAX_MS, NotificationsWsClient;
|
|
5284
|
+
var init_ws_client = __esm({
|
|
5285
|
+
"../core/dist/notifications/ws-client.js"() {
|
|
5286
|
+
"use strict";
|
|
5287
|
+
init_catch_up();
|
|
5288
|
+
PING_INTERVAL_MS = 3e4;
|
|
5289
|
+
RECONNECT_INITIAL_MS = 1e3;
|
|
5290
|
+
RECONNECT_MAX_MS = 3e4;
|
|
5291
|
+
NotificationsWsClient = class {
|
|
5292
|
+
client;
|
|
5293
|
+
inbox;
|
|
5294
|
+
logger;
|
|
5295
|
+
ws = null;
|
|
5296
|
+
pingTimer = null;
|
|
5297
|
+
reconnectTimer = null;
|
|
5298
|
+
reconnectDelay = RECONNECT_INITIAL_MS;
|
|
5299
|
+
stopped = false;
|
|
5300
|
+
constructor(opts) {
|
|
5301
|
+
this.client = opts.client;
|
|
5302
|
+
this.inbox = opts.inbox;
|
|
5303
|
+
this.logger = opts.logger;
|
|
5304
|
+
}
|
|
5305
|
+
async start() {
|
|
5306
|
+
this.stopped = false;
|
|
5307
|
+
await catchUpNotifications(this.client, this.inbox, { logger: this.logger });
|
|
5308
|
+
void this.connect();
|
|
5309
|
+
}
|
|
5310
|
+
stop() {
|
|
5311
|
+
this.stopped = true;
|
|
5312
|
+
if (this.pingTimer)
|
|
5313
|
+
clearInterval(this.pingTimer);
|
|
5314
|
+
if (this.reconnectTimer)
|
|
5315
|
+
clearTimeout(this.reconnectTimer);
|
|
5316
|
+
this.pingTimer = null;
|
|
5317
|
+
this.reconnectTimer = null;
|
|
5318
|
+
if (this.ws) {
|
|
5319
|
+
try {
|
|
5320
|
+
this.ws.close(1e3, "shutdown");
|
|
5321
|
+
} catch {
|
|
5322
|
+
}
|
|
5323
|
+
this.ws = null;
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
async connect() {
|
|
5327
|
+
if (this.stopped)
|
|
5328
|
+
return;
|
|
5329
|
+
let url;
|
|
5330
|
+
try {
|
|
5331
|
+
const ticket = await this.client.getWsTicket();
|
|
5332
|
+
url = ticket.url;
|
|
5333
|
+
} catch (err) {
|
|
5334
|
+
this.logger?.warn?.(`notifications.ws ticket_fetch_failed: ${err?.message ?? err?.code ?? err}`);
|
|
5335
|
+
this.scheduleReconnect();
|
|
5336
|
+
return;
|
|
5337
|
+
}
|
|
5338
|
+
let ws;
|
|
5339
|
+
try {
|
|
5340
|
+
ws = new WebSocket(url);
|
|
5341
|
+
} catch (err) {
|
|
5342
|
+
this.logger?.warn?.(`notifications.ws construct_failed: ${err?.message ?? err}`);
|
|
5343
|
+
this.scheduleReconnect();
|
|
5344
|
+
return;
|
|
5345
|
+
}
|
|
5346
|
+
this.ws = ws;
|
|
5347
|
+
ws.addEventListener("open", () => {
|
|
5348
|
+
this.logger?.info?.("notifications.ws connected");
|
|
5349
|
+
this.reconnectDelay = RECONNECT_INITIAL_MS;
|
|
5350
|
+
void catchUpNotifications(this.client, this.inbox, {
|
|
5351
|
+
logger: this.logger
|
|
5352
|
+
});
|
|
5353
|
+
this.pingTimer = setInterval(() => this.sendPing(), PING_INTERVAL_MS);
|
|
5354
|
+
});
|
|
5355
|
+
ws.addEventListener("message", (ev) => {
|
|
5356
|
+
const text = typeof ev.data === "string" ? ev.data : String(ev.data ?? "");
|
|
5357
|
+
if (!text)
|
|
5358
|
+
return;
|
|
5359
|
+
let msg;
|
|
5360
|
+
try {
|
|
5361
|
+
msg = JSON.parse(text);
|
|
5362
|
+
} catch {
|
|
5363
|
+
this.logger?.warn?.("notifications.ws non_json_frame");
|
|
5364
|
+
return;
|
|
5365
|
+
}
|
|
5366
|
+
this.handleMessage(msg);
|
|
5367
|
+
});
|
|
5368
|
+
ws.addEventListener("error", (ev) => {
|
|
5369
|
+
this.logger?.warn?.(`notifications.ws error: ${ev?.message ?? "(no detail)"}`);
|
|
5370
|
+
});
|
|
5371
|
+
ws.addEventListener("close", (ev) => {
|
|
5372
|
+
this.logger?.info?.(`notifications.ws closed code=${ev.code} reason=${ev.reason || "(none)"}`);
|
|
5373
|
+
if (this.pingTimer)
|
|
5374
|
+
clearInterval(this.pingTimer);
|
|
5375
|
+
this.pingTimer = null;
|
|
5376
|
+
this.ws = null;
|
|
5377
|
+
this.scheduleReconnect();
|
|
5378
|
+
});
|
|
5379
|
+
}
|
|
5380
|
+
handleMessage(msg) {
|
|
5381
|
+
if (msg.type === "pong")
|
|
5382
|
+
return;
|
|
5383
|
+
if (msg.type === "ping") {
|
|
5384
|
+
this.sendRaw({ type: "pong" });
|
|
5385
|
+
return;
|
|
5386
|
+
}
|
|
5387
|
+
if (msg.type !== "notification")
|
|
5388
|
+
return;
|
|
5389
|
+
const { type: _t, ...rest } = msg;
|
|
5390
|
+
void _t;
|
|
5391
|
+
const n = rest;
|
|
5392
|
+
if (n.bulk_progress == null || n.in_progress) {
|
|
5393
|
+
return;
|
|
5394
|
+
}
|
|
5395
|
+
this.inbox.record(n);
|
|
5396
|
+
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"}`);
|
|
5397
|
+
}
|
|
5398
|
+
sendPing() {
|
|
5399
|
+
this.sendRaw({ type: "ping" });
|
|
5400
|
+
}
|
|
5401
|
+
sendRaw(obj) {
|
|
5402
|
+
if (!this.ws)
|
|
5403
|
+
return;
|
|
5404
|
+
if (this.ws.readyState !== WebSocket.OPEN)
|
|
5405
|
+
return;
|
|
5406
|
+
try {
|
|
5407
|
+
this.ws.send(JSON.stringify(obj));
|
|
5408
|
+
} catch (err) {
|
|
5409
|
+
this.logger?.warn?.(`notifications.ws send_failed: ${err?.message ?? err}`);
|
|
5410
|
+
}
|
|
5411
|
+
}
|
|
5412
|
+
scheduleReconnect() {
|
|
5413
|
+
if (this.stopped)
|
|
5414
|
+
return;
|
|
5415
|
+
if (this.reconnectTimer)
|
|
5416
|
+
return;
|
|
5417
|
+
const delay = this.reconnectDelay;
|
|
5418
|
+
this.reconnectDelay = Math.min(this.reconnectDelay * 2, RECONNECT_MAX_MS);
|
|
5419
|
+
this.reconnectTimer = setTimeout(() => {
|
|
5420
|
+
this.reconnectTimer = null;
|
|
5421
|
+
void this.connect();
|
|
5422
|
+
}, delay);
|
|
5423
|
+
this.logger?.info?.(`notifications.ws reconnect_in_${delay}ms`);
|
|
5424
|
+
}
|
|
5425
|
+
};
|
|
5426
|
+
}
|
|
5427
|
+
});
|
|
5428
|
+
|
|
5429
|
+
// ../core/dist/notifications/index.js
|
|
5430
|
+
var init_notifications = __esm({
|
|
5431
|
+
"../core/dist/notifications/index.js"() {
|
|
5432
|
+
"use strict";
|
|
5433
|
+
init_inbox();
|
|
5434
|
+
init_ws_client();
|
|
5435
|
+
init_catch_up();
|
|
5436
|
+
init_revise_hint();
|
|
5437
|
+
}
|
|
5438
|
+
});
|
|
5439
|
+
|
|
5123
5440
|
// ../core/dist/tool-descriptions.generated.js
|
|
5124
|
-
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_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;
|
|
5441
|
+
var 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;
|
|
5125
5442
|
var init_tool_descriptions_generated = __esm({
|
|
5126
5443
|
"../core/dist/tool-descriptions.generated.js"() {
|
|
5127
5444
|
"use strict";
|
|
@@ -5160,9 +5477,52 @@ Show the user's account state \u2014 admin rights, language, last-active lens, c
|
|
|
5160
5477
|
|
|
5161
5478
|
**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.**
|
|
5162
5479
|
|
|
5480
|
+
**\`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.
|
|
5481
|
+
|
|
5482
|
+
## GATE \u2014 INSPECT \`_meta.notifications\` ON EVERY RESPONSE
|
|
5483
|
+
|
|
5484
|
+
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:
|
|
5485
|
+
|
|
5486
|
+
| Kind | What just finished |
|
|
5487
|
+
|---|---|
|
|
5488
|
+
| \`bulk_enrich\` | A bulk contact-enrichment job (emails / phone numbers attached to leads' contacts). |
|
|
5489
|
+
| \`bulk_qualify\` | A bulk lead-qualification job (web-fetch + AI rescore, producing \`ai_agent_lead_score\` and qualification answers). |
|
|
5490
|
+
| \`import\` | A CSV / CRM file import (rows resolved to leads in the user's pipeline). |
|
|
5491
|
+
|
|
5492
|
+
**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.
|
|
5493
|
+
|
|
5494
|
+
| Kind | Outputs you've produced that may now be stale \u2014 refresh them |
|
|
5495
|
+
|---|---|
|
|
5496
|
+
| \`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. |
|
|
5497
|
+
| \`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\`. |
|
|
5498
|
+
| \`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\`. |
|
|
5499
|
+
|
|
5500
|
+
**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.
|
|
5501
|
+
|
|
5502
|
+
**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.
|
|
5503
|
+
|
|
5504
|
+
**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.
|
|
5505
|
+
|
|
5506
|
+
Also surfaced as a top-level \`notifications\` array on \`leadbay_account_status\` \u2014 same shape, same handling.
|
|
5507
|
+
|
|
5508
|
+
|
|
5163
5509
|
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.
|
|
5164
5510
|
|
|
5165
5511
|
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.
|
|
5512
|
+
`;
|
|
5513
|
+
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.
|
|
5514
|
+
|
|
5515
|
+
**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.
|
|
5516
|
+
|
|
5517
|
+
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.
|
|
5518
|
+
|
|
5519
|
+
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.
|
|
5520
|
+
|
|
5521
|
+
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.
|
|
5522
|
+
|
|
5523
|
+
WHEN NOT TO USE: before doing the revision work; for general "mark all read" sweeps (call once per notification id you've actually consumed).
|
|
5524
|
+
|
|
5525
|
+
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\`.
|
|
5166
5526
|
`;
|
|
5167
5527
|
leadbay_add_leads_to_campaign = `## WHEN TO USE
|
|
5168
5528
|
|
|
@@ -5218,7 +5578,37 @@ WHEN NOT TO USE: to log an outreach action \u2014 use leadbay_report_outreach, w
|
|
|
5218
5578
|
|
|
5219
5579
|
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\`.
|
|
5220
5580
|
`;
|
|
5221
|
-
leadbay_adjust_audience =
|
|
5581
|
+
leadbay_adjust_audience = `## WHEN TO USE
|
|
5582
|
+
|
|
5583
|
+
Trigger phrases: "narrow the audience to <sector>", "add <sector> to my <name> lens", "remove <sector> from this lens", "only show me companies of <size>", "stop including <sector>", "broaden this lens to also include <sector>".
|
|
5584
|
+
|
|
5585
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
5586
|
+
|
|
5587
|
+
Do NOT use for: "create a new lens called X" \u2192 \`leadbay_new_lens\`; "make a new audience for Y" \u2192 \`leadbay_new_lens\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`; "focus on a kind of company beyond sector/size (e.g. 'hospitals running their own IT')" \u2192 \`leadbay_refine_prompt\`.
|
|
5588
|
+
|
|
5589
|
+
Prefer when: user wants to change an EXISTING lens's sectors/sizes. If the user NAMES a lens ('my Joinery lens'), you MUST pass lensName with that name \u2014 do NOT edit the active lens. To create a brand-new lens use leadbay_new_lens instead.
|
|
5590
|
+
|
|
5591
|
+
Examples that SHOULD invoke this tool:
|
|
5592
|
+
- "Add fintech to my Joinery lens."
|
|
5593
|
+
- "Narrow my audience to manufacturing companies, 50\u2013500 people."
|
|
5594
|
+
- "Stop including retail in this lens."
|
|
5595
|
+
|
|
5596
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
5597
|
+
- "Create a lens called Joinery for fintech."
|
|
5598
|
+
- "Show me my lenses."
|
|
5599
|
+
- "Focus on hospitals that run their own IT."
|
|
5600
|
+
|
|
5601
|
+
## RENDER (quick)
|
|
5602
|
+
|
|
5603
|
+
On \`applied\`: confirm the lens edited (name) + the sectors/sizes added as
|
|
5604
|
+
chips. On \`ambiguous_sectors\` / \`ambiguous_lens\` / \`lens_not_found\`: surface
|
|
5605
|
+
the candidates and ask the user to pick, then re-call with the id/exact name.
|
|
5606
|
+
|
|
5607
|
+
---
|
|
5608
|
+
|
|
5609
|
+
Restrict (or expand) the lens audience by sector / size. Free-text sectors are auto-resolved against the sector taxonomy; ambiguous matches are surfaced to the agent rather than guessed silently. Permission routing is hidden: the default lens auto-clones to a new user lens; an org-level lens defaults to a per-user draft (admins can override with \`save_for_org:true\`). Filter MERGES with existing criteria (unrelated criteria are not dropped).
|
|
5610
|
+
|
|
5611
|
+
**Targeting a lens \u2014 READ THIS.** By default this edits the user's ACTIVE lens. **If the user names a lens** ("add fintech to my **Joinery** lens", "in my Nordics lens, exclude retail"), you MUST pass \`lensName\` with that name (\`lensName:"Joinery"\`). Do NOT silently edit the active lens when a different one was named \u2014 that corrupts the wrong audience and is a top friction source. The name resolves against the user's lenses (case-insensitive, exact then unique-substring); it is edit-only and does NOT change which lens is active. An unmatched name returns \`status:"lens_not_found"\` with the lens list, and a name matching several returns \`status:"ambiguous_lens"\` with the candidates \u2014 surface them and re-call with the exact \`lensName\` or a \`lensId\`. Use \`leadbay_my_lenses\` if the user first wants to SEE or SWITCH lenses. To CREATE a brand-new lens, use \`leadbay_new_lens\` \u2014 not this tool.
|
|
5222
5612
|
|
|
5223
5613
|
WHEN TO USE: when the user wants to see different kinds of leads (sector / size / etc.).
|
|
5224
5614
|
|
|
@@ -6431,6 +6821,258 @@ WHEN TO USE: at the start of a session if no token is preconfigured (cfg.token /
|
|
|
6431
6821
|
WHEN NOT TO USE: if a token is already preconfigured \u2014 you'll just overwrite it. The user needs a Leadbay account first; they can register at https://wow.leadbay.ai/?register=true.
|
|
6432
6822
|
|
|
6433
6823
|
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\`.
|
|
6824
|
+
`;
|
|
6825
|
+
leadbay_my_lenses = `## WHEN TO USE
|
|
6826
|
+
|
|
6827
|
+
Trigger phrases: "show me my lenses", "list my lenses", "which audiences do I have", "switch to my <name> lens", "change lens", "rename my <name> lens to <X>", "set the description of my <name> lens", "delete my <name> lens", "remove this lens".
|
|
6828
|
+
|
|
6829
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
6830
|
+
|
|
6831
|
+
Do NOT use for: "narrow the audience" \u2192 \`leadbay_adjust_audience\`; "stop showing me <sector>" \u2192 \`leadbay_refine_prompt\`; "more leads on this lens" \u2192 \`leadbay_extend_lens\`; "show me today's leads" \u2192 \`leadbay_pull_leads\`.
|
|
6832
|
+
|
|
6833
|
+
Prefer when: user wants to SEE lenses, CHANGE which is active, RENAME one, or DELETE one \u2014 not edit a lens's sector/size criteria
|
|
6834
|
+
|
|
6835
|
+
Examples that SHOULD invoke this tool:
|
|
6836
|
+
- "Show me my lenses."
|
|
6837
|
+
- "Rename my Auto lens to Automotive and add a description."
|
|
6838
|
+
- "Delete my old Auto lens."
|
|
6839
|
+
|
|
6840
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
6841
|
+
- "Narrow the audience to fintech only."
|
|
6842
|
+
- "I want more leads on this lens."
|
|
6843
|
+
- "Show me today's leads."
|
|
6844
|
+
|
|
6845
|
+
## RENDER (quick)
|
|
6846
|
+
|
|
6847
|
+
Small markdown table, active lens first: col 1 = \u2B50 prefix when active +
|
|
6848
|
+
lens name; col 2 = description (or \`\u2014\`). After a switch lead with
|
|
6849
|
+
"Now showing **<name>**."; after a rename lead with the rename confirmation.
|
|
6850
|
+
Full algorithm below.
|
|
6851
|
+
|
|
6852
|
+
---
|
|
6853
|
+
|
|
6854
|
+
List the user's lenses (saved audiences) and, when asked, switch which one is active. A lens shapes the kind of leads delivered each day; this tool is how the user sees their audiences and moves between them \u2014 it does NOT edit a lens's criteria (that's \`leadbay_adjust_audience\`).
|
|
6855
|
+
|
|
6856
|
+
**Three modes, one tool:**
|
|
6857
|
+
|
|
6858
|
+
- **List (no args)** \u2014 pure read. Returns \`{status:"listed", lenses:[{id, name, description, is_active}], active_lens_id}\`. The active lens is resolved from the user's last-requested lens, so \`is_active\` is authoritative even if a row's flag is stale.
|
|
6859
|
+
- **Switch (\`switchToLensId\`)** \u2014 changes the active lens to that id and returns the REFRESHED list. The id MUST be one of the user's lenses; an unknown id returns \`{status:"not_found"}\` with the current list \u2014 surface it and ask the user to pick, do NOT invent an id. Switching to the already-active lens is a harmless no-op.
|
|
6860
|
+
- **Edit (\`editLensId\` + \`newName\` and/or \`newDescription\`)** \u2014 rename and/or set the description of a lens in one call, returns the REFRESHED list. Provide either or both; pass \`newDescription:""\` to clear a description. Same not_found handling. Use the \`id\` from the list for the lens the user named.
|
|
6861
|
+
- **Delete (\`deleteLensId\`)** \u2014 DESTRUCTIVE and confirm-gated. Without \`confirm:true\` it returns \`status:"delete_preview"\` with \`will_delete\` and removes NOTHING \u2014 show it, get the user's explicit yes, then re-call with \`confirm:true\`. The DEFAULT lens cannot be deleted (\`status:"cannot_delete_default"\`). Deleting the active lens leaves no active lens until the next switch/pull resolves one.
|
|
6862
|
+
|
|
6863
|
+
**Lens ids are strings** (e.g. \`"40005"\`) \u2014 pass the \`id\` value straight from the list when switching/renaming/deleting; it is fine to pass it as the string it came as.
|
|
6864
|
+
|
|
6865
|
+
**When the user is vague** ("switch lens" with no target), list first, then offer the lenses as a quick choice via \`ask_user_input_v0\` rather than guessing.
|
|
6866
|
+
|
|
6867
|
+
WHEN TO USE: when the user wants to see their lenses or switch the active one. Canonical phrasings: "show me my lenses", "which audiences do I have", "switch to my <name> lens".
|
|
6868
|
+
|
|
6869
|
+
WHEN NOT TO USE: to change a lens's audience criteria \u2014 that's \`leadbay_adjust_audience\`. Not for refining beyond firmographics (\`leadbay_refine_prompt\`), not for topping up the same lens (\`leadbay_extend_lens\`), not for the daily pull (\`leadbay_pull_leads\`).
|
|
6870
|
+
|
|
6871
|
+
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\`.
|
|
6872
|
+
|
|
6873
|
+
|
|
6874
|
+
## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
|
|
6875
|
+
|
|
6876
|
+
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.
|
|
6877
|
+
|
|
6878
|
+
**The Big Three** \u2014 when a tool result fits, route there:
|
|
6879
|
+
|
|
6880
|
+
| Host widget | Use when | Field map (from Leadbay payload) |
|
|
6881
|
+
|---|---|---|
|
|
6882
|
+
| \`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 |
|
|
6883
|
+
| \`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") |
|
|
6884
|
+
| \`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 |
|
|
6885
|
+
|
|
6886
|
+
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.
|
|
6887
|
+
|
|
6888
|
+
**Rules:**
|
|
6889
|
+
- 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.
|
|
6890
|
+
- Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
|
|
6891
|
+
- 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.
|
|
6892
|
+
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
6893
|
+
|
|
6894
|
+
|
|
6895
|
+
---
|
|
6896
|
+
|
|
6897
|
+
## RENDERING \u2014 lenses table, active-first
|
|
6898
|
+
|
|
6899
|
+
Markdown table with TWO columns. Sort **active lens first**, then by \`name\`
|
|
6900
|
+
ascending. **No score bar** \u2014 the \`\u25B0\u2756\u25B1\` glyph identity belongs to lead
|
|
6901
|
+
discovery, not lenses.
|
|
6902
|
+
|
|
6903
|
+
**Column 1 \u2014 Lens**
|
|
6904
|
+
- Prefix \`\u2B50 \` when \`is_active\` is true; otherwise no prefix.
|
|
6905
|
+
- The lens name in **bold**. (Lenses have no public URL \u2014 do not fabricate a link.)
|
|
6906
|
+
|
|
6907
|
+
**Column 2 \u2014 Description**
|
|
6908
|
+
- \`description\` verbatim, clipped to \u2264 18 words.
|
|
6909
|
+
- When null/empty: render \`\u2014\`.
|
|
6910
|
+
|
|
6911
|
+
**After a \`switched: true\` response**, open with a single confirmation line
|
|
6912
|
+
ABOVE the table: \`Now showing **<name>**.\` For \`status: "not_found"\`, lead with
|
|
6913
|
+
the \`message\` (the bad id) and render the list so the user can pick a real one.
|
|
6914
|
+
|
|
6915
|
+
**Empty list** (\`lenses: []\`): render \`*You don't have any lenses yet.*\` \u2014 do not
|
|
6916
|
+
render an empty table.
|
|
6917
|
+
|
|
6918
|
+
**Legend:** \u2B50 active lens.
|
|
6919
|
+
|
|
6920
|
+
|
|
6921
|
+
## NEXT STEPS \u2014 after \`leadbay_my_lenses\`
|
|
6922
|
+
|
|
6923
|
+
**RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
|
|
6924
|
+
|
|
6925
|
+
The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
|
|
6926
|
+
|
|
6927
|
+
\`\`\`
|
|
6928
|
+
ask_user_input_v0({
|
|
6929
|
+
questions: [{
|
|
6930
|
+
question: "What next?",
|
|
6931
|
+
type: "single_select",
|
|
6932
|
+
options: [
|
|
6933
|
+
"<Suggest column from row 1>",
|
|
6934
|
+
"<Suggest column from row 2>",
|
|
6935
|
+
"<Suggest column from row 3>"
|
|
6936
|
+
]
|
|
6937
|
+
}]
|
|
6938
|
+
})
|
|
6939
|
+
\`\`\`
|
|
6940
|
+
|
|
6941
|
+
When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
|
|
6942
|
+
|
|
6943
|
+
**Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
|
|
6944
|
+
|
|
6945
|
+
---
|
|
6946
|
+
|
|
6947
|
+
|
|
6948
|
+
|
|
6949
|
+
Pick the 2\u20133 rows that fit what the user is likely to want next. When the user
|
|
6950
|
+
named no target but wants to switch, offer the lenses themselves as the
|
|
6951
|
+
quick-select options (each option = a lens name \u2192 \`leadbay_my_lenses(switchToLensId=<id>)\`).
|
|
6952
|
+
|
|
6953
|
+
| Observation | Suggest | Calls |
|
|
6954
|
+
|--------------------------------------|------------------------------------------|------------------------------------------------------|
|
|
6955
|
+
| User wants a different lens | "Switch to <lens name>" | \`leadbay_my_lenses(switchToLensId=<id>)\` |
|
|
6956
|
+
| User wants to rename / describe a lens| "Rename or describe <lens>" | \`leadbay_my_lenses(editLensId=<id>, newName?=<X>, newDescription?=<Y>)\` |
|
|
6957
|
+
| User wants to delete a lens | "Delete <lens>" | \`leadbay_my_lenses(deleteLensId=<id>)\` \u2192 confirm \u2192 \`confirm=true\` |
|
|
6958
|
+
| \`delete_preview\` (not yet deleted) | "Yes, delete it" | \`leadbay_my_lenses(deleteLensId=<id>, confirm=true)\` |
|
|
6959
|
+
| User wants leads on the active lens | "Pull today's leads" | \`leadbay_pull_leads()\` |
|
|
6960
|
+
| User wants to change the audience | "Adjust this lens's audience" | \`leadbay_adjust_audience(...)\` |
|
|
6961
|
+
| User wants more of the same | "Get a bigger batch on this lens" | \`leadbay_extend_lens(...)\` |
|
|
6962
|
+
|
|
6963
|
+
If nothing fits, default to "pull today's leads on the active lens" \u2014 never
|
|
6964
|
+
invent a tool that doesn't exist.
|
|
6965
|
+
`;
|
|
6966
|
+
leadbay_new_lens = `## WHEN TO USE
|
|
6967
|
+
|
|
6968
|
+
Trigger phrases: "create a lens", "create a new lens called <name>", "create a lens specialized in/into <X>", "make me a new audience for <X>", "set up a lens for <sector>", "new lens named <name>", "I want a lens just for <X>".
|
|
6969
|
+
|
|
6970
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
6971
|
+
|
|
6972
|
+
Do NOT use for: "narrow the audience / add or remove a sector on an EXISTING lens" \u2192 \`leadbay_adjust_audience\`; "add <sector> to my <name> lens" \u2192 \`leadbay_adjust_audience\`; "focus on a qualitative trait beyond sector/size" \u2192 \`leadbay_refine_prompt\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`; "more leads on this lens" \u2192 \`leadbay_extend_lens\`.
|
|
6973
|
+
|
|
6974
|
+
Prefer when: user wants a brand-new lens (create/make/set up, often 'specialized in <X>'). Editing an existing lens \u2192 leadbay_adjust_audience (use lensName). Qualitative refinement \u2192 refine_prompt (admin-only).
|
|
6975
|
+
|
|
6976
|
+
Examples that SHOULD invoke this tool:
|
|
6977
|
+
- "Create a lens called Joinery for the fintech sector."
|
|
6978
|
+
- "Make me a new audience for healthcare companies, 30\u2013300 people."
|
|
6979
|
+
- "Set up a new lens named Nordics SaaS."
|
|
6980
|
+
|
|
6981
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
6982
|
+
- "Add fintech to my Joinery lens."
|
|
6983
|
+
- "Show me my lenses."
|
|
6984
|
+
- "I want more leads on this lens."
|
|
6985
|
+
|
|
6986
|
+
## RENDER (quick)
|
|
6987
|
+
|
|
6988
|
+
On \`preview\` (default \u2014 NOTHING created yet): show the lens that WILL be
|
|
6989
|
+
created (name + resolved sectors/sizes as chips) and ASK the user to confirm
|
|
6990
|
+
via ask_user_input_v0 ("Create this lens?" / "Change something"). Only on
|
|
6991
|
+
"yes" re-call with confirm:true. On \`created\`: confirm "Created **<name>**."
|
|
6992
|
+
On \`ambiguous_sectors\`: surface the candidate sectors to pick from.
|
|
6993
|
+
|
|
6994
|
+
---
|
|
6995
|
+
|
|
6996
|
+
Create a brand-new lens (saved audience) and apply its sector/size criteria. Clones a base lens (the user's active/default lens unless \`base\` is given), names it, and applies the filter.
|
|
6997
|
+
|
|
6998
|
+
**Confirm before creating \u2014 two-step by default.** A call WITHOUT \`confirm:true\` is a dry run: it resolves the sectors/sizes and returns \`status:"preview"\` with \`will_create\` (what it WOULD build) \u2014 **nothing is created**. Show that to the user, get an explicit yes (ask via \`ask_user_input_v0\`), then re-call the SAME args with \`confirm:true\` to actually create. Never pass \`confirm:true\` on the first call \u2014 the user must see the preview first. (Sector ambiguity is still surfaced in the preview step, so they pick before confirming.)
|
|
6999
|
+
|
|
7000
|
+
**Sectors resolve first.** Free-text \`sectors\`/\`exclude_sectors\` are auto-resolved against the taxonomy. If any don't resolve, the tool returns \`status:"ambiguous_sectors"\` with the candidates and **does NOT create the lens** \u2014 so re-calling after picking the right sector won't leave orphan half-built lenses. To discover valid sector labels up front, use \`leadbay_list_sectors\`.
|
|
7001
|
+
|
|
7002
|
+
**Does not switch the active lens.** The new lens is created but the user stays on their current one. Offer \`leadbay_my_lenses(switchToLensId=<new id>)\` as a next step if they want to start pulling from it.
|
|
7003
|
+
|
|
7004
|
+
WHEN TO USE: when the user wants a NEW lens. Canonical phrasings: "create a lens called X", "make a new audience for Y", "set up a lens for <sector>".
|
|
7005
|
+
|
|
7006
|
+
WHEN NOT TO USE: to EDIT an existing lens \u2014 use \`leadbay_adjust_audience\` (pass \`lensName\` to target one by name). Not for listing/switching (\`leadbay_my_lenses\`) or topping up (\`leadbay_extend_lens\`).
|
|
7007
|
+
|
|
7008
|
+
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\`.
|
|
7009
|
+
|
|
7010
|
+
|
|
7011
|
+
## GATE \u2014 PREFER BUILT-IN HOST WIDGETS
|
|
7012
|
+
|
|
7013
|
+
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.
|
|
7014
|
+
|
|
7015
|
+
**The Big Three** \u2014 when a tool result fits, route there:
|
|
7016
|
+
|
|
7017
|
+
| Host widget | Use when | Field map (from Leadbay payload) |
|
|
7018
|
+
|---|---|---|
|
|
7019
|
+
| \`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 |
|
|
7020
|
+
| \`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") |
|
|
7021
|
+
| \`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 |
|
|
7022
|
+
|
|
7023
|
+
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.
|
|
7024
|
+
|
|
7025
|
+
**Rules:**
|
|
7026
|
+
- 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.
|
|
7027
|
+
- Pass identifiers (place_id, lead.id, contact_id) verbatim. Don't rewrite.
|
|
7028
|
+
- 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.
|
|
7029
|
+
- One short intro sentence in chat is enough \u2014 "Here are your 5 NYC follow-ups." Then route into the widget.
|
|
7030
|
+
|
|
7031
|
+
|
|
7032
|
+
---
|
|
7033
|
+
|
|
7034
|
+
## NEXT STEPS \u2014 after \`leadbay_new_lens\`
|
|
7035
|
+
|
|
7036
|
+
**RENDER NEXT STEPS via \`ask_user_input_v0\` when the host exposes it.**
|
|
7037
|
+
|
|
7038
|
+
The (Observation, Suggest, Calls) table below is the source of truth for which moves are valid. Pick the 2\u20134 most relevant rows based on what the response actually contains, then surface them as a \`single_select\` quick-select widget:
|
|
7039
|
+
|
|
7040
|
+
\`\`\`
|
|
7041
|
+
ask_user_input_v0({
|
|
7042
|
+
questions: [{
|
|
7043
|
+
question: "What next?",
|
|
7044
|
+
type: "single_select",
|
|
7045
|
+
options: [
|
|
7046
|
+
"<Suggest column from row 1>",
|
|
7047
|
+
"<Suggest column from row 2>",
|
|
7048
|
+
"<Suggest column from row 3>"
|
|
7049
|
+
]
|
|
7050
|
+
}]
|
|
7051
|
+
})
|
|
7052
|
+
\`\`\`
|
|
7053
|
+
|
|
7054
|
+
When the user picks an option, you call the matching tool from the \`Calls\` column. Constraints carried over from the widget contract: 2\u20134 mutually-exclusive options per question, button-sized labels (\u22646 words), max 3 questions per call.
|
|
7055
|
+
|
|
7056
|
+
**Fallback prose mode** \u2014 when the host doesn't expose \`ask_user_input_v0\` (or it returned an error): surface the same 2\u20133 picks as a short bulleted list of "Suggest" phrasings. The table itself stays internal; never recite the whole table to the user.
|
|
7057
|
+
|
|
7058
|
+
---
|
|
7059
|
+
|
|
7060
|
+
|
|
7061
|
+
|
|
7062
|
+
Pick the rows that fit. On \`created\`, the switch + pull rows are the natural
|
|
7063
|
+
follow-ups. On \`ambiguous_sectors\`, the only move is to pick a sector and re-call.
|
|
7064
|
+
|
|
7065
|
+
| Observation | Suggest | Calls |
|
|
7066
|
+
|-----------------------------------|------------------------------------------|--------------------------------------------------------|
|
|
7067
|
+
| \`preview\` (not yet created) | "Yes, create this lens" | \`leadbay_new_lens(...same args..., confirm=true)\` |
|
|
7068
|
+
| \`preview\` (not yet created) | "Change the sectors/size first" | (re-ask the user, then \`leadbay_new_lens\` with new args) |
|
|
7069
|
+
| Lens created | "Switch to it and pull leads" | \`leadbay_my_lenses(switchToLensId=<new id>)\` then \`leadbay_pull_leads()\` |
|
|
7070
|
+
| Lens created | "Refine the audience further" | \`leadbay_adjust_audience(lensName=<new name>, ...)\` |
|
|
7071
|
+
| Lens created | "Leave it; keep my current lens active" | (no call) |
|
|
7072
|
+
| \`ambiguous_sectors\` | "Pick the right sector and create" | \`leadbay_new_lens(name=..., sectors=[<chosen id>])\` |
|
|
7073
|
+
|
|
7074
|
+
If nothing fits, default to "switch to the new lens and pull leads" \u2014 never
|
|
7075
|
+
invent a tool that doesn't exist.
|
|
6434
7076
|
`;
|
|
6435
7077
|
leadbay_open_billing_portal = `Generate a one-shot Stripe customer-portal URL. Wraps \`GET /1.5/stripe/portal\` \u2192 \`{url}\`. Returns a fresh Stripe-hosted URL the user can open to manage their existing Leadbay subscription: change plan tier, swap payment method, view invoices. The agent does NOT make subscription changes itself \u2014 it surfaces the URL and lets the user act.
|
|
6436
7078
|
|
|
@@ -7064,7 +7706,33 @@ WHEN TO USE: before leadbay_enrich_titles, to plan which titles to order.
|
|
|
7064
7706
|
|
|
7065
7707
|
WHEN NOT TO USE: when you already know the exact titles you want to enrich.
|
|
7066
7708
|
`;
|
|
7067
|
-
leadbay_refine_prompt =
|
|
7709
|
+
leadbay_refine_prompt = `## WHEN TO USE
|
|
7710
|
+
|
|
7711
|
+
Trigger phrases: "focus on companies that <qualitative trait>", "I prefer leads that <behavior/characteristic>", "prioritize companies running their own IT", "deprioritize companies that just raised".
|
|
7712
|
+
|
|
7713
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
7714
|
+
|
|
7715
|
+
Do NOT use for: "create a new lens / a lens specialized into <X>" \u2192 \`leadbay_new_lens\`; "add/remove <sector> to/from my <name> lens" \u2192 \`leadbay_adjust_audience\`; "narrow the audience to <sector> / <size>" \u2192 \`leadbay_adjust_audience\`; "show me / list / switch my lenses" \u2192 \`leadbay_my_lenses\`.
|
|
7716
|
+
|
|
7717
|
+
Prefer when: ADMIN-ONLY. Qualitative refinement of the active lens that sector/size can't express. Creating/naming/listing/switching/sector-editing a lens routes elsewhere. Non-admin user \u2192 do NOT pick this.
|
|
7718
|
+
|
|
7719
|
+
Examples that SHOULD invoke this tool:
|
|
7720
|
+
- "Focus on hospitals that run their own IT in-house."
|
|
7721
|
+
- "Prioritize companies that have recently expanded headcount."
|
|
7722
|
+
|
|
7723
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
7724
|
+
- "Create a lens specialized in automobile."
|
|
7725
|
+
- "Add fintech to my Joinery lens."
|
|
7726
|
+
- "Show me my lenses."
|
|
7727
|
+
|
|
7728
|
+
## RENDER (quick)
|
|
7729
|
+
|
|
7730
|
+
On success: confirm the refinement applied to the active lens. If a
|
|
7731
|
+
clarification was raised, surface its question (route via ask_user_input_v0).
|
|
7732
|
+
|
|
7733
|
+
---
|
|
7734
|
+
|
|
7735
|
+
Refine the kind of leads Leadbay surfaces, beyond firmographics. Free-text instruction (e.g. "focus on hospitals running their own IT"). Sets the org's \`user_prompt\`; if the new prompt produces ambiguous criteria, Leadbay raises a clarification question, which this composite polls for and surfaces. Admin-only on the backend (will return 403 for non-admins).
|
|
7068
7736
|
|
|
7069
7737
|
WHEN TO USE: when audience filters (leadbay_adjust_audience) aren't enough.
|
|
7070
7738
|
|
|
@@ -9889,9 +10557,23 @@ async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseB
|
|
|
9889
10557
|
await pollPreprocess(client, importId, phaseBudget, ctx, signal);
|
|
9890
10558
|
ctx?.logger?.info?.(`import-leads: preprocess done for importId=${importId}`);
|
|
9891
10559
|
if (dryRun) {
|
|
9892
|
-
return { importId, records: [] };
|
|
10560
|
+
return { importId, records: [], notification_id: null };
|
|
10561
|
+
}
|
|
10562
|
+
let updateMappingsResp = null;
|
|
10563
|
+
try {
|
|
10564
|
+
updateMappingsResp = await client.request("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
10565
|
+
} catch (err) {
|
|
10566
|
+
if (err?.code === "API_ERROR" || err?.code === "NOT_FOUND") {
|
|
10567
|
+
ctx?.logger?.warn?.(`import-leads: update_mappings raw error (${err?.code}); retrying void`);
|
|
10568
|
+
await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
10569
|
+
} else {
|
|
10570
|
+
throw err;
|
|
10571
|
+
}
|
|
10572
|
+
}
|
|
10573
|
+
const importNotificationId = updateMappingsResp?.notification_id ?? null;
|
|
10574
|
+
if (importNotificationId) {
|
|
10575
|
+
ctx?.logger?.info?.(`import-leads: notification_id=${importNotificationId} importId=${importId}`);
|
|
9893
10576
|
}
|
|
9894
|
-
await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
9895
10577
|
ctx?.logger?.info?.(`import-leads: mappings committed for importId=${importId}`);
|
|
9896
10578
|
const phaseBudget2 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
9897
10579
|
await pollProcess(client, importId, phaseBudget2, ctx, signal);
|
|
@@ -9899,7 +10581,7 @@ async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseB
|
|
|
9899
10581
|
const phaseBudget3 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
9900
10582
|
const records = await pollRecordsToTerminal(client, importId, phaseBudget3, chunk.length, ctx, signal);
|
|
9901
10583
|
ctx?.logger?.info?.(`import-leads: ${records.length} records terminal for importId=${importId}`);
|
|
9902
|
-
return { importId, records };
|
|
10584
|
+
return { importId, records, notification_id: importNotificationId };
|
|
9903
10585
|
}
|
|
9904
10586
|
function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
9905
10587
|
const seenInputIndex = /* @__PURE__ */ new Set();
|
|
@@ -9955,7 +10637,7 @@ function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
|
9955
10637
|
}
|
|
9956
10638
|
}
|
|
9957
10639
|
}
|
|
9958
|
-
function buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled) {
|
|
10640
|
+
function buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds) {
|
|
9959
10641
|
const leads = [];
|
|
9960
10642
|
const not_imported = [];
|
|
9961
10643
|
if (dryRun) {
|
|
@@ -10020,6 +10702,7 @@ function buildImportLeadsResult(client, prep, importIds, matched, notImported, d
|
|
|
10020
10702
|
leads,
|
|
10021
10703
|
not_imported,
|
|
10022
10704
|
importIds,
|
|
10705
|
+
notification_ids: notificationIds,
|
|
10023
10706
|
region: client.region,
|
|
10024
10707
|
cancelled: cancelled || void 0,
|
|
10025
10708
|
dry_run: dryRun || void 0,
|
|
@@ -10045,17 +10728,21 @@ async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, ha
|
|
|
10045
10728
|
void (async () => {
|
|
10046
10729
|
const bgCtx = { logger: ctx.logger, bulkTracker: tracker };
|
|
10047
10730
|
const importIds = uploadedChunks.map((chunk) => chunk.importId);
|
|
10731
|
+
const notificationIds = [];
|
|
10048
10732
|
const matched = /* @__PURE__ */ new Map();
|
|
10049
10733
|
const notImported = /* @__PURE__ */ new Map();
|
|
10050
10734
|
try {
|
|
10051
10735
|
const totalDeadline = Date.now() + opts.totalBudget;
|
|
10052
10736
|
for (const upload of uploadedChunks) {
|
|
10053
10737
|
const out = await completeUploadedChunk(client, upload, prep.mappings, opts.dryRun, opts.perPhaseBudget, totalDeadline, bgCtx, void 0);
|
|
10738
|
+
if (out.notification_id && !notificationIds.includes(out.notification_id)) {
|
|
10739
|
+
notificationIds.push(out.notification_id);
|
|
10740
|
+
}
|
|
10054
10741
|
if (!opts.dryRun) {
|
|
10055
10742
|
reconcileOneChunk(prep, out, matched, notImported);
|
|
10056
10743
|
}
|
|
10057
10744
|
}
|
|
10058
|
-
const result = buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false);
|
|
10745
|
+
const result = buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false, notificationIds);
|
|
10059
10746
|
await tracker.markImportComplete(handleId, {
|
|
10060
10747
|
leads: result.leads,
|
|
10061
10748
|
not_imported: result.not_imported,
|
|
@@ -10288,6 +10975,7 @@ var init_import_leads = __esm({
|
|
|
10288
10975
|
leads: [],
|
|
10289
10976
|
not_imported,
|
|
10290
10977
|
importIds: [],
|
|
10978
|
+
notification_ids: [],
|
|
10291
10979
|
region: client.region,
|
|
10292
10980
|
dry_run: dryRun || void 0,
|
|
10293
10981
|
_meta: client.lastMeta ?? {
|
|
@@ -10342,6 +11030,10 @@ var init_import_leads = __esm({
|
|
|
10342
11030
|
status: "running",
|
|
10343
11031
|
handle_id: reservation.record.bulk_id,
|
|
10344
11032
|
importIds: importIds2,
|
|
11033
|
+
// Notifications fire from update_mappings, which the background
|
|
11034
|
+
// task hasn't called yet at this point. They surface via the WS
|
|
11035
|
+
// listener / catch-up REST on subsequent agent turns.
|
|
11036
|
+
notification_ids: [],
|
|
10345
11037
|
progress: {
|
|
10346
11038
|
phase: reservation.record.status === "complete" ? "complete" : importIds2.length > 0 ? "preprocess" : "queued",
|
|
10347
11039
|
records_processed: reservation.record.status === "complete" ? reservation.record.records_total : 0,
|
|
@@ -10362,6 +11054,7 @@ var init_import_leads = __esm({
|
|
|
10362
11054
|
}
|
|
10363
11055
|
ctx?.logger?.info?.(`import-leads(${prep.mode}): ${prep.validInputs.length} rows \u2192 ${chunks.length} chunk(s); dry_run=${dryRun}, totalBudgetMs=${totalBudget}`);
|
|
10364
11056
|
const importIds = [];
|
|
11057
|
+
const notificationIds = [];
|
|
10365
11058
|
const matched = /* @__PURE__ */ new Map();
|
|
10366
11059
|
const notImported = /* @__PURE__ */ new Map();
|
|
10367
11060
|
let cancelled = false;
|
|
@@ -10373,6 +11066,9 @@ var init_import_leads = __esm({
|
|
|
10373
11066
|
for (let i = 0; i < chunks.length; i++) {
|
|
10374
11067
|
const chunk = chunks[i];
|
|
10375
11068
|
const out = await runOneChunk(client, chunk, i, chunks.length, prep.header, prep.mappings, dryRun, perPhaseBudget, totalDeadline, ctx, signal, recordImportId);
|
|
11069
|
+
if (out.notification_id && !notificationIds.includes(out.notification_id)) {
|
|
11070
|
+
notificationIds.push(out.notification_id);
|
|
11071
|
+
}
|
|
10376
11072
|
if (!dryRun) {
|
|
10377
11073
|
reconcileOneChunk(prep, out, matched, notImported);
|
|
10378
11074
|
}
|
|
@@ -10393,7 +11089,7 @@ var init_import_leads = __esm({
|
|
|
10393
11089
|
throw err;
|
|
10394
11090
|
}
|
|
10395
11091
|
}
|
|
10396
|
-
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled);
|
|
11092
|
+
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds);
|
|
10397
11093
|
}
|
|
10398
11094
|
};
|
|
10399
11095
|
}
|
|
@@ -11101,6 +11797,70 @@ var init_agent_memory_review = __esm({
|
|
|
11101
11797
|
}
|
|
11102
11798
|
});
|
|
11103
11799
|
|
|
11800
|
+
// ../core/dist/tools/acknowledge-notification.js
|
|
11801
|
+
var UUID_RE, acknowledgeNotification;
|
|
11802
|
+
var init_acknowledge_notification = __esm({
|
|
11803
|
+
"../core/dist/tools/acknowledge-notification.js"() {
|
|
11804
|
+
"use strict";
|
|
11805
|
+
init_tool_descriptions_generated();
|
|
11806
|
+
UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11807
|
+
acknowledgeNotification = {
|
|
11808
|
+
name: "leadbay_acknowledge_notification",
|
|
11809
|
+
annotations: {
|
|
11810
|
+
title: "Acknowledge a Leadbay notification",
|
|
11811
|
+
readOnlyHint: false,
|
|
11812
|
+
destructiveHint: false,
|
|
11813
|
+
idempotentHint: true,
|
|
11814
|
+
openWorldHint: true
|
|
11815
|
+
},
|
|
11816
|
+
description: leadbay_acknowledge_notification,
|
|
11817
|
+
write: true,
|
|
11818
|
+
inputSchema: {
|
|
11819
|
+
type: "object",
|
|
11820
|
+
properties: {
|
|
11821
|
+
notification_id: {
|
|
11822
|
+
type: "string",
|
|
11823
|
+
description: "UUID of the notification to acknowledge. Use the notification_id from `_meta.notifications[]` or `account_status.notifications[]`."
|
|
11824
|
+
},
|
|
11825
|
+
archive: {
|
|
11826
|
+
type: "boolean",
|
|
11827
|
+
description: "If true, archive the notification (won't appear in `archived=false` listings). If false / omitted, mark seen (resets firstSeenAt)."
|
|
11828
|
+
}
|
|
11829
|
+
},
|
|
11830
|
+
required: ["notification_id"],
|
|
11831
|
+
additionalProperties: false
|
|
11832
|
+
},
|
|
11833
|
+
outputSchema: {
|
|
11834
|
+
type: "object",
|
|
11835
|
+
properties: {
|
|
11836
|
+
acknowledged: { type: "boolean" },
|
|
11837
|
+
notification_id: { type: "string" },
|
|
11838
|
+
action: { type: "string", enum: ["seen", "archive"] }
|
|
11839
|
+
},
|
|
11840
|
+
required: ["acknowledged", "notification_id", "action"]
|
|
11841
|
+
},
|
|
11842
|
+
execute: async (client, params, ctx) => {
|
|
11843
|
+
if (!UUID_RE.test(params.notification_id)) {
|
|
11844
|
+
return {
|
|
11845
|
+
error: true,
|
|
11846
|
+
code: "BAD_INPUT",
|
|
11847
|
+
message: "notification_id must be a UUID",
|
|
11848
|
+
hint: "Pass the notification_id verbatim from _meta.notifications[].notification_id or account_status.notifications[].notification_id."
|
|
11849
|
+
};
|
|
11850
|
+
}
|
|
11851
|
+
const action = params.archive ? "archive" : "seen";
|
|
11852
|
+
await client.acknowledgeNotification(params.notification_id, action);
|
|
11853
|
+
ctx?.notificationsInbox?.markSeen(params.notification_id);
|
|
11854
|
+
return {
|
|
11855
|
+
acknowledged: true,
|
|
11856
|
+
notification_id: params.notification_id,
|
|
11857
|
+
action
|
|
11858
|
+
};
|
|
11859
|
+
}
|
|
11860
|
+
};
|
|
11861
|
+
}
|
|
11862
|
+
});
|
|
11863
|
+
|
|
11104
11864
|
// ../core/dist/tools/select-leads.js
|
|
11105
11865
|
var selectLeads;
|
|
11106
11866
|
var init_select_leads = __esm({
|
|
@@ -11296,7 +12056,7 @@ var init_create_lens = __esm({
|
|
|
11296
12056
|
},
|
|
11297
12057
|
execute: async (client, params) => {
|
|
11298
12058
|
const lens = await client.request("POST", "/lenses", {
|
|
11299
|
-
base: params.base,
|
|
12059
|
+
base: String(params.base),
|
|
11300
12060
|
name: params.name,
|
|
11301
12061
|
description: params.description
|
|
11302
12062
|
});
|
|
@@ -14450,6 +15210,11 @@ var init_account_status = __esm({
|
|
|
14450
15210
|
type: ["object", "null"],
|
|
14451
15211
|
description: "Per-resource quota state (llm_completion, ai_rescore, web_fetch, LENS_EXTRA_REFILL) across daily/weekly/monthly windows. Null if /quota_status failed (logged in stderr). Pre-check the LENS_EXTRA_REFILL entry before calling leadbay_extend_lens."
|
|
14452
15212
|
},
|
|
15213
|
+
notifications: {
|
|
15214
|
+
type: "array",
|
|
15215
|
+
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.",
|
|
15216
|
+
items: { type: "object" }
|
|
15217
|
+
},
|
|
14453
15218
|
_meta: {
|
|
14454
15219
|
type: "object",
|
|
14455
15220
|
properties: {
|
|
@@ -14509,6 +15274,13 @@ var init_account_status = __esm({
|
|
|
14509
15274
|
// on /me are intentionally NOT surfaced — they're defunct (see
|
|
14510
15275
|
// SHAPE-DRIFT.md probe round 4).
|
|
14511
15276
|
quota,
|
|
15277
|
+
// Inbox of terminal bulk-progress notifications. Same shape the MCP
|
|
15278
|
+
// server attaches to `_meta.notifications` on every tool response —
|
|
15279
|
+
// duplicated here as a top-level field so the agent's daily-rhythm
|
|
15280
|
+
// check-in (this composite) sees them without having to read _meta.
|
|
15281
|
+
// Empty array when the WS listener isn't wired (OpenClaw, tests) OR
|
|
15282
|
+
// when nothing has completed since the last ack.
|
|
15283
|
+
notifications: ctx?.notificationsInbox?.list() ?? [],
|
|
14512
15284
|
_meta: {
|
|
14513
15285
|
region: client.region
|
|
14514
15286
|
}
|
|
@@ -14516,16 +15288,43 @@ var init_account_status = __esm({
|
|
|
14516
15288
|
}
|
|
14517
15289
|
};
|
|
14518
15290
|
}
|
|
14519
|
-
});
|
|
14520
|
-
|
|
14521
|
-
// ../core/dist/composite/bulk-qualify-leads.js
|
|
14522
|
-
|
|
15291
|
+
});
|
|
15292
|
+
|
|
15293
|
+
// ../core/dist/composite/bulk-qualify-leads.js
|
|
15294
|
+
async function launchBulkQualify(client, leadIds, ctx) {
|
|
15295
|
+
await client.acquireSelectionLock();
|
|
15296
|
+
try {
|
|
15297
|
+
try {
|
|
15298
|
+
const qs = leadIds.map((id) => `leadIds=${encodeURIComponent(id)}`).join("&");
|
|
15299
|
+
await client.requestVoid("POST", `/leads/selection/select?${qs}`);
|
|
15300
|
+
try {
|
|
15301
|
+
const resp = await client.request("POST", "/leads/selection/web_fetch?force_fetch=false", {});
|
|
15302
|
+
return { resp, quotaExceeded: false };
|
|
15303
|
+
} catch (err) {
|
|
15304
|
+
if (err?.code === "QUOTA_EXCEEDED") {
|
|
15305
|
+
ctx?.logger?.warn?.("bulk_qualify_leads: 429 on bulk /leads/selection/web_fetch \u2014 no leads queued");
|
|
15306
|
+
return { resp: null, quotaExceeded: true };
|
|
15307
|
+
}
|
|
15308
|
+
throw err;
|
|
15309
|
+
}
|
|
15310
|
+
} finally {
|
|
15311
|
+
try {
|
|
15312
|
+
await client.requestVoid("POST", "/leads/selection/clear");
|
|
15313
|
+
} catch (e) {
|
|
15314
|
+
ctx?.logger?.warn?.(`bulk_qualify_leads: selection.clear failed: ${e?.message ?? e?.code}`);
|
|
15315
|
+
}
|
|
15316
|
+
}
|
|
15317
|
+
} finally {
|
|
15318
|
+
client.releaseSelectionLock();
|
|
15319
|
+
}
|
|
15320
|
+
}
|
|
15321
|
+
var PAGE_SIZE, DEFAULT_COUNT2, MAX_COUNT, DEFAULT_PER_LEAD_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS2, bulkQualifyLeads;
|
|
14523
15322
|
var init_bulk_qualify_leads = __esm({
|
|
14524
15323
|
"../core/dist/composite/bulk-qualify-leads.js"() {
|
|
14525
15324
|
"use strict";
|
|
14526
15325
|
init_tool_descriptions_generated();
|
|
14527
15326
|
PAGE_SIZE = 50;
|
|
14528
|
-
|
|
15327
|
+
DEFAULT_COUNT2 = 10;
|
|
14529
15328
|
MAX_COUNT = 25;
|
|
14530
15329
|
DEFAULT_PER_LEAD_BUDGET_MS = 9e4;
|
|
14531
15330
|
DEFAULT_TOTAL_BUDGET_MS2 = 5 * 6e4;
|
|
@@ -14546,7 +15345,7 @@ var init_bulk_qualify_leads = __esm({
|
|
|
14546
15345
|
properties: {
|
|
14547
15346
|
count: {
|
|
14548
15347
|
type: "number",
|
|
14549
|
-
description: `How many fresh leads to qualify (default ${
|
|
15348
|
+
description: `How many fresh leads to qualify (default ${DEFAULT_COUNT2}, max ${MAX_COUNT})`
|
|
14550
15349
|
},
|
|
14551
15350
|
leadIds: {
|
|
14552
15351
|
type: "array",
|
|
@@ -14637,7 +15436,7 @@ var init_bulk_qualify_leads = __esm({
|
|
|
14637
15436
|
]
|
|
14638
15437
|
},
|
|
14639
15438
|
execute: async (client, params, ctx) => {
|
|
14640
|
-
const wantCount = Math.min(params.count ??
|
|
15439
|
+
const wantCount = Math.min(params.count ?? DEFAULT_COUNT2, MAX_COUNT);
|
|
14641
15440
|
const perLeadBudget = params.per_lead_budget_ms ?? DEFAULT_PER_LEAD_BUDGET_MS;
|
|
14642
15441
|
const totalBudget = params.total_budget_ms ?? DEFAULT_TOTAL_BUDGET_MS2;
|
|
14643
15442
|
const totalDeadline = Date.now() + totalBudget;
|
|
@@ -14696,69 +15495,50 @@ var init_bulk_qualify_leads = __esm({
|
|
|
14696
15495
|
per_lead_budget_ms: perLeadBudget,
|
|
14697
15496
|
total_budget_ms: totalBudget
|
|
14698
15497
|
});
|
|
14699
|
-
|
|
14700
|
-
|
|
15498
|
+
let launchedCount = 0;
|
|
15499
|
+
let notificationId = null;
|
|
14701
15500
|
let quotaExceeded2 = false;
|
|
15501
|
+
let failed2 = [];
|
|
14702
15502
|
if (!reservation.reused) {
|
|
14703
|
-
|
|
14704
|
-
|
|
14705
|
-
|
|
14706
|
-
|
|
14707
|
-
|
|
14708
|
-
|
|
14709
|
-
|
|
14710
|
-
|
|
14711
|
-
|
|
14712
|
-
|
|
14713
|
-
failed2.push({ lead_id: leadId, error: "lead not found" });
|
|
14714
|
-
} else {
|
|
14715
|
-
failed2.push({
|
|
14716
|
-
lead_id: leadId,
|
|
14717
|
-
error: err?.message ?? err?.code ?? "unknown"
|
|
14718
|
-
});
|
|
14719
|
-
}
|
|
14720
|
-
}
|
|
14721
|
-
}
|
|
14722
|
-
if (failed2.length === candidates.length || launched2.length > 0 || quotaExceeded2) {
|
|
14723
|
-
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id);
|
|
15503
|
+
const launch = await launchBulkQualify(client, candidates, ctx);
|
|
15504
|
+
quotaExceeded2 = launch.quotaExceeded;
|
|
15505
|
+
notificationId = launch.resp?.notification_id ?? null;
|
|
15506
|
+
const queuedIds = launch.resp?.queued_ids ?? [];
|
|
15507
|
+
const skippedIds = launch.resp?.skipped_ids ?? [];
|
|
15508
|
+
launchedCount = queuedIds.length;
|
|
15509
|
+
const seen = /* @__PURE__ */ new Set([...queuedIds, ...skippedIds]);
|
|
15510
|
+
failed2 = candidates.filter((id) => !seen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
|
|
15511
|
+
if (queuedIds.length > 0 || quotaExceeded2 || skippedIds.length > 0 || failed2.length === candidates.length) {
|
|
15512
|
+
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id, notificationId);
|
|
14724
15513
|
}
|
|
15514
|
+
} else {
|
|
15515
|
+
notificationId = reservation.record.notification_id ?? null;
|
|
15516
|
+
launchedCount = reservation.record.lead_ids.length;
|
|
14725
15517
|
}
|
|
14726
15518
|
const out = {
|
|
14727
15519
|
status: "running",
|
|
14728
15520
|
handle_id: reservation.record.bulk_id,
|
|
14729
15521
|
qualify_id: reservation.record.bulk_id,
|
|
14730
15522
|
lead_ids: candidates,
|
|
14731
|
-
launched_count:
|
|
15523
|
+
launched_count: launchedCount,
|
|
14732
15524
|
failed: failed2,
|
|
14733
15525
|
quota_exceeded: quotaExceeded2,
|
|
14734
15526
|
lens_id: lensId,
|
|
15527
|
+
notification_id: notificationId,
|
|
14735
15528
|
_meta: { region: client.region }
|
|
14736
15529
|
};
|
|
14737
15530
|
return out;
|
|
14738
15531
|
}
|
|
14739
|
-
const
|
|
14740
|
-
const
|
|
14741
|
-
|
|
14742
|
-
|
|
14743
|
-
|
|
14744
|
-
|
|
14745
|
-
|
|
14746
|
-
await client.requestVoid("POST", `/leads/${leadId}/web_fetch?force_fetch=false`);
|
|
14747
|
-
launched.push(leadId);
|
|
14748
|
-
} catch (err) {
|
|
14749
|
-
if (err?.code === "QUOTA_EXCEEDED") {
|
|
14750
|
-
quotaExceeded = true;
|
|
14751
|
-
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`);
|
|
14752
|
-
} else if (err?.code === "NOT_FOUND") {
|
|
14753
|
-
failed.push({ lead_id: leadId, error: "lead not found" });
|
|
14754
|
-
} else {
|
|
14755
|
-
failed.push({
|
|
14756
|
-
lead_id: leadId,
|
|
14757
|
-
error: err?.message ?? err?.code ?? "unknown"
|
|
14758
|
-
});
|
|
14759
|
-
}
|
|
14760
|
-
}
|
|
15532
|
+
const inlineLaunch = await launchBulkQualify(client, candidates, ctx);
|
|
15533
|
+
const quotaExceeded = inlineLaunch.quotaExceeded;
|
|
15534
|
+
const launched = inlineLaunch.resp?.queued_ids ?? [];
|
|
15535
|
+
const inlineSkipped = inlineLaunch.resp?.skipped_ids ?? [];
|
|
15536
|
+
const inlineNotificationId = inlineLaunch.resp?.notification_id ?? null;
|
|
15537
|
+
if (inlineNotificationId) {
|
|
15538
|
+
ctx?.logger?.info?.(`bulk_qualify_leads: launched bulk progress_notification_id=${inlineNotificationId} queued=${launched.length} skipped=${inlineSkipped.length}`);
|
|
14761
15539
|
}
|
|
15540
|
+
const inlineFailedSeen = /* @__PURE__ */ new Set([...launched, ...inlineSkipped]);
|
|
15541
|
+
const failed = candidates.filter((id) => !inlineFailedSeen.has(id)).map((id) => ({ lead_id: id, error: "not_queued" }));
|
|
14762
15542
|
let progressDone = 0;
|
|
14763
15543
|
const progressTotal = launched.length;
|
|
14764
15544
|
if (progressTotal > 0) {
|
|
@@ -15594,6 +16374,7 @@ var init_import_and_qualify = __esm({
|
|
|
15594
16374
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
15595
16375
|
qualify_id: null,
|
|
15596
16376
|
import_ids: queued.importIds,
|
|
16377
|
+
notification_ids: queued.notification_ids ?? [],
|
|
15597
16378
|
imported: queued.leads.map((l) => ({
|
|
15598
16379
|
leadId: l.leadId,
|
|
15599
16380
|
...l.domain ? { domain: l.domain } : {},
|
|
@@ -15618,6 +16399,7 @@ var init_import_and_qualify = __esm({
|
|
|
15618
16399
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
15619
16400
|
qualify_id: null,
|
|
15620
16401
|
import_ids: queued.importIds,
|
|
16402
|
+
notification_ids: queued.notification_ids ?? [],
|
|
15621
16403
|
imported: [],
|
|
15622
16404
|
not_imported: [],
|
|
15623
16405
|
qualified: [],
|
|
@@ -15655,6 +16437,7 @@ var init_import_and_qualify = __esm({
|
|
|
15655
16437
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
15656
16438
|
qualify_id: null,
|
|
15657
16439
|
import_ids: importResult.importIds,
|
|
16440
|
+
notification_ids: importResult.notification_ids ?? [],
|
|
15658
16441
|
imported: [],
|
|
15659
16442
|
not_imported: importResult.not_imported.map(toNotImportedEntry),
|
|
15660
16443
|
qualified: [],
|
|
@@ -15692,6 +16475,7 @@ var init_import_and_qualify = __esm({
|
|
|
15692
16475
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
15693
16476
|
qualify_id: null,
|
|
15694
16477
|
import_ids: importResult.importIds,
|
|
16478
|
+
notification_ids: importResult.notification_ids ?? [],
|
|
15695
16479
|
imported,
|
|
15696
16480
|
not_imported,
|
|
15697
16481
|
qualified: [],
|
|
@@ -15802,6 +16586,7 @@ var init_import_and_qualify = __esm({
|
|
|
15802
16586
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
15803
16587
|
qualify_id: reservation.record.bulk_id,
|
|
15804
16588
|
import_ids: importResult.importIds,
|
|
16589
|
+
notification_ids: importResult.notification_ids ?? [],
|
|
15805
16590
|
imported,
|
|
15806
16591
|
not_imported,
|
|
15807
16592
|
qualified,
|
|
@@ -16397,16 +17182,20 @@ var init_bulk_store = __esm({
|
|
|
16397
17182
|
this.logger?.info?.(`bulk.import_failed bulk_id=${bulk_id}`);
|
|
16398
17183
|
});
|
|
16399
17184
|
}
|
|
16400
|
-
async markLaunched(bulk_id) {
|
|
17185
|
+
async markLaunched(bulk_id, notification_id) {
|
|
16401
17186
|
return this.mutex.run(async () => {
|
|
16402
17187
|
const all = this.prune(await this.readAll());
|
|
16403
17188
|
const idx = all.findIndex((r) => r.bulk_id === bulk_id);
|
|
16404
17189
|
if (idx < 0) {
|
|
16405
17190
|
throw new Error(`bulk_id not found: ${bulk_id}`);
|
|
16406
17191
|
}
|
|
16407
|
-
all[idx] = {
|
|
17192
|
+
all[idx] = {
|
|
17193
|
+
...all[idx],
|
|
17194
|
+
status: "launched",
|
|
17195
|
+
...notification_id ? { notification_id } : {}
|
|
17196
|
+
};
|
|
16408
17197
|
await this.writeAll(all);
|
|
16409
|
-
this.logger?.info?.(`bulk.launched bulk_id=${bulk_id}`);
|
|
17198
|
+
this.logger?.info?.(`bulk.launched bulk_id=${bulk_id}${notification_id ? ` notification_id=${notification_id}` : ""}`);
|
|
16410
17199
|
return all[idx];
|
|
16411
17200
|
});
|
|
16412
17201
|
}
|
|
@@ -16658,6 +17447,14 @@ var init_import_status = __esm({
|
|
|
16658
17447
|
});
|
|
16659
17448
|
|
|
16660
17449
|
// ../core/dist/composite/qualify-status.js
|
|
17450
|
+
async function readNotification(client, notificationId) {
|
|
17451
|
+
try {
|
|
17452
|
+
const page = await client.listNotifications({ archived: false, count: 50 });
|
|
17453
|
+
return page.items.find((n) => n.id === notificationId) ?? null;
|
|
17454
|
+
} catch {
|
|
17455
|
+
return null;
|
|
17456
|
+
}
|
|
17457
|
+
}
|
|
16661
17458
|
var qualifyStatus;
|
|
16662
17459
|
var init_qualify_status = __esm({
|
|
16663
17460
|
"../core/dist/composite/qualify-status.js"() {
|
|
@@ -16819,6 +17616,16 @@ var init_qualify_status = __esm({
|
|
|
16819
17616
|
const { _stillRunning, _failedCode, ...rest } = r;
|
|
16820
17617
|
qualified.push(rest);
|
|
16821
17618
|
}
|
|
17619
|
+
let bulkProgress = null;
|
|
17620
|
+
let inProgressFlag = null;
|
|
17621
|
+
const notifId = record.notification_id ?? null;
|
|
17622
|
+
if (notifId) {
|
|
17623
|
+
const n = await readNotification(client, notifId);
|
|
17624
|
+
if (n) {
|
|
17625
|
+
bulkProgress = n.bulk_progress;
|
|
17626
|
+
inProgressFlag = n.in_progress;
|
|
17627
|
+
}
|
|
17628
|
+
}
|
|
16822
17629
|
const out = {
|
|
16823
17630
|
qualify_id: record.bulk_id,
|
|
16824
17631
|
launched_at: record.launched_at,
|
|
@@ -16830,6 +17637,9 @@ var init_qualify_status = __esm({
|
|
|
16830
17637
|
still_running,
|
|
16831
17638
|
failed,
|
|
16832
17639
|
not_in_lens: [...notInLensSet],
|
|
17640
|
+
notification_id: notifId,
|
|
17641
|
+
bulk_progress: bulkProgress,
|
|
17642
|
+
in_progress: inProgressFlag,
|
|
16833
17643
|
region: client.region,
|
|
16834
17644
|
_meta: client.lastMeta ?? {
|
|
16835
17645
|
region: client.region,
|
|
@@ -16842,6 +17652,9 @@ var init_qualify_status = __esm({
|
|
|
16842
17652
|
out.per_lead_budget_ms = record.per_lead_budget_ms;
|
|
16843
17653
|
if (record.total_budget_ms !== void 0)
|
|
16844
17654
|
out.total_budget_ms = record.total_budget_ms;
|
|
17655
|
+
if (bulkProgress && bulkProgress.quota_hit_count > 0) {
|
|
17656
|
+
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.";
|
|
17657
|
+
}
|
|
16845
17658
|
return out;
|
|
16846
17659
|
}
|
|
16847
17660
|
};
|
|
@@ -17119,6 +17932,7 @@ var init_enrich_titles = __esm({
|
|
|
17119
17932
|
bulk_id: res.record.bulk_id,
|
|
17120
17933
|
launched_at: res.record.launched_at,
|
|
17121
17934
|
durability: res.record.durability,
|
|
17935
|
+
notification_id: res.record.notification_id ?? null,
|
|
17122
17936
|
seconds_since_original_launch: bulkSecondsSinceOriginal ?? 0,
|
|
17123
17937
|
titles: params.titles,
|
|
17124
17938
|
email,
|
|
@@ -17134,8 +17948,9 @@ var init_enrich_titles = __esm({
|
|
|
17134
17948
|
total: 3,
|
|
17135
17949
|
message: `Launching enrichment for ${params.titles.length} title${params.titles.length === 1 ? "" : "s"}\u2026`
|
|
17136
17950
|
});
|
|
17951
|
+
let launchResp = null;
|
|
17137
17952
|
try {
|
|
17138
|
-
await client.
|
|
17953
|
+
launchResp = await client.request("POST", "/leads/selection/enrichment/launch", { titles: params.titles, email, phone });
|
|
17139
17954
|
} catch (err) {
|
|
17140
17955
|
const aborted = err?.name === "AbortError" || ctx?.signal?.aborted === true;
|
|
17141
17956
|
if (bulkRecord && tracker) {
|
|
@@ -17159,9 +17974,10 @@ var init_enrich_titles = __esm({
|
|
|
17159
17974
|
}
|
|
17160
17975
|
throw err;
|
|
17161
17976
|
}
|
|
17977
|
+
const notificationId = launchResp?.notification_id ?? null;
|
|
17162
17978
|
if (bulkRecord && tracker) {
|
|
17163
17979
|
try {
|
|
17164
|
-
await tracker.markLaunched(bulkRecord.bulk_id);
|
|
17980
|
+
await tracker.markLaunched(bulkRecord.bulk_id, notificationId);
|
|
17165
17981
|
} catch (e) {
|
|
17166
17982
|
ctx?.logger?.warn?.(`enrich_titles: tracker.markLaunched failed: ${e?.message ?? e}`);
|
|
17167
17983
|
return {
|
|
@@ -17189,8 +18005,9 @@ var init_enrich_titles = __esm({
|
|
|
17189
18005
|
bulk_id: bulkRecord?.bulk_id,
|
|
17190
18006
|
launched_at: bulkRecord?.launched_at,
|
|
17191
18007
|
durability: bulkRecord?.durability,
|
|
17192
|
-
|
|
17193
|
-
|
|
18008
|
+
notification_id: notificationId,
|
|
18009
|
+
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.",
|
|
18010
|
+
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."
|
|
17194
18011
|
};
|
|
17195
18012
|
} finally {
|
|
17196
18013
|
try {
|
|
@@ -17208,6 +18025,14 @@ var init_enrich_titles = __esm({
|
|
|
17208
18025
|
});
|
|
17209
18026
|
|
|
17210
18027
|
// ../core/dist/composite/bulk-enrich-status.js
|
|
18028
|
+
async function readNotification2(client, notificationId) {
|
|
18029
|
+
try {
|
|
18030
|
+
const page = await client.listNotifications({ archived: false, count: 50 });
|
|
18031
|
+
return page.items.find((n) => n.id === notificationId) ?? null;
|
|
18032
|
+
} catch {
|
|
18033
|
+
return null;
|
|
18034
|
+
}
|
|
18035
|
+
}
|
|
17211
18036
|
async function pMap(items, fn, concurrency) {
|
|
17212
18037
|
const out = new Array(items.length);
|
|
17213
18038
|
let next = 0;
|
|
@@ -17379,6 +18204,53 @@ var init_bulk_enrich_status = __esm({
|
|
|
17379
18204
|
launched_at: record.launched_at
|
|
17380
18205
|
};
|
|
17381
18206
|
}
|
|
18207
|
+
const notifId = record.notification_id ?? null;
|
|
18208
|
+
if (notifId) {
|
|
18209
|
+
const n = await readNotification2(client, notifId);
|
|
18210
|
+
if (n && n.bulk_progress) {
|
|
18211
|
+
const bp = n.bulk_progress;
|
|
18212
|
+
const inProgress = n.in_progress;
|
|
18213
|
+
let leads2 = [];
|
|
18214
|
+
if (!inProgress && includeContacts) {
|
|
18215
|
+
leads2 = await pMap(record.lead_ids, async (leadId) => {
|
|
18216
|
+
try {
|
|
18217
|
+
const out = await getContacts.execute(client, { leadId });
|
|
18218
|
+
const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
|
|
18219
|
+
return { lead_id: leadId, contacts };
|
|
18220
|
+
} catch {
|
|
18221
|
+
return { lead_id: leadId };
|
|
18222
|
+
}
|
|
18223
|
+
}, STATUS_FETCH_CONCURRENCY);
|
|
18224
|
+
} else {
|
|
18225
|
+
leads2 = record.lead_ids.map((id) => ({ lead_id: id }));
|
|
18226
|
+
}
|
|
18227
|
+
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}`);
|
|
18228
|
+
return {
|
|
18229
|
+
bulk_id: record.bulk_id,
|
|
18230
|
+
notification_id: notifId,
|
|
18231
|
+
launched_at: record.launched_at,
|
|
18232
|
+
status: record.status,
|
|
18233
|
+
durability: record.durability,
|
|
18234
|
+
titles: record.titles,
|
|
18235
|
+
email: record.email,
|
|
18236
|
+
phone: record.phone,
|
|
18237
|
+
lens_id: record.lens_id,
|
|
18238
|
+
leads: leads2,
|
|
18239
|
+
overall_progress: {
|
|
18240
|
+
done: bp.success_count + bp.failure_count + bp.quota_hit_count,
|
|
18241
|
+
total: bp.total_count,
|
|
18242
|
+
done_ratio: bp.total_count === 0 ? 0 : (bp.success_count + bp.failure_count + bp.quota_hit_count) / bp.total_count
|
|
18243
|
+
},
|
|
18244
|
+
bulk_progress: bp,
|
|
18245
|
+
in_progress: inProgress,
|
|
18246
|
+
all_done: !inProgress,
|
|
18247
|
+
...bp.quota_hit_count > 0 ? {
|
|
18248
|
+
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."
|
|
18249
|
+
} : {}
|
|
18250
|
+
};
|
|
18251
|
+
}
|
|
18252
|
+
ctx?.logger?.info?.(`bulk_enrich_status: notification ${notifId} not yet visible; falling back to per-lead fan-out`);
|
|
18253
|
+
}
|
|
17382
18254
|
let doneSoFar = 0;
|
|
17383
18255
|
const totalLeads = record.lead_ids.length;
|
|
17384
18256
|
const results = await pMap(record.lead_ids, async (leadId) => {
|
|
@@ -17465,6 +18337,8 @@ var init_bulk_enrich_status = __esm({
|
|
|
17465
18337
|
|
|
17466
18338
|
// ../core/dist/composite/adjust-audience.js
|
|
17467
18339
|
function tokens(s) {
|
|
18340
|
+
if (!s)
|
|
18341
|
+
return [];
|
|
17468
18342
|
return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
17469
18343
|
}
|
|
17470
18344
|
function bestMatches(text, taxonomy) {
|
|
@@ -17478,11 +18352,11 @@ function bestMatches(text, taxonomy) {
|
|
|
17478
18352
|
if (have.has(t))
|
|
17479
18353
|
overlap += 1;
|
|
17480
18354
|
const score = overlap / Math.max(want.size, 1);
|
|
17481
|
-
return { id: s.id, name: s.name, score };
|
|
18355
|
+
return { id: s.id, name: s.name ?? "", score };
|
|
17482
18356
|
}).filter((m) => m.score > 0).sort((a, b) => b.score - a.score);
|
|
17483
18357
|
return ranked.slice(0, 5);
|
|
17484
18358
|
}
|
|
17485
|
-
async function resolveSectors(client, texts) {
|
|
18359
|
+
async function resolveSectors(client, texts, ctx) {
|
|
17486
18360
|
const looksLikeId2 = (s) => /^\d+$/.test(s);
|
|
17487
18361
|
const direct = texts.filter(looksLikeId2);
|
|
17488
18362
|
const free = texts.filter((s) => !looksLikeId2(s));
|
|
@@ -17491,6 +18365,10 @@ async function resolveSectors(client, texts) {
|
|
|
17491
18365
|
const me = await client.resolveMe().catch(() => null);
|
|
17492
18366
|
const lang = me?.language ?? "en";
|
|
17493
18367
|
const taxonomy = await client.request("GET", `/sectors/all?lang=${encodeURIComponent(lang)}&includeInvisible=false`);
|
|
18368
|
+
const nullNames = taxonomy.filter((s) => !s.name).length;
|
|
18369
|
+
if (nullNames > 0) {
|
|
18370
|
+
ctx?.logger?.warn?.(`adjust_audience: /sectors/all returned ${nullNames}/${taxonomy.length} sector(s) with a null/missing name`);
|
|
18371
|
+
}
|
|
17494
18372
|
const resolved = [...direct];
|
|
17495
18373
|
const ambiguities = [];
|
|
17496
18374
|
for (const text of free) {
|
|
@@ -17503,6 +18381,22 @@ async function resolveSectors(client, texts) {
|
|
|
17503
18381
|
}
|
|
17504
18382
|
return { resolved, ambiguities };
|
|
17505
18383
|
}
|
|
18384
|
+
async function resolveLensByName(client, name) {
|
|
18385
|
+
const lenses = await client.request("GET", "/lenses");
|
|
18386
|
+
const all = lenses.map((l) => ({ id: l.id, name: l.name }));
|
|
18387
|
+
const needle = name.trim().toLowerCase();
|
|
18388
|
+
const exact = all.filter((l) => (l.name ?? "").trim().toLowerCase() === needle);
|
|
18389
|
+
if (exact.length === 1)
|
|
18390
|
+
return { ok: true, id: exact[0].id };
|
|
18391
|
+
if (exact.length > 1)
|
|
18392
|
+
return { ok: false, reason: "ambiguous", matches: exact };
|
|
18393
|
+
const partial = all.filter((l) => (l.name ?? "").toLowerCase().includes(needle));
|
|
18394
|
+
if (partial.length === 1)
|
|
18395
|
+
return { ok: true, id: partial[0].id };
|
|
18396
|
+
if (partial.length > 1)
|
|
18397
|
+
return { ok: false, reason: "ambiguous", matches: partial };
|
|
18398
|
+
return { ok: false, reason: "not_found", lenses: all };
|
|
18399
|
+
}
|
|
17506
18400
|
function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
|
|
17507
18401
|
const items = current?.lens_filter?.items ?? [];
|
|
17508
18402
|
const item = items[0] ?? { criteria: [] };
|
|
@@ -17536,11 +18430,15 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
|
|
|
17536
18430
|
}
|
|
17537
18431
|
}
|
|
17538
18432
|
if (sizes && sizes.length > 0) {
|
|
18433
|
+
const normalizedSizes = sizes.map((s) => ({
|
|
18434
|
+
min: s.min ?? 0,
|
|
18435
|
+
max: s.max ?? 1e6
|
|
18436
|
+
}));
|
|
17539
18437
|
const idx = criteria.findIndex((c) => c.type === "size");
|
|
17540
18438
|
if (idx >= 0) {
|
|
17541
|
-
criteria[idx] = { type: "size", is_excluded: false, sizes };
|
|
18439
|
+
criteria[idx] = { type: "size", is_excluded: false, sizes: normalizedSizes };
|
|
17542
18440
|
} else {
|
|
17543
|
-
criteria.push({ type: "size", is_excluded: false, sizes });
|
|
18441
|
+
criteria.push({ type: "size", is_excluded: false, sizes: normalizedSizes });
|
|
17544
18442
|
}
|
|
17545
18443
|
}
|
|
17546
18444
|
return {
|
|
@@ -17548,6 +18446,9 @@ function mergeFilter(current, toAddSectors, toExcludeSectors, sizes) {
|
|
|
17548
18446
|
locations: current.locations ?? { results: [], parents: [] }
|
|
17549
18447
|
};
|
|
17550
18448
|
}
|
|
18449
|
+
function filterWriteBody(filter) {
|
|
18450
|
+
return { items: filter.lens_filter.items };
|
|
18451
|
+
}
|
|
17551
18452
|
var adjustAudience;
|
|
17552
18453
|
var init_adjust_audience = __esm({
|
|
17553
18454
|
"../core/dist/composite/adjust-audience.js"() {
|
|
@@ -17594,6 +18495,10 @@ var init_adjust_audience = __esm({
|
|
|
17594
18495
|
description: "Company size buckets, e.g. [{min:30,max:300}]"
|
|
17595
18496
|
},
|
|
17596
18497
|
lensId: { type: "number", description: "Lens id (escape hatch)" },
|
|
18498
|
+
lensName: {
|
|
18499
|
+
type: "string",
|
|
18500
|
+
description: "Target a lens BY NAME (e.g. 'Joinery') instead of the active one. Resolved against your lenses \u2014 edit-only, does NOT switch your active lens. Unknown/ambiguous names are surfaced to pick from. Takes effect only when lensId is not given."
|
|
18501
|
+
},
|
|
17597
18502
|
save_for_org: {
|
|
17598
18503
|
type: "boolean",
|
|
17599
18504
|
description: "Admin only \u2014 propagate the change to the org-level lens for everyone (default false: per-user draft)"
|
|
@@ -17607,21 +18512,35 @@ var init_adjust_audience = __esm({
|
|
|
17607
18512
|
},
|
|
17608
18513
|
outputSchema: {
|
|
17609
18514
|
type: "object",
|
|
17610
|
-
description: "
|
|
18515
|
+
description: "Return shapes: 'applied' on success; 'ambiguous_sectors' when free-text sectors matched multiple candidates (re-call with sector_ids); 'lens_not_found' / 'ambiguous_lens' when a lensName didn't resolve to exactly one lens (re-call with lensId or an exact lensName).",
|
|
17611
18516
|
properties: {
|
|
17612
18517
|
status: {
|
|
17613
18518
|
type: "string",
|
|
17614
|
-
description: "'ambiguous_sectors' or '
|
|
18519
|
+
description: "'applied', 'ambiguous_sectors', 'lens_not_found', or 'ambiguous_lens'."
|
|
17615
18520
|
},
|
|
17616
18521
|
sector_ambiguities: {
|
|
17617
18522
|
type: "array",
|
|
17618
18523
|
description: "Per ambiguous text: {sector_text, matches:[{id, name, score}]}. Agent picks an id and re-calls.",
|
|
17619
18524
|
items: { type: "object" }
|
|
17620
18525
|
},
|
|
18526
|
+
lenses: {
|
|
18527
|
+
type: "array",
|
|
18528
|
+
description: "On 'lens_not_found': the user's lenses [{id, name}] to pick from.",
|
|
18529
|
+
items: { type: "object" }
|
|
18530
|
+
},
|
|
18531
|
+
matches: {
|
|
18532
|
+
type: "array",
|
|
18533
|
+
description: "On 'ambiguous_lens': the lenses [{id, name}] the name matched.",
|
|
18534
|
+
items: { type: "object" }
|
|
18535
|
+
},
|
|
18536
|
+
lens_query: {
|
|
18537
|
+
type: "string",
|
|
18538
|
+
description: "On 'lens_not_found' / 'ambiguous_lens': the lensName the user asked for."
|
|
18539
|
+
},
|
|
17621
18540
|
message: { type: "string" },
|
|
17622
18541
|
lens_used: {
|
|
17623
18542
|
type: "object",
|
|
17624
|
-
description: "Resolved lens metadata: {id, name, was_draft, was_new, save_for_org}."
|
|
18543
|
+
description: "Resolved lens metadata: {id, name, was_draft, was_new, active_lens_changed, save_for_org}."
|
|
17625
18544
|
},
|
|
17626
18545
|
filter_applied: {
|
|
17627
18546
|
type: "object",
|
|
@@ -17634,28 +18553,63 @@ var init_adjust_audience = __esm({
|
|
|
17634
18553
|
execute: async (client, params, ctx) => {
|
|
17635
18554
|
const me = await client.resolveMe();
|
|
17636
18555
|
const isAdmin = me.admin === true;
|
|
17637
|
-
|
|
18556
|
+
let namedLensId;
|
|
18557
|
+
if (params.lensId == null && params.lensName != null && params.lensName.trim() !== "") {
|
|
18558
|
+
const res = await resolveLensByName(client, params.lensName);
|
|
18559
|
+
if (!res.ok && res.reason === "not_found") {
|
|
18560
|
+
return {
|
|
18561
|
+
status: "lens_not_found",
|
|
18562
|
+
lens_query: params.lensName,
|
|
18563
|
+
lenses: res.lenses,
|
|
18564
|
+
message: `No lens named "${params.lensName}". Pick one of the listed lenses (pass lensId or an exact lensName), or create it first.`
|
|
18565
|
+
};
|
|
18566
|
+
}
|
|
18567
|
+
if (!res.ok && res.reason === "ambiguous") {
|
|
18568
|
+
return {
|
|
18569
|
+
status: "ambiguous_lens",
|
|
18570
|
+
lens_query: params.lensName,
|
|
18571
|
+
matches: res.matches,
|
|
18572
|
+
message: `"${params.lensName}" matched multiple lenses. Re-call with the exact lensName or the lensId of the one you mean.`
|
|
18573
|
+
};
|
|
18574
|
+
}
|
|
18575
|
+
if (res.ok)
|
|
18576
|
+
namedLensId = res.id;
|
|
18577
|
+
}
|
|
18578
|
+
const startingLensId = params.lensId ?? namedLensId ?? me.last_requested_lens ?? await client.resolveDefaultLens();
|
|
18579
|
+
const isNamedEdit = namedLensId != null && params.lensId == null;
|
|
17638
18580
|
const includeTexts = [
|
|
17639
18581
|
...params.sectors ?? [],
|
|
17640
18582
|
...params.sector_ids ?? []
|
|
17641
18583
|
];
|
|
17642
18584
|
const excludeTexts = params.exclude_sectors ?? [];
|
|
17643
|
-
const includeRes = await resolveSectors(client, includeTexts);
|
|
17644
|
-
const excludeRes = await resolveSectors(client, excludeTexts);
|
|
18585
|
+
const includeRes = await resolveSectors(client, includeTexts, ctx);
|
|
18586
|
+
const excludeRes = await resolveSectors(client, excludeTexts, ctx);
|
|
17645
18587
|
const ambiguities = [
|
|
17646
18588
|
...includeRes.ambiguities,
|
|
17647
18589
|
...excludeRes.ambiguities
|
|
17648
18590
|
];
|
|
17649
18591
|
if (ambiguities.length > 0) {
|
|
18592
|
+
const noMatch = ambiguities.filter((a) => a.matches.length === 0);
|
|
18593
|
+
const multi = ambiguities.filter((a) => a.matches.length > 0);
|
|
18594
|
+
const parts = [];
|
|
18595
|
+
if (noMatch.length > 0) {
|
|
18596
|
+
const names = noMatch.map((a) => `"${a.sector_text}"`).join(", ");
|
|
18597
|
+
parts.push(`Couldn't find a sector matching ${names}. Ask the user to rephrase or pick a known sector, then re-call with sector_ids=...`);
|
|
18598
|
+
}
|
|
18599
|
+
if (multi.length > 0) {
|
|
18600
|
+
const names = multi.map((a) => `"${a.sector_text}"`).join(", ");
|
|
18601
|
+
parts.push(`${names} matched multiple sectors. Pick from the matches and re-call with sector_ids=...`);
|
|
18602
|
+
}
|
|
17650
18603
|
return {
|
|
17651
18604
|
status: "ambiguous_sectors",
|
|
17652
18605
|
sector_ambiguities: ambiguities,
|
|
17653
|
-
message: "
|
|
18606
|
+
message: parts.join(" ")
|
|
17654
18607
|
};
|
|
17655
18608
|
}
|
|
17656
18609
|
const lens = await client.request("GET", `/lenses/${startingLensId}`);
|
|
17657
18610
|
const currentFilter = await client.request("GET", `/lenses/${startingLensId}/filter`);
|
|
17658
18611
|
const merged = mergeFilter(currentFilter, includeRes.resolved, excludeRes.resolved, params.sizes);
|
|
18612
|
+
const mergedBody = filterWriteBody(merged);
|
|
17659
18613
|
const isDefault = lens.is_default || lens.default;
|
|
17660
18614
|
const isUserLevel = lens.user_id != null;
|
|
17661
18615
|
const isOrgLevel = !isUserLevel && !isDefault;
|
|
@@ -17664,24 +18618,28 @@ var init_adjust_audience = __esm({
|
|
|
17664
18618
|
let wasNew = false;
|
|
17665
18619
|
if (isDefault) {
|
|
17666
18620
|
const name = params.newLensName ?? `Custom audience \u2014 ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}`;
|
|
17667
|
-
const
|
|
17668
|
-
base: startingLensId,
|
|
18621
|
+
const newLens2 = await client.request("POST", "/lenses", {
|
|
18622
|
+
base: String(startingLensId),
|
|
17669
18623
|
name
|
|
17670
18624
|
});
|
|
17671
|
-
targetLensId =
|
|
18625
|
+
targetLensId = newLens2.id;
|
|
17672
18626
|
wasNew = true;
|
|
17673
|
-
await client.requestVoid("POST", `/lenses/${targetLensId}/filter`,
|
|
17674
|
-
|
|
18627
|
+
await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, mergedBody);
|
|
18628
|
+
if (!isNamedEdit) {
|
|
18629
|
+
await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
|
|
18630
|
+
}
|
|
17675
18631
|
} else if (isUserLevel) {
|
|
17676
18632
|
try {
|
|
17677
|
-
await client.requestVoid("POST", `/lenses/${startingLensId}/filter`,
|
|
18633
|
+
await client.requestVoid("POST", `/lenses/${startingLensId}/filter`, mergedBody);
|
|
17678
18634
|
} catch (err) {
|
|
17679
18635
|
if (err?.code === "FORBIDDEN") {
|
|
17680
18636
|
wasDraft = true;
|
|
17681
18637
|
const draft = await client.request("POST", `/lenses/${startingLensId}/draft`);
|
|
17682
18638
|
targetLensId = draft.id;
|
|
17683
|
-
await client.requestVoid("POST", `/lenses/${targetLensId}/filter`,
|
|
17684
|
-
|
|
18639
|
+
await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, mergedBody);
|
|
18640
|
+
if (!isNamedEdit) {
|
|
18641
|
+
await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
|
|
18642
|
+
}
|
|
17685
18643
|
} else {
|
|
17686
18644
|
throw err;
|
|
17687
18645
|
}
|
|
@@ -17693,7 +18651,7 @@ var init_adjust_audience = __esm({
|
|
|
17693
18651
|
const draft = await client.request("POST", `/lenses/${startingLensId}/draft`);
|
|
17694
18652
|
targetLensId = draft.id;
|
|
17695
18653
|
try {
|
|
17696
|
-
await client.requestVoid("POST", `/lenses/${targetLensId}/filter`,
|
|
18654
|
+
await client.requestVoid("POST", `/lenses/${targetLensId}/filter`, mergedBody);
|
|
17697
18655
|
} catch (err) {
|
|
17698
18656
|
ctx?.logger?.warn?.(`adjust_audience: filter on draft ${targetLensId} failed: ${err?.message}`);
|
|
17699
18657
|
try {
|
|
@@ -17709,17 +18667,21 @@ var init_adjust_audience = __esm({
|
|
|
17709
18667
|
}
|
|
17710
18668
|
throw err;
|
|
17711
18669
|
}
|
|
17712
|
-
|
|
18670
|
+
if (!isNamedEdit) {
|
|
18671
|
+
await client.requestVoid("POST", `/lenses/${targetLensId}/update_last_requested`);
|
|
18672
|
+
}
|
|
17713
18673
|
} else {
|
|
17714
18674
|
try {
|
|
17715
|
-
await client.requestVoid("POST", `/lenses/${startingLensId}/filter`,
|
|
18675
|
+
await client.requestVoid("POST", `/lenses/${startingLensId}/filter`, mergedBody);
|
|
17716
18676
|
} catch (err) {
|
|
17717
18677
|
throw err;
|
|
17718
18678
|
}
|
|
17719
18679
|
}
|
|
17720
18680
|
}
|
|
17721
|
-
|
|
18681
|
+
if (!isNamedEdit)
|
|
18682
|
+
client.invalidateMe();
|
|
17722
18683
|
client.invalidateDefaultLens();
|
|
18684
|
+
const namedEditForkedMessage = isNamedEdit && (wasNew || wasDraft) ? ` Note: "${lens.name}" can't be edited in place, so the change was applied to a ${wasDraft ? "personal draft" : "new copy"} (id ${targetLensId}); your active lens is unchanged.` : "";
|
|
17723
18685
|
return {
|
|
17724
18686
|
status: "applied",
|
|
17725
18687
|
lens_used: {
|
|
@@ -17727,10 +18689,11 @@ var init_adjust_audience = __esm({
|
|
|
17727
18689
|
name: lens.name,
|
|
17728
18690
|
was_draft: wasDraft,
|
|
17729
18691
|
was_new: wasNew,
|
|
18692
|
+
active_lens_changed: !isNamedEdit && (wasNew || wasDraft),
|
|
17730
18693
|
save_for_org: params.save_for_org === true && isAdmin && isOrgLevel
|
|
17731
18694
|
},
|
|
17732
18695
|
filter_applied: merged,
|
|
17733
|
-
message: wasDraft ? "Applied to your personal draft of the org lens (your view only)." : wasNew ? `Created a new user-level lens "${lens.name}" with the filter (you can rename via leadbay_update_lens).` : "Applied directly to the lens.",
|
|
18696
|
+
message: (wasDraft ? "Applied to your personal draft of the org lens (your view only)." : wasNew ? `Created a new user-level lens "${lens.name}" with the filter (you can rename via leadbay_update_lens).` : "Applied directly to the lens.") + namedEditForkedMessage,
|
|
17734
18697
|
_meta: { region: client.region }
|
|
17735
18698
|
};
|
|
17736
18699
|
}
|
|
@@ -18125,6 +19088,399 @@ var init_extend_lens = __esm({
|
|
|
18125
19088
|
}
|
|
18126
19089
|
});
|
|
18127
19090
|
|
|
19091
|
+
// ../core/dist/composite/my-lenses.js
|
|
19092
|
+
async function listWithActive(client) {
|
|
19093
|
+
const lenses = await client.request("GET", "/lenses");
|
|
19094
|
+
const me = await client.resolveMe().catch(() => null);
|
|
19095
|
+
const activeFromMe = sid(me?.last_requested_lens);
|
|
19096
|
+
const active_lens_id = activeFromMe ?? sid(lenses.find((l) => l.is_last_active)?.id) ?? null;
|
|
19097
|
+
return {
|
|
19098
|
+
active_lens_id,
|
|
19099
|
+
lenses: lenses.map((l) => ({
|
|
19100
|
+
id: sid(l.id),
|
|
19101
|
+
name: l.name,
|
|
19102
|
+
description: l.description ?? null,
|
|
19103
|
+
is_active: sid(l.id) === active_lens_id,
|
|
19104
|
+
is_default: l.is_default === true || l.default === true
|
|
19105
|
+
}))
|
|
19106
|
+
};
|
|
19107
|
+
}
|
|
19108
|
+
var sid, myLenses;
|
|
19109
|
+
var init_my_lenses = __esm({
|
|
19110
|
+
"../core/dist/composite/my-lenses.js"() {
|
|
19111
|
+
"use strict";
|
|
19112
|
+
init_tool_descriptions_generated();
|
|
19113
|
+
sid = (v) => v == null ? null : String(v);
|
|
19114
|
+
myLenses = {
|
|
19115
|
+
name: "leadbay_my_lenses",
|
|
19116
|
+
annotations: {
|
|
19117
|
+
title: "List, switch, edit, or delete your lenses",
|
|
19118
|
+
// No args → pure read. The delete mode issues DELETE /lenses/:id (an
|
|
19119
|
+
// irreversible side effect), so the tool is destructive — clients must
|
|
19120
|
+
// treat it as approval-required, not auto-run. The delete path is itself
|
|
19121
|
+
// confirm-gated (preview unless confirm:true). switch/edit are not
|
|
19122
|
+
// idempotent across modes either, so don't claim idempotency.
|
|
19123
|
+
readOnlyHint: false,
|
|
19124
|
+
destructiveHint: true,
|
|
19125
|
+
idempotentHint: false,
|
|
19126
|
+
openWorldHint: true
|
|
19127
|
+
},
|
|
19128
|
+
description: leadbay_my_lenses,
|
|
19129
|
+
inputSchema: {
|
|
19130
|
+
type: "object",
|
|
19131
|
+
properties: {
|
|
19132
|
+
switchToLensId: {
|
|
19133
|
+
type: ["string", "number"],
|
|
19134
|
+
description: "When set, switch the active lens to this id (must be one of the user's lenses), then return the refreshed list."
|
|
19135
|
+
},
|
|
19136
|
+
editLensId: {
|
|
19137
|
+
type: ["string", "number"],
|
|
19138
|
+
description: "When set, edit this lens's metadata \u2014 provide newName and/or newDescription. Must be one of the user's lenses."
|
|
19139
|
+
},
|
|
19140
|
+
newName: {
|
|
19141
|
+
type: "string",
|
|
19142
|
+
description: "New lens name (used with editLensId)."
|
|
19143
|
+
},
|
|
19144
|
+
newDescription: {
|
|
19145
|
+
type: "string",
|
|
19146
|
+
description: "New lens description (used with editLensId). Pass an empty string to clear it."
|
|
19147
|
+
},
|
|
19148
|
+
deleteLensId: {
|
|
19149
|
+
type: ["string", "number"],
|
|
19150
|
+
description: "When set, delete this lens. DESTRUCTIVE \u2014 returns a delete_preview unless confirm:true. Cannot delete the default lens."
|
|
19151
|
+
},
|
|
19152
|
+
confirm: {
|
|
19153
|
+
type: "boolean",
|
|
19154
|
+
description: "Required (=true) to actually delete. Without it, deleteLensId returns a preview to confirm with the user first."
|
|
19155
|
+
}
|
|
19156
|
+
},
|
|
19157
|
+
additionalProperties: false
|
|
19158
|
+
},
|
|
19159
|
+
outputSchema: {
|
|
19160
|
+
type: "object",
|
|
19161
|
+
properties: {
|
|
19162
|
+
status: {
|
|
19163
|
+
type: "string",
|
|
19164
|
+
description: "'listed', 'switched', 'already_active', 'edited', 'deleted', 'delete_preview' (confirm to proceed), 'cannot_delete_default', or 'not_found'."
|
|
19165
|
+
},
|
|
19166
|
+
switched: { type: "boolean", description: "True when this call changed the active lens." },
|
|
19167
|
+
edited: { type: "boolean", description: "True when this call renamed/re-described a lens." },
|
|
19168
|
+
deleted: { type: "boolean", description: "True when this call deleted a lens." },
|
|
19169
|
+
will_delete: {
|
|
19170
|
+
type: "object",
|
|
19171
|
+
description: "On 'delete_preview': the lens that WILL be deleted {id, name}. Nothing removed yet."
|
|
19172
|
+
},
|
|
19173
|
+
active_lens_id: { type: ["string", "null"] },
|
|
19174
|
+
lenses: {
|
|
19175
|
+
type: "array",
|
|
19176
|
+
description: "The user's lenses. Each: {id, name, description, is_active}.",
|
|
19177
|
+
items: { type: "object" }
|
|
19178
|
+
},
|
|
19179
|
+
message: { type: "string" }
|
|
19180
|
+
},
|
|
19181
|
+
required: ["status", "lenses", "active_lens_id"]
|
|
19182
|
+
},
|
|
19183
|
+
execute: async (client, params) => {
|
|
19184
|
+
if (params.deleteLensId != null) {
|
|
19185
|
+
const targetId = sid(params.deleteLensId);
|
|
19186
|
+
const before = await listWithActive(client);
|
|
19187
|
+
const target = before.lenses.find((l) => l.id === targetId);
|
|
19188
|
+
if (!target) {
|
|
19189
|
+
return {
|
|
19190
|
+
status: "not_found",
|
|
19191
|
+
switched: false,
|
|
19192
|
+
edited: false,
|
|
19193
|
+
deleted: false,
|
|
19194
|
+
active_lens_id: before.active_lens_id,
|
|
19195
|
+
lenses: before.lenses,
|
|
19196
|
+
message: `No lens with id ${targetId}. Pick one from the list.`
|
|
19197
|
+
};
|
|
19198
|
+
}
|
|
19199
|
+
if (target.is_default) {
|
|
19200
|
+
return {
|
|
19201
|
+
status: "cannot_delete_default",
|
|
19202
|
+
switched: false,
|
|
19203
|
+
edited: false,
|
|
19204
|
+
deleted: false,
|
|
19205
|
+
active_lens_id: before.active_lens_id,
|
|
19206
|
+
lenses: before.lenses,
|
|
19207
|
+
message: `"${target.name}" is the default lens and can't be deleted.`
|
|
19208
|
+
};
|
|
19209
|
+
}
|
|
19210
|
+
if (params.confirm !== true) {
|
|
19211
|
+
return {
|
|
19212
|
+
status: "delete_preview",
|
|
19213
|
+
switched: false,
|
|
19214
|
+
edited: false,
|
|
19215
|
+
deleted: false,
|
|
19216
|
+
active_lens_id: before.active_lens_id,
|
|
19217
|
+
lenses: before.lenses,
|
|
19218
|
+
will_delete: { id: target.id, name: target.name },
|
|
19219
|
+
message: `About to delete "${target.name}". This can't be undone. Confirm with the user, then re-call with confirm:true.`
|
|
19220
|
+
};
|
|
19221
|
+
}
|
|
19222
|
+
await client.requestVoid("DELETE", `/lenses/${targetId}`);
|
|
19223
|
+
client.invalidateMe();
|
|
19224
|
+
client.invalidateDefaultLens();
|
|
19225
|
+
const after = await listWithActive(client);
|
|
19226
|
+
return {
|
|
19227
|
+
status: "deleted",
|
|
19228
|
+
switched: false,
|
|
19229
|
+
edited: false,
|
|
19230
|
+
deleted: true,
|
|
19231
|
+
active_lens_id: after.active_lens_id,
|
|
19232
|
+
lenses: after.lenses,
|
|
19233
|
+
message: `Deleted "${target.name}".`
|
|
19234
|
+
};
|
|
19235
|
+
}
|
|
19236
|
+
if (params.editLensId != null) {
|
|
19237
|
+
const targetId = sid(params.editLensId);
|
|
19238
|
+
const before = await listWithActive(client);
|
|
19239
|
+
const target = before.lenses.find((l) => l.id === targetId);
|
|
19240
|
+
if (!target) {
|
|
19241
|
+
return {
|
|
19242
|
+
status: "not_found",
|
|
19243
|
+
switched: false,
|
|
19244
|
+
edited: false,
|
|
19245
|
+
active_lens_id: before.active_lens_id,
|
|
19246
|
+
lenses: before.lenses,
|
|
19247
|
+
message: `No lens with id ${targetId}. Pick one from the list.`
|
|
19248
|
+
};
|
|
19249
|
+
}
|
|
19250
|
+
const body = {};
|
|
19251
|
+
const newName = params.newName?.trim();
|
|
19252
|
+
if (newName)
|
|
19253
|
+
body.name = newName;
|
|
19254
|
+
if (params.newDescription !== void 0)
|
|
19255
|
+
body.description = params.newDescription;
|
|
19256
|
+
if (Object.keys(body).length === 0) {
|
|
19257
|
+
return {
|
|
19258
|
+
status: "not_found",
|
|
19259
|
+
switched: false,
|
|
19260
|
+
edited: false,
|
|
19261
|
+
active_lens_id: before.active_lens_id,
|
|
19262
|
+
lenses: before.lenses,
|
|
19263
|
+
message: `Nothing to change on "${target.name}" \u2014 provide newName and/or newDescription.`
|
|
19264
|
+
};
|
|
19265
|
+
}
|
|
19266
|
+
await client.requestVoid("POST", `/lenses/${targetId}`, body);
|
|
19267
|
+
client.invalidateDefaultLens();
|
|
19268
|
+
const changed = [
|
|
19269
|
+
body.name != null ? `renamed to "${body.name}"` : null,
|
|
19270
|
+
body.description !== void 0 ? "description updated" : null
|
|
19271
|
+
].filter(Boolean).join(", ");
|
|
19272
|
+
const after = await listWithActive(client);
|
|
19273
|
+
return {
|
|
19274
|
+
status: "edited",
|
|
19275
|
+
switched: false,
|
|
19276
|
+
edited: true,
|
|
19277
|
+
active_lens_id: after.active_lens_id,
|
|
19278
|
+
lenses: after.lenses,
|
|
19279
|
+
message: `"${target.name}" \u2014 ${changed}.`
|
|
19280
|
+
};
|
|
19281
|
+
}
|
|
19282
|
+
if (params.switchToLensId != null) {
|
|
19283
|
+
const targetId = sid(params.switchToLensId);
|
|
19284
|
+
const before = await listWithActive(client);
|
|
19285
|
+
const target = before.lenses.find((l) => l.id === targetId);
|
|
19286
|
+
if (!target) {
|
|
19287
|
+
return {
|
|
19288
|
+
status: "not_found",
|
|
19289
|
+
switched: false,
|
|
19290
|
+
edited: false,
|
|
19291
|
+
active_lens_id: before.active_lens_id,
|
|
19292
|
+
lenses: before.lenses,
|
|
19293
|
+
message: `No lens with id ${targetId}. Pick an id from the list.`
|
|
19294
|
+
};
|
|
19295
|
+
}
|
|
19296
|
+
if (target.is_active) {
|
|
19297
|
+
return {
|
|
19298
|
+
status: "already_active",
|
|
19299
|
+
switched: false,
|
|
19300
|
+
edited: false,
|
|
19301
|
+
active_lens_id: before.active_lens_id,
|
|
19302
|
+
lenses: before.lenses,
|
|
19303
|
+
message: `"${target.name}" is already your active lens.`
|
|
19304
|
+
};
|
|
19305
|
+
}
|
|
19306
|
+
await client.requestVoid("POST", `/lenses/${targetId}/update_last_requested`);
|
|
19307
|
+
client.invalidateMe();
|
|
19308
|
+
client.invalidateDefaultLens();
|
|
19309
|
+
const after = await listWithActive(client);
|
|
19310
|
+
return {
|
|
19311
|
+
status: "switched",
|
|
19312
|
+
switched: true,
|
|
19313
|
+
edited: false,
|
|
19314
|
+
active_lens_id: after.active_lens_id,
|
|
19315
|
+
lenses: after.lenses,
|
|
19316
|
+
message: `Now showing "${target.name}".`
|
|
19317
|
+
};
|
|
19318
|
+
}
|
|
19319
|
+
const { lenses, active_lens_id } = await listWithActive(client);
|
|
19320
|
+
return { status: "listed", switched: false, edited: false, active_lens_id, lenses };
|
|
19321
|
+
}
|
|
19322
|
+
};
|
|
19323
|
+
}
|
|
19324
|
+
});
|
|
19325
|
+
|
|
19326
|
+
// ../core/dist/composite/new-lens.js
|
|
19327
|
+
var EMPTY_FILTER, newLens;
|
|
19328
|
+
var init_new_lens = __esm({
|
|
19329
|
+
"../core/dist/composite/new-lens.js"() {
|
|
19330
|
+
"use strict";
|
|
19331
|
+
init_adjust_audience();
|
|
19332
|
+
init_tool_descriptions_generated();
|
|
19333
|
+
EMPTY_FILTER = {
|
|
19334
|
+
lens_filter: { items: [{ criteria: [] }] },
|
|
19335
|
+
locations: { results: [], parents: [] }
|
|
19336
|
+
};
|
|
19337
|
+
newLens = {
|
|
19338
|
+
name: "leadbay_new_lens",
|
|
19339
|
+
annotations: {
|
|
19340
|
+
title: "Create a new named lens",
|
|
19341
|
+
readOnlyHint: false,
|
|
19342
|
+
destructiveHint: false,
|
|
19343
|
+
idempotentHint: false,
|
|
19344
|
+
// each call creates a distinct lens
|
|
19345
|
+
openWorldHint: true
|
|
19346
|
+
},
|
|
19347
|
+
description: leadbay_new_lens,
|
|
19348
|
+
inputSchema: {
|
|
19349
|
+
type: "object",
|
|
19350
|
+
properties: {
|
|
19351
|
+
name: { type: "string", description: "Display name for the new lens (required)." },
|
|
19352
|
+
sectors: {
|
|
19353
|
+
type: "array",
|
|
19354
|
+
items: { type: "string" },
|
|
19355
|
+
description: "Sectors to include \u2014 free text (auto-resolved) or ids."
|
|
19356
|
+
},
|
|
19357
|
+
exclude_sectors: {
|
|
19358
|
+
type: "array",
|
|
19359
|
+
items: { type: "string" },
|
|
19360
|
+
description: "Sectors to exclude \u2014 free text or ids."
|
|
19361
|
+
},
|
|
19362
|
+
sizes: {
|
|
19363
|
+
type: "array",
|
|
19364
|
+
items: {
|
|
19365
|
+
type: "object",
|
|
19366
|
+
properties: { min: { type: "number" }, max: { type: "number" } }
|
|
19367
|
+
},
|
|
19368
|
+
description: "Company size buckets, e.g. [{min:30,max:300}]."
|
|
19369
|
+
},
|
|
19370
|
+
base: {
|
|
19371
|
+
type: "number",
|
|
19372
|
+
description: "Lens id to clone from. Defaults to the active/default lens."
|
|
19373
|
+
},
|
|
19374
|
+
description: { type: "string", description: "Optional lens description." },
|
|
19375
|
+
confirm: {
|
|
19376
|
+
type: "boolean",
|
|
19377
|
+
description: "Safety gate. Defaults to false \u2192 the tool returns a PREVIEW and creates nothing. Show the preview to the user, get their explicit go-ahead, then re-call the SAME args with confirm:true to actually create the lens."
|
|
19378
|
+
}
|
|
19379
|
+
},
|
|
19380
|
+
required: ["name"],
|
|
19381
|
+
additionalProperties: false
|
|
19382
|
+
},
|
|
19383
|
+
outputSchema: {
|
|
19384
|
+
type: "object",
|
|
19385
|
+
description: "'preview' (default, NOTHING created \u2014 confirm with the user then re-call with confirm:true); 'created' on success; 'ambiguous_sectors' when free-text sectors didn't resolve (re-call with sector ids \u2014 the lens was NOT created).",
|
|
19386
|
+
properties: {
|
|
19387
|
+
status: { type: "string", description: "'preview', 'created', 'ambiguous_sectors', or 'orphan_created' (filter write failed + cleanup failed)." },
|
|
19388
|
+
will_create: {
|
|
19389
|
+
type: "object",
|
|
19390
|
+
description: "On 'preview': what WILL be created \u2014 {name, description, sectors, exclude_sectors, sizes}. Nothing has been written yet."
|
|
19391
|
+
},
|
|
19392
|
+
filter_preview: { type: "object", description: "On 'preview': the FilterPayload that would be applied." },
|
|
19393
|
+
lens: {
|
|
19394
|
+
type: "object",
|
|
19395
|
+
description: "On 'created': the created lens {id, name}."
|
|
19396
|
+
},
|
|
19397
|
+
sector_ambiguities: {
|
|
19398
|
+
type: "array",
|
|
19399
|
+
description: "On 'ambiguous_sectors': per text {sector_text, matches:[{id,name,score}]}.",
|
|
19400
|
+
items: { type: "object" }
|
|
19401
|
+
},
|
|
19402
|
+
filter_applied: { type: "object", description: "On 'created': the FilterPayload POSTed to the new lens." },
|
|
19403
|
+
message: { type: "string" },
|
|
19404
|
+
_meta: { type: "object" }
|
|
19405
|
+
},
|
|
19406
|
+
required: ["status"]
|
|
19407
|
+
},
|
|
19408
|
+
execute: async (client, params, ctx) => {
|
|
19409
|
+
const includeRes = await resolveSectors(client, params.sectors ?? [], ctx);
|
|
19410
|
+
const excludeRes = await resolveSectors(client, params.exclude_sectors ?? [], ctx);
|
|
19411
|
+
const ambiguities = [...includeRes.ambiguities, ...excludeRes.ambiguities];
|
|
19412
|
+
if (ambiguities.length > 0) {
|
|
19413
|
+
const noMatch = ambiguities.filter((a) => a.matches.length === 0);
|
|
19414
|
+
const multi = ambiguities.filter((a) => a.matches.length > 0);
|
|
19415
|
+
const parts = [];
|
|
19416
|
+
if (noMatch.length > 0) {
|
|
19417
|
+
parts.push(`Couldn't find a sector matching ${noMatch.map((a) => `"${a.sector_text}"`).join(", ")}. Pick a known sector and re-call (lens not yet created).`);
|
|
19418
|
+
}
|
|
19419
|
+
if (multi.length > 0) {
|
|
19420
|
+
parts.push(`${multi.map((a) => `"${a.sector_text}"`).join(", ")} matched multiple sectors. Pick from the matches and re-call with the sector id.`);
|
|
19421
|
+
}
|
|
19422
|
+
return {
|
|
19423
|
+
status: "ambiguous_sectors",
|
|
19424
|
+
sector_ambiguities: ambiguities,
|
|
19425
|
+
message: parts.join(" ")
|
|
19426
|
+
};
|
|
19427
|
+
}
|
|
19428
|
+
const merged = mergeFilter(EMPTY_FILTER, includeRes.resolved, excludeRes.resolved, params.sizes);
|
|
19429
|
+
if (params.confirm !== true) {
|
|
19430
|
+
return {
|
|
19431
|
+
status: "preview",
|
|
19432
|
+
will_create: {
|
|
19433
|
+
name: params.name,
|
|
19434
|
+
description: params.description ?? null,
|
|
19435
|
+
sectors: includeRes.resolved,
|
|
19436
|
+
exclude_sectors: excludeRes.resolved,
|
|
19437
|
+
sizes: merged.lens_filter.items[0].criteria.find((c) => c.type === "size") ?? null
|
|
19438
|
+
},
|
|
19439
|
+
filter_preview: merged,
|
|
19440
|
+
message: `About to create "${params.name}". Confirm with the user, then re-call with confirm:true.`,
|
|
19441
|
+
_meta: { region: client.region }
|
|
19442
|
+
};
|
|
19443
|
+
}
|
|
19444
|
+
const base = params.base ?? await client.resolveDefaultLens();
|
|
19445
|
+
const created = await client.request("POST", "/lenses", {
|
|
19446
|
+
base: String(base),
|
|
19447
|
+
name: params.name,
|
|
19448
|
+
description: params.description
|
|
19449
|
+
});
|
|
19450
|
+
const hasCriteria = merged.lens_filter.items[0].criteria.length > 0;
|
|
19451
|
+
if (hasCriteria) {
|
|
19452
|
+
try {
|
|
19453
|
+
await client.requestVoid("POST", `/lenses/${created.id}/filter`, filterWriteBody(merged));
|
|
19454
|
+
} catch (err) {
|
|
19455
|
+
ctx?.logger?.warn?.(`new_lens: filter write on new lens ${created.id} failed: ${err?.message} \u2014 rolling back`);
|
|
19456
|
+
try {
|
|
19457
|
+
await client.requestVoid("DELETE", `/lenses/${created.id}`);
|
|
19458
|
+
} catch {
|
|
19459
|
+
client.invalidateDefaultLens();
|
|
19460
|
+
return {
|
|
19461
|
+
status: "orphan_created",
|
|
19462
|
+
lens: { id: created.id, name: created.name },
|
|
19463
|
+
message: `Created "${created.name}" but applying its filter failed, and cleanup also failed. The lens exists with no criteria \u2014 delete it via leadbay_my_lenses(deleteLensId:"${created.id}", confirm:true) or set its audience with leadbay_adjust_audience.`,
|
|
19464
|
+
_meta: { region: client.region }
|
|
19465
|
+
};
|
|
19466
|
+
}
|
|
19467
|
+
client.invalidateDefaultLens();
|
|
19468
|
+
throw err;
|
|
19469
|
+
}
|
|
19470
|
+
}
|
|
19471
|
+
client.invalidateDefaultLens();
|
|
19472
|
+
return {
|
|
19473
|
+
status: "created",
|
|
19474
|
+
lens: { id: created.id, name: created.name },
|
|
19475
|
+
filter_applied: merged,
|
|
19476
|
+
message: `Created "${created.name}".`,
|
|
19477
|
+
_meta: { region: client.region }
|
|
19478
|
+
};
|
|
19479
|
+
}
|
|
19480
|
+
};
|
|
19481
|
+
}
|
|
19482
|
+
});
|
|
19483
|
+
|
|
18128
19484
|
// ../core/dist/composite/answer-clarification.js
|
|
18129
19485
|
var answerClarification;
|
|
18130
19486
|
var init_answer_clarification = __esm({
|
|
@@ -18663,8 +20019,11 @@ __export(dist_exports, {
|
|
|
18663
20019
|
InMemoryBulkStore: () => InMemoryBulkStore,
|
|
18664
20020
|
LeadbayClient: () => LeadbayClient,
|
|
18665
20021
|
LocalBulkStore: () => LocalBulkStore,
|
|
20022
|
+
NotificationsInbox: () => NotificationsInbox,
|
|
20023
|
+
NotificationsWsClient: () => NotificationsWsClient,
|
|
18666
20024
|
REGIONS: () => REGIONS,
|
|
18667
20025
|
accountStatus: () => accountStatus,
|
|
20026
|
+
acknowledgeNotification: () => acknowledgeNotification,
|
|
18668
20027
|
addLeadsToCampaign: () => addLeadsToCampaign,
|
|
18669
20028
|
addNote: () => addNote,
|
|
18670
20029
|
adjustAudience: () => adjustAudience,
|
|
@@ -18672,6 +20031,7 @@ __export(dist_exports, {
|
|
|
18672
20031
|
agentMemoryRecall: () => agentMemoryRecall,
|
|
18673
20032
|
agentMemoryReview: () => agentMemoryReview,
|
|
18674
20033
|
agentMemoryTools: () => agentMemoryTools,
|
|
20034
|
+
anchorIdFor: () => anchorIdFor,
|
|
18675
20035
|
answerClarification: () => answerClarification,
|
|
18676
20036
|
appendEntry: () => appendEntry,
|
|
18677
20037
|
appendTombstone: () => appendTombstone,
|
|
@@ -18680,6 +20040,7 @@ __export(dist_exports, {
|
|
|
18680
20040
|
bulkQualifyLeads: () => bulkQualifyLeads,
|
|
18681
20041
|
campaignCallSheet: () => campaignCallSheet,
|
|
18682
20042
|
campaignProgression: () => campaignProgression,
|
|
20043
|
+
catchUpNotifications: () => catchUpNotifications,
|
|
18683
20044
|
clearAgentMemoryCache: () => clearAgentMemoryCache,
|
|
18684
20045
|
clearMockJournal: () => clearMockJournal,
|
|
18685
20046
|
clearSelection: () => clearSelection,
|
|
@@ -18729,6 +20090,7 @@ __export(dist_exports, {
|
|
|
18729
20090
|
importAndQualify: () => importAndQualify,
|
|
18730
20091
|
importLeads: () => importLeads,
|
|
18731
20092
|
importStatus: () => importStatus,
|
|
20093
|
+
inferKind: () => inferKind,
|
|
18732
20094
|
invalidateAgentMemoryCache: () => invalidateAgentMemoryCache,
|
|
18733
20095
|
isAgentMemoryEnabled: () => isAgentMemoryEnabled,
|
|
18734
20096
|
isValidBulkId: () => isValidBulkId,
|
|
@@ -18769,12 +20131,14 @@ __export(dist_exports, {
|
|
|
18769
20131
|
resolveAgentMemorySummary: () => resolveAgentMemorySummary,
|
|
18770
20132
|
resolveImportRows: () => resolveImportRows,
|
|
18771
20133
|
resolveRegion: () => resolveRegion,
|
|
20134
|
+
reviseHintFor: () => reviseHintFor,
|
|
18772
20135
|
seedCandidates: () => seedCandidates,
|
|
18773
20136
|
selectLeads: () => selectLeads,
|
|
18774
20137
|
setActiveLens: () => setActiveLens,
|
|
18775
20138
|
setEpilogueStatus: () => setEpilogueStatus,
|
|
18776
20139
|
setPushback: () => setPushback,
|
|
18777
20140
|
setUserPrompt: () => setUserPrompt,
|
|
20141
|
+
toInboxEntry: () => toInboxEntry,
|
|
18778
20142
|
tools: () => tools,
|
|
18779
20143
|
tourPlan: () => tourPlan,
|
|
18780
20144
|
updateLens: () => updateLens,
|
|
@@ -18789,6 +20153,7 @@ var init_dist = __esm({
|
|
|
18789
20153
|
init_types();
|
|
18790
20154
|
init_agent_memory();
|
|
18791
20155
|
init_composite_file_names();
|
|
20156
|
+
init_notifications();
|
|
18792
20157
|
init_login();
|
|
18793
20158
|
init_list_lenses();
|
|
18794
20159
|
init_discover_leads();
|
|
@@ -18818,6 +20183,7 @@ var init_dist = __esm({
|
|
|
18818
20183
|
init_agent_memory_recall();
|
|
18819
20184
|
init_agent_memory_capture();
|
|
18820
20185
|
init_agent_memory_review();
|
|
20186
|
+
init_acknowledge_notification();
|
|
18821
20187
|
init_select_leads();
|
|
18822
20188
|
init_deselect_leads();
|
|
18823
20189
|
init_clear_selection();
|
|
@@ -18867,6 +20233,8 @@ var init_dist = __esm({
|
|
|
18867
20233
|
init_refine_prompt();
|
|
18868
20234
|
init_seed_candidates();
|
|
18869
20235
|
init_extend_lens();
|
|
20236
|
+
init_my_lenses();
|
|
20237
|
+
init_new_lens();
|
|
18870
20238
|
init_answer_clarification();
|
|
18871
20239
|
init_report_outreach();
|
|
18872
20240
|
init_report_friction();
|
|
@@ -18886,7 +20254,6 @@ var init_dist = __esm({
|
|
|
18886
20254
|
getQuota,
|
|
18887
20255
|
getLensFilter,
|
|
18888
20256
|
getLensScoring,
|
|
18889
|
-
listSectors,
|
|
18890
20257
|
listLocations,
|
|
18891
20258
|
getUserPrompt,
|
|
18892
20259
|
getClarification,
|
|
@@ -18957,6 +20324,11 @@ var init_dist = __esm({
|
|
|
18957
20324
|
// it for discoverability; expose it always-on so agents can find custom fields
|
|
18958
20325
|
// without needing LEADBAY_MCP_ADVANCED=1.
|
|
18959
20326
|
listMappableFields,
|
|
20327
|
+
// listSectors is granular-shaped but ALWAYS exposed: it's the sector taxonomy
|
|
20328
|
+
// lookup the agent needs to STOP guessing sector names (and to feed
|
|
20329
|
+
// leadbay_new_lens / leadbay_adjust_audience). Without it the agent can only
|
|
20330
|
+
// probe sectors by trial-and-error or ask the user to read the web UI.
|
|
20331
|
+
listSectors,
|
|
18960
20332
|
// Billing / top-up tools — granular-shaped but ALWAYS exposed because
|
|
18961
20333
|
// they're the canonical recovery path from a QUOTA_EXCEEDED wall. If
|
|
18962
20334
|
// they were gated behind LEADBAY_MCP_ADVANCED=1 the agent would
|
|
@@ -18971,7 +20343,13 @@ var init_dist = __esm({
|
|
|
18971
20343
|
// didn't deliver"). Does not mutate Leadbay state; emits a PostHog
|
|
18972
20344
|
// event only. Companion to leadbay_report_outreach (which DOES write
|
|
18973
20345
|
// to the backend and stays gated behind LEADBAY_MCP_WRITE).
|
|
18974
|
-
reportFriction
|
|
20346
|
+
reportFriction,
|
|
20347
|
+
// Notification ack — ALWAYS exposed even though it POSTs to /seen.
|
|
20348
|
+
// _meta.notifications surfaces terminal bulk-progress notifications on
|
|
20349
|
+
// every tool response regardless of write gating; without ack the agent
|
|
20350
|
+
// sees the same entries on every call forever. Pairing the surfacing
|
|
20351
|
+
// channel with the clearing tool is non-optional.
|
|
20352
|
+
acknowledgeNotification
|
|
18975
20353
|
];
|
|
18976
20354
|
compositeWriteTools = [
|
|
18977
20355
|
bulkQualifyLeads,
|
|
@@ -18999,7 +20377,16 @@ var init_dist = __esm({
|
|
|
18999
20377
|
removeLeadsFromCampaign,
|
|
19000
20378
|
// Lens extend — agent-driven on-demand fill (additive). Gated behind
|
|
19001
20379
|
// LEADBAY_MCP_WRITE=1. Subject to per-org daily LENS_EXTRA_REFILL quota.
|
|
19002
|
-
extendLens
|
|
20380
|
+
extendLens,
|
|
20381
|
+
// Lens list/switch — read-first (no args = pure list); a switchToLensId
|
|
20382
|
+
// changes the active lens. In compositeWriteTools because the switch path
|
|
20383
|
+
// mutates last_requested_lens, but it stays on the default surface
|
|
20384
|
+
// (write is on by default since 0.3.0).
|
|
20385
|
+
myLenses,
|
|
20386
|
+
// Lens creation — make a brand-new named lens with sectors/sizes in one
|
|
20387
|
+
// call. Default-surface so "create a lens called X for Y" works without
|
|
20388
|
+
// the advanced gate.
|
|
20389
|
+
newLens
|
|
19003
20390
|
];
|
|
19004
20391
|
compositeTools = [
|
|
19005
20392
|
...compositeReadTools,
|
|
@@ -21310,6 +22697,22 @@ function buildServer(client, opts = {}) {
|
|
|
21310
22697
|
});
|
|
21311
22698
|
}
|
|
21312
22699
|
};
|
|
22700
|
+
const maybeAttachNotifications = (result) => {
|
|
22701
|
+
const inbox = opts.notificationsInbox;
|
|
22702
|
+
if (!inbox) return;
|
|
22703
|
+
if (result === null || typeof result !== "object" || Array.isArray(result)) {
|
|
22704
|
+
return;
|
|
22705
|
+
}
|
|
22706
|
+
const entries = inbox.list();
|
|
22707
|
+
if (entries.length === 0) return;
|
|
22708
|
+
const envelope = result;
|
|
22709
|
+
const target = envelope.__markdown_envelope === true && envelope.structured !== null && typeof envelope.structured === "object" && !Array.isArray(envelope.structured) ? envelope.structured : envelope;
|
|
22710
|
+
const existingMeta = target._meta && typeof target._meta === "object" && !Array.isArray(target._meta) ? target._meta : {};
|
|
22711
|
+
target._meta = {
|
|
22712
|
+
...existingMeta,
|
|
22713
|
+
notifications: entries
|
|
22714
|
+
};
|
|
22715
|
+
};
|
|
21313
22716
|
const isLeadbayBusinessError = (err) => err != null && typeof err === "object" && err.error === true && typeof err.code === "string";
|
|
21314
22717
|
const buildBusinessCtx = (toolName, envelope, triggered_by) => {
|
|
21315
22718
|
const meta = envelope._meta ?? {};
|
|
@@ -21429,11 +22832,13 @@ function buildServer(client, opts = {}) {
|
|
|
21429
22832
|
const result = await tool.execute(client, args, {
|
|
21430
22833
|
logger: opts.logger,
|
|
21431
22834
|
bulkTracker: opts.bulkTracker,
|
|
22835
|
+
notificationsInbox: opts.notificationsInbox,
|
|
21432
22836
|
signal: extra.signal,
|
|
21433
22837
|
progress,
|
|
21434
22838
|
elicit
|
|
21435
22839
|
});
|
|
21436
22840
|
maybeAttachUpdate(name, result);
|
|
22841
|
+
maybeAttachNotifications(result);
|
|
21437
22842
|
if (result && typeof result === "object" && result.error === true) {
|
|
21438
22843
|
const envText = formatErrorForLLM(result);
|
|
21439
22844
|
const envDur = Date.now() - callStart;
|
|
@@ -22871,7 +24276,7 @@ var OAUTH_BASE_URLS = {
|
|
|
22871
24276
|
fr: "https://staging.api.leadbay.app"
|
|
22872
24277
|
}
|
|
22873
24278
|
};
|
|
22874
|
-
var VERSION = "0.
|
|
24279
|
+
var VERSION = "0.18.0";
|
|
22875
24280
|
var HELP = `
|
|
22876
24281
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
22877
24282
|
|
|
@@ -23968,21 +25373,41 @@ async function main() {
|
|
|
23968
25373
|
logger.warn?.(`update_check.unexpected ${err?.message ?? err}`);
|
|
23969
25374
|
});
|
|
23970
25375
|
}
|
|
25376
|
+
const notificationsInbox = new NotificationsInbox();
|
|
25377
|
+
let notificationsWs = null;
|
|
25378
|
+
const WS_DISABLED = process.env.LEADBAY_NOTIFICATIONS_WS_DISABLED === "1" || authState !== "ok";
|
|
25379
|
+
if (!WS_DISABLED) {
|
|
25380
|
+
notificationsWs = new NotificationsWsClient({
|
|
25381
|
+
client,
|
|
25382
|
+
inbox: notificationsInbox,
|
|
25383
|
+
logger
|
|
25384
|
+
});
|
|
25385
|
+
void notificationsWs.start().catch((err) => {
|
|
25386
|
+
logger.warn?.(
|
|
25387
|
+
`notifications.ws start_failed: ${err?.message ?? err}`
|
|
25388
|
+
);
|
|
25389
|
+
});
|
|
25390
|
+
}
|
|
23971
25391
|
const server = buildServer(client, {
|
|
23972
25392
|
includeAdvanced,
|
|
23973
25393
|
includeWrite,
|
|
23974
25394
|
logger,
|
|
23975
25395
|
bulkTracker,
|
|
25396
|
+
notificationsInbox,
|
|
23976
25397
|
version: VERSION,
|
|
23977
25398
|
telemetry,
|
|
23978
25399
|
updateStateStore
|
|
23979
25400
|
});
|
|
23980
25401
|
const transport = new StdioServerTransport();
|
|
23981
25402
|
logger.info?.(
|
|
23982
|
-
`Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability}, auth_state=${authState})`
|
|
25403
|
+
`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})`
|
|
23983
25404
|
);
|
|
23984
25405
|
await server.connect(transport);
|
|
23985
25406
|
const shutdown = async (code) => {
|
|
25407
|
+
try {
|
|
25408
|
+
notificationsWs?.stop();
|
|
25409
|
+
} catch {
|
|
25410
|
+
}
|
|
23986
25411
|
try {
|
|
23987
25412
|
await telemetry.shutdown();
|
|
23988
25413
|
} finally {
|