@leadbay/mcp 0.31.1 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +184 -0
- package/README.md +2 -1
- package/dist/bin.js +1189 -219
- package/dist/http-server.js +1161 -227
- package/dist/installer-electron.js +1 -1
- package/dist/installer-gui.js +1 -1
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -11,20 +11,47 @@ var __export = (target, all) => {
|
|
|
11
11
|
|
|
12
12
|
// ../core/dist/client.js
|
|
13
13
|
import https from "https";
|
|
14
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
14
15
|
import { readdirSync, readFileSync, existsSync } from "fs";
|
|
15
16
|
import { join } from "path";
|
|
16
|
-
function
|
|
17
|
+
function defaultTimeoutMs() {
|
|
18
|
+
const raw = process.env.LEADBAY_TIMEOUT_MS;
|
|
19
|
+
if (raw === void 0 || raw.trim() === "")
|
|
20
|
+
return DEFAULT_REQUEST_TIMEOUT_MS;
|
|
21
|
+
const n = Number(raw);
|
|
22
|
+
return Number.isFinite(n) ? n : DEFAULT_REQUEST_TIMEOUT_MS;
|
|
23
|
+
}
|
|
24
|
+
function runWithRequestSignal(signal, fn) {
|
|
25
|
+
return requestSignalStore.run(signal, fn);
|
|
26
|
+
}
|
|
27
|
+
function makeCancelledError(method, url) {
|
|
28
|
+
const err = new Error(`Request cancelled: ${method} ${url}`);
|
|
29
|
+
err.name = "AbortError";
|
|
30
|
+
err.code = "CANCELLED";
|
|
31
|
+
return err;
|
|
32
|
+
}
|
|
33
|
+
function httpsRequest(method, url, headers, body, timeoutMs, signal) {
|
|
34
|
+
const deadlineMs = timeoutMs ?? defaultTimeoutMs();
|
|
35
|
+
const abortSignal = signal ?? requestSignalStore.getStore();
|
|
36
|
+
const abortSafe = method.toUpperCase() === "GET";
|
|
17
37
|
return new Promise((resolve, reject) => {
|
|
18
38
|
const start = Date.now();
|
|
39
|
+
if (abortSignal?.aborted) {
|
|
40
|
+
reject(makeCancelledError(method, url));
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
19
43
|
const parsed = new URL(url);
|
|
20
44
|
const reqHeaders = { ...headers };
|
|
21
45
|
if (body !== void 0) {
|
|
22
46
|
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
23
47
|
}
|
|
24
48
|
let deadline;
|
|
49
|
+
let onAbort;
|
|
25
50
|
const clearDeadline = () => {
|
|
26
51
|
if (deadline !== void 0)
|
|
27
52
|
clearTimeout(deadline);
|
|
53
|
+
if (onAbort)
|
|
54
|
+
abortSignal?.removeEventListener("abort", onAbort);
|
|
28
55
|
};
|
|
29
56
|
const req = https.request({
|
|
30
57
|
hostname: parsed.hostname,
|
|
@@ -45,15 +72,24 @@ function httpsRequest(method, url, headers, body, timeoutMs) {
|
|
|
45
72
|
});
|
|
46
73
|
});
|
|
47
74
|
});
|
|
48
|
-
if (
|
|
75
|
+
if (deadlineMs > 0) {
|
|
49
76
|
deadline = setTimeout(() => {
|
|
50
77
|
req.destroy?.();
|
|
51
|
-
const err = new Error(`Request timed out after ${
|
|
78
|
+
const err = new Error(`Request timed out after ${deadlineMs}ms: ${method} ${url}`);
|
|
52
79
|
err.code = "TIMEOUT";
|
|
80
|
+
err.timeout_ms = deadlineMs;
|
|
53
81
|
reject(err);
|
|
54
|
-
},
|
|
82
|
+
}, deadlineMs);
|
|
55
83
|
deadline.unref?.();
|
|
56
84
|
}
|
|
85
|
+
if (abortSignal && abortSafe) {
|
|
86
|
+
onAbort = () => {
|
|
87
|
+
req.destroy?.();
|
|
88
|
+
clearDeadline();
|
|
89
|
+
reject(makeCancelledError(method, url));
|
|
90
|
+
};
|
|
91
|
+
abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
92
|
+
}
|
|
57
93
|
req.on("error", (e) => {
|
|
58
94
|
clearDeadline();
|
|
59
95
|
reject(e);
|
|
@@ -177,7 +213,7 @@ function parseRetryAfter(value) {
|
|
|
177
213
|
}
|
|
178
214
|
return null;
|
|
179
215
|
}
|
|
180
|
-
var LENS_CACHE_TTL_MS, TASTE_CACHE_TTL_MS, ME_CACHE_TTL_MS, MAX_CONCURRENT, REGIONS, API_VERSION, API_PREFIX, _mockFixtures, _mockJournal, LeadbayClient;
|
|
216
|
+
var LENS_CACHE_TTL_MS, TASTE_CACHE_TTL_MS, ME_CACHE_TTL_MS, MAX_CONCURRENT, DEFAULT_REQUEST_TIMEOUT_MS, requestSignalStore, REGIONS, API_VERSION, API_PREFIX, _mockFixtures, _mockJournal, LeadbayClient;
|
|
181
217
|
var init_client = __esm({
|
|
182
218
|
"../core/dist/client.js"() {
|
|
183
219
|
"use strict";
|
|
@@ -185,6 +221,8 @@ var init_client = __esm({
|
|
|
185
221
|
TASTE_CACHE_TTL_MS = 10 * 60 * 1e3;
|
|
186
222
|
ME_CACHE_TTL_MS = 60 * 1e3;
|
|
187
223
|
MAX_CONCURRENT = 5;
|
|
224
|
+
DEFAULT_REQUEST_TIMEOUT_MS = 6e5;
|
|
225
|
+
requestSignalStore = new AsyncLocalStorage();
|
|
188
226
|
REGIONS = {
|
|
189
227
|
us: "https://api-us.leadbay.app",
|
|
190
228
|
fr: "https://api-fr.leadbay.app"
|
|
@@ -408,6 +446,8 @@ var init_client = __esm({
|
|
|
408
446
|
throw this.mapErrorResponse(res.status, res.body, path, res.headers);
|
|
409
447
|
}
|
|
410
448
|
return JSON.parse(res.body);
|
|
449
|
+
} catch (e) {
|
|
450
|
+
throw this.mapTransportError(e, `${method} ${path}`);
|
|
411
451
|
} finally {
|
|
412
452
|
this.releaseSemaphore();
|
|
413
453
|
}
|
|
@@ -439,6 +479,8 @@ var init_client = __esm({
|
|
|
439
479
|
if (res.status < 200 || res.status >= 300) {
|
|
440
480
|
throw this.mapErrorResponse(res.status, res.body, path, res.headers);
|
|
441
481
|
}
|
|
482
|
+
} catch (e) {
|
|
483
|
+
throw this.mapTransportError(e, `${method} ${path}`);
|
|
442
484
|
} finally {
|
|
443
485
|
this.releaseSemaphore();
|
|
444
486
|
}
|
|
@@ -476,6 +518,8 @@ var init_client = __esm({
|
|
|
476
518
|
throw this.mapErrorResponse(res.status, res.body, path, res.headers);
|
|
477
519
|
}
|
|
478
520
|
return JSON.parse(res.body);
|
|
521
|
+
} catch (e) {
|
|
522
|
+
throw this.mapTransportError(e, `${method} ${path}`);
|
|
479
523
|
} finally {
|
|
480
524
|
this.releaseSemaphore();
|
|
481
525
|
}
|
|
@@ -536,6 +580,29 @@ var init_client = __esm({
|
|
|
536
580
|
would_call: { method, path: fullPath, body: journalBody }
|
|
537
581
|
};
|
|
538
582
|
}
|
|
583
|
+
/**
|
|
584
|
+
* Turn httpsRequest's raw TIMEOUT rejection into the `{error:true, code, …}`
|
|
585
|
+
* envelope every other failure already speaks, so the agent gets something it
|
|
586
|
+
* can read out to the user and act on rather than a bare Error string. Any
|
|
587
|
+
* other rejection (ECONNRESET, DNS, a mapped 4xx/5xx) passes through untouched
|
|
588
|
+
* — this is a translation, not a catch-all.
|
|
589
|
+
*
|
|
590
|
+
* The code stays "TIMEOUT" so the hosted auth probe's existing branch
|
|
591
|
+
* (auth-http.ts) keeps classifying it as a transient fault and moves to the
|
|
592
|
+
* sibling region instead of declaring a live token expired.
|
|
593
|
+
*/
|
|
594
|
+
mapTransportError(e, endpoint) {
|
|
595
|
+
const err = e;
|
|
596
|
+
if (err?.code !== "TIMEOUT")
|
|
597
|
+
return e;
|
|
598
|
+
const ms = err.timeout_ms ?? defaultTimeoutMs();
|
|
599
|
+
const envelope = this.makeError("TIMEOUT", `Leadbay did not respond within ${ms}ms \u2014 the request was cancelled`, "The connection was accepted but no response came back, so this is a Leadbay-side stall, not a bad request. It is transient: retry the same call once. If it times out again, tell the user Leadbay is not responding right now and offer to report it with leadbay_report_friction.", endpoint);
|
|
600
|
+
if (envelope._meta) {
|
|
601
|
+
envelope._meta.timeout_ms = ms;
|
|
602
|
+
envelope._meta.latency_ms = ms;
|
|
603
|
+
}
|
|
604
|
+
return envelope;
|
|
605
|
+
}
|
|
539
606
|
mapErrorResponse(status, rawBody, endpoint, headers) {
|
|
540
607
|
let parsed;
|
|
541
608
|
try {
|
|
@@ -657,6 +724,8 @@ var init_client = __esm({
|
|
|
657
724
|
this.telemetryEnabledFromStamp = false;
|
|
658
725
|
}
|
|
659
726
|
return observed;
|
|
727
|
+
} catch (e) {
|
|
728
|
+
throw this.mapTransportError(e, "GET /users/me");
|
|
660
729
|
} finally {
|
|
661
730
|
this.releaseSemaphore();
|
|
662
731
|
}
|
|
@@ -5693,7 +5762,7 @@ var init_notifications = __esm({
|
|
|
5693
5762
|
});
|
|
5694
5763
|
|
|
5695
5764
|
// ../core/dist/tool-descriptions.generated.js
|
|
5696
|
-
var leadbay_account_history, leadbay_account_status, leadbay_acknowledge_notification, leadbay_add_contact, leadbay_add_leads_to_campaign, leadbay_add_note, leadbay_adjust_audience, leadbay_agent_memory_capture, leadbay_agent_memory_recall, leadbay_agent_memory_review, leadbay_answer_clarification, leadbay_artifact_kit, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_delete_custom_field, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_custom_fields, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_qualification_questions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_getting_started, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_pin_contact, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_contact, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_scan_portfolio_signals, leadbay_seed_candidates, leadbay_select_leads, leadbay_send_feedback, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_pushback, leadbay_set_qualification_questions, leadbay_set_telemetry, leadbay_set_user_prompt, leadbay_team_activity, leadbay_tour_plan, leadbay_unpin_contact, leadbay_update_contact, leadbay_update_custom_field, leadbay_update_lens, leadbay_update_lens_filter;
|
|
5765
|
+
var leadbay_account_history, leadbay_account_status, leadbay_acknowledge_notification, leadbay_add_contact, leadbay_add_leads_to_campaign, leadbay_add_note, leadbay_adjust_audience, leadbay_agent_memory_capture, leadbay_agent_memory_recall, leadbay_agent_memory_review, leadbay_answer_clarification, leadbay_artifact_kit, leadbay_bulk_enrich_status, leadbay_bulk_qualify_leads, leadbay_campaign_call_sheet, leadbay_campaign_progression, leadbay_clear_selection, leadbay_clear_user_prompt, leadbay_create_campaign, leadbay_create_custom_field, leadbay_create_lens, leadbay_create_lens_draft, leadbay_create_topup_link, leadbay_delete_custom_field, leadbay_deselect_leads, leadbay_discover_leads, leadbay_dislike_lead, leadbay_dismiss_clarification, leadbay_enrich_contacts, leadbay_enrich_titles, leadbay_extend_lens, leadbay_followups_map, leadbay_get_clarification, leadbay_get_contacts, leadbay_get_enrichment_job_titles, leadbay_get_epilogue_responses, leadbay_get_lead_activities, leadbay_get_lead_custom_fields, leadbay_get_lead_notes, leadbay_get_lead_profile, leadbay_get_lens_filter, leadbay_get_lens_scoring, leadbay_get_prospecting_actions, leadbay_get_qualification_questions, leadbay_get_quota, leadbay_get_selection_ids, leadbay_get_taste_profile, leadbay_get_user_prompt, leadbay_get_web_fetch, leadbay_getting_started, leadbay_import_and_qualify, leadbay_import_leads, leadbay_import_status, leadbay_launch_bulk_enrichment, leadbay_like_lead, leadbay_list_campaigns, leadbay_list_lenses, leadbay_list_locations, leadbay_list_mappable_fields, leadbay_list_sectors, leadbay_login, leadbay_my_lenses, leadbay_new_lens, leadbay_open_billing_portal, leadbay_pick_clarification, leadbay_pin_contact, leadbay_prepare_outreach, leadbay_preview_bulk_enrichment, leadbay_promote_lens, leadbay_pull_followups, leadbay_pull_leads, leadbay_qualify_lead, leadbay_qualify_status, leadbay_recall_ordered_titles, leadbay_refine_prompt, leadbay_remove_contact, leadbay_remove_epilogue, leadbay_remove_leads_from_campaign, leadbay_remove_pushback, leadbay_report_friction, leadbay_report_outreach, leadbay_research_lead_by_id, leadbay_research_lead_by_name_fuzzy, leadbay_resolve_import_rows, leadbay_scan_portfolio_signals, leadbay_seed_candidates, leadbay_select_leads, leadbay_send_feedback, leadbay_set_active_lens, leadbay_set_epilogue_status, leadbay_set_lead_status, leadbay_set_pushback, leadbay_set_qualification_questions, leadbay_set_telemetry, leadbay_set_user_prompt, leadbay_team_activity, leadbay_tour_plan, leadbay_unpin_contact, leadbay_update_contact, leadbay_update_custom_field, leadbay_update_lens, leadbay_update_lens_filter;
|
|
5697
5766
|
var init_tool_descriptions_generated = __esm({
|
|
5698
5767
|
"../core/dist/tool-descriptions.generated.js"() {
|
|
5699
5768
|
"use strict";
|
|
@@ -6247,7 +6316,7 @@ The model \u2014 two layers. Primitives: \`lb.field\` (value + API-populated opt
|
|
|
6247
6316
|
|
|
6248
6317
|
Canonical uses: a cold-call sheet (\`lb.callList\` + per-row \`lb.outreach\`/\`lb.leadHistory\`); a manager dashboard (\`lb.teamActivity\` \u2192 leaderboard table + Chart.js trend); a live enrichment view (\`lb.enrichment\` \u2192 progress + refresh). Live auto-poll is host-dependent \u2014 always wire a Refresh.
|
|
6249
6318
|
|
|
6250
|
-
Write-call footguns (in the guide, repeated because they bite): for \`leadbay_report_outreach\` (status/disposition) the \`args\` MUST include \`verification:{source:"user_confirmed",ref:"\u2026"}\` AND \`_triggered_by:"<the user's request>"\`, or the call is rejected. \`leadbay_add_leads_to_campaign\` needs \`_triggered_by\` too. \`leadbay_add_note\`/\`leadbay_like_lead\`/\`leadbay_dislike_lead\` need only their own args. Snoozing (pushback)
|
|
6319
|
+
Write-call footguns (in the guide, repeated because they bite): for \`leadbay_report_outreach\` (status/disposition) the \`args\` MUST include \`verification:{source:"user_confirmed",ref:"\u2026"}\` AND \`_triggered_by:"<the user's request>"\`, or the call is rejected. \`leadbay_add_leads_to_campaign\` needs \`_triggered_by\` too. \`leadbay_add_note\`/\`leadbay_like_lead\`/\`leadbay_dislike_lead\` need only their own args. Snoozing (pushback) is advanced-gated \u2014 not callable from a default artifact. Org CRM status IS available: \`lb.leadStatus()\` gives the Wanted/Won/Lost/Unwanted picker field and \`lb.setStatus()\` the write (\`leadbay_set_lead_status\`), with the partial-write check baked in. Keep it distinct from \`report_outreach\`'s \`epilogue_status\`, which records how one outreach ATTEMPT went.
|
|
6251
6320
|
|
|
6252
6321
|
WHEN TO USE: the user asks for a clickable / interactive artifact, dashboard, or call sheet that DOES things (not just displays data).
|
|
6253
6322
|
|
|
@@ -7105,9 +7174,31 @@ WHEN NOT TO USE: to answer the question \u2014 use leadbay_answer_clarification.
|
|
|
7105
7174
|
`;
|
|
7106
7175
|
leadbay_get_contacts = `Get contacts for a lead, including enriched email and phone data. Returns both organization contacts and enrichable contacts with IDs, tagged with \`source:'org'|'paid'\`.
|
|
7107
7176
|
|
|
7108
|
-
WHEN TO USE: to check enrichment status (\`contact.enrichment
|
|
7177
|
+
WHEN TO USE: to check enrichment status (\`contact.enrichment\`) on individual leads after a bulk enrichment was launched, or to find the \`contact_id\` needed by leadbay_enrich_contacts.
|
|
7109
7178
|
|
|
7110
7179
|
WHEN NOT TO USE: as a substitute for leadbay_research_lead_by_id, which already includes enriched contacts in its return.
|
|
7180
|
+
|
|
7181
|
+
## Reading \`contact.enrichment\`
|
|
7182
|
+
|
|
7183
|
+
\`enrichment\` is the per-contact reveal record. Four states. Read \`done\` and \`credits_used\` **together** \u2014 neither is a verdict on its own.
|
|
7184
|
+
|
|
7185
|
+
| \`enrichment\` | \`done\` | \`credits_used\` | Meaning | What to do |
|
|
7186
|
+
|---|---|---|---|---|
|
|
7187
|
+
| missing / \`null\` | \u2014 | \u2014 | Never requested. | Enrichable \u2014 launch it. |
|
|
7188
|
+
| present | \`false\` | any | Reservation in flight. | Poll. Do NOT re-launch. |
|
|
7189
|
+
| present | \`true\` | \`0\` | Settled, found nothing. | **Terminal. Stop.** |
|
|
7190
|
+
| present | \`true\` | \`> 0\` | Resolved. | The channel is on the org-source twin, not here \u2014 see below. |
|
|
7191
|
+
|
|
7192
|
+
**\`done: true\` with \`credits_used: 0\` is terminal for that contact.** The enrichment completed and the provider returned nothing. Do not re-attempt it on a later run \u2014 the answer will not change. Roughly 29% of all enrichments land here, so it is an ordinary outcome, not an anomaly. Tell the user plainly that there is no reachable contact rather than reporting it as still pending. The one exception: a launch that errored in this same session settles its reservation as a zero-credit failure, so retry that one once, then treat it as terminal.
|
|
7193
|
+
|
|
7194
|
+
Two ways to misread the pair:
|
|
7195
|
+
|
|
7196
|
+
- **\`credits_used: 0\` alone means nothing.** An in-flight reservation also reports \`0\` (with \`done: false\`). Gate every read of \`credits_used\` on \`done: true\`.
|
|
7197
|
+
- **An absent \`credits_used\` is unknown, not zero.** The field is optional. When it is missing you cannot conclude terminal-empty \u2014 only an explicit \`0\` alongside \`done: true\` means "we looked and found nothing".
|
|
7198
|
+
|
|
7199
|
+
A missing or \`null\` \`enrichment\` is not the same as \`done: false\`. It means the contact was never requested, so \`enrichment?.done\` reading falsy does NOT mean a reveal is running. Treat missing/\`null\` as enrichable and \`done: false\` as in-flight.
|
|
7200
|
+
|
|
7201
|
+
**Where a resolved channel lands.** A \`source:'paid'\` contact keeps its \`enrichment\` record but never carries the revealed \`email\` / \`phone_number\` itself \u2014 the reveal materializes a \`source:'org'\` twin of the same person, and the channel is on that entry. So a paid contact reading \`done: true, credits_used: 1\` with a null \`email\` is RESOLVED, not failed: find the org-source entry with the same name and read the channel there. Judge success by the channel you can actually see across both entries, never by \`enrichment\` on the paid record alone.
|
|
7111
7202
|
`;
|
|
7112
7203
|
leadbay_get_enrichment_job_titles = `List the actual job titles present across the leads currently in the user's selection \u2014 the candidate set the user can ask to enrich.
|
|
7113
7204
|
|
|
@@ -7422,7 +7513,7 @@ WHEN NOT TO USE: discovery (use leadbay_pull_leads); single-lead deep dive (use
|
|
|
7422
7513
|
|
|
7423
7514
|
Budgets: \`total_budget_ms\` caps wall-clock; \`per_lead_budget_ms\` caps each lead's poll. For short transport timeouts, pass \`wait_for_completion:false\` and poll \`leadbay_import_status\`. Outputs \`qualified[]\`, \`still_running[]\`, \`not_imported[]\`, \`qualify_id\` (resumable handle). Idempotent within a 5-min window. \`dry_run:'preview'\` returns mapping hints + custom-field candidates without importing.
|
|
7424
7515
|
|
|
7425
|
-
\`not_imported\` rows with \`reason:"uncrawled"\` are **pending a background crawl**, NOT failures: Leadbay just hasn't matched/crawled that domain yet and will add the lead asynchronously (the label doesn't verify the URL resolves \u2014 don't call the site bad, but don't certify it valid either). Surface them as pending; the leads populate in the user's Leadbay account as the crawl completes (no tool here fetches them on demand \u2014 \`leadbay_import_status\`
|
|
7516
|
+
\`not_imported\` rows with \`reason:"uncrawled"\` are **pending a background crawl**, NOT failures: Leadbay just hasn't matched/crawled that domain yet and will add the lead asynchronously (the label doesn't verify the URL resolves \u2014 don't call the site bad, but don't certify it valid either). Surface them as pending; the leads populate in the user's Leadbay account as the crawl completes (no tool here fetches them on demand \u2014 \`leadbay_import_status\` reports the rows the wizard has already placed, not leads a later crawl adds, and \`leadbay_pull_leads\` reads the active lens's wishlist so an imported lead outside that lens may not appear). To pull those specific companies back through the MCP, re-run the import later. A large \`uncrawled\` share on a fresh list is normal.
|
|
7426
7517
|
|
|
7427
7518
|
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\`.
|
|
7428
7519
|
|
|
@@ -7447,7 +7538,8 @@ Otherwise, partition \`not_imported\` by \`reason\` into these buckets before yo
|
|
|
7447
7538
|
**Header \u2014 single line, choose by status:**
|
|
7448
7539
|
|
|
7449
7540
|
- Completed: \`"\u2713 Import complete \u2014 N imported \xB7 P pending crawl \xB7 Q need attention"\` (drop any segment whose count is 0)
|
|
7450
|
-
- Running: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
|
|
7541
|
+
- Running, \`handle_id\` present: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
|
|
7542
|
+
- Running with \`timed_out:true\` (blocking call ran out of poll budget): the import is FINE and still running server-side \u2014 never render this as an error or a failure. \`"\u23F3 Import still running (the backend is slow today) \u2014 I'll check back."\` Then call \`leadbay_import_status({importIds})\`, do NOT re-run leadbay_import_leads. If \`rows_pending_upload\` is present, add \`"\u26A0 K rows weren't submitted \u2014 re-import just those."\`
|
|
7451
7543
|
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 qualify_id <id>"\`
|
|
7452
7544
|
|
|
7453
7545
|
Count \`uncrawled\` rows as **pending**, never as failures \u2014 never say "M failed" when the M is mostly/entirely uncrawled rows.
|
|
@@ -7496,7 +7588,9 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
7496
7588
|
|
|
7497
7589
|
| Observation | Suggest | Calls |
|
|
7498
7590
|
|------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
7499
|
-
| Status: running
|
|
7591
|
+
| Status: running, \`handle_id\` present | "Check progress" | leadbay_import_status(handle_id) |
|
|
7592
|
+
| Status: running with \`timed_out:true\` | "Check progress" \u2014 NOT "retry the import" | leadbay_import_status(importIds, dry_run if the result carried it) after ~30s; \`result.leads\` carries the leadIds once complete |
|
|
7593
|
+
| \`rows_pending_upload\` present | "Import the rows that never got submitted" | leadbay_import_leads (that subset only) |
|
|
7500
7594
|
| Status: complete, imports succeeded | "Run AI qualification on the imported leads" | leadbay_bulk_qualify_leads([leadIds]) \u2014 or use leadbay_import_and_qualify next time |
|
|
7501
7595
|
| Pending-crawl (\`uncrawled\`) rows present | "Re-run the import for those domains later, once Leadbay has crawled them" | leadbay_import_leads (re-run with just the uncrawled domains, later \u2014 they re-reconcile once crawled). NOTE: not a live-fetch of the added leads; those populate in the user's Leadbay account as the crawl completes |
|
|
7502
7596
|
| Ambiguous / unresolved rows present | "Resolve the ambiguous rows" | leadbay_resolve_import_rows(records, identity_mappings)|
|
|
@@ -7506,9 +7600,11 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
7506
7600
|
`;
|
|
7507
7601
|
leadbay_import_leads = `Import leads into Leadbay's CRM via the file-import wizard. Returns stable Leadbay leadIds for downstream chaining into leadbay_bulk_qualify_leads / leadbay_research_lead_by_id. For MCP clients with short transport timeouts, pass \`wait_for_completion:false\` to return quickly with \`{status:'running', handle_id}\`; poll leadbay_import_status with that handle. For end-to-end import+qualify in one call, prefer leadbay_import_and_qualify. For messy files, prefer the \`leadbay_import_file\` prompt which walks an agent through scan \u2192 resolve \u2192 preserve \u2192 commit phases.
|
|
7508
7602
|
|
|
7603
|
+
SLOW BACKEND \u21D2 \`{status:'running', timed_out:true, importIds}\`. The wizard is sometimes slow; when the poll budget runs out this tool returns that SUCCESS result, not an error. The import is still running server-side. **Do NOT call leadbay_import_leads again** \u2014 that re-uploads the file and leaves a duplicate CRM-imports row. Call \`leadbay_import_status({importIds})\` after ~30s \u2014 and pass \`dry_run:true\` too if the result carried it; on \`complete\` it returns \`result.leads\` with the leadIds, while \`phase:"committing"\` just means keep polling. Tell the user it's running and you'll check back \u2014 it is not a problem to report. Exception: \`rows_pending_upload\` rows never reached the backend and DO need a fresh call for that subset only. In records mode the result also carries \`row_ids\` \u2014 the synthetic id of each input row, in your \`records[]\` order \u2014 because \`leadbay_import_status\` reports recovered leads by that id; keep it to map them back to your source rows.
|
|
7604
|
+
|
|
7509
7605
|
TWO MODES: (A) Domain-list shortcut \u2014 pass \`domains: [{domain, name?}]\`. The tool builds a 2-column CSV (LEAD_NAME, LEAD_WEBSITE) and imports with the default mapping. (B) Custom records + mapping \u2014 pass \`records: [{Col1, Col2, ...}]\` plus \`mappings.fields: {Col1: 'LEAD_NAME', ...}\`. \`mappings.fields\` must include LEADBAY_ID, CRM_ID, SIREN, LEAD_NAME, or LEAD_WEBSITE (resolver needs at least one identity key). Pass exactly one of \`domains\` / \`records\`. Reserved column \`MCP_ROW_ID\` cannot appear in records/mappings \u2014 the tool injects it for stable reconciliation.
|
|
7510
7606
|
|
|
7511
|
-
\`not_imported\` rows with \`reason:"uncrawled"\` are **pending a background crawl**, NOT failures: Leadbay just hasn't matched/crawled that domain yet and will add the lead asynchronously (the label doesn't verify the URL resolves \u2014 don't call the site bad, but don't certify it valid either). Surface them as pending; the leads populate in the user's Leadbay account as the crawl completes (no tool here fetches them on demand \u2014 \`leadbay_import_status\`
|
|
7607
|
+
\`not_imported\` rows with \`reason:"uncrawled"\` are **pending a background crawl**, NOT failures: Leadbay just hasn't matched/crawled that domain yet and will add the lead asynchronously (the label doesn't verify the URL resolves \u2014 don't call the site bad, but don't certify it valid either). Surface them as pending; the leads populate in the user's Leadbay account as the crawl completes (no tool here fetches them on demand \u2014 \`leadbay_import_status\` reports the rows the wizard has already placed, not leads a later crawl adds, and \`leadbay_pull_leads\` reads the active lens's wishlist so an imported lead outside that lens may not appear). To pull those specific companies back through the MCP, re-run the import later. A large \`uncrawled\` share on a fresh list is normal.
|
|
7512
7608
|
|
|
7513
7609
|
MUTATES USER STATE: each call creates a row in the user's CRM-imports list (visible in the web UI) and touches onboarding state. Suitable for occasional automation, NOT for high-cadence (>5 calls/day). Imported leads are NOT auto-promoted to the user's Monitor view; lens-scoring threshold decides. For messy files call leadbay_resolve_import_rows first, then pass \`records_for_import\`/\`mappings_for_import\` here. Agents should inspect every column, build a preservation plan, and pass an explicit final mapping. For each meaningful column decide standard field, CONTACT_* field, Leadbay note, custom field, derived helper, or skip with a reason. For contact-only exports, derive a company-domain column from CONTACT_EMAIL only when it's a real business domain. Multiple rows can share the same LEADBAY_ID and import as separate contacts on that lead. Custom fields use \`CUSTOM.<id>\` in \`mappings.fields\` or the \`mappings.custom_fields\` shorthand. For source-system deep links create a custom field via leadbay_create_custom_field first (prefer EXTERNAL_ID + url_template). Preserve meaningful per-lead notes by calling leadbay_add_note after import returns lead IDs.
|
|
7514
7610
|
|
|
@@ -7539,7 +7635,8 @@ Otherwise, partition \`not_imported\` by \`reason\` into these buckets before yo
|
|
|
7539
7635
|
**Header \u2014 single line, choose by status:**
|
|
7540
7636
|
|
|
7541
7637
|
- Completed: \`"\u2713 Import complete \u2014 N imported \xB7 P pending crawl \xB7 Q need attention"\` (drop any segment whose count is 0)
|
|
7542
|
-
- Running: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
|
|
7638
|
+
- Running, \`handle_id\` present: \`"\u23F3 Import running \u2014 handle_id <id>; poll leadbay_import_status"\`
|
|
7639
|
+
- Running with \`timed_out:true\` (blocking call ran out of poll budget): the import is FINE and still running server-side \u2014 never render this as an error or a failure. \`"\u23F3 Import still running (the backend is slow today) \u2014 I'll check back."\` Then call \`leadbay_import_status({importIds})\`, do NOT re-run leadbay_import_leads. If \`rows_pending_upload\` is present, add \`"\u26A0 K rows weren't submitted \u2014 re-import just those."\`
|
|
7543
7640
|
- Pending qualification (\`leadbay_import_and_qualify\`): \`"\u2713 Imported N leads \xB7 qualifying M of them \u2014 qualify_id <id>"\`
|
|
7544
7641
|
|
|
7545
7642
|
Count \`uncrawled\` rows as **pending**, never as failures \u2014 never say "M failed" when the M is mostly/entirely uncrawled rows.
|
|
@@ -7588,7 +7685,9 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
7588
7685
|
|
|
7589
7686
|
| Observation | Suggest | Calls |
|
|
7590
7687
|
|------------------------------------------------|---------------------------------------------------------------|--------------------------------------------------------|
|
|
7591
|
-
| Status: running
|
|
7688
|
+
| Status: running, \`handle_id\` present | "Check progress" | leadbay_import_status(handle_id) |
|
|
7689
|
+
| Status: running with \`timed_out:true\` | "Check progress" \u2014 NOT "retry the import" | leadbay_import_status(importIds, dry_run if the result carried it) after ~30s; \`result.leads\` carries the leadIds once complete |
|
|
7690
|
+
| \`rows_pending_upload\` present | "Import the rows that never got submitted" | leadbay_import_leads (that subset only) |
|
|
7592
7691
|
| Status: complete, imports succeeded | "Run AI qualification on the imported leads" | leadbay_bulk_qualify_leads([leadIds]) \u2014 or use leadbay_import_and_qualify next time |
|
|
7593
7692
|
| Pending-crawl (\`uncrawled\`) rows present | "Re-run the import for those domains later, once Leadbay has crawled them" | leadbay_import_leads (re-run with just the uncrawled domains, later \u2014 they re-reconcile once crawled). NOTE: not a live-fetch of the added leads; those populate in the user's Leadbay account as the crawl completes |
|
|
7594
7693
|
| Ambiguous / unresolved rows present | "Resolve the ambiguous rows" | leadbay_resolve_import_rows(records, identity_mappings)|
|
|
@@ -7596,7 +7695,7 @@ User picks \u2192 call the matching \`Calls\` tool. Constraints: 2\u20134 mutual
|
|
|
7596
7695
|
| User wants to see the imported leads | "See the imported leads in your view" | leadbay_pull_leads |
|
|
7597
7696
|
| User had follow-up intent for the imports | "Prep outreach for [a specific imported lead]" | leadbay_prepare_outreach(leadId) |
|
|
7598
7697
|
`;
|
|
7599
|
-
leadbay_import_status = `Retrieve the current **status/progress** of a lead import. Pass \`handle_id\` \u2014 returned by either \`leadbay_import_leads\` OR \`leadbay_import_and_qualify\` when called with \`wait_for_completion:false\` \u2014 to resolve the stored result (leads + not_imported)
|
|
7698
|
+
leadbay_import_status = `Retrieve the current **status/progress** of a lead import, and its leadIds once it finishes. Pass \`handle_id\` \u2014 returned by either \`leadbay_import_leads\` OR \`leadbay_import_and_qualify\` when called with \`wait_for_completion:false\` \u2014 to resolve the stored result (leads + not_imported) for that async run. Pass \`importIds[]\` when you don't have a handle: after a blocking \`leadbay_import_leads\` returned \`{status:'running', timed_out:true, importIds}\`, or from any completed import's \`importIds\` (\`leadbay_import_and_qualify\` calls the same field \`import_ids\`). **Both paths return leads.** The \`importIds[]\` path reads the wizard's records directly, so once every named import is \`complete\` (and it wasn't a dry run) the response carries \`result.leads\` + \`result.not_imported\` \u2014 that is how you recover the leadIds from an import that timed out mid-poll, without re-importing. \`result.still_settling\` counts rows the wizard hasn't finished placing; they are neither imported nor failed, so poll again rather than reporting them. If \`result\` is absent on a \`complete\` import the records weren't readable \u2014 report completion without inventing counts. \`phase:"committing"\` means the import is still being committed, NOT that it finished with nothing: keep polling. Pass \`dry_run:true\` when the importIds came from a dry run (a \`{timed_out:true}\` result carries \`dry_run\` \u2014 hand it straight back); a finished dry run and an import mid-commit are identical on the wire, so without that flag this tool reports \`committing\` rather than risk rendering a validation pass as a real import. This status call performs a single refresh pass and never polls in a loop.
|
|
7600
7699
|
|
|
7601
7700
|
WHEN TO USE: after an async import (\`leadbay_import_leads\` OR \`leadbay_import_and_qualify\` with \`wait_for_completion:false\`) returns \`{status:'running', handle_id}\`, poll with that \`handle_id\`; OR to check whether a finished import is still processing. This tool does NOT surface the leads Leadbay adds later for pending-crawl (\`uncrawled\`) rows \u2014 those populate in the user's Leadbay account as the crawl completes; no tool here fetches them on demand (re-run the import to pull them back through the MCP).
|
|
7602
7701
|
|
|
@@ -7620,12 +7719,14 @@ After the status line, propose the obvious refresh / progress-check / recovery a
|
|
|
7620
7719
|
|
|
7621
7720
|
Specifically for import status:
|
|
7622
7721
|
|
|
7623
|
-
This tool returns \`status\`, \`importIds\`, and \`progress\` ({phase, records_processed, records_total}). It carries a \`result\` object (with \`leads\` + \`not_imported\`)
|
|
7722
|
+
This tool returns \`status\`, \`importIds\`, and \`progress\` ({phase, records_processed, records_total}). It carries a \`result\` object (with \`leads\` + \`not_imported\`) when resolving an async \`handle_id\` whose run completed in this MCP instance, AND on the \`importIds[]\` path once every named import is \`complete\` and it wasn't a dry run. A \`complete\` import with no \`result\` means the records weren't readable \u2014 render completion only. **Render only from the fields actually present; never invent counts.**
|
|
7624
7723
|
|
|
7625
7724
|
Caveat on \`progress\`: \`records_processed\` counts only the rows that MATCHED an existing lead (backend \`imported_records\`), not every row that finished processing \u2014 so for a complete import whose rows are mostly/all \`uncrawled\` (pending crawl), \`records_processed\` is legitimately low or 0. Never read a low \`records_processed\` on a \`complete\` import as "stuck" or "failed": once \`status:"complete"\`, processing is done; the pending-crawl rows just matched no existing lead yet.
|
|
7626
7725
|
|
|
7627
|
-
- Running \u2192 \`"\u23F3 Import still running \u2014 phase <phase>; check back in ~M minutes."\` (use the phase; don't turn the matched-count into an "X/Y processed" progress bar).
|
|
7628
|
-
- Complete
|
|
7726
|
+
- Running \u2192 \`"\u23F3 Import still running \u2014 phase <phase>; check back in ~M minutes."\` (use the phase; don't turn the matched-count into an "X/Y processed" progress bar). \`phase:"committing"\` is normal for an import that timed out \u2014 say it's still being committed, never that it failed or finished empty.
|
|
7727
|
+
- Complete with **\`dry_run:true\`** on the response \u2192 a VALIDATION pass; nothing was committed. \`"\u{1F50E} Dry run complete \u2014 input validated, nothing imported. Re-run without dry_run to commit."\` Never render this as a completed import, and never quote a lead count.
|
|
7728
|
+
- Complete, **no \`result\`** \u2192 \`"\u2713 Import complete."\` Do NOT append a \`records_processed/records_total\` fraction (it undercounts pending-crawl rows and looks stuck) and do NOT report pending-crawl / need-attention bucket counts \u2014 the row-level \`not_imported\` breakdown isn't in this response.
|
|
7729
|
+
- Complete with \`result.still_settling > 0\` \u2192 say \`"\u2713 Import complete \u2014 N imported, S rows still being placed."\` Never count \`still_settling\` rows as failures.
|
|
7629
7730
|
- Complete, **\`result\` present AND it was a dry run** (\`result.dry_run:true\`, or every \`result.not_imported\` row has \`reason:"dry_run"\`) \u2192 this resolved handle was a VALIDATION pass, nothing committed. Render \`"\u{1F50E} Dry run complete \u2014 V rows validated, nothing imported. Re-run without dry_run to commit."\` \u2014 do NOT render it as a real import completion or use the pending/attention buckets.
|
|
7630
7731
|
- Complete, **\`result\` present** (async handle resolved, real import) \u2192 then, and only then, partition \`result.not_imported\` as in the shared import-result render block below \u2014 \`"\u2713 Import complete \u2014 N imported \xB7 P pending crawl \xB7 Q need attention"\` where **pending crawl** is \`uncrawled\` rows that HAVE a \`domain\` (not failures) and no-\`domain\` \`uncrawled\` rows fall under need-attention. Drop any zero segment.
|
|
7631
7732
|
- Error / failed \u2192 \`"\u26A0 Import failed: <error>. See leadbay_resolve_import_rows for diagnosis."\` \u2014 reserve this ONLY for a true transport/backend error on the import itself, never for \`uncrawled\` rows.
|
|
@@ -7649,9 +7750,10 @@ How the OTHER reasons map to the "Need attention" bucket (see the render block a
|
|
|
7649
7750
|
|
|
7650
7751
|
| Observation | Suggest | Calls |
|
|
7651
7752
|
|--------------------------------------|------------------------------------------------------|--------------------------------|
|
|
7753
|
+
| Status: complete, \`result.leads\` present | "Qualify the imported leads" | leadbay_bulk_qualify_leads(result.leads[].leadId) |
|
|
7652
7754
|
| Status: complete | "See the imported (matched) leads" | leadbay_pull_leads |
|
|
7653
7755
|
| Pending-crawl (\`uncrawled\`) rows | "Re-run the import for those domains later, once Leadbay has crawled them" | leadbay_import_leads (re-run with just the uncrawled domains, later \u2014 they re-reconcile once crawled). The added leads otherwise populate in the user's Leadbay account as the crawl completes; no live-fetch here |
|
|
7654
|
-
| Status: running
|
|
7756
|
+
| Status: running (incl. \`committing\`) | "Check again in N minutes" | leadbay_import_status \u2014 re-call (pass \`dry_run:true\` if the ids came from a dry run) |
|
|
7655
7757
|
| Status: error / failed (true error) | "Diagnose the failure" | leadbay_resolve_import_rows |
|
|
7656
7758
|
`;
|
|
7657
7759
|
leadbay_launch_bulk_enrichment = `Launch a bulk-enrichment job against the current selection. The backend requires \`email=true\` OR \`phone=true\` (both can be true). Returns 204 with no body \u2014 there is no bulk_id and no per-job status endpoint. Track results by polling individual leads via leadbay_get_contacts after ~60s; a contact is done for this run only when the REQUESTED channel landed (requested \`email\` and/or \`phone_number\` present), not \`contact.enrichment.done\` alone (that flag is already true for a contact enriched on the other channel earlier). \`dry_run:true\` returns the call shape without contacting the backend.
|
|
@@ -9183,35 +9285,38 @@ Trigger phrases: "look up <Company>", "research <Company>", "what do we know abo
|
|
|
9183
9285
|
|
|
9184
9286
|
Do NOT use for: "picked row with leadId" \u2192 \`leadbay_research_lead_by_id\`; "draft outreach for <Contact>" \u2192 \`leadbay_prepare_outreach\`.
|
|
9185
9287
|
|
|
9186
|
-
Prefer when: company name in prose
|
|
9288
|
+
Prefer when: a company name or domain in prose, no Leadbay id yet \u2014 always pass \`website\` if a domain was mentioned
|
|
9187
9289
|
|
|
9188
9290
|
Examples that SHOULD invoke this tool:
|
|
9189
9291
|
- "Look up Acme Corp for me."
|
|
9190
9292
|
- "Find Initech in my pipeline."
|
|
9293
|
+
- "Who is Wink Lab? Their email is at @wink-lab.com."
|
|
9191
9294
|
|
|
9192
9295
|
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
9193
9296
|
- "Tell me about that lead I just picked."
|
|
9194
9297
|
- "Draft outreach to Acme's CTO."
|
|
9298
|
+
- "Show me today's leads."
|
|
9195
9299
|
|
|
9196
9300
|
---
|
|
9197
9301
|
|
|
9198
|
-
Resolves
|
|
9199
|
-
|
|
9200
|
-
|
|
9201
|
-
\`_by_id\`, plus:
|
|
9302
|
+
Resolves across the user's visible Discover/Monitor/Activate leads AND the
|
|
9303
|
+
**Leadbay company registry** \u2014 so a company they do not own yet is still
|
|
9304
|
+
findable \u2014 then delegates to **leadbay_research_lead_by_id**.
|
|
9202
9305
|
|
|
9203
|
-
|
|
9204
|
-
|
|
9205
|
-
|
|
9306
|
+
**Pass \`website\` whenever the user mentioned a domain** \u2014 the strongest match
|
|
9307
|
+
key. It survives a misspelled company name and is what turns "not in your
|
|
9308
|
+
list" into an answer. With only a contact email, pass \`email\`: the company
|
|
9309
|
+
domain is derived from it, consumer mailboxes ignored.
|
|
9206
9310
|
|
|
9207
|
-
|
|
9208
|
-
|
|
9311
|
+
When the registry cannot pick one company it returns \`{resolution:
|
|
9312
|
+
"ambiguous", query, candidates:[{leadId, name, website, location, \u2026}]}\`
|
|
9313
|
+
instead of a card. Ask which one; never guess from \`score\`.
|
|
9209
9314
|
|
|
9210
|
-
|
|
9211
|
-
|
|
9315
|
+
\`LEAD_NOT_FOUND\` is not a dead end: its hint names the field that would have
|
|
9316
|
+
found it \u2014 \`website\` or \`registry_number\`, both params. Ask for it and call
|
|
9317
|
+
again. Do not offer an import before asking.
|
|
9212
9318
|
|
|
9213
|
-
|
|
9214
|
-
leadbay_research_lead_by_id directly.
|
|
9319
|
+
Offer \`_meta.match_candidates\` when present.
|
|
9215
9320
|
|
|
9216
9321
|
---
|
|
9217
9322
|
|
|
@@ -9350,6 +9455,10 @@ out?"\`
|
|
|
9350
9455
|
| User is done with this lead | "Back to the inbox" | leadbay_pull_leads |
|
|
9351
9456
|
|
|
9352
9457
|
|
|
9458
|
+
When \`resolution\` is \`"ambiguous"\`, render no card: use \`ask_user_input_v0\`,
|
|
9459
|
+
ONE \`single_select\` question ("Which one?"), one short label per candidate
|
|
9460
|
+
combining \`name\` and \`location\`.
|
|
9461
|
+
|
|
9353
9462
|
When \`_meta.match_candidates\` is non-empty, prepend one extra NEXT STEPS row:
|
|
9354
9463
|
|
|
9355
9464
|
| Observation | Suggest | Calls |
|
|
@@ -9744,6 +9853,84 @@ WHEN TO USE: low-level.
|
|
|
9744
9853
|
|
|
9745
9854
|
WHEN NOT TO USE: from agent flow \u2014 leadbay_report_outreach pairs this with a note + verification, which is what humans actually need to see in Leadbay.
|
|
9746
9855
|
|
|
9856
|
+
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\`.
|
|
9857
|
+
`;
|
|
9858
|
+
leadbay_set_lead_status = `## WHEN TO USE
|
|
9859
|
+
|
|
9860
|
+
Trigger phrases: "we won this deal", "mark this lead as won", "we lost them", "mark as lost", "this one is a target", "add them to my wanted list", "set the status on these leads", "closed the deal with", "they signed", "not a target anymore".
|
|
9861
|
+
|
|
9862
|
+
**Memory:** recall + capture via \`leadbay_agent_memory_*\` tools.
|
|
9863
|
+
|
|
9864
|
+
Do NOT use for: "I sent the email / left a voicemail \u2014 log the outcome" \u2192 \`leadbay_report_outreach\`; "thumbs up, I like this lead" \u2192 \`leadbay_like_lead\`; "remind me about this lead next week / snooze it" \u2192 \`leadbay_set_pushback\`.
|
|
9865
|
+
|
|
9866
|
+
Prefer when: user states a COMMERCIAL outcome or pipeline stage, not an outreach event; pass \`lead_ids\` + the uppercase \`status\`, and \`status_date\` when they name a close date
|
|
9867
|
+
|
|
9868
|
+
Examples that SHOULD invoke this tool:
|
|
9869
|
+
- "We just signed Acme Corp \u2014 mark them as won."
|
|
9870
|
+
- "Mark these three as lost, they went with a competitor."
|
|
9871
|
+
- "Add Northwind to my wanted list, they're a priority target."
|
|
9872
|
+
|
|
9873
|
+
Examples that should NOT invoke this tool (sound similar, route elsewhere):
|
|
9874
|
+
- "I emailed the CTO this morning, log it."
|
|
9875
|
+
- "Thumbs up on this one, show me more like it."
|
|
9876
|
+
- "Snooze this lead until next quarter."
|
|
9877
|
+
|
|
9878
|
+
## RENDER (quick)
|
|
9879
|
+
|
|
9880
|
+
One short confirmation line per status applied ("\u2705 **Acme Corp** \u2192 WON
|
|
9881
|
+
(closed 2026-03-14)"). If \`failed\` is non-empty, list those leads with
|
|
9882
|
+
their error underneath. Don't re-render the full lead card.
|
|
9883
|
+
|
|
9884
|
+
---
|
|
9885
|
+
|
|
9886
|
+
Set the **org-wide CRM lead status** \u2014 the same field the Leadbay website's status
|
|
9887
|
+
selector writes, and the one a CSV import maps via \`mappings.statuses\`. It is shared
|
|
9888
|
+
across the whole organization: every rep sees the value this call sets.
|
|
9889
|
+
|
|
9890
|
+
Two distinct status systems exist in Leadbay. Do not confuse them:
|
|
9891
|
+
|
|
9892
|
+
| System | Values | Written by | Means |
|
|
9893
|
+
|---|---|---|---|
|
|
9894
|
+
| **Lead status** (this tool) | \`WANTED\` \`WON\` \`LOST\` \`UNWANTED\` (plus system-set \`DEFAULT\`, \`INBOUND\`) | this tool, CSV import | Commercial/pipeline outcome, org-wide |
|
|
9895
|
+
| **Epilogue status** | \`STILL_CHASING\` \`COULD_NOT_REACH_STILL_TRYING\` \`INTEREST_VALIDATED_OR_MEETING_PLANED\` \`NOT_INTERESTED_LOST\` | \`leadbay_report_outreach\`, \`leadbay_set_epilogue_status\` | Disposition of a specific outreach attempt; drives \`leadbay_pull_followups\` ranking |
|
|
9896
|
+
|
|
9897
|
+
A deal outcome is a **lead status**. "She didn't pick up" is an **epilogue status**.
|
|
9898
|
+
Setting one never sets the other \u2014 when the user reports both in one breath ("called
|
|
9899
|
+
them, they signed"), make both calls.
|
|
9900
|
+
|
|
9901
|
+
## Parameters
|
|
9902
|
+
|
|
9903
|
+
- \`lead_ids\` (required) \u2014 1\u2013200 lead UUIDs. Every lead gets the same status.
|
|
9904
|
+
- \`status\` (required) \u2014 one of \`WANTED\`, \`WON\`, \`LOST\`, \`UNWANTED\`. Accepted
|
|
9905
|
+
case-insensitively (\`won\` \u2192 \`WON\`); no synonyms are guessed, so "closed-won"
|
|
9906
|
+
or "dead" are rejected rather than silently mapped. \`DEFAULT\` and \`INBOUND\`
|
|
9907
|
+
are accepted but are normally set by Leadbay itself \u2014 don't offer them as
|
|
9908
|
+
user choices.
|
|
9909
|
+
- \`status_date\` (optional) \u2014 \`YYYY-MM-DD\`, the date the status was actually
|
|
9910
|
+
reached (a close date, the day the deal was lost). Omit it and the backend
|
|
9911
|
+
stamps now. Pass it whenever the user names a date; a deal closed last month
|
|
9912
|
+
stamped as today distorts every pipeline report.
|
|
9913
|
+
|
|
9914
|
+
## Behaviour
|
|
9915
|
+
|
|
9916
|
+
Each lead is written individually (\`POST /leads/{leadId}/set_status\`, then
|
|
9917
|
+
\`POST /leads/{leadId}/set_status_date\` when \`status_date\` is given), so a partial
|
|
9918
|
+
failure is possible. The return is
|
|
9919
|
+
\`{ applied, count, status, status_date?, failed: [{lead_id, message}] }\` \u2014
|
|
9920
|
+
**always check \`failed\`** and report those leads to the user rather than claiming
|
|
9921
|
+
a clean sweep. \`applied\` is \`false\` when every lead failed.
|
|
9922
|
+
|
|
9923
|
+
Re-sending the same status is idempotent \u2014 no error, no duplicate entry.
|
|
9924
|
+
|
|
9925
|
+
WHEN TO USE: the user states a commercial outcome or pipeline
|
|
9926
|
+
position for specific leads: "we won them", "that one's dead", "these are my targets
|
|
9927
|
+
this quarter". Also use it from an artifact's status dropdown.
|
|
9928
|
+
|
|
9929
|
+
WHEN NOT TO USE: the user is reporting that an outreach
|
|
9930
|
+
*happened* (use \`leadbay_report_outreach\` \u2014 its \`epilogue_status\` covers the
|
|
9931
|
+
follow-up disposition), expressing taste rather than an outcome (\`leadbay_like_lead\`
|
|
9932
|
+
/ \`leadbay_dislike_lead\`), or temporarily deferring a lead (\`leadbay_set_pushback\`).
|
|
9933
|
+
|
|
9747
9934
|
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\`.
|
|
9748
9935
|
`;
|
|
9749
9936
|
leadbay_set_pushback = `Snooze (pushback) one or more leads for 3, 6, or 12 months. The leads remain in the user's pipeline but are excluded from \`leadbay_pull_followups\` until the pushback window expires. Use this when the user says "not now", "next quarter", "follow up in 3 months", "6 months out", "next year", or any equivalent deferral.
|
|
@@ -10535,6 +10722,52 @@ var init_get_contacts = __esm({
|
|
|
10535
10722
|
required: ["leadId"],
|
|
10536
10723
|
additionalProperties: false
|
|
10537
10724
|
},
|
|
10725
|
+
outputSchema: {
|
|
10726
|
+
type: "object",
|
|
10727
|
+
properties: {
|
|
10728
|
+
contacts: {
|
|
10729
|
+
type: "array",
|
|
10730
|
+
description: "Merged org+paid contacts. Each: {id, first_name, last_name, email, phone_number, linkedin_page, job_title, recommended, enrichment, source:'org'|'paid'}.",
|
|
10731
|
+
items: {
|
|
10732
|
+
type: "object",
|
|
10733
|
+
properties: {
|
|
10734
|
+
id: { type: "string" },
|
|
10735
|
+
first_name: { type: ["string", "null"] },
|
|
10736
|
+
last_name: { type: ["string", "null"] },
|
|
10737
|
+
email: { type: ["string", "null"] },
|
|
10738
|
+
phone_number: { type: ["string", "null"] },
|
|
10739
|
+
linkedin_page: { type: ["string", "null"] },
|
|
10740
|
+
job_title: { type: ["string", "null"] },
|
|
10741
|
+
recommended: { type: "boolean" },
|
|
10742
|
+
source: { type: "string", enum: ["org", "paid"] },
|
|
10743
|
+
enrichment: {
|
|
10744
|
+
type: ["object", "null"],
|
|
10745
|
+
description: "Per-contact reveal record. Missing or null = the contact was NEVER requested (enrichable \u2014 not the same as done:false). Read `done` and `credits_used` TOGETHER: done:false = reservation in flight, poll and do not re-launch; done:true with credits_used:0 = the reveal SETTLED and found nothing, which is TERMINAL \u2014 do not re-attempt it on a later run; done:true with credits_used>0 = resolved, but the revealed channel lands on the source:'org' twin of this person, not on the source:'paid' record itself, so a null email here is not a failure. credits_used:0 on its own is NOT a verdict (an in-flight reservation reports 0 too), and an ABSENT credits_used means the cost is unknown, not zero.",
|
|
10746
|
+
properties: {
|
|
10747
|
+
done: {
|
|
10748
|
+
type: "boolean",
|
|
10749
|
+
description: "False = reservation in flight. True = settled, either with a result (credits_used>0) or empty (credits_used:0). Per-contact, not per-channel."
|
|
10750
|
+
},
|
|
10751
|
+
credits_used: {
|
|
10752
|
+
type: "number",
|
|
10753
|
+
description: "Credits charged for this reveal. Only meaningful when done:true. An explicit 0 alongside done:true means the provider returned nothing. Optional \u2014 when absent the cost is unknown and terminal-empty must NOT be inferred."
|
|
10754
|
+
},
|
|
10755
|
+
email_requested: { type: "boolean" },
|
|
10756
|
+
phone_requested: { type: "boolean" }
|
|
10757
|
+
}
|
|
10758
|
+
}
|
|
10759
|
+
},
|
|
10760
|
+
required: ["id", "source"]
|
|
10761
|
+
}
|
|
10762
|
+
},
|
|
10763
|
+
_fetch_errors: {
|
|
10764
|
+
type: "array",
|
|
10765
|
+
description: "Present only when one of the two contact endpoints failed. Each: {endpoint:'org'|'paid', code?, retry_after?}. A rejected endpoint contributes no contacts, so an empty `contacts` alongside this field is a fetch failure, NOT 'no contacts'.",
|
|
10766
|
+
items: { type: "object" }
|
|
10767
|
+
}
|
|
10768
|
+
},
|
|
10769
|
+
required: ["contacts"]
|
|
10770
|
+
},
|
|
10538
10771
|
execute: async (client, params) => {
|
|
10539
10772
|
const [orgResult, paidResult] = await Promise.allSettled([
|
|
10540
10773
|
client.request("GET", `/leads/${params.leadId}/contacts?IncludeEnriched=true`),
|
|
@@ -12902,18 +13135,7 @@ var init_qualify_helpers = __esm({
|
|
|
12902
13135
|
}
|
|
12903
13136
|
});
|
|
12904
13137
|
|
|
12905
|
-
// ../core/dist/composite/
|
|
12906
|
-
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
12907
|
-
function isImportLeadsRunningResult(result) {
|
|
12908
|
-
return "status" in result && result.status === "running";
|
|
12909
|
-
}
|
|
12910
|
-
function isCustomFieldMappingValue(v) {
|
|
12911
|
-
return CUSTOM_FIELD_RE.test(v);
|
|
12912
|
-
}
|
|
12913
|
-
function customFieldIdOf(v) {
|
|
12914
|
-
const m = CUSTOM_FIELD_RE.exec(v);
|
|
12915
|
-
return m ? m[1] : null;
|
|
12916
|
-
}
|
|
13138
|
+
// ../core/dist/composite/_import-records.js
|
|
12917
13139
|
function normalizeDomain(input) {
|
|
12918
13140
|
if (!input || typeof input !== "string")
|
|
12919
13141
|
return null;
|
|
@@ -12944,6 +13166,214 @@ function normalizeDomain(input) {
|
|
|
12944
13166
|
return null;
|
|
12945
13167
|
return v;
|
|
12946
13168
|
}
|
|
13169
|
+
function ourRowId(raw) {
|
|
13170
|
+
if (raw == null)
|
|
13171
|
+
return void 0;
|
|
13172
|
+
const v = raw.trim();
|
|
13173
|
+
return MCP_ROW_ID_RE.test(v) ? v : void 0;
|
|
13174
|
+
}
|
|
13175
|
+
function cellNames(c) {
|
|
13176
|
+
return [c?.column_name, c?.key, c?.field].filter((n) => n != null && n !== "").map((n) => String(n).toLowerCase());
|
|
13177
|
+
}
|
|
13178
|
+
function readCell(record, key) {
|
|
13179
|
+
const want = key.toLowerCase();
|
|
13180
|
+
const arr = record.records;
|
|
13181
|
+
if (Array.isArray(arr)) {
|
|
13182
|
+
for (const c of arr) {
|
|
13183
|
+
if (cellNames(c).includes(want)) {
|
|
13184
|
+
const v = c?.value ?? null;
|
|
13185
|
+
return v != null ? String(v) : null;
|
|
13186
|
+
}
|
|
13187
|
+
}
|
|
13188
|
+
}
|
|
13189
|
+
const cells = record.cells;
|
|
13190
|
+
if (cells && typeof cells === "object" && !Array.isArray(cells)) {
|
|
13191
|
+
for (const [k, v] of Object.entries(cells)) {
|
|
13192
|
+
if (k.toLowerCase() === want) {
|
|
13193
|
+
return v != null ? String(v) : null;
|
|
13194
|
+
}
|
|
13195
|
+
}
|
|
13196
|
+
}
|
|
13197
|
+
if (Array.isArray(cells)) {
|
|
13198
|
+
for (const c of cells) {
|
|
13199
|
+
if (cellNames(c).includes(want)) {
|
|
13200
|
+
const v = c?.value ?? null;
|
|
13201
|
+
return v != null ? String(v) : null;
|
|
13202
|
+
}
|
|
13203
|
+
}
|
|
13204
|
+
}
|
|
13205
|
+
return null;
|
|
13206
|
+
}
|
|
13207
|
+
function recordMatchType(record) {
|
|
13208
|
+
return (record.match_type ?? record.matchType ?? "").toString().toUpperCase();
|
|
13209
|
+
}
|
|
13210
|
+
function isRecordTerminal(record) {
|
|
13211
|
+
const status = (record.status ?? "").toString().toUpperCase();
|
|
13212
|
+
return recordMatchType(record) === "NO_MATCH" || status === "IMPORTED";
|
|
13213
|
+
}
|
|
13214
|
+
function settlingDeficit(declaredTotal, fetched) {
|
|
13215
|
+
return Math.max(0, declaredTotal - fetched);
|
|
13216
|
+
}
|
|
13217
|
+
function reconcileRecords(records) {
|
|
13218
|
+
const leads = [];
|
|
13219
|
+
const not_imported = [];
|
|
13220
|
+
const pendingLeadIds = /* @__PURE__ */ new Set();
|
|
13221
|
+
let pending = 0;
|
|
13222
|
+
let distinct = 0;
|
|
13223
|
+
const seenRowIds = /* @__PURE__ */ new Set();
|
|
13224
|
+
for (const rec of records) {
|
|
13225
|
+
const rowId = ourRowId(readCell(rec, MCP_ROW_ID_COLUMN));
|
|
13226
|
+
const dedupeKey = rowId !== void 0 ? `row:${rowId}` : rec.id != null ? `rec:${String(rec.id)}` : null;
|
|
13227
|
+
if (dedupeKey !== null) {
|
|
13228
|
+
if (seenRowIds.has(dedupeKey))
|
|
13229
|
+
continue;
|
|
13230
|
+
seenRowIds.add(dedupeKey);
|
|
13231
|
+
}
|
|
13232
|
+
distinct++;
|
|
13233
|
+
const websiteCell = readCell(rec, "LEAD_WEBSITE");
|
|
13234
|
+
const domain = normalizeDomain(websiteCell ?? "") ?? normalizeDomain(rec.lead?.website ?? "") ?? void 0;
|
|
13235
|
+
if (!isRecordTerminal(rec)) {
|
|
13236
|
+
pending++;
|
|
13237
|
+
if (rec.lead?.id)
|
|
13238
|
+
pendingLeadIds.add(rec.lead.id);
|
|
13239
|
+
continue;
|
|
13240
|
+
}
|
|
13241
|
+
if (rec.lead?.id) {
|
|
13242
|
+
leads.push({
|
|
13243
|
+
...rowId ? { rowId } : {},
|
|
13244
|
+
...domain ? { domain } : {},
|
|
13245
|
+
leadId: rec.lead.id,
|
|
13246
|
+
name: rec.lead.name ?? null
|
|
13247
|
+
});
|
|
13248
|
+
continue;
|
|
13249
|
+
}
|
|
13250
|
+
if (recordMatchType(rec) === "NO_MATCH") {
|
|
13251
|
+
not_imported.push({
|
|
13252
|
+
...rowId ? { rowId } : {},
|
|
13253
|
+
...domain ? { domain } : {},
|
|
13254
|
+
reason: domain && PUBLIC_MAILBOX_DOMAINS.has(domain) ? "no_match" : "uncrawled"
|
|
13255
|
+
});
|
|
13256
|
+
continue;
|
|
13257
|
+
}
|
|
13258
|
+
pending++;
|
|
13259
|
+
}
|
|
13260
|
+
return { leads, not_imported, pending, distinct, pendingLeadIds };
|
|
13261
|
+
}
|
|
13262
|
+
var PUBLIC_MAILBOX_DOMAINS, MCP_ROW_ID_COLUMN, MCP_ROW_ID_RE;
|
|
13263
|
+
var init_import_records = __esm({
|
|
13264
|
+
"../core/dist/composite/_import-records.js"() {
|
|
13265
|
+
"use strict";
|
|
13266
|
+
PUBLIC_MAILBOX_DOMAINS = /* @__PURE__ */ new Set([
|
|
13267
|
+
"gmail.com",
|
|
13268
|
+
"googlemail.com",
|
|
13269
|
+
"yahoo.com",
|
|
13270
|
+
"ymail.com",
|
|
13271
|
+
"outlook.com",
|
|
13272
|
+
"hotmail.com",
|
|
13273
|
+
"live.com",
|
|
13274
|
+
"icloud.com",
|
|
13275
|
+
"me.com",
|
|
13276
|
+
"mac.com",
|
|
13277
|
+
"aol.com",
|
|
13278
|
+
"proton.me",
|
|
13279
|
+
"protonmail.com",
|
|
13280
|
+
"tutanota.com",
|
|
13281
|
+
"gmx.com",
|
|
13282
|
+
"gmx.net",
|
|
13283
|
+
"gmx.de",
|
|
13284
|
+
"mail.com",
|
|
13285
|
+
"yandex.com",
|
|
13286
|
+
"yandex.ru",
|
|
13287
|
+
"qq.com",
|
|
13288
|
+
"163.com",
|
|
13289
|
+
"126.com",
|
|
13290
|
+
// Regional aliases of the same providers, plus the consumer ISP mailboxes
|
|
13291
|
+
// that dominate a French user base. Without these, `orange.fr` or
|
|
13292
|
+
// `yahoo.fr` reads as a company domain (codex review, mcp#188).
|
|
13293
|
+
"yahoo.fr",
|
|
13294
|
+
"yahoo.co.uk",
|
|
13295
|
+
"yahoo.es",
|
|
13296
|
+
"yahoo.it",
|
|
13297
|
+
"yahoo.de",
|
|
13298
|
+
"yahoo.ca",
|
|
13299
|
+
"yahoo.com.br",
|
|
13300
|
+
"yahoo.co.jp",
|
|
13301
|
+
"hotmail.fr",
|
|
13302
|
+
"hotmail.co.uk",
|
|
13303
|
+
"hotmail.es",
|
|
13304
|
+
"hotmail.it",
|
|
13305
|
+
"hotmail.de",
|
|
13306
|
+
"hotmail.be",
|
|
13307
|
+
"outlook.fr",
|
|
13308
|
+
"outlook.es",
|
|
13309
|
+
"outlook.de",
|
|
13310
|
+
"outlook.it",
|
|
13311
|
+
"live.fr",
|
|
13312
|
+
"live.be",
|
|
13313
|
+
"live.co.uk",
|
|
13314
|
+
"msn.com",
|
|
13315
|
+
"orange.fr",
|
|
13316
|
+
"wanadoo.fr",
|
|
13317
|
+
"free.fr",
|
|
13318
|
+
"sfr.fr",
|
|
13319
|
+
"laposte.net",
|
|
13320
|
+
"bbox.fr",
|
|
13321
|
+
"neuf.fr",
|
|
13322
|
+
"aliceadsl.fr",
|
|
13323
|
+
"numericable.fr",
|
|
13324
|
+
"club-internet.fr",
|
|
13325
|
+
"gmx.fr",
|
|
13326
|
+
"gmx.at",
|
|
13327
|
+
"gmx.ch",
|
|
13328
|
+
"web.de",
|
|
13329
|
+
"t-online.de",
|
|
13330
|
+
"libero.it",
|
|
13331
|
+
"wp.pl",
|
|
13332
|
+
"seznam.cz"
|
|
13333
|
+
]);
|
|
13334
|
+
MCP_ROW_ID_COLUMN = "MCP_ROW_ID";
|
|
13335
|
+
MCP_ROW_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
13336
|
+
}
|
|
13337
|
+
});
|
|
13338
|
+
|
|
13339
|
+
// ../core/dist/composite/_import-commit-log.js
|
|
13340
|
+
function recordCommitFailure(importId, reason) {
|
|
13341
|
+
if (failures.size >= MAX_ENTRIES) {
|
|
13342
|
+
const oldest = failures.keys().next().value;
|
|
13343
|
+
if (oldest !== void 0)
|
|
13344
|
+
failures.delete(oldest);
|
|
13345
|
+
}
|
|
13346
|
+
failures.set(importId, reason);
|
|
13347
|
+
}
|
|
13348
|
+
function commitFailureFor(importIds) {
|
|
13349
|
+
for (const id of importIds) {
|
|
13350
|
+
const reason = failures.get(id);
|
|
13351
|
+
if (reason !== void 0)
|
|
13352
|
+
return reason;
|
|
13353
|
+
}
|
|
13354
|
+
return void 0;
|
|
13355
|
+
}
|
|
13356
|
+
var MAX_ENTRIES, failures;
|
|
13357
|
+
var init_import_commit_log = __esm({
|
|
13358
|
+
"../core/dist/composite/_import-commit-log.js"() {
|
|
13359
|
+
"use strict";
|
|
13360
|
+
MAX_ENTRIES = 500;
|
|
13361
|
+
failures = /* @__PURE__ */ new Map();
|
|
13362
|
+
}
|
|
13363
|
+
});
|
|
13364
|
+
|
|
13365
|
+
// ../core/dist/composite/import-leads.js
|
|
13366
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
13367
|
+
function isImportLeadsRunningResult(result) {
|
|
13368
|
+
return "status" in result && result.status === "running";
|
|
13369
|
+
}
|
|
13370
|
+
function isCustomFieldMappingValue(v) {
|
|
13371
|
+
return CUSTOM_FIELD_RE.test(v);
|
|
13372
|
+
}
|
|
13373
|
+
function customFieldIdOf(v) {
|
|
13374
|
+
const m = CUSTOM_FIELD_RE.exec(v);
|
|
13375
|
+
return m ? m[1] : null;
|
|
13376
|
+
}
|
|
12947
13377
|
function escapeCsvCell(raw) {
|
|
12948
13378
|
if (raw == null)
|
|
12949
13379
|
return "";
|
|
@@ -13023,37 +13453,6 @@ async function sleepWithAbort2(ms, signal) {
|
|
|
13023
13453
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
13024
13454
|
});
|
|
13025
13455
|
}
|
|
13026
|
-
function readCell(record, key) {
|
|
13027
|
-
const want = key.toLowerCase();
|
|
13028
|
-
const arr = record.records;
|
|
13029
|
-
if (Array.isArray(arr)) {
|
|
13030
|
-
for (const c of arr) {
|
|
13031
|
-
const k = (c?.column_name ?? c?.key ?? c?.field ?? "").toString().toLowerCase();
|
|
13032
|
-
if (k === want) {
|
|
13033
|
-
const v = c?.value ?? null;
|
|
13034
|
-
return v != null ? String(v) : null;
|
|
13035
|
-
}
|
|
13036
|
-
}
|
|
13037
|
-
}
|
|
13038
|
-
const cells = record.cells;
|
|
13039
|
-
if (cells && typeof cells === "object" && !Array.isArray(cells)) {
|
|
13040
|
-
for (const [k, v] of Object.entries(cells)) {
|
|
13041
|
-
if (k.toLowerCase() === want) {
|
|
13042
|
-
return v != null ? String(v) : null;
|
|
13043
|
-
}
|
|
13044
|
-
}
|
|
13045
|
-
}
|
|
13046
|
-
if (Array.isArray(cells)) {
|
|
13047
|
-
for (const c of cells) {
|
|
13048
|
-
const k = (c?.key ?? c?.field ?? c?.column_name ?? "").toString().toLowerCase();
|
|
13049
|
-
if (k === want) {
|
|
13050
|
-
const v = c?.value ?? null;
|
|
13051
|
-
return v != null ? String(v) : null;
|
|
13052
|
-
}
|
|
13053
|
-
}
|
|
13054
|
-
}
|
|
13055
|
-
return null;
|
|
13056
|
-
}
|
|
13057
13456
|
function validateColumnName(client, name, path) {
|
|
13058
13457
|
if (typeof name !== "string" || name.length === 0) {
|
|
13059
13458
|
throw client.makeError("IMPORT_INVALID_COLUMN_NAME", `Column name at ${path} must be a non-empty string`, `Use a plain string column name (1-${MAX_COLUMN_NAME_LEN} chars).`, "POST /imports");
|
|
@@ -13303,7 +13702,7 @@ async function pollUntil(fn, done, budgetMs, signal, ctx, label) {
|
|
|
13303
13702
|
async function pollPreprocess(client, importId, budgetMs, ctx, signal) {
|
|
13304
13703
|
const result = await pollUntil(() => client.request("GET", `/imports/${importId}`), (r) => Boolean(r.pre_processing?.finished), budgetMs, signal, ctx, "preprocess");
|
|
13305
13704
|
if (!result.pre_processing?.finished) {
|
|
13306
|
-
throw
|
|
13705
|
+
throw new ImportPhaseTimeout("preprocess", importId, budgetMs);
|
|
13307
13706
|
}
|
|
13308
13707
|
if (result.pre_processing.error) {
|
|
13309
13708
|
throw client.makeError("IMPORT_PREPROCESS_FAILED", `Preprocess failed: ${result.pre_processing.error}`, `Check the input domains. importId=${importId} for backend debugging.`, `GET /imports/${importId}`);
|
|
@@ -13313,7 +13712,7 @@ async function pollPreprocess(client, importId, budgetMs, ctx, signal) {
|
|
|
13313
13712
|
async function pollProcess(client, importId, budgetMs, ctx, signal) {
|
|
13314
13713
|
const result = await pollUntil(() => client.request("GET", `/imports/${importId}`), (r) => Boolean(r.processing?.finished), budgetMs, signal, ctx, "process");
|
|
13315
13714
|
if (!result.processing?.finished) {
|
|
13316
|
-
throw
|
|
13715
|
+
throw new ImportPhaseTimeout("process", importId, budgetMs);
|
|
13317
13716
|
}
|
|
13318
13717
|
if (result.processing.error != null) {
|
|
13319
13718
|
throw client.makeError("IMPORT_PROCESSING_FAILED", `Backend processing failed: ${result.processing.error}`, `importId=${importId}.`, `GET /imports/${importId}`);
|
|
@@ -13340,10 +13739,7 @@ async function pollRecordsToTerminal(client, importId, budgetMs, expectedRowCoun
|
|
|
13340
13739
|
records.push(...res.items);
|
|
13341
13740
|
total = res.pagination.total ?? records.length;
|
|
13342
13741
|
for (const r of res.items) {
|
|
13343
|
-
|
|
13344
|
-
const matchType = (r.match_type ?? r.matchType ?? "").toString().toUpperCase();
|
|
13345
|
-
const isTerminal = matchType === "NO_MATCH" || status === "IMPORTED";
|
|
13346
|
-
if (!isTerminal)
|
|
13742
|
+
if (!isRecordTerminal(r))
|
|
13347
13743
|
transient++;
|
|
13348
13744
|
}
|
|
13349
13745
|
const totalPages = res.pagination.pages ?? 0;
|
|
@@ -13371,14 +13767,15 @@ async function pollRecordsToTerminal(client, importId, budgetMs, expectedRowCoun
|
|
|
13371
13767
|
}
|
|
13372
13768
|
if (Date.now() >= deadline) {
|
|
13373
13769
|
ctx?.logger?.warn?.(`import-leads: records did not stabilize (transient=${transient}, total=${total}); returning best-effort`);
|
|
13374
|
-
throw
|
|
13770
|
+
throw new ImportPhaseTimeout("reconcile", importId, budgetMs);
|
|
13375
13771
|
}
|
|
13376
13772
|
await sleepWithAbort2(POLL_INTERVAL_MS2, signal);
|
|
13377
13773
|
}
|
|
13378
13774
|
}
|
|
13379
|
-
async function runOneChunk(client, chunk, chunkIdx, totalChunks, header, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal, onImportId) {
|
|
13775
|
+
async function runOneChunk(client, chunk, chunkIdx, totalChunks, header, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal, onImportId, onNotificationId, onUploaded) {
|
|
13380
13776
|
const upload = await uploadOneChunk(client, chunk, chunkIdx, totalChunks, header, ctx, onImportId);
|
|
13381
|
-
|
|
13777
|
+
onUploaded?.(upload);
|
|
13778
|
+
return completeUploadedChunk(client, upload, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal, onNotificationId);
|
|
13382
13779
|
}
|
|
13383
13780
|
async function uploadOneChunk(client, chunk, chunkIdx, totalChunks, header, ctx, onImportId) {
|
|
13384
13781
|
const csv = synthesizeCsv(header, chunk.map((c) => c.row));
|
|
@@ -13390,27 +13787,30 @@ async function uploadOneChunk(client, chunk, chunkIdx, totalChunks, header, ctx,
|
|
|
13390
13787
|
onImportId(importId);
|
|
13391
13788
|
return { importId, chunk, chunkIdx, totalChunks };
|
|
13392
13789
|
}
|
|
13393
|
-
async function
|
|
13394
|
-
const { importId, chunk } = upload;
|
|
13395
|
-
const phaseBudget = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
13396
|
-
await pollPreprocess(client, importId, phaseBudget, ctx, signal);
|
|
13397
|
-
ctx?.logger?.info?.(`import-leads: preprocess done for importId=${importId}`);
|
|
13398
|
-
if (dryRun) {
|
|
13399
|
-
return { importId, records: [], notification_id: null };
|
|
13400
|
-
}
|
|
13401
|
-
let updateMappingsResp = null;
|
|
13790
|
+
async function commitMappings(client, importId, mappings, ctx) {
|
|
13402
13791
|
try {
|
|
13403
|
-
|
|
13792
|
+
const resp = await client.request("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
13793
|
+
return resp?.notification_id ?? null;
|
|
13404
13794
|
} catch (err) {
|
|
13405
13795
|
if (err?.code === "API_ERROR" || err?.code === "NOT_FOUND") {
|
|
13406
13796
|
ctx?.logger?.warn?.(`import-leads: update_mappings raw error (${err?.code}); retrying void`);
|
|
13407
13797
|
await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
13408
|
-
|
|
13409
|
-
throw err;
|
|
13798
|
+
return null;
|
|
13410
13799
|
}
|
|
13800
|
+
throw err;
|
|
13411
13801
|
}
|
|
13412
|
-
|
|
13802
|
+
}
|
|
13803
|
+
async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal, onNotificationId) {
|
|
13804
|
+
const { importId, chunk } = upload;
|
|
13805
|
+
const phaseBudget = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
13806
|
+
await pollPreprocess(client, importId, phaseBudget, ctx, signal);
|
|
13807
|
+
ctx?.logger?.info?.(`import-leads: preprocess done for importId=${importId}`);
|
|
13808
|
+
if (dryRun) {
|
|
13809
|
+
return { importId, records: [], notification_id: null };
|
|
13810
|
+
}
|
|
13811
|
+
const importNotificationId = await commitMappings(client, importId, mappings, ctx);
|
|
13413
13812
|
if (importNotificationId) {
|
|
13813
|
+
onNotificationId?.(importNotificationId);
|
|
13414
13814
|
ctx?.logger?.info?.(`import-leads: notification_id=${importNotificationId} importId=${importId}`);
|
|
13415
13815
|
}
|
|
13416
13816
|
ctx?.logger?.info?.(`import-leads: mappings committed for importId=${importId}`);
|
|
@@ -13461,7 +13861,7 @@ function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
|
13461
13861
|
}
|
|
13462
13862
|
seenInputIndex.add(inputIdx);
|
|
13463
13863
|
const inp = prep.validInputs[inputIdx];
|
|
13464
|
-
const matchType = (rec
|
|
13864
|
+
const matchType = recordMatchType(rec);
|
|
13465
13865
|
if (rec.lead?.id) {
|
|
13466
13866
|
matched.set(inputIdx, {
|
|
13467
13867
|
domain: inp.outputDomain,
|
|
@@ -13553,6 +13953,19 @@ function buildImportLeadsResult(client, prep, importIds, matched, notImported, d
|
|
|
13553
13953
|
}
|
|
13554
13954
|
};
|
|
13555
13955
|
}
|
|
13956
|
+
function resumeParkedUpload(client, upload, mappings, ctx) {
|
|
13957
|
+
const bgCtx = { logger: ctx?.logger };
|
|
13958
|
+
const { importId } = upload;
|
|
13959
|
+
setTimeout(() => {
|
|
13960
|
+
void (async () => {
|
|
13961
|
+
await pollPreprocess(client, importId, RESUME_COMMIT_BUDGET_MS, bgCtx, void 0);
|
|
13962
|
+
await commitMappings(client, importId, mappings, bgCtx);
|
|
13963
|
+
})().then(() => ctx?.logger?.info?.(`import-leads: parked upload ${importId} committed; backend is processing`), (err) => {
|
|
13964
|
+
recordCommitFailure(importId, err?.message ?? err?.code ?? "mapping commit failed");
|
|
13965
|
+
ctx?.logger?.warn?.(`import-leads: parked upload ${importId} could not be committed (${err?.code ?? err?.message ?? "unknown"})`);
|
|
13966
|
+
});
|
|
13967
|
+
}, 0);
|
|
13968
|
+
}
|
|
13556
13969
|
async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, handleId) {
|
|
13557
13970
|
const tracker = ctx.bulkTracker;
|
|
13558
13971
|
if (!tracker)
|
|
@@ -13593,11 +14006,13 @@ async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, ha
|
|
|
13593
14006
|
})();
|
|
13594
14007
|
}, 0);
|
|
13595
14008
|
}
|
|
13596
|
-
var CHUNK_SIZE, POLL_INTERVAL_MS2, DEFAULT_PER_PHASE_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS, STABILIZATION_POLLS, MAX_COLUMN_NAME_LEN, RESERVED_COLUMN_RE, CUSTOM_FIELD_RE, IMPORT_RESOLVER_FIELDS,
|
|
14009
|
+
var CHUNK_SIZE, POLL_INTERVAL_MS2, DEFAULT_PER_PHASE_BUDGET_MS, DEFAULT_TOTAL_BUDGET_MS, STABILIZATION_POLLS, MAX_COLUMN_NAME_LEN, RESERVED_COLUMN_RE, CUSTOM_FIELD_RE, IMPORT_RESOLVER_FIELDS, ImportPhaseTimeout, LEAD_STATUSES, LEAD_STATUS_SET, importLeads, RESUME_COMMIT_BUDGET_MS;
|
|
13597
14010
|
var init_import_leads = __esm({
|
|
13598
14011
|
"../core/dist/composite/import-leads.js"() {
|
|
13599
14012
|
"use strict";
|
|
13600
14013
|
init_tool_descriptions_generated();
|
|
14014
|
+
init_import_records();
|
|
14015
|
+
init_import_commit_log();
|
|
13601
14016
|
CHUNK_SIZE = 100;
|
|
13602
14017
|
POLL_INTERVAL_MS2 = 2e3;
|
|
13603
14018
|
DEFAULT_PER_PHASE_BUDGET_MS = 6e4;
|
|
@@ -13613,31 +14028,19 @@ var init_import_leads = __esm({
|
|
|
13613
14028
|
"LEAD_WEBSITE",
|
|
13614
14029
|
"SIREN"
|
|
13615
14030
|
]);
|
|
13616
|
-
|
|
13617
|
-
|
|
13618
|
-
|
|
13619
|
-
|
|
13620
|
-
"
|
|
13621
|
-
|
|
13622
|
-
|
|
13623
|
-
|
|
13624
|
-
|
|
13625
|
-
|
|
13626
|
-
|
|
13627
|
-
|
|
13628
|
-
|
|
13629
|
-
"protonmail.com",
|
|
13630
|
-
"tutanota.com",
|
|
13631
|
-
"gmx.com",
|
|
13632
|
-
"gmx.net",
|
|
13633
|
-
"gmx.de",
|
|
13634
|
-
"mail.com",
|
|
13635
|
-
"yandex.com",
|
|
13636
|
-
"yandex.ru",
|
|
13637
|
-
"qq.com",
|
|
13638
|
-
"163.com",
|
|
13639
|
-
"126.com"
|
|
13640
|
-
]);
|
|
14031
|
+
ImportPhaseTimeout = class extends Error {
|
|
14032
|
+
phase;
|
|
14033
|
+
importId;
|
|
14034
|
+
budgetMs;
|
|
14035
|
+
code = "IMPORT_TIMEOUT";
|
|
14036
|
+
constructor(phase, importId, budgetMs) {
|
|
14037
|
+
super(`Import ${phase} phase did not finish within ${budgetMs}ms; the wizard is still running server-side. Poll leadbay_import_status with importIds=["${importId}"].`);
|
|
14038
|
+
this.phase = phase;
|
|
14039
|
+
this.importId = importId;
|
|
14040
|
+
this.budgetMs = budgetMs;
|
|
14041
|
+
this.name = "ImportPhaseTimeout";
|
|
14042
|
+
}
|
|
14043
|
+
};
|
|
13641
14044
|
LEAD_STATUSES = [
|
|
13642
14045
|
"DEFAULT",
|
|
13643
14046
|
"INBOUND",
|
|
@@ -13750,7 +14153,20 @@ var init_import_leads = __esm({
|
|
|
13750
14153
|
},
|
|
13751
14154
|
handle_id: {
|
|
13752
14155
|
type: "string",
|
|
13753
|
-
description: "Persisted UUID handle to pass to leadbay_import_status."
|
|
14156
|
+
description: "Persisted UUID handle to pass to leadbay_import_status. Only on the wait_for_completion=false path; absent when a blocking call timed out (use importIds then)."
|
|
14157
|
+
},
|
|
14158
|
+
timed_out: {
|
|
14159
|
+
type: "boolean",
|
|
14160
|
+
description: "True when a blocking call ran out of poll budget. The import is still running server-side \u2014 poll leadbay_import_status(importIds). Do NOT re-issue the import."
|
|
14161
|
+
},
|
|
14162
|
+
rows_pending_upload: {
|
|
14163
|
+
type: "number",
|
|
14164
|
+
description: "Rows from later chunks that were never uploaded before the budget ran out. These are NOT running anywhere; re-import just those rows."
|
|
14165
|
+
},
|
|
14166
|
+
row_ids: {
|
|
14167
|
+
type: "array",
|
|
14168
|
+
description: "Records mode only: the synthetic MCP_ROW_ID of each input row, in the order you passed `records[]`. leadbay_import_status reports recovered leads by that id \u2014 use this to map them back to your source rows.",
|
|
14169
|
+
items: { type: "string" }
|
|
13754
14170
|
},
|
|
13755
14171
|
progress: {
|
|
13756
14172
|
type: "object",
|
|
@@ -13780,7 +14196,7 @@ var init_import_leads = __esm({
|
|
|
13780
14196
|
required: ["importIds", "region", "_meta"],
|
|
13781
14197
|
anyOf: [
|
|
13782
14198
|
{ required: ["leads", "not_imported", "importIds", "region", "_meta"] },
|
|
13783
|
-
{ required: ["status", "
|
|
14199
|
+
{ required: ["status", "importIds", "progress", "region", "_meta"] }
|
|
13784
14200
|
]
|
|
13785
14201
|
},
|
|
13786
14202
|
execute: async (client, params, ctx) => {
|
|
@@ -13907,23 +14323,33 @@ var init_import_leads = __esm({
|
|
|
13907
14323
|
const matched = /* @__PURE__ */ new Map();
|
|
13908
14324
|
const notImported = /* @__PURE__ */ new Map();
|
|
13909
14325
|
let cancelled = false;
|
|
14326
|
+
let timedOut = null;
|
|
14327
|
+
let rowsStarted = 0;
|
|
14328
|
+
const lastUpload = { current: null };
|
|
13910
14329
|
const recordImportId = (id) => {
|
|
13911
14330
|
if (!importIds.includes(id))
|
|
13912
14331
|
importIds.push(id);
|
|
13913
14332
|
};
|
|
14333
|
+
const recordNotificationId = (id) => {
|
|
14334
|
+
if (!notificationIds.includes(id))
|
|
14335
|
+
notificationIds.push(id);
|
|
14336
|
+
};
|
|
13914
14337
|
try {
|
|
13915
14338
|
for (let i = 0; i < chunks.length; i++) {
|
|
13916
14339
|
const chunk = chunks[i];
|
|
13917
|
-
|
|
13918
|
-
|
|
13919
|
-
|
|
13920
|
-
}
|
|
14340
|
+
rowsStarted += chunk.length;
|
|
14341
|
+
const out = await runOneChunk(client, chunk, i, chunks.length, prep.header, prep.mappings, dryRun, perPhaseBudget, totalDeadline, ctx, signal, recordImportId, recordNotificationId, (u) => {
|
|
14342
|
+
lastUpload.current = u;
|
|
14343
|
+
});
|
|
13921
14344
|
if (!dryRun) {
|
|
13922
14345
|
reconcileOneChunk(prep, out, matched, notImported);
|
|
13923
14346
|
}
|
|
13924
14347
|
}
|
|
13925
14348
|
} catch (err) {
|
|
13926
|
-
if (err
|
|
14349
|
+
if (err instanceof ImportPhaseTimeout) {
|
|
14350
|
+
timedOut = err;
|
|
14351
|
+
ctx?.logger?.warn?.(`import-leads: ${err.phase} budget exhausted after ${err.budgetMs}ms; returning status=running importIds=${importIds.join(",")}`);
|
|
14352
|
+
} else if (err?.name === "AbortError") {
|
|
13927
14353
|
cancelled = true;
|
|
13928
14354
|
ctx?.logger?.info?.(`import-leads: aborted via signal; importIds=${importIds.join(",")}`);
|
|
13929
14355
|
} else if (err?.error === true) {
|
|
@@ -13938,9 +14364,41 @@ var init_import_leads = __esm({
|
|
|
13938
14364
|
throw err;
|
|
13939
14365
|
}
|
|
13940
14366
|
}
|
|
14367
|
+
if (timedOut) {
|
|
14368
|
+
if (timedOut.phase === "preprocess" && // A dry run is SUPPOSED to stop after preprocess — committing its
|
|
14369
|
+
// mappings would turn a validation pass into a real import.
|
|
14370
|
+
!dryRun && lastUpload.current && lastUpload.current.importId === timedOut.importId) {
|
|
14371
|
+
resumeParkedUpload(client, lastUpload.current, prep.mappings, ctx);
|
|
14372
|
+
}
|
|
14373
|
+
const rowsPendingUpload = prep.validInputs.length - rowsStarted;
|
|
14374
|
+
const malformed = prep.malformedDomains.map((d) => ({ domain: d, reason: "malformed" }));
|
|
14375
|
+
return {
|
|
14376
|
+
status: "running",
|
|
14377
|
+
timed_out: true,
|
|
14378
|
+
importIds,
|
|
14379
|
+
notification_ids: notificationIds,
|
|
14380
|
+
...malformed.length > 0 ? { not_imported: malformed } : {},
|
|
14381
|
+
...dryRun ? { dry_run: true } : {},
|
|
14382
|
+
...prep.mode === "records" ? { row_ids: prep.validInputs.map((i) => i.rowId) } : {},
|
|
14383
|
+
progress: {
|
|
14384
|
+
phase: timedOut.phase,
|
|
14385
|
+
records_processed: matched.size,
|
|
14386
|
+
records_total: prep.validInputs.length
|
|
14387
|
+
},
|
|
14388
|
+
...rowsPendingUpload > 0 ? { rows_pending_upload: rowsPendingUpload } : {},
|
|
14389
|
+
region: client.region,
|
|
14390
|
+
_meta: client.lastMeta ?? {
|
|
14391
|
+
region: client.region,
|
|
14392
|
+
endpoint: `GET /imports/${timedOut.importId}`,
|
|
14393
|
+
latency_ms: null,
|
|
14394
|
+
retry_after: null
|
|
14395
|
+
}
|
|
14396
|
+
};
|
|
14397
|
+
}
|
|
13941
14398
|
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled, notificationIds);
|
|
13942
14399
|
}
|
|
13943
14400
|
};
|
|
14401
|
+
RESUME_COMMIT_BUDGET_MS = 10 * 6e4;
|
|
13944
14402
|
}
|
|
13945
14403
|
});
|
|
13946
14404
|
|
|
@@ -15958,6 +16416,133 @@ var init_dislike_lead = __esm({
|
|
|
15958
16416
|
}
|
|
15959
16417
|
});
|
|
15960
16418
|
|
|
16419
|
+
// ../core/dist/tools/set-lead-status.js
|
|
16420
|
+
function messageOf(e) {
|
|
16421
|
+
if (e && typeof e === "object" && "message" in e)
|
|
16422
|
+
return String(e.message);
|
|
16423
|
+
return String(e);
|
|
16424
|
+
}
|
|
16425
|
+
async function writeAll(client, leadIds, status, statusDate) {
|
|
16426
|
+
const failed = [];
|
|
16427
|
+
let cursor = 0;
|
|
16428
|
+
async function worker() {
|
|
16429
|
+
for (; ; ) {
|
|
16430
|
+
const i = cursor++;
|
|
16431
|
+
if (i >= leadIds.length)
|
|
16432
|
+
return;
|
|
16433
|
+
const id = leadIds[i];
|
|
16434
|
+
try {
|
|
16435
|
+
await client.requestVoid("POST", statusPath(id), statusBody(status));
|
|
16436
|
+
if (statusDate) {
|
|
16437
|
+
await client.requestVoid("POST", statusDatePath(id), statusDateBody(statusDate));
|
|
16438
|
+
}
|
|
16439
|
+
} catch (e) {
|
|
16440
|
+
failed.push({ lead_id: id, message: messageOf(e) });
|
|
16441
|
+
}
|
|
16442
|
+
}
|
|
16443
|
+
}
|
|
16444
|
+
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, leadIds.length) }, () => worker()));
|
|
16445
|
+
return failed;
|
|
16446
|
+
}
|
|
16447
|
+
var statusPath, statusDatePath, statusBody, statusDateBody, MAX_LEADS, CONCURRENCY, LEAD_STATUS_SET2, SETTABLE_LEAD_STATUSES, ISO_DATE, setLeadStatus;
|
|
16448
|
+
var init_set_lead_status = __esm({
|
|
16449
|
+
"../core/dist/tools/set-lead-status.js"() {
|
|
16450
|
+
"use strict";
|
|
16451
|
+
init_tool_descriptions_generated();
|
|
16452
|
+
init_import_leads();
|
|
16453
|
+
statusPath = (leadId) => `/leads/${encodeURIComponent(leadId)}/set_status`;
|
|
16454
|
+
statusDatePath = (leadId) => `/leads/${encodeURIComponent(leadId)}/set_status_date`;
|
|
16455
|
+
statusBody = (status) => ({ status });
|
|
16456
|
+
statusDateBody = (date) => ({ date: `${date}T00:00:00Z` });
|
|
16457
|
+
MAX_LEADS = 200;
|
|
16458
|
+
CONCURRENCY = 6;
|
|
16459
|
+
LEAD_STATUS_SET2 = new Set(LEAD_STATUSES);
|
|
16460
|
+
SETTABLE_LEAD_STATUSES = ["WANTED", "WON", "LOST", "UNWANTED"];
|
|
16461
|
+
ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
16462
|
+
setLeadStatus = {
|
|
16463
|
+
name: "leadbay_set_lead_status",
|
|
16464
|
+
annotations: {
|
|
16465
|
+
title: "Set lead CRM status",
|
|
16466
|
+
readOnlyHint: false,
|
|
16467
|
+
// Org-wide and overwrites whatever the last rep set — destructive in the
|
|
16468
|
+
// MCP sense (not reversible from the value we replaced).
|
|
16469
|
+
destructiveHint: true,
|
|
16470
|
+
idempotentHint: true,
|
|
16471
|
+
openWorldHint: true
|
|
16472
|
+
},
|
|
16473
|
+
description: leadbay_set_lead_status,
|
|
16474
|
+
optional: true,
|
|
16475
|
+
write: true,
|
|
16476
|
+
inputSchema: {
|
|
16477
|
+
type: "object",
|
|
16478
|
+
properties: {
|
|
16479
|
+
lead_ids: {
|
|
16480
|
+
type: "array",
|
|
16481
|
+
items: { type: "string" },
|
|
16482
|
+
description: `Lead UUIDs (1-${MAX_LEADS}). Every lead gets the same status.`
|
|
16483
|
+
},
|
|
16484
|
+
status: {
|
|
16485
|
+
type: "string",
|
|
16486
|
+
description: "One of: WANTED, WON, LOST, UNWANTED (case-insensitive). DEFAULT and INBOUND are accepted but are normally set by Leadbay itself."
|
|
16487
|
+
},
|
|
16488
|
+
status_date: {
|
|
16489
|
+
type: "string",
|
|
16490
|
+
description: "Optional YYYY-MM-DD \u2014 the date the status was actually reached (close date). Omit to let the backend stamp now."
|
|
16491
|
+
}
|
|
16492
|
+
},
|
|
16493
|
+
required: ["lead_ids", "status"],
|
|
16494
|
+
additionalProperties: false
|
|
16495
|
+
},
|
|
16496
|
+
execute: async (client, params) => {
|
|
16497
|
+
const leadIds = (params.lead_ids ?? []).filter((id) => typeof id === "string" && id.trim() !== "");
|
|
16498
|
+
if (leadIds.length === 0) {
|
|
16499
|
+
return {
|
|
16500
|
+
error: true,
|
|
16501
|
+
code: "BAD_INPUT",
|
|
16502
|
+
message: "lead_ids is empty",
|
|
16503
|
+
hint: "Pass at least one lead UUID."
|
|
16504
|
+
};
|
|
16505
|
+
}
|
|
16506
|
+
if (leadIds.length > MAX_LEADS) {
|
|
16507
|
+
return {
|
|
16508
|
+
error: true,
|
|
16509
|
+
code: "BAD_INPUT",
|
|
16510
|
+
message: `lead_ids has ${leadIds.length} entries, max is ${MAX_LEADS}`,
|
|
16511
|
+
hint: `Call leadbay_set_lead_status again per chunk of ${MAX_LEADS} lead_ids or fewer.`
|
|
16512
|
+
};
|
|
16513
|
+
}
|
|
16514
|
+
const status = String(params.status ?? "").trim().toUpperCase();
|
|
16515
|
+
if (!LEAD_STATUS_SET2.has(status)) {
|
|
16516
|
+
return {
|
|
16517
|
+
error: true,
|
|
16518
|
+
code: "BAD_INPUT",
|
|
16519
|
+
message: `Unknown lead status: ${JSON.stringify(params.status)}`,
|
|
16520
|
+
hint: `Use one of ${SETTABLE_LEAD_STATUSES.join(", ")} (case-insensitive).`
|
|
16521
|
+
};
|
|
16522
|
+
}
|
|
16523
|
+
const statusDate = params.status_date?.trim() || void 0;
|
|
16524
|
+
if (statusDate && !ISO_DATE.test(statusDate)) {
|
|
16525
|
+
return {
|
|
16526
|
+
error: true,
|
|
16527
|
+
code: "BAD_INPUT",
|
|
16528
|
+
message: `status_date ${JSON.stringify(params.status_date)} is not YYYY-MM-DD`,
|
|
16529
|
+
hint: "Pass a calendar date like 2026-03-14, or omit it to stamp now."
|
|
16530
|
+
};
|
|
16531
|
+
}
|
|
16532
|
+
const failed = await writeAll(client, leadIds, status, statusDate);
|
|
16533
|
+
const count = leadIds.length - failed.length;
|
|
16534
|
+
return {
|
|
16535
|
+
applied: count > 0,
|
|
16536
|
+
count,
|
|
16537
|
+
status,
|
|
16538
|
+
...statusDate ? { status_date: statusDate } : {},
|
|
16539
|
+
failed
|
|
16540
|
+
};
|
|
16541
|
+
}
|
|
16542
|
+
};
|
|
16543
|
+
}
|
|
16544
|
+
});
|
|
16545
|
+
|
|
15961
16546
|
// ../core/dist/tools/set-telemetry.js
|
|
15962
16547
|
function isEnabled(telemetry_enabled) {
|
|
15963
16548
|
return telemetry_enabled !== false;
|
|
@@ -16493,6 +17078,51 @@ var init_prepare_outreach = __esm({
|
|
|
16493
17078
|
}
|
|
16494
17079
|
});
|
|
16495
17080
|
|
|
17081
|
+
// ../core/dist/lead-order.js
|
|
17082
|
+
function resolveLeadOrder(raw, tool) {
|
|
17083
|
+
const order = raw?.trim().toUpperCase();
|
|
17084
|
+
if (!order)
|
|
17085
|
+
return {};
|
|
17086
|
+
if (!LEAD_ORDER_SET.has(order)) {
|
|
17087
|
+
return {
|
|
17088
|
+
error: {
|
|
17089
|
+
error: true,
|
|
17090
|
+
code: "BAD_INPUT",
|
|
17091
|
+
message: `Unknown order: ${JSON.stringify(raw)}`,
|
|
17092
|
+
hint: `Call ${tool} again with one of: ${LEAD_ORDERS.join(", ")}.`
|
|
17093
|
+
}
|
|
17094
|
+
};
|
|
17095
|
+
}
|
|
17096
|
+
return { order };
|
|
17097
|
+
}
|
|
17098
|
+
var LEAD_ORDERS, LEAD_ORDER_SET;
|
|
17099
|
+
var init_lead_order = __esm({
|
|
17100
|
+
"../core/dist/lead-order.js"() {
|
|
17101
|
+
"use strict";
|
|
17102
|
+
LEAD_ORDERS = [
|
|
17103
|
+
"SCORE:DESC",
|
|
17104
|
+
"SCORE:ASC",
|
|
17105
|
+
"NAME:ASC",
|
|
17106
|
+
"NAME:DESC",
|
|
17107
|
+
"SIZE:DESC",
|
|
17108
|
+
"SIZE:ASC",
|
|
17109
|
+
"SECTOR:ASC",
|
|
17110
|
+
"SECTOR:DESC",
|
|
17111
|
+
"STATUS:ASC",
|
|
17112
|
+
"STATUS:DESC",
|
|
17113
|
+
"CONTACT_COUNT:DESC",
|
|
17114
|
+
"CONTACT_COUNT:ASC",
|
|
17115
|
+
"LAST_PROSPECTING_ACTION_AT:DESC",
|
|
17116
|
+
"LAST_PROSPECTING_ACTION_AT:ASC",
|
|
17117
|
+
"EPILOGUE_STATUS_SET_AT:DESC",
|
|
17118
|
+
"EPILOGUE_STATUS_SET_AT:ASC",
|
|
17119
|
+
"LIKED:DESC",
|
|
17120
|
+
"DISLIKED:DESC"
|
|
17121
|
+
];
|
|
17122
|
+
LEAD_ORDER_SET = new Set(LEAD_ORDERS);
|
|
17123
|
+
}
|
|
17124
|
+
});
|
|
17125
|
+
|
|
16496
17126
|
// ../core/dist/composite/_empty-lens-reason.js
|
|
16497
17127
|
function criteriaOf(filter) {
|
|
16498
17128
|
return filter?.lens_filter?.items?.flatMap((i) => i.criteria ?? []) ?? [];
|
|
@@ -16678,6 +17308,7 @@ var init_pull_leads = __esm({
|
|
|
16678
17308
|
"../core/dist/composite/pull-leads.js"() {
|
|
16679
17309
|
"use strict";
|
|
16680
17310
|
init_agent_memory();
|
|
17311
|
+
init_lead_order();
|
|
16681
17312
|
init_empty_lens_reason();
|
|
16682
17313
|
init_tool_descriptions_generated();
|
|
16683
17314
|
pullLeads = {
|
|
@@ -16699,6 +17330,10 @@ var init_pull_leads = __esm({
|
|
|
16699
17330
|
},
|
|
16700
17331
|
count: { type: "number", description: "Leads per page, max 50 (default 20)" },
|
|
16701
17332
|
page: { type: "number", description: "Page number, 0-indexed (default 0)" },
|
|
17333
|
+
order: {
|
|
17334
|
+
type: "string",
|
|
17335
|
+
description: "Optional sort, FIELD:ASC|DESC (SCORE, NAME, SIZE, SECTOR, STATUS, CONTACT_COUNT, LAST_PROSPECTING_ACTION_AT, LIKED). Omit for the lens's own Discover ranking. An unknown value is rejected and the error lists every accepted order."
|
|
17336
|
+
},
|
|
16702
17337
|
verbose: {
|
|
16703
17338
|
type: "boolean",
|
|
16704
17339
|
description: "If true, include the full set of lead-summary fields. Default false: returns the trimmed agent-friendly form."
|
|
@@ -16807,7 +17442,11 @@ var init_pull_leads = __esm({
|
|
|
16807
17442
|
const page = params.page ?? 0;
|
|
16808
17443
|
const count = Math.min(params.count ?? 20, 50);
|
|
16809
17444
|
const verbose = params.verbose ?? false;
|
|
16810
|
-
const
|
|
17445
|
+
const resolvedOrder = resolveLeadOrder(params.order, "leadbay_pull_leads");
|
|
17446
|
+
if (resolvedOrder.error)
|
|
17447
|
+
return resolvedOrder.error;
|
|
17448
|
+
const orderQs = resolvedOrder.order ? `&order=${encodeURIComponent(resolvedOrder.order)}` : "";
|
|
17449
|
+
const res = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${count}&page=${page}&contacts=true${orderQs}`);
|
|
16811
17450
|
const summaries = await Promise.all(res.items.map(async (lead) => {
|
|
16812
17451
|
try {
|
|
16813
17452
|
const r = await client.request("GET", `/leads/${lead.id}/ai_agent_responses`);
|
|
@@ -17060,6 +17699,7 @@ var init_pull_followups = __esm({
|
|
|
17060
17699
|
"../core/dist/composite/pull-followups.js"() {
|
|
17061
17700
|
"use strict";
|
|
17062
17701
|
init_agent_memory();
|
|
17702
|
+
init_lead_order();
|
|
17063
17703
|
init_tool_descriptions_generated();
|
|
17064
17704
|
init_geo_helpers();
|
|
17065
17705
|
init_country_guard();
|
|
@@ -17092,6 +17732,10 @@ var init_pull_followups = __esm({
|
|
|
17092
17732
|
type: "number",
|
|
17093
17733
|
description: "Leads per page, max 200 (default 20)."
|
|
17094
17734
|
},
|
|
17735
|
+
order: {
|
|
17736
|
+
type: "string",
|
|
17737
|
+
description: "Optional sort, FIELD:ASC|DESC (SCORE, NAME, SIZE, SECTOR, STATUS, CONTACT_COUNT, LAST_PROSPECTING_ACTION_AT, LIKED). Omit for the Monitor's own ranking. An unknown value is rejected and the error lists every accepted order."
|
|
17738
|
+
},
|
|
17095
17739
|
page: {
|
|
17096
17740
|
type: "number",
|
|
17097
17741
|
description: "Page number, 0-indexed (default 0)."
|
|
@@ -17201,7 +17845,7 @@ var init_pull_followups = __esm({
|
|
|
17201
17845
|
if (params.city_id)
|
|
17202
17846
|
geoTexts.push(params.city_id);
|
|
17203
17847
|
if (geoTexts.length > 0) {
|
|
17204
|
-
const { resolved, ambiguities } = await resolveLocations(client, geoTexts);
|
|
17848
|
+
const { resolved: resolved2, ambiguities } = await resolveLocations(client, geoTexts);
|
|
17205
17849
|
if (ambiguities.length > 0) {
|
|
17206
17850
|
return withAgentMemoryMeta(client, {
|
|
17207
17851
|
status: "ambiguous_locations",
|
|
@@ -17216,8 +17860,8 @@ var init_pull_followups = __esm({
|
|
|
17216
17860
|
}
|
|
17217
17861
|
}, ctx);
|
|
17218
17862
|
}
|
|
17219
|
-
if (
|
|
17220
|
-
effectiveSetFilter = mergeLocationIds(effectiveSetFilter,
|
|
17863
|
+
if (resolved2.length > 0) {
|
|
17864
|
+
effectiveSetFilter = mergeLocationIds(effectiveSetFilter, resolved2);
|
|
17221
17865
|
}
|
|
17222
17866
|
}
|
|
17223
17867
|
if (effectiveSetFilter) {
|
|
@@ -17227,12 +17871,17 @@ var init_pull_followups = __esm({
|
|
|
17227
17871
|
ctx?.logger?.warn?.(`pull_followups: POST /monitor/filter failed: ${err?.message ?? err?.code ?? err}`);
|
|
17228
17872
|
}
|
|
17229
17873
|
}
|
|
17874
|
+
const resolved = resolveLeadOrder(params.order, "leadbay_pull_followups");
|
|
17875
|
+
if (resolved.error)
|
|
17876
|
+
return resolved.error;
|
|
17877
|
+
const order = resolved.order;
|
|
17230
17878
|
const qs = new URLSearchParams({
|
|
17231
17879
|
personal: String(personal),
|
|
17232
17880
|
liked: String(liked),
|
|
17233
17881
|
filtered: String(filtered),
|
|
17234
17882
|
count: String(count),
|
|
17235
|
-
page: String(page)
|
|
17883
|
+
page: String(page),
|
|
17884
|
+
...order ? { order } : {}
|
|
17236
17885
|
}).toString();
|
|
17237
17886
|
const [filterR, monitorR] = await Promise.allSettled([
|
|
17238
17887
|
filtered ? client.request("GET", "/monitor/filter") : Promise.resolve(null),
|
|
@@ -18511,7 +19160,7 @@ var init_research_lead_by_id = __esm({
|
|
|
18511
19160
|
},
|
|
18512
19161
|
_meta: {
|
|
18513
19162
|
type: "object",
|
|
18514
|
-
description: "Operator context: region (us/fr/custom), lens_id (the lens used for the lead-by-id fetch), web_fetch_in_progress (true if the backend is still hydrating signals), has_reachable_contact (true if at least one contact or recommended_contact has email or phone \u2014 drives NEXT STEPS routing between enrichment vs outreach). When the call was routed via leadbay_research_lead_by_name_fuzzy, also: resolved_from='companyName', resolved_query='<needle>', match_candidates=[{leadId,name,score}].",
|
|
19163
|
+
description: "Operator context: region (us/fr/custom), lens_id (the lens used for the lead-by-id fetch), web_fetch_in_progress (true if the backend is still hydrating signals), has_reachable_contact (true if at least one contact or recommended_contact has email or phone \u2014 drives NEXT STEPS routing between enrichment vs outreach). When the call was routed via leadbay_research_lead_by_name_fuzzy, also: resolved_from='companyName' (matched in the user's own leads) or 'resolver' (matched in the Leadbay company registry), resolved_query='<needle>', resolved_matched_on=['website_exact',\u2026] on the resolver path, match_candidates=[{leadId,name,score}].",
|
|
18515
19164
|
properties: {
|
|
18516
19165
|
region: { type: "string" },
|
|
18517
19166
|
lens_id: { type: "number" },
|
|
@@ -18523,6 +19172,10 @@ var init_research_lead_by_id = __esm({
|
|
|
18523
19172
|
type: ["array", "null"],
|
|
18524
19173
|
items: { type: "object" }
|
|
18525
19174
|
},
|
|
19175
|
+
resolved_matched_on: {
|
|
19176
|
+
type: ["array", "null"],
|
|
19177
|
+
items: { type: "string" }
|
|
19178
|
+
},
|
|
18526
19179
|
agent_memory: { type: "object" }
|
|
18527
19180
|
},
|
|
18528
19181
|
// _meta is an open envelope: the MCP server layer injects
|
|
@@ -18695,7 +19348,8 @@ var init_research_lead_by_id = __esm({
|
|
|
18695
19348
|
has_reachable_contact: hasReachableContact,
|
|
18696
19349
|
resolved_from: params._resolved?.from ?? null,
|
|
18697
19350
|
resolved_query: params._resolved?.query ?? null,
|
|
18698
|
-
match_candidates: params._resolved?.candidates ?? null
|
|
19351
|
+
match_candidates: params._resolved?.candidates ?? null,
|
|
19352
|
+
resolved_matched_on: params._resolved?.matched_on ?? null
|
|
18699
19353
|
}
|
|
18700
19354
|
}, _ctx);
|
|
18701
19355
|
}
|
|
@@ -18719,16 +19373,6 @@ var init_research_lead_by_id = __esm({
|
|
|
18719
19373
|
});
|
|
18720
19374
|
|
|
18721
19375
|
// ../core/dist/composite/research-lead-by-name-fuzzy.js
|
|
18722
|
-
function rankSubstringMatches(needle, candidates) {
|
|
18723
|
-
const n = needle.toLowerCase();
|
|
18724
|
-
const hits = candidates.filter((c) => typeof c.name === "string" && c.name.toLowerCase().includes(n));
|
|
18725
|
-
hits.sort((a, b) => {
|
|
18726
|
-
const aScore = a.score ?? -Infinity;
|
|
18727
|
-
const bScore = b.score ?? -Infinity;
|
|
18728
|
-
return bScore - aScore;
|
|
18729
|
-
});
|
|
18730
|
-
return hits;
|
|
18731
|
-
}
|
|
18732
19376
|
function parseLensId(value) {
|
|
18733
19377
|
const parsed = typeof value === "number" ? value : typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
|
|
18734
19378
|
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : void 0;
|
|
@@ -18743,6 +19387,34 @@ function suggestionLeadId(suggestion) {
|
|
|
18743
19387
|
function isLeadbayError(error) {
|
|
18744
19388
|
return typeof error === "object" && error !== null && error.error === true && typeof error.code === "string" && typeof error.message === "string" && typeof error.hint === "string";
|
|
18745
19389
|
}
|
|
19390
|
+
function businessDomainFromEmail(email) {
|
|
19391
|
+
if (!email || typeof email !== "string")
|
|
19392
|
+
return null;
|
|
19393
|
+
const at = email.lastIndexOf("@");
|
|
19394
|
+
if (at === -1)
|
|
19395
|
+
return null;
|
|
19396
|
+
const domain = normalizeDomain(email.slice(at + 1));
|
|
19397
|
+
if (!domain)
|
|
19398
|
+
return null;
|
|
19399
|
+
return PUBLIC_MAILBOX_DOMAINS.has(domain) ? null : domain;
|
|
19400
|
+
}
|
|
19401
|
+
function buildResolvePayload(params) {
|
|
19402
|
+
const queryDomain = normalizeDomain(params.query);
|
|
19403
|
+
const website = (params.website ? normalizeDomain(params.website) : null) ?? queryDomain ?? businessDomainFromEmail(params.email);
|
|
19404
|
+
const payload = {};
|
|
19405
|
+
if (!queryDomain)
|
|
19406
|
+
payload.name = params.query;
|
|
19407
|
+
if (website)
|
|
19408
|
+
payload.website = website;
|
|
19409
|
+
if (params.email)
|
|
19410
|
+
payload.email = params.email;
|
|
19411
|
+
if (params.registry_number)
|
|
19412
|
+
payload.registry_number = params.registry_number;
|
|
19413
|
+
return payload;
|
|
19414
|
+
}
|
|
19415
|
+
function hasStrongIdentityKey(payload) {
|
|
19416
|
+
return Boolean(payload.website || payload.registry_number);
|
|
19417
|
+
}
|
|
18746
19418
|
async function resolveWithinLens(client, query, lensId) {
|
|
18747
19419
|
const results = await client.request("GET", `/lenses/${lensId}/leads/wishlist?q=${encodeURIComponent(query)}&count=50&page=0&contacts=false`);
|
|
18748
19420
|
return results.items.map((lead) => ({
|
|
@@ -18764,12 +19436,33 @@ async function resolveAcrossVisibleCorpus(client, query) {
|
|
|
18764
19436
|
};
|
|
18765
19437
|
}).filter((suggestion) => suggestion.id !== "" && suggestion.name !== "");
|
|
18766
19438
|
}
|
|
18767
|
-
|
|
19439
|
+
async function hydrateAmbiguous(client, candidates, lensId) {
|
|
19440
|
+
const selected = candidates.slice(0, MAX_AMBIGUOUS_CANDIDATES);
|
|
19441
|
+
const settled = await Promise.allSettled(selected.map((c) => client.request("GET", `/lenses/${lensId}/leads/${c.lead_id}`)));
|
|
19442
|
+
return selected.map((c, i) => {
|
|
19443
|
+
const r = settled[i];
|
|
19444
|
+
const lead = r.status === "fulfilled" ? r.value : null;
|
|
19445
|
+
return {
|
|
19446
|
+
leadId: c.lead_id,
|
|
19447
|
+
name: lead?.name ?? null,
|
|
19448
|
+
website: lead?.website ?? null,
|
|
19449
|
+
location: lead?.location?.full ?? lead?.location?.city ?? lead?.location?.country ?? null,
|
|
19450
|
+
registry_ids: lead?.registry_ids ?? null,
|
|
19451
|
+
score: c.score,
|
|
19452
|
+
matched_on: c.matched_on,
|
|
19453
|
+
lead_fields_populated: c.lead_fields_populated
|
|
19454
|
+
};
|
|
19455
|
+
});
|
|
19456
|
+
}
|
|
19457
|
+
var RESOLVE_TIMEOUT_MS, MAX_AMBIGUOUS_CANDIDATES, researchLeadByNameFuzzy;
|
|
18768
19458
|
var init_research_lead_by_name_fuzzy = __esm({
|
|
18769
19459
|
"../core/dist/composite/research-lead-by-name-fuzzy.js"() {
|
|
18770
19460
|
"use strict";
|
|
18771
19461
|
init_research_lead_by_id();
|
|
19462
|
+
init_import_leads();
|
|
18772
19463
|
init_tool_descriptions_generated();
|
|
19464
|
+
RESOLVE_TIMEOUT_MS = 1e4;
|
|
19465
|
+
MAX_AMBIGUOUS_CANDIDATES = 4;
|
|
18773
19466
|
researchLeadByNameFuzzy = {
|
|
18774
19467
|
name: "leadbay_research_lead_by_name_fuzzy",
|
|
18775
19468
|
annotations: {
|
|
@@ -18785,11 +19478,23 @@ var init_research_lead_by_name_fuzzy = __esm({
|
|
|
18785
19478
|
properties: {
|
|
18786
19479
|
companyName: {
|
|
18787
19480
|
type: "string",
|
|
18788
|
-
description: "Company name, domain, or contact name
|
|
19481
|
+
description: "Company name, domain, or contact name. Resolved against the user's own Discover/Monitor/Activate leads and the Leadbay company registry."
|
|
19482
|
+
},
|
|
19483
|
+
website: {
|
|
19484
|
+
type: "string",
|
|
19485
|
+
description: "Company domain or website when you have one (`acme.com`, `https://www.acme.com/` \u2014 both fine). This is the single strongest match key; pass it whenever the user mentioned a domain, and a company outside their leads becomes findable."
|
|
19486
|
+
},
|
|
19487
|
+
email: {
|
|
19488
|
+
type: "string",
|
|
19489
|
+
description: "A contact email at the company. Used to derive the company domain when `website` is absent; consumer mailboxes (gmail, orange.fr, \u2026) are ignored."
|
|
19490
|
+
},
|
|
19491
|
+
registry_number: {
|
|
19492
|
+
type: "string",
|
|
19493
|
+
description: "Company registry number (SIREN/SIRET in France, company number elsewhere). The other exact match key \u2014 pass it when the user supplies one, or when a previous LEAD_NOT_FOUND hint asked for it."
|
|
18789
19494
|
},
|
|
18790
19495
|
lensId: {
|
|
18791
19496
|
type: "number",
|
|
18792
|
-
description: "Optional strict scope. When supplied, search only this lens's wishlist
|
|
19497
|
+
description: "Optional strict scope. When supplied, search only this lens's wishlist and do NOT fall through to the registry; normally omit."
|
|
18793
19498
|
},
|
|
18794
19499
|
concise: {
|
|
18795
19500
|
type: "boolean",
|
|
@@ -18805,59 +19510,123 @@ var init_research_lead_by_name_fuzzy = __esm({
|
|
|
18805
19510
|
additionalProperties: false
|
|
18806
19511
|
},
|
|
18807
19512
|
// Output shape matches leadbay_research_lead_by_id; the only additions are
|
|
18808
|
-
// _meta.resolved_from / resolved_query /
|
|
18809
|
-
// documented on _by_id's output schema. Defer to
|
|
18810
|
-
// duplicating it would just rot.
|
|
19513
|
+
// _meta.resolved_from / resolved_query / resolved_matched_on /
|
|
19514
|
+
// match_candidates which are documented on _by_id's output schema. Defer to
|
|
19515
|
+
// _by_id for the schema — duplicating it would just rot. The one exception
|
|
19516
|
+
// is the ambiguous branch, which returns a disambiguation payload instead
|
|
19517
|
+
// of a research card.
|
|
18811
19518
|
outputSchema: {
|
|
18812
19519
|
type: "object",
|
|
18813
|
-
description: "Same shape as leadbay_research_lead_by_id, with _meta.resolved_from='companyName', _meta.resolved_query='<needle>', and _meta.match_candidates=[{leadId,name,score}] populated.",
|
|
19520
|
+
description: "Same shape as leadbay_research_lead_by_id, with _meta.resolved_from='companyName'|'resolver', _meta.resolved_query='<needle>', _meta.resolved_matched_on=[...], and _meta.match_candidates=[{leadId,name,score}] populated. When the registry resolver cannot pick one company, returns {resolution:'ambiguous', query, candidates:[{leadId,name,website,location,registry_ids,score,matched_on}]} instead \u2014 ask the user which one, then call leadbay_research_lead_by_id.",
|
|
18814
19521
|
additionalProperties: true
|
|
18815
19522
|
},
|
|
18816
19523
|
execute: async (client, params, ctx) => {
|
|
18817
19524
|
if (!params.companyName || typeof params.companyName !== "string" || params.companyName.trim() === "") {
|
|
18818
|
-
throw client.makeError("INVALID_PARAMS", "companyName is required", "Pass the company name as
|
|
19525
|
+
throw client.makeError("INVALID_PARAMS", "companyName is required and must be a non-empty string", "Pass the company name, domain, or contact name as `companyName` \u2014 e.g. companyName:'Wink Lab'. Add `website` (the strongest match key) or `email` when you have one. If you already have the lead UUID, call leadbay_research_lead_by_id with leadId instead.");
|
|
18819
19526
|
}
|
|
18820
19527
|
const query = params.companyName.trim();
|
|
18821
|
-
let ranked;
|
|
18822
19528
|
let lensId = params.lensId;
|
|
18823
|
-
|
|
18824
|
-
|
|
18825
|
-
|
|
18826
|
-
|
|
19529
|
+
if (params.lensId !== void 0) {
|
|
19530
|
+
const scoped = await resolveWithinLens(client, query, params.lensId);
|
|
19531
|
+
if (scoped.length > 0) {
|
|
19532
|
+
return await delegate(scoped, params.lensId);
|
|
19533
|
+
}
|
|
19534
|
+
throw client.makeError("LEAD_NOT_FOUND", `No lead matching "${query}" in lens ${params.lensId}`, "This lookup was intentionally restricted to the supplied lens. Omit lensId to search your visible leads across Discover, Monitor, and Activate and then the Leadbay company registry.");
|
|
19535
|
+
}
|
|
19536
|
+
async function delegate(matches, fallbackLens) {
|
|
19537
|
+
const [primary, ...rest] = matches;
|
|
19538
|
+
const resolvedLens = primary.lensId ?? fallbackLens ?? await client.resolveDefaultLens();
|
|
19539
|
+
return await researchLeadById.execute(client, {
|
|
19540
|
+
leadId: primary.id,
|
|
19541
|
+
lensId: resolvedLens,
|
|
19542
|
+
concise: params.concise,
|
|
19543
|
+
response_format: params.response_format,
|
|
19544
|
+
_resolved: {
|
|
19545
|
+
from: "companyName",
|
|
19546
|
+
query,
|
|
19547
|
+
candidates: rest.slice(0, MAX_AMBIGUOUS_CANDIDATES).map((m) => ({
|
|
19548
|
+
leadId: m.id,
|
|
19549
|
+
name: m.name,
|
|
19550
|
+
score: m.score
|
|
19551
|
+
}))
|
|
19552
|
+
}
|
|
19553
|
+
}, ctx);
|
|
19554
|
+
}
|
|
19555
|
+
const payload = buildResolvePayload({
|
|
19556
|
+
query,
|
|
19557
|
+
website: params.website,
|
|
19558
|
+
email: params.email,
|
|
19559
|
+
registry_number: params.registry_number
|
|
19560
|
+
});
|
|
19561
|
+
let corpusSearched = false;
|
|
19562
|
+
let ranked = [];
|
|
19563
|
+
const searchCorpus = async (strict) => {
|
|
18827
19564
|
try {
|
|
18828
19565
|
ranked = await resolveAcrossVisibleCorpus(client, query);
|
|
19566
|
+
corpusSearched = true;
|
|
18829
19567
|
} catch (error) {
|
|
18830
|
-
if (isLeadbayError(error))
|
|
19568
|
+
if (strict && isLeadbayError(error))
|
|
18831
19569
|
throw error;
|
|
18832
|
-
|
|
18833
|
-
usedActiveLensFallback = true;
|
|
18834
|
-
ctx?.logger?.warn?.("Cross-tab company search was unavailable; falling back to the active lens for this lookup.");
|
|
18835
|
-
ranked = rankSubstringMatches(query, await resolveWithinLens(client, query, lensId));
|
|
18836
|
-
}
|
|
18837
|
-
}
|
|
18838
|
-
if (ranked.length === 0) {
|
|
18839
|
-
const scope = usedActiveLensFallback ? `in active lens ${lensId}; cross-tab search was unavailable` : params.lensId === void 0 ? "across your visible Leadbay leads" : `in lens ${params.lensId}`;
|
|
18840
|
-
const hint = usedActiveLensFallback ? `Only active lens ${lensId} was checked because cross-tab search was unavailable. Retry later before concluding the company is missing or offering to import it.` : params.lensId === void 0 ? "Search checks company names, domains, and contact names across Discover, Monitor, and Activate. Confirm the spelling or domain, or add/import the company first." : "This lookup was intentionally restricted to the supplied lens. Omit lensId to search visible leads across Discover, Monitor, and Activate.";
|
|
18841
|
-
throw client.makeError("LEAD_NOT_FOUND", `No lead matching "${query}" ${scope}`, hint);
|
|
18842
|
-
}
|
|
18843
|
-
const [primary, ...rest] = ranked;
|
|
18844
|
-
lensId = primary.lensId ?? lensId ?? await client.resolveDefaultLens();
|
|
18845
|
-
const candidates = rest.slice(0, 4).map((m) => ({
|
|
18846
|
-
leadId: m.id,
|
|
18847
|
-
name: m.name,
|
|
18848
|
-
score: m.score
|
|
18849
|
-
}));
|
|
18850
|
-
return await researchLeadById.execute(client, {
|
|
18851
|
-
leadId: primary.id,
|
|
18852
|
-
lensId,
|
|
18853
|
-
concise: params.concise,
|
|
18854
|
-
response_format: params.response_format,
|
|
18855
|
-
_resolved: {
|
|
18856
|
-
from: "companyName",
|
|
18857
|
-
query,
|
|
18858
|
-
candidates
|
|
19570
|
+
ctx?.logger?.warn?.("Cross-tab company search was unavailable; resolving against the Leadbay registry instead.");
|
|
18859
19571
|
}
|
|
18860
|
-
}
|
|
19572
|
+
};
|
|
19573
|
+
const registryFirst = hasStrongIdentityKey(payload);
|
|
19574
|
+
if (!registryFirst) {
|
|
19575
|
+
await searchCorpus(true);
|
|
19576
|
+
if (ranked.length > 0)
|
|
19577
|
+
return await delegate(ranked);
|
|
19578
|
+
}
|
|
19579
|
+
let resolved;
|
|
19580
|
+
try {
|
|
19581
|
+
resolved = await client.request("POST", "/leads/resolve", payload, { timeoutMs: RESOLVE_TIMEOUT_MS });
|
|
19582
|
+
} catch (error) {
|
|
19583
|
+
if (isLeadbayError(error))
|
|
19584
|
+
throw error;
|
|
19585
|
+
throw client.makeError("LEAD_NOT_FOUND", `Could not reach the Leadbay company registry while looking up "${query}"`, "The registry lookup did not complete. Retry once; if it fails again, say so rather than concluding the company is missing.", "POST /leads/resolve");
|
|
19586
|
+
}
|
|
19587
|
+
if (resolved.type === "matched") {
|
|
19588
|
+
lensId = lensId ?? await client.resolveDefaultLens();
|
|
19589
|
+
return await researchLeadById.execute(client, {
|
|
19590
|
+
leadId: resolved.lead_id,
|
|
19591
|
+
lensId,
|
|
19592
|
+
concise: params.concise,
|
|
19593
|
+
response_format: params.response_format,
|
|
19594
|
+
_resolved: {
|
|
19595
|
+
from: "resolver",
|
|
19596
|
+
query,
|
|
19597
|
+
candidates: [],
|
|
19598
|
+
matched_on: resolved.matched_on
|
|
19599
|
+
}
|
|
19600
|
+
}, ctx);
|
|
19601
|
+
}
|
|
19602
|
+
if (resolved.type === "ambiguous" && resolved.candidates.length > 0) {
|
|
19603
|
+
const hydrationLens = lensId ?? await client.resolveDefaultLens();
|
|
19604
|
+
const candidates = await hydrateAmbiguous(client, resolved.candidates, hydrationLens);
|
|
19605
|
+
return {
|
|
19606
|
+
resolution: "ambiguous",
|
|
19607
|
+
query,
|
|
19608
|
+
resolver_payload: payload,
|
|
19609
|
+
candidates,
|
|
19610
|
+
next_step: "Ask the user which company they mean, then call leadbay_research_lead_by_id with the chosen leadId. Do not guess from score \u2014 it is a tied evidence band, not a confidence.",
|
|
19611
|
+
_meta: {
|
|
19612
|
+
region: client.region,
|
|
19613
|
+
lens_id: hydrationLens,
|
|
19614
|
+
resolved_from: "resolver",
|
|
19615
|
+
resolved_query: query
|
|
19616
|
+
}
|
|
19617
|
+
};
|
|
19618
|
+
}
|
|
19619
|
+
if (!corpusSearched) {
|
|
19620
|
+
await searchCorpus(false);
|
|
19621
|
+
if (ranked.length > 0)
|
|
19622
|
+
return await delegate(ranked);
|
|
19623
|
+
}
|
|
19624
|
+
const registryScope = payload.website ? `the Leadbay company registry (domain ${payload.website})` : "the Leadbay company registry";
|
|
19625
|
+
const searched = corpusSearched ? `in your visible Leadbay leads and in ${registryScope}` : `in ${registryScope} \u2014 your own leads could NOT be searched, the search route was unreachable`;
|
|
19626
|
+
const wanted = resolved.type === "none" && resolved.would_help.length > 0 ? resolved.would_help : ["website", "registry_number"];
|
|
19627
|
+
const asks = wanted.map((f) => f === "registry_number" ? "a registry number (SIREN/SIRET) for `registry_number`" : f === "website" ? "the company website for `website`" : `\`${f}\``).join(" or ");
|
|
19628
|
+
const hint = resolved.type === "unidentifiable" ? `The registry could not identify a company from this input (${resolved.reason}). Ask the user for ${asks}, then call this tool again with it.` : `The registry found no company for what was supplied. It would match on ${asks}. Ask the user for that \u2014 "what's their website?" usually settles it \u2014 then call this tool again. Do not offer an import before asking.`;
|
|
19629
|
+
throw client.makeError("LEAD_NOT_FOUND", `No company matching "${query}" ${searched}`, hint, "POST /leads/resolve");
|
|
18861
19630
|
}
|
|
18862
19631
|
};
|
|
18863
19632
|
}
|
|
@@ -20937,7 +21706,7 @@ async function runPreview(client, params, ctx, perPhaseBudget, _totalBudget) {
|
|
|
20937
21706
|
throw Object.assign(new Error("aborted"), { name: "AbortError" });
|
|
20938
21707
|
}
|
|
20939
21708
|
if (!fileImport) {
|
|
20940
|
-
throw client.makeError("
|
|
21709
|
+
throw client.makeError("IMPORT_TIMEOUT", `Preview preprocess did not finish within ${perPhaseBudget}ms`, `Raise per_phase_budget_ms above ${perPhaseBudget} or send fewer rows \u2014 re-running the identical call will not help. The wizard row will eventually be cleaned up.`, `GET /imports/${importId}`);
|
|
20941
21710
|
}
|
|
20942
21711
|
if (fileImport.pre_processing?.error) {
|
|
20943
21712
|
throw client.makeError("IMPORT_PREPROCESS_FAILED", `Preview preprocess failed: ${fileImport.pre_processing.error}`, "Inspect the input rows for encoding / shape issues.", `GET /imports/${importId}`);
|
|
@@ -21104,6 +21873,19 @@ var init_import_and_qualify = __esm({
|
|
|
21104
21873
|
type: "string",
|
|
21105
21874
|
description: "Import handle to pass to leadbay_import_status when wait_for_completion=false."
|
|
21106
21875
|
},
|
|
21876
|
+
timed_out: {
|
|
21877
|
+
type: "boolean",
|
|
21878
|
+
description: "True when the underlying import ran out of poll budget. Still running server-side \u2014 poll leadbay_import_status(import_ids). Do NOT re-issue the import."
|
|
21879
|
+
},
|
|
21880
|
+
rows_pending_upload: {
|
|
21881
|
+
type: "number",
|
|
21882
|
+
description: "Rows from later chunks that never reached the backend. These are NOT running anywhere; re-import just those rows."
|
|
21883
|
+
},
|
|
21884
|
+
row_ids: {
|
|
21885
|
+
type: "array",
|
|
21886
|
+
description: "Records mode only: the synthetic row id of each input row, in the order you passed `records[]`. leadbay_import_status reports recovered leads by that id \u2014 use this to map them back to your source rows.",
|
|
21887
|
+
items: { type: "string" }
|
|
21888
|
+
},
|
|
21107
21889
|
// preview-shape keys
|
|
21108
21890
|
mapping_hints: {
|
|
21109
21891
|
type: "array",
|
|
@@ -21247,7 +22029,7 @@ var init_import_and_qualify = __esm({
|
|
|
21247
22029
|
return {
|
|
21248
22030
|
kind: "result",
|
|
21249
22031
|
status: "running",
|
|
21250
|
-
handle_id: queued.handle_id,
|
|
22032
|
+
...queued.handle_id ? { handle_id: queued.handle_id } : {},
|
|
21251
22033
|
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
21252
22034
|
qualify_id: null,
|
|
21253
22035
|
import_ids: queued.importIds,
|
|
@@ -21279,7 +22061,35 @@ var init_import_and_qualify = __esm({
|
|
|
21279
22061
|
wait_for_completion: true
|
|
21280
22062
|
}, ctx);
|
|
21281
22063
|
if (isImportLeadsRunningResult(importResultRaw)) {
|
|
21282
|
-
|
|
22064
|
+
return {
|
|
22065
|
+
kind: "result",
|
|
22066
|
+
status: "running",
|
|
22067
|
+
...importResultRaw.handle_id ? { handle_id: importResultRaw.handle_id } : {},
|
|
22068
|
+
// Everything the rendering contract keys off has to survive the
|
|
22069
|
+
// wrapper. Without `timed_out` the agent can't tell this from a
|
|
22070
|
+
// deliberate async launch; without `rows_pending_upload` a >100-row
|
|
22071
|
+
// batch silently loses every unuploaded chunk; without `dry_run`
|
|
22072
|
+
// leadbay_import_status can't tell a validation pass from a real
|
|
22073
|
+
// import still committing.
|
|
22074
|
+
...importResultRaw.timed_out ? { timed_out: true } : {},
|
|
22075
|
+
...importResultRaw.rows_pending_upload !== void 0 ? { rows_pending_upload: importResultRaw.rows_pending_upload } : {},
|
|
22076
|
+
...importResultRaw.dry_run ? { dry_run: true } : {},
|
|
22077
|
+
...importResultRaw.row_ids ? { row_ids: importResultRaw.row_ids } : {},
|
|
22078
|
+
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
22079
|
+
qualify_id: null,
|
|
22080
|
+
import_ids: importResultRaw.importIds,
|
|
22081
|
+
notification_ids: importResultRaw.notification_ids ?? [],
|
|
22082
|
+
imported: [],
|
|
22083
|
+
not_imported: (importResultRaw.not_imported ?? []).map(toNotImportedEntry),
|
|
22084
|
+
qualified: [],
|
|
22085
|
+
still_running: [],
|
|
22086
|
+
failed: [],
|
|
22087
|
+
quota_exceeded: false,
|
|
22088
|
+
skipped_already_qualified: [],
|
|
22089
|
+
not_in_lens: [],
|
|
22090
|
+
region: client.region,
|
|
22091
|
+
_meta: importResultRaw._meta
|
|
22092
|
+
};
|
|
21283
22093
|
}
|
|
21284
22094
|
const importResult = importResultRaw;
|
|
21285
22095
|
if (importResult.cancelled) {
|
|
@@ -22140,12 +22950,80 @@ function summarizeImports(imports, dryRun) {
|
|
|
22140
22950
|
records_total: recordsTotal
|
|
22141
22951
|
};
|
|
22142
22952
|
}
|
|
22143
|
-
|
|
22953
|
+
function isInProgress(err) {
|
|
22954
|
+
return /in_progress/i.test(String(err?.message ?? ""));
|
|
22955
|
+
}
|
|
22956
|
+
async function fetchReconciledRecords(client, importIds, declaredTotal, ctx) {
|
|
22957
|
+
const canonicalLeadIds = /* @__PURE__ */ new Set();
|
|
22958
|
+
for (const importId of importIds) {
|
|
22959
|
+
try {
|
|
22960
|
+
const res = await client.request("GET", `/imports/${importId}/leads`);
|
|
22961
|
+
for (const id of res?.lead_ids ?? [])
|
|
22962
|
+
canonicalLeadIds.add(id);
|
|
22963
|
+
} catch (err) {
|
|
22964
|
+
if (isInProgress(err))
|
|
22965
|
+
throw new ImportNotReady();
|
|
22966
|
+
if (err?.code !== "NOT_FOUND" && err?._meta?.http_status !== 404)
|
|
22967
|
+
throw err;
|
|
22968
|
+
ctx?.logger?.warn?.(`import-status: /imports/${importId}/leads not available on this backend (404) \u2014 using records only`);
|
|
22969
|
+
}
|
|
22970
|
+
}
|
|
22971
|
+
const all = [];
|
|
22972
|
+
for (const importId of importIds) {
|
|
22973
|
+
for (let page = 0; page < RECORDS_MAX_PAGES; page++) {
|
|
22974
|
+
const qs = `count=${RECORDS_PAGE_SIZE}&page=${page}&automatic_match=true&manual_match=true&no_match=true&matching=true&importing=true&imported=true`;
|
|
22975
|
+
let res;
|
|
22976
|
+
try {
|
|
22977
|
+
res = await client.request("GET", `/imports/${importId}/records?${qs}`);
|
|
22978
|
+
} catch (err) {
|
|
22979
|
+
if (isInProgress(err))
|
|
22980
|
+
throw new ImportNotReady();
|
|
22981
|
+
throw err;
|
|
22982
|
+
}
|
|
22983
|
+
all.push(...res.items);
|
|
22984
|
+
const totalPages = res.pagination.pages ?? 0;
|
|
22985
|
+
if (page + 1 >= totalPages)
|
|
22986
|
+
break;
|
|
22987
|
+
if (page + 1 === RECORDS_MAX_PAGES) {
|
|
22988
|
+
ctx?.logger?.warn?.(`import-status: importId=${importId} has >${RECORDS_MAX_PAGES} record pages; skipping reconciliation`);
|
|
22989
|
+
return null;
|
|
22990
|
+
}
|
|
22991
|
+
}
|
|
22992
|
+
}
|
|
22993
|
+
const { leads, not_imported, pending, distinct, pendingLeadIds } = reconcileRecords(all);
|
|
22994
|
+
const deficit = settlingDeficit(declaredTotal, distinct);
|
|
22995
|
+
const seenLeadIds = new Set(leads.map((l) => l.leadId));
|
|
22996
|
+
const merged = [...leads];
|
|
22997
|
+
if (deficit === 0) {
|
|
22998
|
+
for (const id of canonicalLeadIds) {
|
|
22999
|
+
if (seenLeadIds.has(id))
|
|
23000
|
+
continue;
|
|
23001
|
+
if (pendingLeadIds.has(id))
|
|
23002
|
+
continue;
|
|
23003
|
+
merged.push({ leadId: id, name: null });
|
|
23004
|
+
}
|
|
23005
|
+
}
|
|
23006
|
+
return {
|
|
23007
|
+
leads: merged,
|
|
23008
|
+
not_imported,
|
|
23009
|
+
// A snapshot short of the declared row count is not final — see
|
|
23010
|
+
// `settlingDeficit`. Measured on DISTINCT rows: `all.length` counts a
|
|
23011
|
+
// re-paged row twice and would mask a genuine shortfall.
|
|
23012
|
+
still_settling: pending + deficit
|
|
23013
|
+
};
|
|
23014
|
+
}
|
|
23015
|
+
var RECORDS_PAGE_SIZE, RECORDS_MAX_PAGES, ImportNotReady, importStatus;
|
|
22144
23016
|
var init_import_status = __esm({
|
|
22145
23017
|
"../core/dist/composite/import-status.js"() {
|
|
22146
23018
|
"use strict";
|
|
22147
23019
|
init_bulk_store();
|
|
23020
|
+
init_import_records();
|
|
23021
|
+
init_import_commit_log();
|
|
22148
23022
|
init_tool_descriptions_generated();
|
|
23023
|
+
RECORDS_PAGE_SIZE = 100;
|
|
23024
|
+
RECORDS_MAX_PAGES = 20;
|
|
23025
|
+
ImportNotReady = class extends Error {
|
|
23026
|
+
};
|
|
22149
23027
|
importStatus = {
|
|
22150
23028
|
name: "leadbay_import_status",
|
|
22151
23029
|
annotations: {
|
|
@@ -22165,8 +23043,12 @@ var init_import_status = __esm({
|
|
|
22165
23043
|
},
|
|
22166
23044
|
importIds: {
|
|
22167
23045
|
type: "array",
|
|
22168
|
-
description: "
|
|
23046
|
+
description: "Backend file-import ids to inspect directly \u2014 from a completed import's `importIds`, or from a `{status:'running', timed_out:true}` result.",
|
|
22169
23047
|
items: { type: "string" }
|
|
23048
|
+
},
|
|
23049
|
+
dry_run: {
|
|
23050
|
+
type: "boolean",
|
|
23051
|
+
description: "Pass true when the importIds came from a dry run. A dry run and an import still committing its mappings look identical on the wire, so without this the tool reports the dry run as still running rather than risk rendering it as a real import."
|
|
22170
23052
|
}
|
|
22171
23053
|
},
|
|
22172
23054
|
additionalProperties: false
|
|
@@ -22180,9 +23062,13 @@ var init_import_status = __esm({
|
|
|
22180
23062
|
progress: { type: "object" },
|
|
22181
23063
|
result: {
|
|
22182
23064
|
type: "object",
|
|
22183
|
-
description: "Final import result when
|
|
23065
|
+
description: "Final import result: {leads, not_imported, importIds, still_settling?}. Present when a handle_id resolves a completed run in this MCP instance, OR when the importIds[] path finds every import complete and reconciles the wizard's records."
|
|
22184
23066
|
},
|
|
22185
23067
|
error: { type: "string" },
|
|
23068
|
+
dry_run: {
|
|
23069
|
+
type: "boolean",
|
|
23070
|
+
description: "True when these importIds came from a dry run. NOTHING was committed to the CRM \u2014 render it as a validation pass, never as a completed import."
|
|
23071
|
+
},
|
|
22186
23072
|
region: { type: "string" },
|
|
22187
23073
|
_meta: { type: "object" }
|
|
22188
23074
|
},
|
|
@@ -22191,7 +23077,7 @@ var init_import_status = __esm({
|
|
|
22191
23077
|
execute: async (client, params, ctx) => {
|
|
22192
23078
|
let handleId = params.handle_id;
|
|
22193
23079
|
let importIds = params.importIds ?? [];
|
|
22194
|
-
let handleDryRun;
|
|
23080
|
+
let handleDryRun = params.dry_run;
|
|
22195
23081
|
if (handleId) {
|
|
22196
23082
|
if (!isValidBulkId(handleId)) {
|
|
22197
23083
|
throw client.makeError("BULK_INVALID_ID", "handle_id is not a valid UUIDv4", "Pass the handle_id returned by leadbay_import_leads verbatim.", "");
|
|
@@ -22208,7 +23094,7 @@ var init_import_status = __esm({
|
|
|
22208
23094
|
throw client.makeError("BULK_NOT_FOUND", "No import record for that handle_id", "It may have expired (30-day TTL) or the MCP process was restarted without persistence.", "");
|
|
22209
23095
|
}
|
|
22210
23096
|
importIds = record.import_ids;
|
|
22211
|
-
handleDryRun = record.dry_run;
|
|
23097
|
+
handleDryRun = record.dry_run ?? handleDryRun;
|
|
22212
23098
|
if (record.status === "complete" && record.result) {
|
|
22213
23099
|
return {
|
|
22214
23100
|
status: "complete",
|
|
@@ -22269,6 +23155,7 @@ var init_import_status = __esm({
|
|
|
22269
23155
|
};
|
|
22270
23156
|
}
|
|
22271
23157
|
}
|
|
23158
|
+
importIds = [...new Set(importIds)];
|
|
22272
23159
|
if (importIds.length === 0) {
|
|
22273
23160
|
throw client.makeError("IMPORT_STATUS_INPUT_REQUIRED", "Pass either handle_id or importIds[]", "Call leadbay_import_leads with wait_for_completion=false first, then pass its handle_id.", "");
|
|
22274
23161
|
}
|
|
@@ -22284,14 +23171,40 @@ var init_import_status = __esm({
|
|
|
22284
23171
|
return Boolean(i.processing?.finished);
|
|
22285
23172
|
return Boolean(i.processing?.finished || i.pre_processing?.finished && !i.processing);
|
|
22286
23173
|
});
|
|
23174
|
+
let reconciled = null;
|
|
23175
|
+
const commitError = commitFailureFor(importIds);
|
|
23176
|
+
let notReady = false;
|
|
23177
|
+
const declaredTotal = imports.reduce((n, i) => n + Number(i.total_records ?? 0), 0);
|
|
23178
|
+
if (!failed && complete && handleDryRun !== true && importIds.length > 0) {
|
|
23179
|
+
try {
|
|
23180
|
+
reconciled = await fetchReconciledRecords(client, importIds, declaredTotal, ctx);
|
|
23181
|
+
} catch (err) {
|
|
23182
|
+
if (err instanceof ImportNotReady) {
|
|
23183
|
+
notReady = true;
|
|
23184
|
+
ctx?.logger?.info?.(`import-status: wizard reports in_progress; mappings not committed yet \u2014 reporting running`);
|
|
23185
|
+
} else {
|
|
23186
|
+
ctx?.logger?.warn?.(`import-status: records reconciliation failed (${err?.code ?? err?.message ?? "unknown"}); returning status only`);
|
|
23187
|
+
}
|
|
23188
|
+
}
|
|
23189
|
+
}
|
|
23190
|
+
const settled = complete && !notReady;
|
|
22287
23191
|
return {
|
|
22288
|
-
status: failed ? "failed" :
|
|
23192
|
+
status: failed || commitError ? "failed" : settled ? "complete" : "running",
|
|
22289
23193
|
...handleId ? { handle_id: handleId } : {},
|
|
22290
23194
|
importIds,
|
|
22291
|
-
|
|
23195
|
+
...handleDryRun === true ? { dry_run: true } : {},
|
|
23196
|
+
progress: notReady ? { ...progress, phase: "committing" } : progress,
|
|
23197
|
+
...reconciled ? {
|
|
23198
|
+
result: {
|
|
23199
|
+
leads: reconciled.leads,
|
|
23200
|
+
not_imported: reconciled.not_imported,
|
|
23201
|
+
importIds,
|
|
23202
|
+
...reconciled.still_settling > 0 ? { still_settling: reconciled.still_settling } : {}
|
|
23203
|
+
}
|
|
23204
|
+
} : {},
|
|
22292
23205
|
...failed ? {
|
|
22293
23206
|
error: failed.pre_processing?.error ?? failed.processing?.error ?? "import failed"
|
|
22294
|
-
} : {},
|
|
23207
|
+
} : commitError ? { error: `Import mappings were rejected: ${commitError}` } : {},
|
|
22295
23208
|
region: client.region,
|
|
22296
23209
|
_meta: client.lastMeta ?? {
|
|
22297
23210
|
region: client.region,
|
|
@@ -25564,9 +26477,9 @@ var ARTIFACT_KIT_VERSION, ARTIFACT_RUNTIME, ARTIFACT_USAGE_GUIDE;
|
|
|
25564
26477
|
var init_artifact_runtime_generated = __esm({
|
|
25565
26478
|
"../core/dist/artifact-runtime.generated.js"() {
|
|
25566
26479
|
"use strict";
|
|
25567
|
-
ARTIFACT_KIT_VERSION = "0.
|
|
25568
|
-
ARTIFACT_RUNTIME = '"use strict";(()=>{var _=Object.defineProperty;var k=(e,t,n)=>t in e?_(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n;var o=(e,t,n)=>k(e,typeof t!="symbol"?t+"":t,n);var T="0.3.1",d=class extends Error{constructor(n,i={}){super(n);o(this,"code");o(this,"raw");this.name="LbError",this.code=i.code,this.raw=i.raw}},v=null,f=3e4;function L(){let e=globalThis.cowork;return e&&typeof e.callMcpTool=="function"?(t,n)=>e.callMcpTool(t,n):null}function p(e){if(e&&typeof e=="object"&&"content"in e){let t=e.content;if(Array.isArray(t)&&t[0]&&typeof t[0].text=="string")return t[0].text}return null}function y(e){if(!e||typeof e!="object")return e;let t=e;if(t.isError)throw new d(p(e)??"tool call failed",{raw:e});if("structuredContent"in t&&t.structuredContent!=null)return t.structuredContent;let n=p(e);if(n!=null)try{return JSON.parse(n)}catch{return n}return e}function E(e){return e instanceof Error?e.message:String(e)}function m(e){let t=e instanceof d?e.code:void 0;return{message:E(e),unavailable:t==="unavailable",code:t}}function S(e={}){v=e.call??null,f=e.timeoutMs??3e4}async function w(e,t){if(!f||f<=0)return e;let n,i=new Promise((r,a)=>{n=setTimeout(()=>a(new d(`"${t}" timed out after ${f}ms`,{code:"timeout"})),f)});try{return await Promise.race([e,i])}finally{n&&clearTimeout(n)}}async function s(e,t={}){if(v)return y(await w(Promise.resolve(v(e,t)),e));let n=L();if(!n)throw new d("Leadbay bridge unavailable (window.cowork absent)",{code:"unavailable"});return y(await w(Promise.resolve(n(e,t)),e))}var c=class{constructor(){o(this,"subs",new Set)}subscribe(t){return this.subs.add(t),t(this),()=>this.subs.delete(t)}emit(){for(let t of this.subs)t(this)}};function A(e){return Array.isArray(e)?e.map(t=>t&&typeof t=="object"?t:{value:t,label:String(t)}):[]}var h=class extends c{constructor(n={}){super();o(this,"kind");o(this,"value");o(this,"options",[]);o(this,"loading",!1);o(this,"error",null);o(this,"ready",!1);o(this,"cfg");o(this,"depUnsubs",[]);o(this,"seq",0);this.cfg=n,this.kind=n.kind,this.value=n.value??"";for(let i of n.dependsOn??[]){let r=i.value;this.depUnsubs.push(i.subscribe(()=>{i.value!==r&&(r=i.value,this.cfg.load&&this.load())}))}n.load&&(n.autoLoad??!0)&&this.load()}async load(){if(!this.cfg.load)return;let n=++this.seq;this.loading=!0,this.error=null,this.emit();try{let i=await this.cfg.load();if(n!==this.seq)return;this.options=this.cfg.options?this.cfg.options(i):A(i),this.ready=!0;let r=this.value==null?"":String(this.value);this.options.length&&(r===""||!this.options.some(a=>String(a.value)===r))&&(this.value=this.options[0].value)}catch(i){if(n!==this.seq)return;this.options=[],this.error=m(i)}finally{n===this.seq&&(this.loading=!1,this.emit())}}setValue(n){this.value=n;let i=this.validate();this.error=i?{message:i,unavailable:!1}:null,this.emit()}validate(){return this.cfg.validate?this.cfg.validate(this.value):null}get valid(){return this.validate()==null}reset(){this.value=this.cfg.value??"",this.error=null,this.emit()}dispose(){for(let n of this.depUnsubs)n();this.depUnsubs=[]}},l=class extends c{constructor(n){super();o(this,"loading",!1);o(this,"error",null);o(this,"lastResult",null);o(this,"cfg");this.cfg=n}async run(){if(this.loading)return;for(let i of this.cfg.fields??[]){let r=i.validate();if(r!=null){this.error={message:r,unavailable:!1},this.emit();return}}if(this.cfg.confirm&&typeof globalThis.confirm=="function"&&!globalThis.confirm(this.cfg.confirm))return;this.loading=!0,this.error=null,this.emit();let n;try{let i=typeof this.cfg.args=="function"?this.cfg.args():this.cfg.args??{};n=await s(this.cfg.tool,i)}catch(i){this.error=m(i),this.loading=!1,this.emit(),this.cfg.onError?.(this.error);return}return this.lastResult=n,this.loading=!1,this.emit(),this.cfg.onSuccess?.(n),n}reset(){this.error=null,this.lastResult=null,this.emit()}},u=class extends c{constructor(n){super();o(this,"data",null);o(this,"loading",!1);o(this,"refreshing",!1);o(this,"error",null);o(this,"done",!1);o(this,"cfg");o(this,"timer",null);o(this,"seq",0);this.cfg=n,(n.autoLoad??!0)&&this.load()}async load(){this.clearTimer();let n=++this.seq;this.data==null?this.loading=!0:this.refreshing=!0,this.error=null,this.emit();try{let r=await this.cfg.load();if(n!==this.seq)return;this.data=r,this.done=this.cfg.until?this.cfg.until(r):!0,this.cfg.pollEvery&&!this.done&&(this.timer=setTimeout(()=>void this.load(),this.cfg.pollEvery))}catch(r){if(n!==this.seq)return;this.error=m(r)}finally{n===this.seq&&(this.loading=!1,this.refreshing=!1,this.emit())}}refresh(){return this.load()}stop(){this.clearTimer()}clearTimer(){this.timer&&(clearTimeout(this.timer),this.timer=null)}},b=class extends c{constructor(n){super();o(this,"items",[]);o(this,"page",0);o(this,"pageSize");o(this,"total",0);o(this,"loading",!1);o(this,"error",null);o(this,"cfg");o(this,"seq",0);this.cfg=n,this.pageSize=n.pageSize??20,(n.autoLoad??!0)&&this.loadPage(0)}async loadPage(n){let i=++this.seq;this.loading=!0,this.error=null,this.emit();try{let r=await this.cfg.load({page:n,pageSize:this.pageSize});if(i!==this.seq)return;this.items=r.items??[],this.total=r.total??this.items.length,this.page=n}catch(r){if(i!==this.seq)return;this.error=m(r)}finally{i===this.seq&&(this.loading=!1,this.emit())}}next(){return this.loadPage(this.page+1)}prev(){return this.loadPage(Math.max(0,this.page-1))}get hasMore(){return(this.page+1)*this.pageSize<this.total}};function x(e,t){let n=t.error?.unavailable?"unavailable":t.loading?"loading":t.error?"error":"ready";e.setAttribute("data-lb-state",n),t.error?e.setAttribute("data-lb-error",t.error.message):e.removeAttribute("data-lb-error")}function C(e,t){let n=()=>t.setValue(e.value);e.addEventListener("change",n);let i=t.subscribe(()=>{x(e,t),e.disabled=t.loading,e.innerHTML="";for(let r of t.options){let a=document.createElement("option");a.value=String(r.value),a.textContent=r.label,e.appendChild(a)}e.value=t.value==null?"":String(t.value)});return()=>{e.removeEventListener("change",n),i()}}function R(e,t){let n=e.type==="checkbox",i=e.tagName==="SELECT"?"change":"input",r=()=>t.setValue(n?e.checked:e.value);e.addEventListener(i,r);let a=t.subscribe(()=>{if(n)e.checked=!!t.value;else{let g=t.value==null?"":String(t.value);e.value!==g&&(e.value=g)}e.setAttribute("data-lb-state",t.error?"error":"ready"),t.error?e.setAttribute("data-lb-error",t.error.message):e.removeAttribute("data-lb-error")});return()=>{e.removeEventListener(i,r),a()}}function I(e,t){let n=r=>{r.preventDefault(),t.run()};e.addEventListener("click",n);let i=t.subscribe(()=>{let r=t.error?.unavailable?"unavailable":t.loading?"loading":t.error?"error":t.lastResult!=null?"success":"idle";e.setAttribute("data-lb-state",r),"disabled"in e&&(e.disabled=t.loading),t.error?e.setAttribute("data-lb-error",t.error.message):e.removeAttribute("data-lb-error")});return()=>{e.removeEventListener("click",n),i()}}var O=["STILL_CHASING","COULD_NOT_REACH_STILL_TRYING","INTEREST_VALIDATED_OR_MEETING_PLANED","NOT_INTERESTED_LOST"];function P(e){return new h({kind:"select",load:()=>s("leadbay_list_campaigns",{_triggered_by:e}),options:t=>(t?.campaigns??[]).map(i=>{let r=i?.campaign??i;return r?.id?{value:r.id,label:r.name??r.ai_generated_name??String(r.id)}:null}).filter(i=>i!=null)})}function M(e){return new l({tool:"leadbay_report_outreach",fields:e.note?[e.note]:[],args:()=>({lead_id:e.leadId,...e.status?{epilogue_status:e.status.value}:{},note:e.note?e.note.value:"",verification:{source:"user_confirmed",ref:e.ref??"logged from artifact"},_triggered_by:e.ask})})}function F(e){return new l({tool:"leadbay_add_note",fields:[e.note],args:()=>({leadId:e.leadId,note:e.note.value})})}function q(e){return new l({tool:"leadbay_like_lead",args:{lead_id:e}})}function H(e){return new l({tool:"leadbay_dislike_lead",args:{lead_id:e}})}function z(e,t){return new u({autoLoad:!1,load:()=>s("leadbay_account_history",{leadId:e,_triggered_by:t})})}function N(e,t){return new u({autoLoad:!1,load:()=>s("leadbay_research_lead_by_id",{leadId:e,_triggered_by:t})})}function U(e){let t=null;return new u({...e.autoLoad!==void 0?{autoLoad:e.autoLoad}:{},pollEvery:e.pollEvery??4e3,until:n=>!!n?.all_done,load:async()=>{if(!t){let n=await s("leadbay_enrich_titles",{...e.leadIds?{leadIds:e.leadIds}:{},titles:e.titles,...e.email!==void 0?{email:e.email}:{},...e.phone!==void 0?{phone:e.phone}:{},...e.confirm!==void 0?{confirm:e.confirm}:{},_triggered_by:e.ask});if(t=n?.bulk_id??null,!t)return{...n,all_done:!0,no_job:!0}}return s("leadbay_bulk_enrich_status",{bulk_id:t,_triggered_by:e.ask})}})}function D(e){let t=e.source??"followups";return new b({pageSize:e.pageSize??20,load:async({page:n,pageSize:i})=>{let a=t==="campaign"?await s("leadbay_campaign_call_sheet",{campaign_id:e.campaignId,page:n,count:i,_triggered_by:e.ask}):await s("leadbay_pull_followups",{page:n,count:i,...e.city?{city:e.city}:{},_triggered_by:e.ask}),g=a.leads??a.items??[];return{items:g,total:a.total_leads??a.pagination?.total??g.length}}})}function V(e){return new u({load:()=>s("leadbay_team_activity",{weeks:e.weeks??4,_triggered_by:e.ask})})}var j={VERSION:T,configure:S,call:s,field:e=>new h(e),action:e=>new l(e),resource:e=>new u(e),list:e=>new b(e),bindSelect:C,bindValue:R,bindAction:I,campaigns:P,outreach:M,note:F,like:q,dislike:H,leadHistory:z,leadProfile:N,enrichment:U,callList:D,teamActivity:V,EPILOGUE_STATUSES:O};typeof globalThis<"u"&&(globalThis.LeadbayArtifacts=j);})();';
|
|
25569
|
-
ARTIFACT_USAGE_GUIDE = '# Leadbay Artifact Kit \u2014 headless domain components\n\nYou are building a single-file HTML **artifact** the user runs inside cowork. This\nkit gives you **headless view-models** that own a control\'s whole data lifecycle \u2014\nload/populate from a Leadbay call, hold value/state, poll, validate, and\nencapsulate the API call + business rules. **You own 100% of markup/layout/style.**\nThe library renders nothing. Inline the runtime once as a `<script>`; it exposes\none global `window.LeadbayArtifacts` (call it `lb`). Vanilla, no React, no build.\n\nPass every tool you use as the artifact\'s `mcp_tools` so the host permits it.\n\n## Two layers\n\n**Primitives** (generic):\n- `lb.field({ load, options, value, validate, dependsOn })` \u2014 a value + optionally\n API-populated options. `.value/.setValue/.options/.loading/.error/.valid/.subscribe`.\n- `lb.action({ tool, args, fields, confirm, onSuccess, onError })` \u2014 a write/submit.\n `.run()/.loading/.error/.lastResult/.subscribe`.\n- `lb.resource({ load, pollEvery?, until?, autoLoad? })` \u2014 one read that may change:\n load-on-click or poll-until-`until`. `.data/.loading/.refreshing/.error/.done/.load()/.refresh()/.stop()/.subscribe`.\n- `lb.list({ load, pageSize })` \u2014 paginated rows. `.items/.page/.total/.loading/.loadPage(n)/.next()/.prev()/.hasMore/.subscribe`.\n\n`.error` is `{ message, unavailable } | null`. `subscribe(cb)` fires immediately\nthen on every change \u2014 render your own DOM from it.\n\n**Domain components** (pre-wired \u2014 bake in the tool name, arg shape, and footguns):\n\n| Call | Returns | For |\n|---|---|---|\n| `lb.campaigns(ask)` | field | a campaign `<select>`, options from `leadbay_list_campaigns` |\n| `lb.outreach({leadId, ask, status?, note?})` | action | log a call \u2192 `report_outreach` (verification + `_triggered_by` baked in) |\n| `lb.note({leadId, note})` | action | add a note \u2192 `add_note` |\n| `lb.like(leadId)` / `lb.dislike(leadId)` | action | taste signal |\n| `lb.leadHistory(leadId, ask)` | resource (lazy) | notes + activities + engagement \u2192 `account_history` |\n| `lb.leadProfile(leadId, ask)` | resource (lazy) | full lead profile \u2192 `research_lead_by_id` |\n| `lb.callList({source:\'followups\'\\|\'campaign\', campaignId?, city?, ask})` | list | a cold-call list (Monitor or a campaign) |\n| `lb.enrichment({leadIds, titles, ask, pollEvery?})` | resource (polling) | launch + watch contact enrichment |\n| `lb.teamActivity({weeks, ask})` | resource | manager leaderboard + activity trend \u2192 `leadbay_team_activity` |\n\n`lb.EPILOGUE_STATUSES` = the 4 disposition values\n(`STILL_CHASING`, `COULD_NOT_REACH_STILL_TRYING`, `INTEREST_VALIDATED_OR_MEETING_PLANED`, `NOT_INTERESTED_LOST`).\n\n**Binding sugar** (optional; binds a view-model to YOUR native element, no style):\n`lb.bindSelect(selectEl, field)` (populates options + value), `lb.bindValue(inputEl, field)`,\n`lb.bindAction(buttonEl, action)`. They set `data-lb-state`\n(`ready|loading|error|success|unavailable`) + `data-lb-error` on your element as\nstyling hooks. For lists/resources, use `.subscribe()` and render yourself.\n\n`ask` is the user\'s request this artifact serves \u2014 it becomes `_triggered_by`.\n\n## Recipe: cold-call sheet (one row per lead)\n\n```js\nconst lb = window.LeadbayArtifacts; lb.configure();\nconst ASK = "<the user\'s request>";\n\nconst list = lb.callList({ source: "campaign", campaignId: CID, ask: ASK });\nlist.subscribe((l) => renderRows(l.items, l.loading)); // your render\n\n// per lead row (call when you build a row):\nfunction wireRow(lead, els) {\n const status = lb.field({ value: "STILL_CHASING" }); // static-enum <select>\n const note = lb.field({ validate: (v) => (v && v.trim() ? null : "Add a note") });\n lb.bindValue(els.status, status);\n lb.bindValue(els.note, note);\n lb.bindAction(els.log, lb.outreach({ leadId: lead.id, ask: ASK, status, note }));\n lb.bindAction(els.like, lb.like(lead.id));\n\n const history = lb.leadHistory(lead.id, ASK); // lazy\n history.subscribe((h) => renderHistory(els.history, h));\n els.expand.onclick = () => history.load(); // load on click\n}\n```\n\n## Recipe: manager dashboard\n\n```js\nconst team = lb.teamActivity({ weeks: 4, ask: ASK });\nteam.subscribe((t) => {\n if (t.loading) showSpinner();\n if (t.data) {\n renderLeaderboard(t.data.reps); // sorted by total_activities; cols: name, notes, meetings_or_interest, lost\u2026\n renderTrendChart(t.data.trend); // [{date,count}] \u2192 Chart.js (allowed from CDN)\n }\n});\nrefreshBtn.onclick = () => team.refresh();\n```\n\n## Recipe: live enrichment\n\n```js\nconst job = lb.enrichment({ leadIds: [LEAD], titles: ["CEO", "VP Sales"], ask: ASK });\njob.subscribe((j) => {\n const p = j.data && j.data.overall_progress; // {done,total,done_ratio}\n renderBar(p);\n if (j.done) renderContacts(j.data.leads); // enriched contacts\n});\nrefreshBtn.onclick = () => job.refresh();\n```\n\n## Write-call rules\n\nThe domain factories handle these for you. If you hand-roll an action:\n`leadbay_report_outreach` args MUST include `verification:{source:"user_confirmed", ref}`\nAND `_triggered_by`; `leadbay_add_leads_to_campaign` needs `_triggered_by`;\n`add_note`/`like_lead`/`dislike_lead` take only their own args. `epilogue_status` is\none of `lb.EPILOGUE_STATUSES`. Snoozing (pushback) and org WON/LOST status are\nadvanced-gated \u2014 not callable from a default artifact; use the epilogue values.\n\n## Degradation + live updates\n\nIf the host bridge is absent, a view-model\'s `.error` is set with `.error.unavailable\n=== true` (bind helpers set `data-lb-state="unavailable"`) \u2014 nothing throws. Every\ncall also has a **30s timeout** (configurable via `lb.configure({ timeoutMs })`): a\nhost call that never settles becomes `.error` with `code:"timeout"`, so a control is\nnever stuck loading forever \u2014 always render the `.error` branch so the user can retry.\nAuto-poll (`pollEvery`) depends on the cowork host serving FRESH reads; `.refresh()`\nis the guaranteed manual path \u2014 always wire a Refresh control for polling resources.';
|
|
26480
|
+
ARTIFACT_KIT_VERSION = "0.5.0";
|
|
26481
|
+
ARTIFACT_RUNTIME = '"use strict";(()=>{var L=Object.defineProperty;var A=(e,r,t)=>r in e?L(e,r,{enumerable:!0,configurable:!0,writable:!0,value:t}):e[r]=t;var a=(e,r,t)=>A(e,typeof r!="symbol"?r+"":r,t);var S=`\n:root{\n--color-black:#191919;--color-white:#fff;\n--color-gray-1:#f9f9f9;--color-gray-2:#f0f0f0;--color-gray-3:#e0e0e0;--color-gray-4:#cecece;\n--color-gray-5:#c4c4c4;--color-gray-6:#8d8d8d;--color-gray-7:#787878;--color-gray-8:#646464;\n--color-gray-9:#202020;\n--color-linkedin:#0a66c2;\n--color-blue-background:oklch(0.947 0.029 251);--color-blue-foreground:oklch(0.564 0.181 251);\n--color-green-background:oklch(0.947 0.029 141);--color-green-foreground:oklch(0.564 0.181 141);\n--color-red-background:oklch(0.947 0.029 26);--color-red-foreground:oklch(0.564 0.191 26);\n--color-gold-background:oklch(0.972 0.049 91);--color-gold-foreground:oklch(0.667 0.177 91);\n--color-cherry-background:oklch(0.947 0.029 15);--color-cherry-foreground:oklch(0.44 0.146 15);\n--color-red-like:var(--color-cherry-foreground);\n--lb-font:"Nikkei Maru",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif;\n--lb-radius:1rem;--lb-radius-sm:0.625rem;--lb-gap:0.75rem;\n--lb-surface:var(--color-gray-1);--lb-border:var(--color-gray-3);\n--lb-fg:var(--color-black);--lb-muted:var(--color-gray-8);--lb-field:var(--color-white);\n}\n:root[data-theme=dark],:root[data-lb-theme=dark]{\n--lb-surface:var(--color-gray-9);--lb-border:var(--color-gray-8);\n--lb-fg:var(--color-white);--lb-muted:var(--color-gray-3);--lb-field:var(--color-gray-9);\n}\n@media(prefers-color-scheme:dark){:root:not([data-theme=light]):not([data-lb-theme=light]){\n--lb-surface:var(--color-gray-9);--lb-border:var(--color-gray-8);\n--lb-fg:var(--color-white);--lb-muted:var(--color-gray-3);--lb-field:var(--color-gray-9);\n}}\n.lb-card{display:grid;gap:var(--lb-gap);padding:0.875rem;\nbackground-color:var(--lb-surface);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius);corner-shape:squircle;color:var(--lb-fg);\nfont-family:var(--lb-font);\nbox-shadow:0 1rem 2.5rem color-mix(in srgb,var(--color-gray-9) 6%,transparent),\n0 0.125rem 0.5rem color-mix(in srgb,var(--color-gray-9) 4%,transparent)}\n.lb-card-head{display:flex;justify-content:space-between;align-items:baseline;gap:var(--lb-gap)}\n.lb-title{font-size:0.875rem;font-weight:600;line-height:1.25rem;color:var(--lb-fg)}\n.lb-sub{font-size:0.8125rem;line-height:1.125rem;color:var(--lb-muted)}\n.lb-row{display:flex;align-items:center;gap:var(--lb-gap);flex-wrap:wrap}\n.lb-stack{display:grid;gap:var(--lb-gap)}\n.lb-select,.lb-input{font:inherit;font-family:var(--lb-font);font-size:0.8125rem;color:var(--lb-fg);\nbackground-color:var(--lb-field);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;padding:0.4rem 0.55rem;min-height:2.125rem}\n.lb-btn{font:inherit;font-family:var(--lb-font);font-size:0.8125rem;font-weight:600;\ncolor:var(--lb-fg);background-color:var(--lb-field);border:1px solid var(--lb-border);\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;padding:0.4rem 0.85rem;min-height:2.125rem;\ncursor:pointer;transition:background-color .15s,border-color .15s,color .15s}\n.lb-btn:hover:not([disabled]){border-color:var(--color-gray-6)}\n.lb-btn:focus-visible,.lb-select:focus-visible,.lb-input:focus-visible{\noutline:2px solid var(--color-blue-foreground);outline-offset:1px}\n.lb-btn[data-lb-state=loading]{opacity:.55;cursor:progress}\n.lb-btn[data-lb-state=success]{background-color:var(--color-green-background);\nborder-color:var(--color-green-foreground);color:var(--color-green-foreground)}\n.lb-btn[data-lb-state=error],.lb-select[data-lb-state=error]{\nbackground-color:var(--color-red-background);border-color:var(--color-red-foreground);\ncolor:var(--color-red-foreground)}\n.lb-btn[data-lb-state=unavailable],.lb-btn[disabled]{opacity:.5;cursor:not-allowed}\n.lb-msg{font-size:0.8125rem;line-height:1.125rem;color:var(--lb-muted)}\n.lb-msg[data-tone=error]{color:var(--color-red-foreground)}\n.lb-msg[data-tone=ok]{color:var(--color-green-foreground)}\n.lb-chip{display:inline-flex;align-items:center;gap:.25rem;white-space:nowrap;\nfont-size:0.75rem;font-weight:600;line-height:1rem;padding:0.125rem 0.5rem;\nborder-radius:var(--lb-radius-sm);corner-shape:squircle;\nbackground-color:var(--color-gray-2);color:var(--lb-muted)}\n.lb-chip[data-status=WANTED]{background-color:var(--color-blue-background);color:var(--color-blue-foreground)}\n.lb-chip[data-status=WON]{background-color:var(--color-green-background);color:var(--color-green-foreground)}\n.lb-chip[data-status=LOST]{background-color:var(--color-red-background);color:var(--color-red-foreground)}\n.lb-chip[data-status=UNWANTED]{background-color:var(--color-gray-2);color:var(--color-gray-7)}\n.lb-chip[data-taste=liked]{background-color:var(--color-cherry-background);color:var(--color-red-like)}\n.lb-chip[data-taste=disliked]{background-color:var(--color-gray-2);color:var(--color-gray-7)}\n.lb-chips{display:flex;align-items:center;gap:.35rem;flex-wrap:wrap}\n.lb-chip[hidden]{display:none}\n.lb-table{width:100%;border-collapse:collapse;font-family:var(--lb-font);color:var(--lb-fg)}\n.lb-table th,.lb-table td{text-align:left;padding:0.5rem 0.4rem;\nborder-bottom:1px solid var(--lb-border);vertical-align:middle;font-size:0.8125rem}\n.lb-table th{font-size:0.75rem;font-weight:600;color:var(--lb-muted);\ntext-transform:uppercase;letter-spacing:.04em}\n.lb-link{color:var(--color-blue-foreground);text-decoration:none}\n.lb-link:hover{text-decoration:underline}\n/* Quiet text link out of the artifact. Button-height so it shares the row\'s\n baseline; understated so it never competes with the actions beside it. The\n arrow is a bare diagonal stroke \\u2014 an escape-hatch marker, not an icon that\n asks to be read. */\n.lb-link-out{display:inline-flex;align-items:center;gap:.3rem;\nfont-size:0.75rem;line-height:1rem;min-height:2.125rem;\ncolor:var(--lb-fg);text-decoration:none;opacity:.65;transition:opacity .15s}\n.lb-link-out:hover{opacity:1;text-decoration:underline}\n.lb-link-out:focus-visible{outline:2px solid var(--color-blue-foreground);outline-offset:1px;\nborder-radius:var(--lb-radius-sm)}\n.lb-link-out svg{width:.85em;height:.85em;flex-shrink:0}\n/* Pushes whatever follows it to the right edge of an .lb-row, so a trailing\n link sits on the SAME baseline as the row\'s buttons instead of on its own\n line below them. */\n.lb-spacer{flex:1 1 auto}\n.lb-spinner{display:inline-block;width:.7em;height:.7em;border:2px solid var(--lb-border);\nborder-top-color:var(--color-blue-foreground);border-radius:50%;animation:lb-spin .8s linear infinite}\n@keyframes lb-spin{to{transform:rotate(1turn)}}\n@media(prefers-reduced-motion:reduce){.lb-spinner{animation:none}\n.lb-btn{transition-property:none}}\n`,p="lb-styles";var x="0.5.0",c=class extends Error{constructor(t,o={}){super(t);a(this,"code");a(this,"raw");this.name="LbError",this.code=o.code,this.raw=o.raw}},y=null,f=3e4;function C(){let e=globalThis.cowork;return e&&typeof e.callMcpTool=="function"?(r,t)=>e.callMcpTool(r,t):null}function _(e){if(e&&typeof e=="object"&&"content"in e){let r=e.content;if(Array.isArray(r)&&r[0]&&typeof r[0].text=="string")return r[0].text}return null}function E(e){if(!e||typeof e!="object")return e;let r=e;if(r.isError)throw new c(_(e)??"tool call failed",{raw:e});if("structuredContent"in r&&r.structuredContent!=null)return r.structuredContent;let t=_(e);if(t!=null)try{return JSON.parse(t)}catch{return t}return e}function I(e){return e instanceof Error?e.message:String(e)}function v(e){let r=e instanceof c?e.code:void 0;return{message:I(e),unavailable:r==="unavailable",code:r}}function O(e={}){y=e.call??null,f=e.timeoutMs??3e4}function R(){if(typeof document>"u"||!document.head)return null;let e=document.getElementById(p);if(e)return e;let r=document.createElement("style");return r.id=p,r.textContent=S,document.head.appendChild(r),r}async function T(e,r){if(!f||f<=0)return e;let t,o=new Promise((n,i)=>{t=setTimeout(()=>i(new c(`"${r}" timed out after ${f}ms`,{code:"timeout"})),f)});try{return await Promise.race([e,o])}finally{t&&clearTimeout(t)}}async function l(e,r={}){if(y)return E(await T(Promise.resolve(y(e,r)),e));let t=C();if(!t)throw new c("Leadbay bridge unavailable (window.cowork absent)",{code:"unavailable"});return E(await T(Promise.resolve(t(e,r)),e))}var g=class{constructor(){a(this,"subs",new Set)}subscribe(r){return this.subs.add(r),r(this),()=>this.subs.delete(r)}emit(){for(let r of this.subs)r(this)}};function N(e){return Array.isArray(e)?e.map(r=>r&&typeof r=="object"?r:{value:r,label:String(r)}):[]}var b=class extends g{constructor(t={}){super();a(this,"kind");a(this,"value");a(this,"options",[]);a(this,"loading",!1);a(this,"error",null);a(this,"ready",!1);a(this,"cfg");a(this,"depUnsubs",[]);a(this,"seq",0);this.cfg=t,this.kind=t.kind,this.value=t.value??"";for(let o of t.dependsOn??[]){let n=o.value;this.depUnsubs.push(o.subscribe(()=>{o.value!==n&&(n=o.value,this.cfg.load&&this.load())}))}t.load&&(t.autoLoad??!0)&&this.load()}async load(){if(!this.cfg.load)return;let t=++this.seq;this.loading=!0,this.error=null,this.emit();try{let o=await this.cfg.load();if(t!==this.seq)return;this.options=this.cfg.options?this.cfg.options(o):N(o),this.ready=!0;let n=this.value==null?"":String(this.value);this.options.length&&(n===""||!this.options.some(i=>String(i.value)===n))&&(this.value=this.options[0].value)}catch(o){if(t!==this.seq)return;this.options=[],this.error=v(o)}finally{t===this.seq&&(this.loading=!1,this.emit())}}setValue(t){this.value=t;let o=this.validate();this.error=o?{message:o,unavailable:!1}:null,this.emit()}validate(){return this.cfg.validate?this.cfg.validate(this.value):null}get valid(){return this.validate()==null}reset(){this.value=this.cfg.value??"",this.error=null,this.emit()}dispose(){for(let t of this.depUnsubs)t();this.depUnsubs=[]}};function P(e){if(!e||typeof e!="object")return null;let r=e;if(r.error!==!0)return null;let t=typeof r.message=="string"&&r.message?r.message:"tool call failed",o=typeof r.hint=="string"&&r.hint?` \\u2014 ${r.hint}`:"";return`${t}${o}`}var s=class extends g{constructor(t){super();a(this,"loading",!1);a(this,"error",null);a(this,"lastResult",null);a(this,"cfg");this.cfg=t}async run(){if(this.loading)return;for(let n of this.cfg.fields??[]){let i=n.validate();if(i!=null){this.error={message:i,unavailable:!1},this.emit();return}}if(this.cfg.confirm&&typeof globalThis.confirm=="function"&&!globalThis.confirm(this.cfg.confirm))return;this.loading=!0,this.error=null,this.emit();let t;try{let n=typeof this.cfg.args=="function"?this.cfg.args():this.cfg.args??{};t=await l(this.cfg.tool,n)}catch(n){this.error=v(n),this.loading=!1,this.emit(),this.cfg.onError?.(this.error);return}let o=P(t)??this.cfg.checkResult?.(t)??null;if(o!=null){this.error={message:o,unavailable:!1},this.loading=!1,this.emit(),this.cfg.onError?.(this.error);return}return this.lastResult=t,this.loading=!1,this.emit(),this.cfg.onSuccess?.(t),t}reset(){this.error=null,this.lastResult=null,this.emit()}},d=class extends g{constructor(t){super();a(this,"data",null);a(this,"loading",!1);a(this,"refreshing",!1);a(this,"error",null);a(this,"done",!1);a(this,"cfg");a(this,"timer",null);a(this,"seq",0);this.cfg=t,(t.autoLoad??!0)&&this.load()}async load(){this.clearTimer();let t=++this.seq;this.data==null?this.loading=!0:this.refreshing=!0,this.error=null,this.emit();try{let n=await this.cfg.load();if(t!==this.seq)return;this.data=n,this.done=this.cfg.until?this.cfg.until(n):!0,this.cfg.pollEvery&&!this.done&&(this.timer=setTimeout(()=>void this.load(),this.cfg.pollEvery))}catch(n){if(t!==this.seq)return;this.error=v(n)}finally{t===this.seq&&(this.loading=!1,this.refreshing=!1,this.emit())}}refresh(){return this.load()}stop(){this.clearTimer()}clearTimer(){this.timer&&(clearTimeout(this.timer),this.timer=null)}},h=class extends g{constructor(t){super();a(this,"items",[]);a(this,"page",0);a(this,"pageSize");a(this,"total",0);a(this,"loading",!1);a(this,"error",null);a(this,"cfg");a(this,"seq",0);this.cfg=t,this.pageSize=t.pageSize??20,(t.autoLoad??!0)&&this.loadPage(0)}async loadPage(t){let o=++this.seq;this.loading=!0,this.error=null,this.emit();try{let n=await this.cfg.load({page:t,pageSize:this.pageSize});if(o!==this.seq)return;this.items=n.items??[],this.total=n.total??this.items.length,this.page=t}catch(n){if(o!==this.seq)return;this.error=v(n)}finally{o===this.seq&&(this.loading=!1,this.emit())}}next(){return this.loadPage(this.page+1)}prev(){return this.loadPage(Math.max(0,this.page-1))}get hasMore(){return(this.page+1)*this.pageSize<this.total}};function M(e,r){let t=r.error?.unavailable?"unavailable":r.loading?"loading":r.error?"error":"ready";e.setAttribute("data-lb-state",t),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")}function D(e,r){let t=()=>r.setValue(e.value);e.addEventListener("change",t);let o=r.subscribe(()=>{M(e,r),e.disabled=r.loading,e.innerHTML="";for(let n of r.options){let i=document.createElement("option");i.value=String(n.value),i.textContent=n.label,e.appendChild(i)}e.value=r.value==null?"":String(r.value)});return()=>{e.removeEventListener("change",t),o()}}function z(e,r){let t=e.type==="checkbox",o=e.tagName==="SELECT"?"change":"input",n=()=>r.setValue(t?e.checked:e.value);e.addEventListener(o,n);let i=r.subscribe(()=>{if(t)e.checked=!!r.value;else{let u=r.value==null?"":String(r.value);e.value!==u&&(e.value=u)}e.setAttribute("data-lb-state",r.error?"error":"ready"),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")});return()=>{e.removeEventListener(o,n),i()}}function F(e,r){let t=n=>{n.preventDefault(),r.run()};e.addEventListener("click",t);let o=r.subscribe(()=>{let n=r.error?.unavailable?"unavailable":r.loading?"loading":r.error?"error":r.lastResult!=null?"success":"idle";e.setAttribute("data-lb-state",n),"disabled"in e&&(e.disabled=r.loading),r.error?e.setAttribute("data-lb-error",r.error.message):e.removeAttribute("data-lb-error")});return()=>{e.removeEventListener("click",t),o()}}var q=["STILL_CHASING","COULD_NOT_REACH_STILL_TRYING","INTEREST_VALIDATED_OR_MEETING_PLANED","NOT_INTERESTED_LOST"],k=[{value:"",label:"Default ranking"},{value:"SCORE:DESC",label:"Score \\u2193"},{value:"SCORE:ASC",label:"Score \\u2191"},{value:"NAME:ASC",label:"Name A\\u2192Z"},{value:"NAME:DESC",label:"Name Z\\u2192A"},{value:"SIZE:DESC",label:"Size \\u2193"},{value:"SIZE:ASC",label:"Size \\u2191"},{value:"SECTOR:ASC",label:"Sector A\\u2192Z"},{value:"STATUS:ASC",label:"Status A\\u2192Z"},{value:"CONTACT_COUNT:DESC",label:"Contacts \\u2193"},{value:"LAST_PROSPECTING_ACTION_AT:DESC",label:"Last action \\u2193"},{value:"LAST_PROSPECTING_ACTION_AT:ASC",label:"Last action \\u2191"},{value:"EPILOGUE_STATUS_SET_AT:DESC",label:"Outcome set \\u2193"},{value:"LIKED:DESC",label:"Liked first"},{value:"DISLIKED:DESC",label:"Disliked first"}];function U(e){let r=String(e??"").trim().toUpperCase(),t=k.some(o=>o.value===r);return new b({kind:"select",value:t?r:"",load:async()=>k.slice()})}var m=[{value:"WANTED",label:"Wanted"},{value:"WON",label:"Won"},{value:"LOST",label:"Lost"},{value:"UNWANTED",label:"Unwanted"}],H={value:"",label:"\\u2014 Not set \\u2014"};function W(e){let r=String(e??"").trim().toUpperCase(),t=m.some(o=>o.value===r);return new b({kind:"select",value:t?r:"",validate:o=>String(o??"")===""?"Pick a status":null,load:async()=>t?m.slice():[H,...m]})}function $(e){let r=()=>{let t=typeof e.leadIds=="function"?e.leadIds():e.leadIds;return Array.isArray(t)?t:e.leadId?[e.leadId]:[]};return new s({tool:"leadbay_set_lead_status",fields:e.date?[e.status,e.date]:[e.status],confirm:e.confirm,args:()=>({lead_ids:r(),status:e.status.value,...e.date&&e.date.value?{status_date:e.date.value}:{},...e.ask?{_triggered_by:e.ask}:{}}),checkResult:t=>{let o=t?.failed;if(!Array.isArray(o)||o.length===0)return null;let n=r().length,i=o[0]?.message??"write rejected";return o.length===n?`Status not applied: ${i}`:`${o.length} of ${n} leads failed: ${i}`}})}function j(e){return new b({kind:"select",load:()=>l("leadbay_list_campaigns",{_triggered_by:e}),options:r=>(r?.campaigns??[]).map(o=>{let n=o?.campaign??o;return n?.id?{value:n.id,label:n.name??n.ai_generated_name??String(n.id)}:null}).filter(o=>o!=null)})}function V(e){return new s({tool:"leadbay_report_outreach",fields:e.note?[e.note]:[],args:()=>({lead_id:e.leadId,...e.status?{epilogue_status:e.status.value}:{},note:e.note?e.note.value:"",verification:{source:"user_confirmed",ref:e.ref??"logged from artifact"},_triggered_by:e.ask})})}function G(e){return new s({tool:"leadbay_add_note",fields:[e.note],args:()=>({leadId:e.leadId,note:e.note.value})})}function Z(e){return new s({tool:"leadbay_like_lead",args:{lead_id:e}})}function Y(e){return new s({tool:"leadbay_dislike_lead",args:{lead_id:e}})}function B(e,r){return new d({autoLoad:!1,load:()=>l("leadbay_account_history",{leadId:e,_triggered_by:r})})}function K(e,r){return new d({autoLoad:!1,load:()=>l("leadbay_research_lead_by_id",{leadId:e,_triggered_by:r})})}function J(e){let r=null;return new d({...e.autoLoad!==void 0?{autoLoad:e.autoLoad}:{},pollEvery:e.pollEvery??4e3,until:t=>!!t?.all_done,load:async()=>{if(!r){let t=await l("leadbay_enrich_titles",{...e.leadIds?{leadIds:e.leadIds}:{},titles:e.titles,...e.email!==void 0?{email:e.email}:{},...e.phone!==void 0?{phone:e.phone}:{},...e.confirm!==void 0?{confirm:e.confirm}:{},_triggered_by:e.ask});if(r=t?.bulk_id??null,!r)return{...t,all_done:!0,no_job:!0}}return l("leadbay_bulk_enrich_status",{bulk_id:r,_triggered_by:e.ask})}})}function Q(e){let r=()=>typeof e.order=="string"?e.order:String(e.order?.value??"");return new h({pageSize:e.pageSize??20,load:async({page:t,pageSize:o})=>{let n=await l("leadbay_pull_leads",{page:t,count:o,...e.lensId?{lensId:e.lensId}:{},...r()?{order:r()}:{},_triggered_by:e.ask}),i=n.leads??[];return{items:i,total:n.pagination?.total??i.length}}})}function X(e){let r=e.source??"followups",t=()=>typeof e.order=="string"?e.order:String(e.order?.value??"");return new h({pageSize:e.pageSize??20,load:async({page:o,pageSize:n})=>{let u=r==="campaign"?await l("leadbay_campaign_call_sheet",{campaign_id:e.campaignId,page:o,count:n,_triggered_by:e.ask}):await l("leadbay_pull_followups",{page:o,count:n,...e.city?{city:e.city}:{},...t()?{order:t()}:{},_triggered_by:e.ask}),w=u.leads??u.items??[];return{items:w,total:u.total_leads??u.pagination?.total??w.length}}})}function ee(e){return new d({load:()=>l("leadbay_team_activity",{weeks:e.weeks??4,_triggered_by:e.ask})})}var re={VERSION:x,configure:O,styles:R,call:l,field:e=>new b(e),action:e=>new s(e),resource:e=>new d(e),list:e=>new h(e),bindSelect:D,bindValue:z,bindAction:F,campaigns:j,outreach:V,note:G,like:Z,dislike:Y,leadStatus:W,setStatus:$,sortOrder:U,leadHistory:B,leadProfile:K,enrichment:J,callList:X,leadList:Q,teamActivity:ee,EPILOGUE_STATUSES:q,LEAD_STATUSES:m,SORT_ORDERS:k};typeof globalThis<"u"&&(globalThis.LeadbayArtifacts=re);})();';
|
|
26482
|
+
ARTIFACT_USAGE_GUIDE = '# Leadbay Artifact Kit \u2014 headless domain components\n\nYou are building a single-file HTML **artifact** the user runs inside cowork. This\nkit gives you **headless view-models** that own a control\'s whole data lifecycle \u2014\nload/populate from a Leadbay call, hold value/state, poll, validate, and\nencapsulate the API call + business rules. **You own 100% of markup/layout/style.**\nThe library renders nothing. Inline the runtime once as a `<script>`; it exposes\none global `window.LeadbayArtifacts` (call it `lb`). Vanilla, no React, no build.\n\nPass every tool you use as the artifact\'s `mcp_tools` so the host permits it.\n\n## Two layers\n\n**Primitives** (generic):\n- `lb.field({ load, options, value, validate, dependsOn })` \u2014 a value + optionally\n API-populated options. `.value/.setValue/.options/.loading/.error/.valid/.subscribe`.\n- `lb.action({ tool, args, fields, confirm, onSuccess, onError })` \u2014 a write/submit.\n `.run()/.loading/.error/.lastResult/.subscribe`.\n- `lb.resource({ load, pollEvery?, until?, autoLoad? })` \u2014 one read that may change:\n load-on-click or poll-until-`until`. `.data/.loading/.refreshing/.error/.done/.load()/.refresh()/.stop()/.subscribe`.\n- `lb.list({ load, pageSize })` \u2014 paginated rows. `.items/.page/.total/.loading/.loadPage(n)/.next()/.prev()/.hasMore/.subscribe`.\n\n`.error` is `{ message, unavailable } | null`. `subscribe(cb)` fires immediately\nthen on every change \u2014 render your own DOM from it.\n\n**Domain components** (pre-wired \u2014 bake in the tool name, arg shape, and footguns):\n\n| Call | Returns | For |\n|---|---|---|\n| `lb.campaigns(ask)` | field | a campaign `<select>`, options from `leadbay_list_campaigns` |\n| `lb.outreach({leadId, ask, status?, note?})` | action | log a call \u2192 `report_outreach` (verification + `_triggered_by` baked in) |\n| `lb.note({leadId, note})` | action | add a note \u2192 `add_note` |\n| `lb.like(leadId)` / `lb.dislike(leadId)` | action | taste signal |\n| `lb.leadStatus(current?)` | field | a status `<select>` (Wanted/Won/Lost/Unwanted) |\n| `lb.setStatus({leadId or leadIds, status, date?, ask})` | action | write the org CRM status \u2192 `set_lead_status` |\n| `lb.leadHistory(leadId, ask)` | resource (lazy) | notes + activities + engagement \u2192 `account_history` |\n| `lb.leadProfile(leadId, ask)` | resource (lazy) | full lead profile \u2192 `research_lead_by_id` |\n| `lb.sortOrder(current?)` | field | a sort `<select>` mirroring the app\'s TableSort |\n| `lb.leadList({lensId?, order?, ask})` | list | a sortable Discover batch \u2192 `pull_leads` |\n| `lb.callList({source:\'followups\'\\|\'campaign\', campaignId?, city?, ask})` | list | a cold-call list (Monitor or a campaign) |\n| `lb.enrichment({leadIds, titles, ask, pollEvery?})` | resource (polling) | launch + watch contact enrichment |\n| `lb.teamActivity({weeks, ask})` | resource | manager leaderboard + activity trend \u2192 `leadbay_team_activity` |\n\n`lb.EPILOGUE_STATUSES` = the 4 disposition values\n(`STILL_CHASING`, `COULD_NOT_REACH_STILL_TRYING`, `INTEREST_VALIDATED_OR_MEETING_PLANED`, `NOT_INTERESTED_LOST`).\n`lb.LEAD_STATUSES` = the 4 org CRM statuses as `{value,label}` (`WANTED`, `WON`, `LOST`, `UNWANTED`).\n`lb.SORT_ORDERS` = the sort options as `{value,label}`; values are the backend `FIELD:ASC|DESC` enum.\n\n**Sorting is a SERVER concern.** `lb.leadList` and `lb.callList` take an `order`\n(a `lb.sortOrder()` field or a literal) and send it upstream; the backend sorts\nthe whole lens / Monitor and returns the requested page of that. Never re-sort\nrows in the browser \u2014 you would be reordering one page of a larger set, showing\nleads that do not belong at that position. The empty value means "no order\nparam", i.e. the tab\'s own ranking, which is the right default. Changing the\nsort should reset to page 0. Campaign call sheets cannot sort:\n`leadbay_campaign_call_sheet` has no `order` param, and `lb.callList` drops it\nfor that source rather than sending something the tool would reject.\n\n**Two different systems.** Epilogue = how one outreach attempt went (drives\nfollow-up ranking). Lead status = the commercial outcome, org-wide \u2014 the same\nfield the website\'s status selector writes. A won deal is a LEAD STATUS;\n"she didn\'t pick up" is an EPILOGUE. Setting one never sets the other, so when\nthe user reports both in one breath, fire both actions.\n\n**Binding sugar** (optional; binds a view-model to YOUR native element, no style):\n`lb.bindSelect(selectEl, field)` (populates options + value), `lb.bindValue(inputEl, field)`,\n`lb.bindAction(buttonEl, action)`. They set `data-lb-state`\n(`ready|loading|error|success|unavailable`) + `data-lb-error` on your element as\nstyling hooks. For lists/resources, use `.subscribe()` and render yourself.\n\n`ask` is the user\'s request this artifact serves \u2014 it becomes `_triggered_by`.\n\n## The skin (optional) \u2014 `lb.styles()`\n\nCall it once and you get a small `lb-*` stylesheet, so every artifact you build\nshares one visual language instead of re-inventing padding and colours. It is\n**opt-in**: skip it and you get exactly the unstyled HTML you wrote. It injects\nno markup and never touches your `class` attributes.\n\n```js\nlb.styles(); // idempotent \u2014 safe to call per row\n```\n\n| Class | For |\n|---|---|\n| `lb-card` / `lb-card-head` / `lb-title` / `lb-sub` | a lead card + its header |\n| `lb-row` / `lb-stack` / `lb-spacer` | control row / vertical spacing / flex filler that right-aligns what follows |\n| `lb-link-out` | quiet external link (icon inherits currentColor) \u2014 "Open in Leadbay" |\n| `lb-select` / `lb-input` / `lb-btn` | form controls (state-aware, see below) |\n| `lb-msg` (`data-tone="error\\|ok"`) | inline feedback |\n| `lb-chip` (`data-status="WON\\|LOST"`) | a status pill |\n| `lb-table` | leads table |\n| `lb-spinner` | inline busy indicator |\n\nControls react to the `data-lb-state` the bind helpers already set \u2014 a bound\n`lb-btn` dims while loading, goes green on success, red on error, all with no\nextra CSS from you.\n\nThe palette is the **product design system**, ported from\n`frontend/packages/style/color.css` \u2014 same `--color-gray-1\u20269` ramp, same\nsemantic `--color-{green,red,blue,gold}-{background,foreground}` pairs, same\n`1rem` / `0.625rem` radii and `corner-shape: squircle` as the app\'s components.\nAn artifact therefore looks like Leadbay, not like a generic page.\n\nUse the tokens rather than hardcoded colours \u2014 the same rule the style package\nenforces. Re-theme by overriding them; don\'t fight specificity:\n\n```css\n:root { --lb-surface: var(--color-gray-2); --lb-radius: 0.5rem; }\n```\n\nDark mode works two ways: `data-theme="dark"` on `<html>` (the frontend\'s own\nhook) **and** `prefers-color-scheme`, because an artifact renders inside a host\nwhose theme attribute it cannot set. Never hardcode a light background over the\nskin.\n\nThe product face is `Nikkei Maru`; the stack names it first and falls back to\nthe system UI font. Do **not** add an `@font-face` \u2014 artifacts are inline-only\nand a remote font URL will silently fail.\n\n## What every lead card MUST carry\n\nA card is the artifact form of the `pull_leads` table, and it inherits that\ntable\'s rules. A card with a name and a button is not enough: the rep cannot\ntell *why* this lead is on screen. Four lines, in this order.\n\n```html\n<div class="lb-card">\n <div class="lb-card-head">\n <span class="lb-title"></span> <!-- 1. company -->\n <span class="lb-chips"> <!-- 2. state -->\n <span class="lb-chip" data-taste hidden></span>\n <span class="lb-chip" data-status hidden></span>\n </span>\n </div>\n <div class="lb-sub"></div> <!-- 3. firmographics -->\n <div class="lb-sub" data-why></div> <!-- 4. why it fits -->\n <div class="lb-row"><!-- actions --></div>\n</div>\n```\n\n1. **Company** \u2014 `name`, linked to `website` (prefix `https://` on a bare host).\n Never render the numeric `score`; use the `\u25B0\u2756\u25B1` bar if you want the signal.\n\n Also give every card an **Open in Leadbay** link to the lead\'s panel in the\n product. Put it at the **right-hand end of the card\'s last action row** \u2014\n same row as the buttons, pushed right by an `lb-spacer`, not on a line of\n its own. Style it `lb-link-out`: quiet text plus a plain arrow-up-right,\n never a filled button. It is an escape hatch, not a call to action.\n\n ```html\n <div class="lb-row">\n <button class="lb-btn">Like</button>\n <button class="lb-btn">Set status</button>\n <span class="lb-spacer"></span> <!-- pushes the link right -->\n <a class="lb-link-out" data-k="open" target="_blank" rel="noopener">\n Open in Leadbay\n <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"\n stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">\n <line x1="7" y1="17" x2="17" y2="7"/><polyline points="7 7 17 7 17 17"/>\n </svg>\n </a>\n </div>\n ```\n\n Keep the arrow a bare diagonal stroke \u2014 the text already says where the link\n goes, so the glyph only has to mark "leaves this page". Mark the `<svg>`\n `aria-hidden="true"`: it is decorative, and the link text is the accessible\n name.\n **Pick the view the lead actually lives in** \u2014 the URL is\n `/app/<view>?lead=<uuid>`, and the three views are `discover`, `monitor`,\n `campaign`. Landing a Monitor lead on Discover drops the rep into a list\n that does not contain it:\n\n ```js\n function leadUrl(lead, campaignId) {\n const id = encodeURIComponent(lead.id);\n // A campaign card carries TWO params \u2014 the campaign selects the list, the\n // lead opens the panel inside it. Campaign wins even when in_monitor is\n // also true, because that is the list the rep is looking at.\n if (campaignId) {\n return `https://leadbay.app/app/campaign?campaign=${encodeURIComponent(campaignId)}&lead=${id}`;\n }\n const view = lead.in_monitor ? "monitor" : "discover";\n return `https://leadbay.app/app/${view}?lead=${id}`;\n }\n openEl.href = leadUrl(lead, campaignId);\n ```\n\n `in_monitor` / `in_discover` are booleans on the `pull_followups` payload \u2014\n every follow-up carries `in_monitor: true`, so a call sheet must link to\n `monitor`. `pull_leads` omits both flags entirely; its leads are the Discover\n batch by definition, so `discover` is the default. A campaign card\n (`lb.callList({source:"campaign", campaignId})`) needs `?campaign=<id>&lead=<id>`\n \u2014 the param names are `CAMPAIGN_QUERY_PARAM` and `LEAD_QUERY_PARAM`, and the\n app\'s own `useLeadPanel` preserves whatever params are already set, so the\n two coexist by design. Omitting `campaign=` opens an empty campaign view.\n\n Inline the glyph as SVG rather than an emoji or `\u2197` \u2014 it inherits\n `currentColor` and scales with the text, so it stays legible in both themes.\n `?lead=<uuid>` is the real deep-link (`LEAD_QUERY_PARAM` in the web app, read\n on load; the panel is an overlay, so the view choice only decides what sits\n behind it). This is the ONE place a card may use `lead.id`: as a link target,\n never as visible text.\n2. **State chips** \u2014 taste (`data-taste`) and CRM status (`data-status`) are\n INDEPENDENT axes; render both, hide the empty one. Never collapse to one chip.\n3. **Firmographics** \u2014 sector of activity first, then city, then size, then the\n contact. `sector_id` is a RAW ID (`"5136"`), not a label: resolve it via\n `leadbay_list_sectors` (1346 rows \u2014 fetch once, cache, never inline the lot)\n or omit it. Never print the raw id.\n\n **Always show whether the lead is reachable \u2014 and never merge the person\n with the company\'s switchboard.** These are two separate lines:\n\n ```js\n // WHO \u2014 recommended_contact. Name, and job_title ONLY when present; on list\n // payloads it is usually null, and inventing one is worse than omitting it.\n const rc = lead.recommended_contact;\n const who = rc ? [rc.first_name, rc.last_name].filter(Boolean).join(" ") : null;\n const whoLine = who ? who + (rc.job_title ? " \xB7 " + rc.job_title : "") : "No named contact";\n\n // HOW \u2014 company-level channels. `phone_numbers` and `email` belong to the\n // COMPANY, not to `recommended_contact`. Rendering "Jean \xB7 \u260E 0123\u2026" claims a\n // direct line that does not exist; it is the switchboard.\n const phone = (lead.phone_numbers || [])[0] || null;\n // The API returns the STRING "null" for a missing email \u2014 guard for it or\n // you will print the word "null" as an address.\n const email = lead.email && lead.email !== "null" ? lead.email : null;\n const howLine = [phone && "\u260E " + phone, email && "\u2709 " + email].filter(Boolean)\n .join(" \xB7 ") || "No direct channel \u2014 enrich to reveal";\n ```\n\n ```html\n <div class="lb-sub">Sector \xB7 City \xB7 Size</div>\n <div class="lb-sub">\u{1F464} Jean-Fran\xE7ois Froemer \xB7 G\xE9rant</div> <!-- WHO -->\n <div class="lb-sub">\u{1F3E2} \u260E 01 23 45 67 89</div> <!-- HOW: company -->\n ```\n\n Label the channel line as the **company\'s**, so a rep reading fast cannot\n mistake it for a direct line. A per-contact email or phone exists only after\n enrichment \u2014 `research_lead_by_id` exposes it as `contacts.reachable[]`, and\n `_meta.has_reachable_contact` is the authoritative flag. The list payloads\n carry neither, so a card built from `pull_leads` / `pull_followups` can only\n ever show company channels. Say "enrich to reveal" rather than implying the\n contact is callable.\n\n Two things that look like reachability and are not: a `linkedin_page` alone\n (the rep cannot message a URL without leaving the artifact \u2014 same rule\n `research_lead_by_id` applies), and `contacts_count > 0` (it counts known\n people, not people you can contact; a lead can show 2518 contacts and zero\n channels). `pull_followups` carries `has_phone` as a ready-made boolean;\n `pull_leads` omits it, so derive from `phone_numbers` there.\n4. **Why it fits** \u2014 one sentence, \u226420 words. Walk this chain and stop at the\n first hit:\n\n 1. `short_description`\n 2. `description` (longer; only on `research_lead_by_id` /\n `research_lead_by_name_fuzzy` \u2014 the trim payloads omit it)\n 3. top 2 `tags[].display_name`\n 4. `qualification_summary.best_response_excerpt`, trimmed to one sentence\n 5. `keywords`, first 3, joined with ` \xB7 `\n 6. the resolved sector label \u2014 better than nothing, and if step 3 already\n printed the sector on the firmographics line, skip to step 7\n 7. the literal *"No description yet \u2014 run qualification to generate one"*\n\n Never leave this line blank: a silent gap reads as a rendering bug, whereas\n the fallback tells the rep the data is missing and what fixes it.\n\n **The two list payloads are complementary, so the chain must span both.**\n `pull_leads` returns `short_description` on every lead but no `sector_id`;\n `pull_followups` returns `sector_id` but no `short_description` at all. A\n card fed by one will fall through to a different step than the same card fed\n by the other \u2014 that is expected, not a bug. Never call\n `research_lead_by_id` per row just to fill this line: it is one request per\n lead. Fetch it lazily when the rep expands a card.\n\n**Never show** on a card: `id`, `sector_id`, `location.pos`, `location.country`\n(unless city and state are both missing), `is_hq`, `*_in_progress`,\n`highlighted_fields`, `custom_fields`, `stale_at`, `deal_insights`,\n`need_attention*`, any count that is 0, any value that is the string `"null"`.\n\n**Minimum actions.** A card that only displays is a table row that costs more \u2014\nif you are not wiring an action, render the markdown table instead. Wire at\nleast one write, and prefer the set the rep actually needs:\n\n| Card is for | Wire |\n|---|---|\n| triage a discovery batch | `lb.like` / `lb.dislike` + `lb.setStatus` |\n| working a call list | `lb.outreach` (gated on a note) + `lb.leadHistory` |\n| pipeline review | `lb.setStatus` + `lb.note` |\n\nAlways render the `.error` branch of every view-model \u2014 a control that cannot\nreach the host must say so, not sit silent.\n\n## Recipe: cold-call sheet (one row per lead)\n\n```js\nconst lb = window.LeadbayArtifacts; lb.configure();\nconst ASK = "<the user\'s request>";\n\nconst list = lb.callList({ source: "campaign", campaignId: CID, ask: ASK });\nlist.subscribe((l) => renderRows(l.items, l.loading)); // your render\n\n// per lead row (call when you build a row):\nfunction wireRow(lead, els) {\n const status = lb.field({ value: "STILL_CHASING" }); // static-enum <select>\n const note = lb.field({ validate: (v) => (v && v.trim() ? null : "Add a note") });\n lb.bindValue(els.status, status);\n lb.bindValue(els.note, note);\n lb.bindAction(els.log, lb.outreach({ leadId: lead.id, ask: ASK, status, note }));\n lb.bindAction(els.like, lb.like(lead.id));\n\n const history = lb.leadHistory(lead.id, ASK); // lazy\n history.subscribe((h) => renderHistory(els.history, h));\n els.expand.onclick = () => history.load(); // load on click\n}\n```\n\n## Recipe: lead-status dropdown (Wanted / Won / Lost)\n\nThe org-wide CRM status, as a `<select>` + Apply button. You write the markup;\n`lb.leadStatus` fills the options and holds the value, `lb.setStatus` does the write.\n\n```html\n<div class="lb-card">\n <div class="lb-card-head">\n <span class="lb-title">Acme Corp</span>\n <span class="lb-chips">\n <span id="taste" class="lb-chip" data-taste="liked">Liked</span>\n <span id="crm" class="lb-chip" data-status="WANTED">Wanted</span>\n </span>\n </div>\n <div class="lb-row">\n <select id="st" class="lb-select"></select>\n <button id="go" class="lb-btn">Apply</button>\n <span id="msg" class="lb-msg"></span>\n </div>\n</div>\n```\n\n**Two badges, never one.** Taste (`liked`/`disliked`, from `lb.like`/`lb.dislike`)\nand CRM status (`WANTED`/`WON`/`LOST`/`UNWANTED`, from `lb.setStatus`) are\nindependent axes \u2014 a lead can be liked *and* lost. Collapsing them into a single\nchip destroys information: the rep can no longer see that a lead they liked went\nnowhere. Render `data-taste` and `data-status` as separate chips inside\n`lb-chips`, and hide the one that has no value rather than reusing it.\n\n```js\nlb.styles(); // once per artifact \u2014 see below\n\nconst status = lb.leadStatus(lead.org_lead_status); // seed with the CURRENT value\nconst save = lb.setStatus({ leadId: lead.id, status, ask: ASK });\n\nlb.bindSelect(document.getElementById("st"), status); // populates the 4 options\nlb.bindAction(document.getElementById("go"), save); // click \u2192 write\n\nsave.subscribe((a) => { // render your own feedback\n msg.textContent = a.loading ? "Saving\u2026"\n : a.error ? a.error.message // includes partial failures\n : a.lastResult ? `Set to ${a.lastResult.status}` : "";\n msg.dataset.tone = a.error ? "error" : a.lastResult ? "ok" : "";\n});\n```\n\nLoading / success / error styling comes free: `bindAction` and `bindSelect` set\n`data-lb-state` (`ready|loading|error|success|unavailable`) and the skin already\ntargets those attributes. No extra wiring.\n\nSave-on-change instead of an Apply button \u2014 drop `bindAction` and run it yourself:\n\n```js\ndocument.getElementById("st").addEventListener("change", () => save.run());\n```\n\n**Bulk apply** across checked rows \u2014 pass `leadIds` and a `confirm`, since one\nclick rewrites a field every rep in the org sees:\n\n```js\nconst bulk = lb.setStatus({\n leadIds: () => checkedIds, // \u2190 read at run() time, not at build time\n status, ask: ASK,\n confirm: "Set this status on every selected lead?",\n});\n```\n\n`leadIds` is read when the action runs, so a live selection works \u2014 but pass the\narray itself if your selection is fixed. A partial write (some leads rejected)\nsurfaces as `.error`, never as a green button: `setStatus` checks the `failed[]`\nthe tool returns.\n\nThe backend stamps the status date as "now" on every write, which is what a rep\nclicking a dropdown means. Don\'t add a date picker unless the user asks to\nbackdate \u2014 then pass an optional `date` field holding `YYYY-MM-DD`:\n`lb.setStatus({ leadId, status, date, ask })`.\n\n## Recipe: manager dashboard\n\n```js\nconst team = lb.teamActivity({ weeks: 4, ask: ASK });\nteam.subscribe((t) => {\n if (t.loading) showSpinner();\n if (t.data) {\n renderLeaderboard(t.data.reps); // sorted by total_activities; cols: name, notes, meetings_or_interest, lost\u2026\n renderTrendChart(t.data.trend); // [{date,count}] \u2192 Chart.js (allowed from CDN)\n }\n});\nrefreshBtn.onclick = () => team.refresh();\n```\n\n## Recipe: live enrichment\n\n```js\nconst job = lb.enrichment({ leadIds: [LEAD], titles: ["CEO", "VP Sales"], ask: ASK });\njob.subscribe((j) => {\n const p = j.data && j.data.overall_progress; // {done,total,done_ratio}\n renderBar(p);\n if (j.done) renderContacts(j.data.leads); // enriched contacts\n});\nrefreshBtn.onclick = () => job.refresh();\n```\n\n## Write-call rules\n\nThe domain factories handle these for you. If you hand-roll an action:\n`leadbay_report_outreach` args MUST include `verification:{source:"user_confirmed", ref}`\nAND `_triggered_by`; `leadbay_add_leads_to_campaign` needs `_triggered_by`;\n`add_note`/`like_lead`/`dislike_lead` take only their own args. `epilogue_status` is\none of `lb.EPILOGUE_STATUSES`. Snoozing (pushback) is advanced-gated \u2014 not\ncallable from a default artifact. Org lead status IS on the default surface:\nuse `lb.setStatus`, which owns the arg shape AND the partial-write check \u2014\n`leadbay_set_lead_status` writes each lead separately, so it can resolve 200\nwith a non-empty `failed[]`. Hand-rolling that action will report a green\nbutton over a write that never landed.\n\n## Degradation + live updates\n\nIf the host bridge is absent, a view-model\'s `.error` is set with `.error.unavailable\n=== true` (bind helpers set `data-lb-state="unavailable"`) \u2014 nothing throws. Every\ncall also has a **30s timeout** (configurable via `lb.configure({ timeoutMs })`): a\nhost call that never settles becomes `.error` with `code:"timeout"`, so a control is\nnever stuck loading forever \u2014 always render the `.error` branch so the user can retry.\nAuto-poll (`pollEvery`) depends on the cowork host serving FRESH reads; `.refresh()`\nis the guaranteed manual path \u2014 always wire a Refresh control for polling resources.';
|
|
25570
26483
|
}
|
|
25571
26484
|
});
|
|
25572
26485
|
|
|
@@ -25620,6 +26533,7 @@ __export(dist_exports, {
|
|
|
25620
26533
|
AgentMemorySourceSchema: () => AgentMemorySourceSchema,
|
|
25621
26534
|
AgentMemoryTombstoneSchema: () => AgentMemoryTombstoneSchema,
|
|
25622
26535
|
COMPOSITE_FILE_TOOL_NAMES: () => COMPOSITE_FILE_TOOL_NAMES,
|
|
26536
|
+
DEFAULT_REQUEST_TIMEOUT_MS: () => DEFAULT_REQUEST_TIMEOUT_MS,
|
|
25623
26537
|
GETTING_STARTED_MANIFEST: () => GETTING_STARTED_MANIFEST,
|
|
25624
26538
|
InMemoryBulkStore: () => InMemoryBulkStore,
|
|
25625
26539
|
LeadbayClient: () => LeadbayClient,
|
|
@@ -25743,12 +26657,14 @@ __export(dist_exports, {
|
|
|
25743
26657
|
resolveImportRows: () => resolveImportRows,
|
|
25744
26658
|
resolveRegion: () => resolveRegion,
|
|
25745
26659
|
reviseHintFor: () => reviseHintFor,
|
|
26660
|
+
runWithRequestSignal: () => runWithRequestSignal,
|
|
25746
26661
|
scanPortfolioSignals: () => scanPortfolioSignals,
|
|
25747
26662
|
seedCandidates: () => seedCandidates,
|
|
25748
26663
|
selectLeads: () => selectLeads,
|
|
25749
26664
|
sendFeedback: () => sendFeedback,
|
|
25750
26665
|
setActiveLens: () => setActiveLens,
|
|
25751
26666
|
setEpilogueStatus: () => setEpilogueStatus,
|
|
26667
|
+
setLeadStatus: () => setLeadStatus,
|
|
25752
26668
|
setPushback: () => setPushback,
|
|
25753
26669
|
setQualificationQuestions: () => setQualificationQuestions,
|
|
25754
26670
|
setTelemetry: () => setTelemetry,
|
|
@@ -25825,6 +26741,7 @@ var init_dist = __esm({
|
|
|
25825
26741
|
init_delete_custom_field();
|
|
25826
26742
|
init_like_lead();
|
|
25827
26743
|
init_dislike_lead();
|
|
26744
|
+
init_set_lead_status();
|
|
25828
26745
|
init_set_telemetry();
|
|
25829
26746
|
init_add_contact();
|
|
25830
26747
|
init_remove_contact();
|
|
@@ -26076,6 +26993,13 @@ var init_dist = __esm({
|
|
|
26076
26993
|
// to the agent without requiring LEADBAY_MCP_ADVANCED=1.
|
|
26077
26994
|
likeLead,
|
|
26078
26995
|
dislikeLead,
|
|
26996
|
+
// Org-wide CRM status (WANTED/WON/LOST/UNWANTED). Granular-shaped but
|
|
26997
|
+
// registered HERE, not in granularWriteTools: reps state deal outcomes in
|
|
26998
|
+
// ordinary conversation, and the artifact-kit status dropdown calls it —
|
|
26999
|
+
// both need it on the default surface without LEADBAY_MCP_ADVANCED=1.
|
|
27000
|
+
// Distinct from setEpilogueStatus (outreach disposition), which stays
|
|
27001
|
+
// advanced-gated.
|
|
27002
|
+
setLeadStatus,
|
|
26079
27003
|
// Campaign write composites — persist a hand-picked cohort of leads.
|
|
26080
27004
|
// Backend POST endpoints; gated behind LEADBAY_MCP_WRITE=1 in MCP.
|
|
26081
27005
|
createCampaign,
|
|
@@ -27260,7 +28184,7 @@ Build the final mappings yourself. Start from \`leadbay_resolve_import_rows.mapp
|
|
|
27260
28184
|
|
|
27261
28185
|
# PHASE 5 \u2014 QUALIFY (optional) + REPORT
|
|
27262
28186
|
|
|
27263
|
-
Prefer \`leadbay_import_and_qualify\` when the user asks to qualify/research after import; otherwise use \`leadbay_import_leads\`. For large files or short client timeouts, pass \`wait_for_completion=false\` and poll \`leadbay_import_status\`. After import, qualify only lead IDs returned by the import. Rows that came back \`uncrawled\` are pending a background crawl (not failures); the leads Leadbay adds for them populate in the user's Leadbay account as the crawl completes \u2014 tell the user that, not that a tool call will fetch them (\`import_status\`
|
|
28187
|
+
Prefer \`leadbay_import_and_qualify\` when the user asks to qualify/research after import; otherwise use \`leadbay_import_leads\`. For large files or short client timeouts, pass \`wait_for_completion=false\` and poll \`leadbay_import_status\`. After import, qualify only lead IDs returned by the import. Rows that came back \`uncrawled\` are pending a background crawl (not failures); the leads Leadbay adds for them populate in the user's Leadbay account as the crawl completes \u2014 tell the user that, not that a tool call will fetch them (\`import_status\` reports the rows the wizard already placed, not leads a later crawl adds; \`pull_leads\` reads the active lens, so an imported lead outside it may not appear; re-running the import later re-reconciles those companies).
|
|
27264
28188
|
|
|
27265
28189
|
**Deliver the augmented file back to the user**: the original file plus a new \`LEADBAY_ID\` column populated from the resolution step. This is the second deliverable of a job well done.
|
|
27266
28190
|
|
|
@@ -27825,10 +28749,17 @@ Research the company name or domain '{{arg:domain}}' for me using Leadbay.
|
|
|
27825
28749
|
|
|
27826
28750
|
# PHASE 1 \u2014 RESOLVE + DEEP DIVE
|
|
27827
28751
|
Call \`leadbay_research_lead_by_name_fuzzy\` with
|
|
27828
|
-
\`companyName:'{{arg:domain}}'\`.
|
|
27829
|
-
|
|
27830
|
-
|
|
27831
|
-
|
|
28752
|
+
\`companyName:'{{arg:domain}}'\`. If the user gave a domain or a contact email
|
|
28753
|
+
anywhere in the conversation, also pass \`website\` (or \`email\`) \u2014 that is the
|
|
28754
|
+
match key that finds a company they do not own yet. Omit \`lensId\`: the default
|
|
28755
|
+
search covers the user's visible Discover, Monitor, and Activate corpus \u2014
|
|
28756
|
+
including other lenses and leads outside the active lens's first page \u2014 and
|
|
28757
|
+
then the Leadbay company registry. The composite resolves the lead and returns
|
|
28758
|
+
the full deep-research payload in one call.
|
|
28759
|
+
|
|
28760
|
+
If it returns \`{resolution:"ambiguous"}\`, several companies match. Ask the user
|
|
28761
|
+
which one via \`ask_user_input_v0\`, then call \`leadbay_research_lead_by_id\` with
|
|
28762
|
+
the leadId they pick.
|
|
27832
28763
|
|
|
27833
28764
|
Render the result using the canonical single-record card layout \u2014 detect MODE A
|
|
27834
28765
|
(Discovery) since the user asked to research a company rather than prepare
|
|
@@ -27905,10 +28836,14 @@ When the response carries \`social_urls\` (the post-fix multi-platform URL block
|
|
|
27905
28836
|
|
|
27906
28837
|
|
|
27907
28838
|
# PHASE 2 \u2014 NOT FOUND
|
|
27908
|
-
If the resolver returns \`LEAD_NOT_FOUND\`,
|
|
27909
|
-
|
|
27910
|
-
|
|
27911
|
-
it
|
|
28839
|
+
If the resolver returns \`LEAD_NOT_FOUND\`, read its hint: it names the field
|
|
28840
|
+
that would have found the company (\`would_help\`, usually \`website\`). **Ask the
|
|
28841
|
+
user for that field first** \u2014 "what's their website?" \u2014 and call the tool again
|
|
28842
|
+
with it. Only when they cannot supply it should you say both their leads and
|
|
28843
|
+
the Leadbay registry were searched.
|
|
28844
|
+
**Do NOT call \`leadbay_import_and_qualify\` automatically.**
|
|
28845
|
+
Offer to import and qualify as a separate, explicit next step; only call it
|
|
28846
|
+
after the user agrees.
|
|
27912
28847
|
|
|
27913
28848
|
# PHASE 3 \u2014 SUMMARY
|
|
27914
28849
|
Place a 2\u20133 sentence summary ABOVE the card with:
|
|
@@ -28973,6 +29908,7 @@ var BUILTIN_WIDGETS_PARAGRAPH = 'Prefer host-native widgets over inline markdown
|
|
|
28973
29908
|
|
|
28974
29909
|
// src/server.ts
|
|
28975
29910
|
init_dist();
|
|
29911
|
+
init_dist();
|
|
28976
29912
|
|
|
28977
29913
|
// src/telemetry.ts
|
|
28978
29914
|
import { PostHog } from "posthog-node";
|
|
@@ -28986,6 +29922,7 @@ var EMBEDDED_SENTRY_DSN = "https://301f1c433433b76132956ed5415bea19@o45058744368
|
|
|
28986
29922
|
// src/telemetry-events.ts
|
|
28987
29923
|
var EV_TOOL_CALL = "mcp tool called";
|
|
28988
29924
|
var EV_QUOTA_HIT = "mcp quota hit";
|
|
29925
|
+
var EV_TOOL_TIMEOUT = "mcp tool timeout";
|
|
28989
29926
|
var EV_TOPUP_LINK = "mcp topup link created";
|
|
28990
29927
|
var EV_STARTUP = "mcp startup";
|
|
28991
29928
|
var EV_MCP_UPDATE_CHECK = "mcp update check";
|
|
@@ -28996,6 +29933,7 @@ var EV_MCP_VERSION_UPDATED = "mcp version updated";
|
|
|
28996
29933
|
var EV_AGENT_MEMORY_CAPTURED = "agent_memory_captured";
|
|
28997
29934
|
var EV_AGENT_MEMORY_RECALLED = "agent_memory_recalled";
|
|
28998
29935
|
var EV_AGENT_MEMORY_PRUNED = "agent_memory_pruned";
|
|
29936
|
+
var DURATION_PLAUSIBILITY_CEILING_MS = 6e5;
|
|
28999
29937
|
var EV_FRICTION_REPORTED = "mcp friction reported";
|
|
29000
29938
|
var EV_COMPOSITE_CALL = "mcp composite call";
|
|
29001
29939
|
|
|
@@ -29009,6 +29947,8 @@ var NOOP_TELEMETRY = {
|
|
|
29009
29947
|
},
|
|
29010
29948
|
captureQuotaHit: (_props, _identity) => {
|
|
29011
29949
|
},
|
|
29950
|
+
captureToolTimeout: (_props, _identity) => {
|
|
29951
|
+
},
|
|
29012
29952
|
captureTopupLink: (_props, _identity) => {
|
|
29013
29953
|
},
|
|
29014
29954
|
captureStartup: (_props, _identity) => {
|
|
@@ -29043,6 +29983,13 @@ function parseTelemetryEnv(raw) {
|
|
|
29043
29983
|
if (v === "false" || v === "0" || v === "no" || v === "off") return false;
|
|
29044
29984
|
return true;
|
|
29045
29985
|
}
|
|
29986
|
+
function withPlausibleDuration(props) {
|
|
29987
|
+
const { duration_ms, ...rest } = props;
|
|
29988
|
+
if (Number.isFinite(duration_ms) && duration_ms >= 0 && duration_ms <= DURATION_PLAUSIBILITY_CEILING_MS) {
|
|
29989
|
+
return { ...rest, duration_ms };
|
|
29990
|
+
}
|
|
29991
|
+
return { ...rest, duration_ms_raw: duration_ms, duration_implausible: true };
|
|
29992
|
+
}
|
|
29046
29993
|
function initTelemetry(opts) {
|
|
29047
29994
|
if (!parseTelemetryEnv(process.env.LEADBAY_TELEMETRY_ENABLED)) return NOOP_TELEMETRY;
|
|
29048
29995
|
if (process.env.NODE_ENV === "test") return NOOP_TELEMETRY;
|
|
@@ -29211,14 +30158,17 @@ function initTelemetry(opts) {
|
|
|
29211
30158
|
return identityPromise;
|
|
29212
30159
|
},
|
|
29213
30160
|
captureToolCall(props, identity) {
|
|
29214
|
-
emit(EV_TOOL_CALL,
|
|
30161
|
+
emit(EV_TOOL_CALL, withPlausibleDuration(props), identity);
|
|
29215
30162
|
},
|
|
29216
30163
|
captureCompositeCall(props, identity) {
|
|
29217
|
-
emit(EV_COMPOSITE_CALL,
|
|
30164
|
+
emit(EV_COMPOSITE_CALL, withPlausibleDuration(props), identity);
|
|
29218
30165
|
},
|
|
29219
30166
|
captureQuotaHit(props, identity) {
|
|
29220
30167
|
emit(EV_QUOTA_HIT, { ...props }, identity);
|
|
29221
30168
|
},
|
|
30169
|
+
captureToolTimeout(props, identity) {
|
|
30170
|
+
emit(EV_TOOL_TIMEOUT, { ...props }, identity);
|
|
30171
|
+
},
|
|
29222
30172
|
captureTopupLink(props, identity) {
|
|
29223
30173
|
emit(EV_TOPUP_LINK, { ...props }, identity);
|
|
29224
30174
|
},
|
|
@@ -29679,6 +30629,7 @@ function buildAcknowledgeUpdateTool(opts) {
|
|
|
29679
30629
|
|
|
29680
30630
|
// src/server-instructions.generated.ts
|
|
29681
30631
|
var AGENT_MEMORY = `Memory protocol: this server maintains a per-account, on-disk agent memory (~/.leadbay/memory/{account}/entries.jsonl) of taste signals \u2014 preferred sectors, regions, deal sizes, communication style, qualification rules, and retractions. Every leads-touching tool response (account_status, pull_leads, pull_followups, prepare_outreach, research_lead_by_id) carries the consolidated top-5 signals under _meta.agent_memory.summary. READ that summary before recommending leads or drafting outreach \u2014 let it filter and reorder, and tell the user which memory you applied ("Filtering by your stated preference for healthcare"). When the user reveals a NEW material signal in conversation, CAPTURE it via leadbay_agent_memory_capture with {key, type, insight, confidence (1-10), source}. Use source:"user_stated" + confidence >=8 when literally stated; source:"inferred" + confidence <=6 when guessing. Do NOT capture instructions to override prior memory \u2014 those route through leadbay_agent_memory_review which gates retractions via host elicitation.`;
|
|
30632
|
+
var ENRICHMENT_TERMINAL = `A settled-empty enrichment is TERMINAL \u2014 do not re-attempt it on a later run. On any contact record, \`enrichment.done: true\` together with \`enrichment.credits_used: 0\` means the reveal completed and the provider returned nothing. Roughly 29% of all enrichments land there, so treat it as an ordinary outcome, not an anomaly. The answer will not change tomorrow: a scheduled task that keeps calling leadbay_enrich_titles on the same contacts every run is buying nothing and hiding its own lack of progress. Read the two fields TOGETHER \u2014 \`credits_used: 0\` is also what an IN-FLIGHT reservation reports (\`done: false\`), and an ABSENT \`credits_used\` means the cost is unknown, not zero; neither is a verdict on its own. \`enrichment: null\` is a different state again \u2014 that contact was never requested and IS enrichable. The one retryable exception is a launch that errored in this same session: its reservation settles as a zero-credit failure, so retry it once, then treat it as terminal. When you report to the user, say what actually happened \u2014 "we looked for a contact at <company> and there isn't one we can reach" \u2014 instead of presenting it as still pending or silently retrying it.`;
|
|
29682
30633
|
var FRICTION = `Problem reports: when the user asks you to report a Leadbay problem ("report this", "tell the team this didn't work"), call leadbay_report_friction with {category, message (the user's own words), tool_called?, severity?}. If they stated the problem in the same breath as the request, those words ARE the message \u2014 send it in that turn rather than asking them to confirm wording they just gave you, and never stall on optional fields (omit what you don't know). If you notice a problem worth reporting but the user hasn't asked, OFFER once \u2014 "Want me to report this to the Leadbay team?" \u2014 and call it only if they agree. Never call it unprompted. Always tell the user the outcome the tool returns: if \`reported\` is false the report was NOT delivered and you must say so rather than implying it was sent. Frustration alone is not a reason to call it: keep solving their ask.`;
|
|
29683
30634
|
var MENTAL_MODEL = `How Leadbay works (mental model): Leadbay is a sales inbox, not a queryable database. Each day the user logs back in, a fresh batch of leads is delivered. Batch size is paced by how many leads the user has actually acted on recently \u2014 some workflows produce a big stream of smaller prospects, others a narrow stream of bigger ones. Pulling more won't produce more; the user acting on leads (outreach, skips, saves) does.`;
|
|
29684
30635
|
var QUOTA_TOPUP = `Quota & top-ups: when a tool returns QUOTA_EXCEEDED / 429, the user has TWO options \u2014 wait for the window reset (daily / weekly / monthly resets shown in leadbay_account_status), OR top up AI credits (top-ups clear the throttle IMMEDIATELY \u2014 they are not subject to the same window). Always offer BOTH options; default-recommending 'wait until tomorrow' is wrong when a 30-second top-up unblocks the same call. If the host exposes leadbay_create_topup_link, OFFER it on every quota wall: 'Want me to generate a top-up link?' \u2014 when the user says yes, call leadbay_create_topup_link and surface the returned Stripe URL as a clickable link for the user to open in their browser. (Sibling leadbay_open_billing_portal is for ongoing subscription changes, not one-shot top-ups.) AFTER the user has topped up: do NOT keep refusing operations. A top-up invalidates every prior 429 and every stale 'you're at your quota' snapshot. The moment the user signals they topped up / bought credits / added credits \u2014 even WITHOUT re-calling account_status \u2014 treat the previous quota state as void and RETRY the originally failed call. (Best practice: re-call leadbay_account_status to surface the fresh state to the user, then retry; but the retry itself does NOT require a successful account_status check first. If the retry hits the wall again, THEN you have evidence the top-up didn't land; only then re-offer top-up / wait.) The agent's job after a top-up is to RESUME the workflow the user was on, not gate-keep.
|
|
@@ -29828,6 +30779,9 @@ function buildServerInstructions(exposed) {
|
|
|
29828
30779
|
parts.push(TRIGGERED_BY);
|
|
29829
30780
|
parts.push(MENTAL_MODEL);
|
|
29830
30781
|
parts.push(QUOTA_TOPUP);
|
|
30782
|
+
if (has("leadbay_enrich_titles")) {
|
|
30783
|
+
parts.push(ENRICHMENT_TERMINAL);
|
|
30784
|
+
}
|
|
29831
30785
|
parts.push(TRANSIENT_401);
|
|
29832
30786
|
parts.push(buildScoringParagraph(has));
|
|
29833
30787
|
parts.push(FIRST_RUN_ROUTING);
|
|
@@ -30124,6 +31078,16 @@ function buildServer(client, opts = {}) {
|
|
|
30124
31078
|
source: "business"
|
|
30125
31079
|
};
|
|
30126
31080
|
};
|
|
31081
|
+
const captureTimeoutAlert = (toolName, envelope, triggeredBy) => {
|
|
31082
|
+
const ms = envelope._meta?.timeout_ms;
|
|
31083
|
+
telemetry.captureToolTimeout({
|
|
31084
|
+
tool: toolName,
|
|
31085
|
+
...typeof ms === "number" ? { timeout_ms: ms } : {},
|
|
31086
|
+
...envelope._meta?.endpoint ? { endpoint: envelope._meta.endpoint } : {},
|
|
31087
|
+
...envelope._meta?.region ? { region: envelope._meta.region } : {},
|
|
31088
|
+
...triggeredBy !== void 0 ? { triggered_by: triggeredBy } : {}
|
|
31089
|
+
});
|
|
31090
|
+
};
|
|
30127
31091
|
const captureAgentMemoryTelemetry = (toolName, result) => {
|
|
30128
31092
|
if (!result || typeof result !== "object") return;
|
|
30129
31093
|
const meta = result._meta ?? {};
|
|
@@ -30288,7 +31252,7 @@ ${url}
|
|
|
30288
31252
|
isError: true
|
|
30289
31253
|
};
|
|
30290
31254
|
}
|
|
30291
|
-
const result = await tool.execute(client, args, {
|
|
31255
|
+
const result = await runWithRequestSignal(extra.signal, () => tool.execute(client, args, {
|
|
30292
31256
|
logger: opts.logger,
|
|
30293
31257
|
bulkTracker: opts.bulkTracker,
|
|
30294
31258
|
notificationsInbox: opts.notificationsInbox,
|
|
@@ -30321,7 +31285,7 @@ ${url}
|
|
|
30321
31285
|
...report.tool_called ? { tool_called: report.tool_called } : {},
|
|
30322
31286
|
...report.severity ? { severity: report.severity } : {}
|
|
30323
31287
|
}) === true
|
|
30324
|
-
});
|
|
31288
|
+
}));
|
|
30325
31289
|
await maybeAttachUpdate(name, result);
|
|
30326
31290
|
maybeAttachNotifications(result);
|
|
30327
31291
|
if (result && typeof result === "object" && result.error === true) {
|
|
@@ -30337,6 +31301,9 @@ ${url}
|
|
|
30337
31301
|
endpoint: result._meta?.endpoint
|
|
30338
31302
|
});
|
|
30339
31303
|
}
|
|
31304
|
+
if (envCode === "TIMEOUT") {
|
|
31305
|
+
captureTimeoutAlert(name, result, triggered_by);
|
|
31306
|
+
}
|
|
30340
31307
|
telemetry.captureToolCall({
|
|
30341
31308
|
tool: name,
|
|
30342
31309
|
ok: false,
|
|
@@ -30467,6 +31434,9 @@ ${url}
|
|
|
30467
31434
|
endpoint: err._meta?.endpoint
|
|
30468
31435
|
});
|
|
30469
31436
|
}
|
|
31437
|
+
if (!skipAnalytics && err.code === "TIMEOUT") {
|
|
31438
|
+
captureTimeoutAlert(name, err, triggered_by);
|
|
31439
|
+
}
|
|
30470
31440
|
const httpStatus2 = err._meta?.http_status;
|
|
30471
31441
|
if (!skipAnalytics) {
|
|
30472
31442
|
telemetry.captureToolCall({
|
|
@@ -31970,7 +32940,7 @@ var OAUTH_BASE_URLS = {
|
|
|
31970
32940
|
fr: "https://staging.api.leadbay.app"
|
|
31971
32941
|
}
|
|
31972
32942
|
};
|
|
31973
|
-
var VERSION = "0.
|
|
32943
|
+
var VERSION = "0.32.1";
|
|
31974
32944
|
var HELP = `
|
|
31975
32945
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
31976
32946
|
|