@leadbay/mcp 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -0
- package/MIGRATION.md +72 -0
- package/README.md +74 -23
- package/dist/bin.js +358 -75
- package/dist/{chunk-FJBO2MY2.js → chunk-NED7ATJI.js} +608 -12
- package/dist/{dist-FENQ2I7R.js → dist-YMZYFHZK.js} +5 -1
- package/package.json +1 -1
|
@@ -17,7 +17,7 @@ function httpsRequest(method, url, headers, body) {
|
|
|
17
17
|
const start = Date.now();
|
|
18
18
|
const parsed = new URL(url);
|
|
19
19
|
const reqHeaders = { ...headers };
|
|
20
|
-
if (body) {
|
|
20
|
+
if (body !== void 0) {
|
|
21
21
|
reqHeaders["Content-Length"] = Buffer.byteLength(body);
|
|
22
22
|
}
|
|
23
23
|
const req = https.request({
|
|
@@ -39,7 +39,7 @@ function httpsRequest(method, url, headers, body) {
|
|
|
39
39
|
});
|
|
40
40
|
});
|
|
41
41
|
req.on("error", reject);
|
|
42
|
-
if (body)
|
|
42
|
+
if (body !== void 0)
|
|
43
43
|
req.write(body);
|
|
44
44
|
req.end();
|
|
45
45
|
});
|
|
@@ -52,6 +52,14 @@ function createClient(config = {}) {
|
|
|
52
52
|
}
|
|
53
53
|
return new LeadbayClient(baseUrl, config.token, region);
|
|
54
54
|
}
|
|
55
|
+
function formatLoginError(status, body, baseUrl) {
|
|
56
|
+
const trimmed = body.trim();
|
|
57
|
+
const head = `login failed (${status}) at ${baseUrl}`;
|
|
58
|
+
const hint = status === 401 ? " (wrong email or password?)" : status === 429 ? " (rate-limited; wait and retry)" : status >= 500 ? " (server error; try again shortly)" : "";
|
|
59
|
+
if (!trimmed)
|
|
60
|
+
return head + hint;
|
|
61
|
+
return `${head}: ${trimmed.slice(0, 200)}${hint}`;
|
|
62
|
+
}
|
|
55
63
|
async function resolveRegion(email, password, startWith = "us") {
|
|
56
64
|
const order = startWith === "fr" ? ["fr", "us"] : ["us", "fr"];
|
|
57
65
|
let lastErr = null;
|
|
@@ -71,12 +79,13 @@ async function resolveRegion(email, password, startWith = "us") {
|
|
|
71
79
|
};
|
|
72
80
|
}
|
|
73
81
|
}
|
|
74
|
-
lastErr = { status: res.status, body: res.body, region };
|
|
82
|
+
lastErr = { kind: "http", status: res.status, body: res.body, region, baseUrl };
|
|
75
83
|
} catch (e) {
|
|
76
|
-
lastErr = { error: e, region };
|
|
84
|
+
lastErr = { kind: "network", error: e, region, baseUrl };
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
|
-
|
|
87
|
+
const detail = lastErr?.kind === "http" ? formatLoginError(lastErr.status, lastErr.body, lastErr.baseUrl) : lastErr?.kind === "network" ? `network error at ${lastErr.baseUrl}: ${lastErr.error?.message ?? String(lastErr.error)}` : "no attempts made";
|
|
88
|
+
throw new Error(`Leadbay login failed in both regions (us, fr). ${detail}`);
|
|
80
89
|
}
|
|
81
90
|
var _mockFixtures = null;
|
|
82
91
|
var _mockJournal = [];
|
|
@@ -300,6 +309,43 @@ var LeadbayClient = class {
|
|
|
300
309
|
this.releaseSemaphore();
|
|
301
310
|
}
|
|
302
311
|
}
|
|
312
|
+
// Like request(), but the caller supplies the Content-Type and the already-
|
|
313
|
+
// serialized body (string for text payloads such as CSV; Buffer for binary
|
|
314
|
+
// uploads). Auth, semaphore, error mapping, _lastMeta, and mock-mode all
|
|
315
|
+
// mirror request() exactly. Used by leadbay_import_leads to upload CSVs to
|
|
316
|
+
// the wizard at POST /1.5/imports.
|
|
317
|
+
async requestRawBinary(method, path, contentType, body) {
|
|
318
|
+
if (process.env.LEADBAY_MOCK === "1") {
|
|
319
|
+
return this.mockRequestBinary(method, path, contentType, body);
|
|
320
|
+
}
|
|
321
|
+
if (!this.token) {
|
|
322
|
+
throw this.makeError("NOT_AUTHENTICATED", "Not logged in to Leadbay", "Set LEADBAY_TOKEN in your MCP client config, or run: npx -y @leadbay/mcp install --email <you> --region <us|fr>", path);
|
|
323
|
+
}
|
|
324
|
+
await this.acquireSemaphore();
|
|
325
|
+
try {
|
|
326
|
+
const url = `${this._baseUrl}/1.5${path}`;
|
|
327
|
+
const headers = {
|
|
328
|
+
Authorization: `Bearer ${this.token}`,
|
|
329
|
+
"Content-Type": contentType
|
|
330
|
+
};
|
|
331
|
+
const res = await httpsRequest(method, url, headers, body);
|
|
332
|
+
this._lastMeta = {
|
|
333
|
+
region: this._region,
|
|
334
|
+
endpoint: `${method} ${path}`,
|
|
335
|
+
latency_ms: res.latency_ms,
|
|
336
|
+
retry_after: parseRetryAfter(res.headers["retry-after"])
|
|
337
|
+
};
|
|
338
|
+
if (res.status === 204) {
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
if (res.status < 200 || res.status >= 300) {
|
|
342
|
+
throw this.mapErrorResponse(res.status, res.body, path, res.headers);
|
|
343
|
+
}
|
|
344
|
+
return JSON.parse(res.body);
|
|
345
|
+
} finally {
|
|
346
|
+
this.releaseSemaphore();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
303
349
|
mockRequest(method, path, body) {
|
|
304
350
|
const fullPath = `/1.5${path}`;
|
|
305
351
|
this._lastMeta = {
|
|
@@ -323,6 +369,39 @@ var LeadbayClient = class {
|
|
|
323
369
|
would_call: { method, path: fullPath, body }
|
|
324
370
|
};
|
|
325
371
|
}
|
|
372
|
+
mockRequestBinary(method, path, contentType, body) {
|
|
373
|
+
const fullPath = `/1.5${path}`;
|
|
374
|
+
this._lastMeta = {
|
|
375
|
+
region: this._region,
|
|
376
|
+
endpoint: `${method} ${path}`,
|
|
377
|
+
latency_ms: 0,
|
|
378
|
+
retry_after: null
|
|
379
|
+
};
|
|
380
|
+
if (method === "GET") {
|
|
381
|
+
const fixture = findMockFixture("GET", fullPath);
|
|
382
|
+
if (!fixture) {
|
|
383
|
+
throw this.makeError("MOCK_NOT_FOUND", `LEADBAY_MOCK=1: no fixture for GET ${path}`, `Add a fixture to LEADBAY_MOCK_DIR (default: ./.context/leadbay-live-shapes/) \u2014 run a probe script to generate one.`, path);
|
|
384
|
+
}
|
|
385
|
+
if (fixture.status === 204)
|
|
386
|
+
return null;
|
|
387
|
+
return fixture.body;
|
|
388
|
+
}
|
|
389
|
+
const journalBody = {
|
|
390
|
+
_binary: true,
|
|
391
|
+
length: Buffer.byteLength(body),
|
|
392
|
+
content_type: contentType
|
|
393
|
+
};
|
|
394
|
+
_mockJournal.push({
|
|
395
|
+
method,
|
|
396
|
+
path: fullPath,
|
|
397
|
+
body: journalBody,
|
|
398
|
+
ts: Date.now()
|
|
399
|
+
});
|
|
400
|
+
return {
|
|
401
|
+
mocked: true,
|
|
402
|
+
would_call: { method, path: fullPath, body: journalBody }
|
|
403
|
+
};
|
|
404
|
+
}
|
|
326
405
|
mapErrorResponse(status, rawBody, endpoint, headers) {
|
|
327
406
|
let parsed;
|
|
328
407
|
try {
|
|
@@ -587,6 +666,11 @@ var getLeadProfile = {
|
|
|
587
666
|
},
|
|
588
667
|
execute: async (client, params) => {
|
|
589
668
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
669
|
+
void client.request("POST", "/interactions", [
|
|
670
|
+
{ type: "LEAD_SEEN", leadId: params.leadId, lensId: String(lensId) },
|
|
671
|
+
{ type: "LEAD_CLICKED", leadId: params.leadId, lensId: String(lensId) }
|
|
672
|
+
]).catch(() => {
|
|
673
|
+
});
|
|
590
674
|
const [leadResult, qualResult, contactsResult, paidContactsResult, webFetchResult] = await Promise.allSettled([
|
|
591
675
|
client.request("GET", `/lenses/${lensId}/leads/${params.leadId}`),
|
|
592
676
|
client.request("GET", `/leads/${params.leadId}/ai_agent_responses`),
|
|
@@ -1322,12 +1406,12 @@ var setUserPrompt = {
|
|
|
1322
1406
|
would_call: {
|
|
1323
1407
|
method: "POST",
|
|
1324
1408
|
path: `/organizations/${orgId}/user_prompt`,
|
|
1325
|
-
body: {
|
|
1409
|
+
body: { user_prompt: params.prompt }
|
|
1326
1410
|
}
|
|
1327
1411
|
};
|
|
1328
1412
|
}
|
|
1329
1413
|
await client.requestVoid("POST", `/organizations/${orgId}/user_prompt`, {
|
|
1330
|
-
|
|
1414
|
+
user_prompt: params.prompt
|
|
1331
1415
|
});
|
|
1332
1416
|
client.invalidateMe();
|
|
1333
1417
|
return { set: true };
|
|
@@ -1802,6 +1886,11 @@ var researchLead = {
|
|
|
1802
1886
|
execute: async (client, params, ctx) => {
|
|
1803
1887
|
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
1804
1888
|
const leadId = params.leadId;
|
|
1889
|
+
void client.request("POST", "/interactions", [
|
|
1890
|
+
{ type: "LEAD_SEEN", leadId, lensId: String(lensId) },
|
|
1891
|
+
{ type: "LEAD_CLICKED", leadId, lensId: String(lensId) }
|
|
1892
|
+
]).catch(() => {
|
|
1893
|
+
});
|
|
1805
1894
|
const [profileR, qualR, contactsR, webFetchR] = await Promise.allSettled([
|
|
1806
1895
|
client.request("GET", `/lenses/${lensId}/leads/${leadId}`),
|
|
1807
1896
|
client.request("GET", `/leads/${leadId}/ai_agent_responses`),
|
|
@@ -2190,6 +2279,510 @@ var bulkQualifyLeads = {
|
|
|
2190
2279
|
}
|
|
2191
2280
|
};
|
|
2192
2281
|
|
|
2282
|
+
// ../core/dist/composite/import-leads.js
|
|
2283
|
+
import { randomUUID } from "crypto";
|
|
2284
|
+
var CHUNK_SIZE = 100;
|
|
2285
|
+
var POLL_INTERVAL_MS = 2e3;
|
|
2286
|
+
var DEFAULT_PER_PHASE_BUDGET_MS = 6e4;
|
|
2287
|
+
var DEFAULT_TOTAL_BUDGET_MS2 = 3e5;
|
|
2288
|
+
var STABILIZATION_POLLS = 2;
|
|
2289
|
+
var PUBLIC_MAILBOX_DOMAINS = /* @__PURE__ */ new Set([
|
|
2290
|
+
"gmail.com",
|
|
2291
|
+
"googlemail.com",
|
|
2292
|
+
"yahoo.com",
|
|
2293
|
+
"ymail.com",
|
|
2294
|
+
"outlook.com",
|
|
2295
|
+
"hotmail.com",
|
|
2296
|
+
"live.com",
|
|
2297
|
+
"icloud.com",
|
|
2298
|
+
"me.com",
|
|
2299
|
+
"mac.com",
|
|
2300
|
+
"aol.com",
|
|
2301
|
+
"proton.me",
|
|
2302
|
+
"protonmail.com",
|
|
2303
|
+
"tutanota.com",
|
|
2304
|
+
"gmx.com",
|
|
2305
|
+
"gmx.net",
|
|
2306
|
+
"gmx.de",
|
|
2307
|
+
"mail.com",
|
|
2308
|
+
"yandex.com",
|
|
2309
|
+
"yandex.ru",
|
|
2310
|
+
"qq.com",
|
|
2311
|
+
"163.com",
|
|
2312
|
+
"126.com"
|
|
2313
|
+
]);
|
|
2314
|
+
function normalizeDomain(input) {
|
|
2315
|
+
if (!input || typeof input !== "string")
|
|
2316
|
+
return null;
|
|
2317
|
+
let v = input.trim().toLowerCase();
|
|
2318
|
+
if (!v)
|
|
2319
|
+
return null;
|
|
2320
|
+
v = v.replace(/^https?:\/\//, "");
|
|
2321
|
+
v = v.replace(/^www\./, "");
|
|
2322
|
+
v = v.split("/")[0].split("?")[0].split("#")[0];
|
|
2323
|
+
v = v.replace(/\.+$/, "");
|
|
2324
|
+
if (!v)
|
|
2325
|
+
return null;
|
|
2326
|
+
if (/\s/.test(v))
|
|
2327
|
+
return null;
|
|
2328
|
+
if (!v.includes("."))
|
|
2329
|
+
return null;
|
|
2330
|
+
if (v.startsWith(".") || v.endsWith("."))
|
|
2331
|
+
return null;
|
|
2332
|
+
const parts = v.split(".");
|
|
2333
|
+
if (parts.length < 2)
|
|
2334
|
+
return null;
|
|
2335
|
+
if (parts.some((p) => p.length === 0))
|
|
2336
|
+
return null;
|
|
2337
|
+
const tld = parts[parts.length - 1];
|
|
2338
|
+
if (!/^[a-z]{2,}$/.test(tld) && !tld.startsWith("xn--"))
|
|
2339
|
+
return null;
|
|
2340
|
+
if (!/^[a-z0-9-]+$/.test(parts[parts.length - 2]))
|
|
2341
|
+
return null;
|
|
2342
|
+
return v;
|
|
2343
|
+
}
|
|
2344
|
+
function escapeCsvCell(raw) {
|
|
2345
|
+
if (raw == null)
|
|
2346
|
+
return "";
|
|
2347
|
+
let s = String(raw);
|
|
2348
|
+
const trimmed = s.replace(/^[\s\r\n\t]+/, "");
|
|
2349
|
+
if (trimmed.length > 0) {
|
|
2350
|
+
const first = trimmed[0];
|
|
2351
|
+
if (first === "=" || first === "+" || first === "-" || first === "@") {
|
|
2352
|
+
s = "'" + s;
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
if (/[",\n\r]/.test(s)) {
|
|
2356
|
+
s = '"' + s.replace(/"/g, '""') + '"';
|
|
2357
|
+
}
|
|
2358
|
+
return s;
|
|
2359
|
+
}
|
|
2360
|
+
function synthesizeCsv(rows) {
|
|
2361
|
+
const lines = ["MCP_ROW_ID,LEAD_NAME,LEAD_WEBSITE"];
|
|
2362
|
+
for (const r of rows) {
|
|
2363
|
+
lines.push([escapeCsvCell(r.rowId), escapeCsvCell(r.name), escapeCsvCell(r.website)].join(","));
|
|
2364
|
+
}
|
|
2365
|
+
return lines.join("\n") + "\n";
|
|
2366
|
+
}
|
|
2367
|
+
function chunkAt100(items) {
|
|
2368
|
+
if (items.length === 0)
|
|
2369
|
+
return [];
|
|
2370
|
+
const chunks = [];
|
|
2371
|
+
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
|
|
2372
|
+
chunks.push(items.slice(i, i + CHUNK_SIZE));
|
|
2373
|
+
}
|
|
2374
|
+
return chunks;
|
|
2375
|
+
}
|
|
2376
|
+
function checkAborted(signal) {
|
|
2377
|
+
if (signal?.aborted) {
|
|
2378
|
+
throw Object.assign(new Error("aborted"), { name: "AbortError" });
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
async function sleepWithAbort(ms, signal) {
|
|
2382
|
+
if (!signal) {
|
|
2383
|
+
await new Promise((r) => setTimeout(r, ms));
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
if (signal.aborted) {
|
|
2387
|
+
checkAborted(signal);
|
|
2388
|
+
}
|
|
2389
|
+
await new Promise((resolve, reject) => {
|
|
2390
|
+
const t = setTimeout(() => {
|
|
2391
|
+
signal.removeEventListener("abort", onAbort);
|
|
2392
|
+
resolve();
|
|
2393
|
+
}, ms);
|
|
2394
|
+
const onAbort = () => {
|
|
2395
|
+
clearTimeout(t);
|
|
2396
|
+
signal.removeEventListener("abort", onAbort);
|
|
2397
|
+
reject(Object.assign(new Error("aborted"), { name: "AbortError" }));
|
|
2398
|
+
};
|
|
2399
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
2400
|
+
});
|
|
2401
|
+
}
|
|
2402
|
+
function readCell(record, key) {
|
|
2403
|
+
const want = key.toLowerCase();
|
|
2404
|
+
const arr = record.records;
|
|
2405
|
+
if (Array.isArray(arr)) {
|
|
2406
|
+
for (const c of arr) {
|
|
2407
|
+
const k = (c?.column_name ?? c?.key ?? c?.field ?? "").toString().toLowerCase();
|
|
2408
|
+
if (k === want) {
|
|
2409
|
+
const v = c?.value ?? null;
|
|
2410
|
+
return v != null ? String(v) : null;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
}
|
|
2414
|
+
const cells = record.cells;
|
|
2415
|
+
if (cells && typeof cells === "object" && !Array.isArray(cells)) {
|
|
2416
|
+
for (const [k, v] of Object.entries(cells)) {
|
|
2417
|
+
if (k.toLowerCase() === want) {
|
|
2418
|
+
return v != null ? String(v) : null;
|
|
2419
|
+
}
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
if (Array.isArray(cells)) {
|
|
2423
|
+
for (const c of cells) {
|
|
2424
|
+
const k = (c?.key ?? c?.field ?? c?.column_name ?? "").toString().toLowerCase();
|
|
2425
|
+
if (k === want) {
|
|
2426
|
+
const v = c?.value ?? null;
|
|
2427
|
+
return v != null ? String(v) : null;
|
|
2428
|
+
}
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
return null;
|
|
2432
|
+
}
|
|
2433
|
+
function buildInputLookup(inputs) {
|
|
2434
|
+
const validInputs = [];
|
|
2435
|
+
const malformed = [];
|
|
2436
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
2437
|
+
const byRowId = /* @__PURE__ */ new Map();
|
|
2438
|
+
inputs.forEach((inp, i) => {
|
|
2439
|
+
void i;
|
|
2440
|
+
const norm = normalizeDomain(inp.domain ?? "");
|
|
2441
|
+
if (!norm) {
|
|
2442
|
+
malformed.push(inp.domain ?? "");
|
|
2443
|
+
return;
|
|
2444
|
+
}
|
|
2445
|
+
if (byDomain.has(norm))
|
|
2446
|
+
return;
|
|
2447
|
+
const rowId = randomUUID();
|
|
2448
|
+
const idx = validInputs.length;
|
|
2449
|
+
validInputs.push({ index: idx, rowId, domain: norm, name: inp.name?.trim() || norm });
|
|
2450
|
+
byDomain.set(norm, idx);
|
|
2451
|
+
byRowId.set(rowId, idx);
|
|
2452
|
+
});
|
|
2453
|
+
return { validInputs, malformed, byDomain, byRowId };
|
|
2454
|
+
}
|
|
2455
|
+
async function pollUntil(fn, done, budgetMs, signal, ctx, label) {
|
|
2456
|
+
const deadline = Date.now() + budgetMs;
|
|
2457
|
+
let last;
|
|
2458
|
+
while (true) {
|
|
2459
|
+
checkAborted(signal);
|
|
2460
|
+
last = await fn();
|
|
2461
|
+
if (done(last))
|
|
2462
|
+
return last;
|
|
2463
|
+
if (Date.now() >= deadline) {
|
|
2464
|
+
ctx?.logger?.warn?.(`import-leads: ${label} budget exhausted (${budgetMs}ms)`);
|
|
2465
|
+
return last;
|
|
2466
|
+
}
|
|
2467
|
+
await sleepWithAbort(POLL_INTERVAL_MS, signal);
|
|
2468
|
+
}
|
|
2469
|
+
}
|
|
2470
|
+
async function pollPreprocess(client, importId, budgetMs, ctx, signal) {
|
|
2471
|
+
const result = await pollUntil(() => client.request("GET", `/imports/${importId}`), (r) => Boolean(r.pre_processing?.finished), budgetMs, signal, ctx, "preprocess");
|
|
2472
|
+
if (!result.pre_processing?.finished) {
|
|
2473
|
+
throw client.makeError("IMPORT_BUDGET_EXHAUSTED", `Preprocess phase did not finish within ${budgetMs}ms`, `Increase per_phase_budget_ms (current: ${budgetMs}) or split the batch. importId=${importId}.`, `GET /imports/${importId}`);
|
|
2474
|
+
}
|
|
2475
|
+
if (result.pre_processing.error) {
|
|
2476
|
+
throw client.makeError("IMPORT_PREPROCESS_FAILED", `Preprocess failed: ${result.pre_processing.error}`, `Check the input domains. importId=${importId} for backend debugging.`, `GET /imports/${importId}`);
|
|
2477
|
+
}
|
|
2478
|
+
return result;
|
|
2479
|
+
}
|
|
2480
|
+
async function pollProcess(client, importId, budgetMs, ctx, signal) {
|
|
2481
|
+
const result = await pollUntil(() => client.request("GET", `/imports/${importId}`), (r) => Boolean(r.processing?.finished), budgetMs, signal, ctx, "process");
|
|
2482
|
+
if (!result.processing?.finished) {
|
|
2483
|
+
throw client.makeError("IMPORT_BUDGET_EXHAUSTED", `Process phase did not finish within ${budgetMs}ms`, `Increase per_phase_budget_ms (current: ${budgetMs}) or split the batch. importId=${importId}.`, `GET /imports/${importId}`);
|
|
2484
|
+
}
|
|
2485
|
+
if (result.processing.error != null) {
|
|
2486
|
+
throw client.makeError("IMPORT_PROCESSING_FAILED", `Backend processing failed: ${result.processing.error}`, `importId=${importId}.`, `GET /imports/${importId}`);
|
|
2487
|
+
}
|
|
2488
|
+
return result;
|
|
2489
|
+
}
|
|
2490
|
+
async function pollRecordsToTerminal(client, importId, budgetMs, expectedRowCount, ctx, signal) {
|
|
2491
|
+
const deadline = Date.now() + budgetMs;
|
|
2492
|
+
const maxPagesPerPoll = Math.max(2, Math.ceil(expectedRowCount / 100) * 2 + 4);
|
|
2493
|
+
let stableCounts = 0;
|
|
2494
|
+
let lastSnapshot = null;
|
|
2495
|
+
while (true) {
|
|
2496
|
+
checkAborted(signal);
|
|
2497
|
+
let total = 0;
|
|
2498
|
+
let transient = 0;
|
|
2499
|
+
let pagesFetched = 0;
|
|
2500
|
+
let exhaustedPagination = false;
|
|
2501
|
+
const records = [];
|
|
2502
|
+
for (let page = 0; page < maxPagesPerPoll; page++) {
|
|
2503
|
+
checkAborted(signal);
|
|
2504
|
+
const qs = `count=100&page=${page}&automatic_match=true&manual_match=true&no_match=true&matching=true&importing=true&imported=true`;
|
|
2505
|
+
const res = await client.request("GET", `/imports/${importId}/records?${qs}`);
|
|
2506
|
+
pagesFetched++;
|
|
2507
|
+
records.push(...res.items);
|
|
2508
|
+
total = res.pagination.total ?? records.length;
|
|
2509
|
+
for (const r of res.items) {
|
|
2510
|
+
const status = (r.status ?? "").toString().toUpperCase();
|
|
2511
|
+
const matchType = (r.match_type ?? r.matchType ?? "").toString().toUpperCase();
|
|
2512
|
+
const isTerminal = matchType === "NO_MATCH" || status === "IMPORTED";
|
|
2513
|
+
if (!isTerminal)
|
|
2514
|
+
transient++;
|
|
2515
|
+
}
|
|
2516
|
+
const totalPages = res.pagination.pages ?? 0;
|
|
2517
|
+
if (page + 1 >= totalPages) {
|
|
2518
|
+
exhaustedPagination = true;
|
|
2519
|
+
break;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
if (!exhaustedPagination) {
|
|
2523
|
+
throw client.makeError("IMPORT_PAGINATION_RUNAWAY", `Records pagination exceeded ${maxPagesPerPoll} pages`, `importId=${importId}. Please file a bug at https://github.com/leadbay/leadclaw/issues.`, `GET /imports/${importId}/records`);
|
|
2524
|
+
}
|
|
2525
|
+
const snapshot = { total, transient };
|
|
2526
|
+
const settled = transient === 0;
|
|
2527
|
+
const stableVsLast = lastSnapshot != null && lastSnapshot.total === snapshot.total && lastSnapshot.transient === snapshot.transient;
|
|
2528
|
+
if (settled && stableVsLast) {
|
|
2529
|
+
stableCounts++;
|
|
2530
|
+
} else if (settled) {
|
|
2531
|
+
stableCounts = 1;
|
|
2532
|
+
} else {
|
|
2533
|
+
stableCounts = 0;
|
|
2534
|
+
}
|
|
2535
|
+
lastSnapshot = snapshot;
|
|
2536
|
+
if (settled && stableCounts >= STABILIZATION_POLLS) {
|
|
2537
|
+
return records;
|
|
2538
|
+
}
|
|
2539
|
+
if (Date.now() >= deadline) {
|
|
2540
|
+
ctx?.logger?.warn?.(`import-leads: records did not stabilize (transient=${transient}, total=${total}); returning best-effort`);
|
|
2541
|
+
throw client.makeError("IMPORT_NOT_TERMINAL", `Backend hasn't fully settled records within ${budgetMs}ms`, `Retry leadbay_import_leads with the same domains in 30s, or split the batch. importId=${importId}.`, `GET /imports/${importId}/records`);
|
|
2542
|
+
}
|
|
2543
|
+
await sleepWithAbort(POLL_INTERVAL_MS, signal);
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
async function runOneChunk(client, chunk, chunkIdx, totalChunks, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal, onImportId) {
|
|
2547
|
+
const csv = synthesizeCsv(chunk.map((c) => ({ rowId: c.rowId, name: c.name, website: c.domain })));
|
|
2548
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2549
|
+
const fileName = `mcp-import-${ts}-${chunkIdx}.csv`;
|
|
2550
|
+
ctx?.logger?.info?.(`import-leads: uploading chunk ${chunkIdx + 1}/${totalChunks} (${chunk.length} rows, ${csv.length}B)`);
|
|
2551
|
+
const upload = await client.requestRawBinary("POST", `/imports?file_name=${encodeURIComponent(fileName)}`, "text/csv", csv);
|
|
2552
|
+
const importId = upload.id;
|
|
2553
|
+
onImportId(importId);
|
|
2554
|
+
const phaseBudget = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
2555
|
+
await pollPreprocess(client, importId, phaseBudget, ctx, signal);
|
|
2556
|
+
ctx?.logger?.info?.(`import-leads: preprocess done for importId=${importId}`);
|
|
2557
|
+
if (dryRun) {
|
|
2558
|
+
return { importId, records: [] };
|
|
2559
|
+
}
|
|
2560
|
+
const mappings = {
|
|
2561
|
+
fields: { LEAD_NAME: "LEAD_NAME", LEAD_WEBSITE: "LEAD_WEBSITE" },
|
|
2562
|
+
statuses: {},
|
|
2563
|
+
default_status: null
|
|
2564
|
+
};
|
|
2565
|
+
await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
2566
|
+
ctx?.logger?.info?.(`import-leads: mappings committed for importId=${importId}`);
|
|
2567
|
+
const phaseBudget2 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
2568
|
+
await pollProcess(client, importId, phaseBudget2, ctx, signal);
|
|
2569
|
+
ctx?.logger?.info?.(`import-leads: process done for importId=${importId}`);
|
|
2570
|
+
const phaseBudget3 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
2571
|
+
const records = await pollRecordsToTerminal(client, importId, phaseBudget3, chunk.length, ctx, signal);
|
|
2572
|
+
ctx?.logger?.info?.(`import-leads: ${records.length} records terminal for importId=${importId}`);
|
|
2573
|
+
return { importId, records };
|
|
2574
|
+
}
|
|
2575
|
+
function reconcileOneChunk(chunk, byRowIdGlobal, byDomainGlobal, validInputsGlobal, matched, notImported) {
|
|
2576
|
+
const seenInputIndex = /* @__PURE__ */ new Set();
|
|
2577
|
+
const sortedRecords = [...chunk.records].sort((a, b) => {
|
|
2578
|
+
const aHasLead = a.lead?.id ? 0 : 1;
|
|
2579
|
+
const bHasLead = b.lead?.id ? 0 : 1;
|
|
2580
|
+
return aHasLead - bHasLead;
|
|
2581
|
+
});
|
|
2582
|
+
for (const rec of sortedRecords) {
|
|
2583
|
+
let inputIdx;
|
|
2584
|
+
const rowIdCell = readCell(rec, "MCP_ROW_ID");
|
|
2585
|
+
if (rowIdCell && byRowIdGlobal.has(rowIdCell)) {
|
|
2586
|
+
inputIdx = byRowIdGlobal.get(rowIdCell);
|
|
2587
|
+
}
|
|
2588
|
+
if (inputIdx === void 0) {
|
|
2589
|
+
const websiteCell = readCell(rec, "LEAD_WEBSITE");
|
|
2590
|
+
if (websiteCell) {
|
|
2591
|
+
const norm = normalizeDomain(websiteCell);
|
|
2592
|
+
if (norm && byDomainGlobal.has(norm)) {
|
|
2593
|
+
inputIdx = byDomainGlobal.get(norm);
|
|
2594
|
+
}
|
|
2595
|
+
}
|
|
2596
|
+
}
|
|
2597
|
+
if (inputIdx === void 0 && rec.lead?.website) {
|
|
2598
|
+
const norm = normalizeDomain(rec.lead.website);
|
|
2599
|
+
if (norm && byDomainGlobal.has(norm)) {
|
|
2600
|
+
inputIdx = byDomainGlobal.get(norm);
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
if (inputIdx === void 0)
|
|
2604
|
+
continue;
|
|
2605
|
+
if (seenInputIndex.has(inputIdx)) {
|
|
2606
|
+
if (!matched.has(inputIdx) && !notImported.has(inputIdx)) {
|
|
2607
|
+
const inp2 = validInputsGlobal[inputIdx];
|
|
2608
|
+
notImported.set(inputIdx, { domain: inp2.domain, reason: "ambiguous" });
|
|
2609
|
+
}
|
|
2610
|
+
continue;
|
|
2611
|
+
}
|
|
2612
|
+
seenInputIndex.add(inputIdx);
|
|
2613
|
+
const inp = validInputsGlobal[inputIdx];
|
|
2614
|
+
const matchType = (rec.match_type ?? rec.matchType ?? "").toString();
|
|
2615
|
+
if (rec.lead?.id) {
|
|
2616
|
+
matched.set(inputIdx, {
|
|
2617
|
+
domain: inp.domain,
|
|
2618
|
+
leadId: rec.lead.id,
|
|
2619
|
+
name: rec.lead.name ?? null
|
|
2620
|
+
});
|
|
2621
|
+
} else if (matchType === "NO_MATCH") {
|
|
2622
|
+
const reason = PUBLIC_MAILBOX_DOMAINS.has(inp.domain) ? "no_match" : "uncrawled";
|
|
2623
|
+
notImported.set(inputIdx, { domain: inp.domain, reason });
|
|
2624
|
+
} else {
|
|
2625
|
+
notImported.set(inputIdx, { domain: inp.domain, reason: "internal_error" });
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
var importLeads = {
|
|
2630
|
+
name: "leadbay_import_leads",
|
|
2631
|
+
description: "Import a list of company domains and get back stable Leadbay leadIds for downstream chaining into leadbay_bulk_qualify_leads / leadbay_research_lead.\n\n\u26A0\uFE0F MUTATES USER STATE. This tool wraps Leadbay's CRM-import wizard. Each call:\n - creates a row in the user's CRM-imports list (visible in the web UI)\n - touches onboarding state (startFileless, onboarding step \u2192 PROCESSING)\nSuitable for occasional automation. NOT suitable for high-cadence (>5 calls/day) \u2014 wait for the backend programmatic endpoint (issue: leadbay/backend prolonged-import-with-crawl).\n\nReturns: leads = leadIds for domains Leadbay already knows about (via crawler). not_imported = domains Leadbay doesn't know yet, with a reason. The tool does NOT create new leads for unknown domains; the caller decides what to do.\n\nWhen to use: you have a list of domains from another system (CRM, analytics, email correspondents) and need to map them to Leadbay leadIds.\nWhen NOT to use: for prospect discovery (use leadbay_pull_leads); for one specific company's profile (use leadbay_research_company); when you can't tolerate the side effects above.\n\nRequires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role on the Leadbay account; active billing.",
|
|
2632
|
+
write: true,
|
|
2633
|
+
version: "0.1.0",
|
|
2634
|
+
inputSchema: {
|
|
2635
|
+
type: "object",
|
|
2636
|
+
properties: {
|
|
2637
|
+
domains: {
|
|
2638
|
+
type: "array",
|
|
2639
|
+
description: "List of company domains to map to Leadbay leadIds.",
|
|
2640
|
+
items: {
|
|
2641
|
+
type: "object",
|
|
2642
|
+
properties: {
|
|
2643
|
+
domain: {
|
|
2644
|
+
type: "string",
|
|
2645
|
+
description: "Company domain (e.g. 'apple.com'). Protocol/path are stripped."
|
|
2646
|
+
},
|
|
2647
|
+
name: {
|
|
2648
|
+
type: "string",
|
|
2649
|
+
description: "Optional display name override; defaults to the domain."
|
|
2650
|
+
}
|
|
2651
|
+
},
|
|
2652
|
+
required: ["domain"]
|
|
2653
|
+
}
|
|
2654
|
+
},
|
|
2655
|
+
dry_run: {
|
|
2656
|
+
type: "boolean",
|
|
2657
|
+
description: "If true, run preprocess only \u2014 do NOT commit lead-CRM linking. Note: an import row still appears in the user's CRM-imports list as 'incomplete'. Use to verify domain format / wizard reachability without polluting the CRM."
|
|
2658
|
+
},
|
|
2659
|
+
per_phase_budget_ms: {
|
|
2660
|
+
type: "number",
|
|
2661
|
+
description: `Single poll-loop cap (default ${DEFAULT_PER_PHASE_BUDGET_MS}ms).`
|
|
2662
|
+
},
|
|
2663
|
+
total_budget_ms: {
|
|
2664
|
+
type: "number",
|
|
2665
|
+
description: `Overall cap across all phases (default ${DEFAULT_TOTAL_BUDGET_MS2}ms).`
|
|
2666
|
+
}
|
|
2667
|
+
},
|
|
2668
|
+
required: ["domains"]
|
|
2669
|
+
},
|
|
2670
|
+
execute: async (client, params, ctx) => {
|
|
2671
|
+
const signal = ctx?.signal;
|
|
2672
|
+
const dryRun = Boolean(params.dry_run);
|
|
2673
|
+
const perPhaseBudget = params.per_phase_budget_ms ?? DEFAULT_PER_PHASE_BUDGET_MS;
|
|
2674
|
+
const totalBudget = params.total_budget_ms ?? DEFAULT_TOTAL_BUDGET_MS2;
|
|
2675
|
+
const totalDeadline = Date.now() + totalBudget;
|
|
2676
|
+
if (!Array.isArray(params.domains) || params.domains.length === 0) {
|
|
2677
|
+
throw client.makeError("IMPORT_EMPTY_INPUT", "domains[] must contain at least one entry", "Pass at least one domain in domains[].", "POST /imports");
|
|
2678
|
+
}
|
|
2679
|
+
const me = await client.resolveMe();
|
|
2680
|
+
if (!me.admin) {
|
|
2681
|
+
throw client.makeError("IMPORT_ADMIN_REQUIRED", "This tool requires admin role on the Leadbay account", "Ask the account owner to grant import permission, or use a token from an admin user.", "POST /imports");
|
|
2682
|
+
}
|
|
2683
|
+
const lookup = buildInputLookup(params.domains);
|
|
2684
|
+
if (lookup.validInputs.length === 0) {
|
|
2685
|
+
const not_imported2 = lookup.malformed.map((d) => ({
|
|
2686
|
+
domain: d,
|
|
2687
|
+
reason: "malformed"
|
|
2688
|
+
}));
|
|
2689
|
+
return {
|
|
2690
|
+
leads: [],
|
|
2691
|
+
not_imported: not_imported2,
|
|
2692
|
+
importIds: [],
|
|
2693
|
+
region: client.region,
|
|
2694
|
+
dry_run: dryRun || void 0,
|
|
2695
|
+
_meta: client.lastMeta ?? {
|
|
2696
|
+
region: client.region,
|
|
2697
|
+
endpoint: "POST /imports",
|
|
2698
|
+
latency_ms: null,
|
|
2699
|
+
retry_after: null
|
|
2700
|
+
}
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
2703
|
+
const chunks = chunkAt100(lookup.validInputs);
|
|
2704
|
+
ctx?.logger?.info?.(`import-leads: ${lookup.validInputs.length} domains \u2192 ${chunks.length} chunk(s); dry_run=${dryRun}, totalBudgetMs=${totalBudget}`);
|
|
2705
|
+
const importIds = [];
|
|
2706
|
+
const matched = /* @__PURE__ */ new Map();
|
|
2707
|
+
const notImported = /* @__PURE__ */ new Map();
|
|
2708
|
+
let cancelled = false;
|
|
2709
|
+
const recordImportId = (id) => {
|
|
2710
|
+
if (!importIds.includes(id))
|
|
2711
|
+
importIds.push(id);
|
|
2712
|
+
};
|
|
2713
|
+
try {
|
|
2714
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2715
|
+
const chunk = chunks[i];
|
|
2716
|
+
const out = await runOneChunk(client, chunk, i, chunks.length, dryRun, perPhaseBudget, totalDeadline, ctx, signal, recordImportId);
|
|
2717
|
+
if (!dryRun) {
|
|
2718
|
+
reconcileOneChunk(out, lookup.byRowId, lookup.byDomain, lookup.validInputs, matched, notImported);
|
|
2719
|
+
} else {
|
|
2720
|
+
for (const c of chunk) {
|
|
2721
|
+
notImported.set(c.index, { domain: c.domain, reason: "dry_run" });
|
|
2722
|
+
}
|
|
2723
|
+
}
|
|
2724
|
+
}
|
|
2725
|
+
} catch (err) {
|
|
2726
|
+
if (err?.name === "AbortError") {
|
|
2727
|
+
cancelled = true;
|
|
2728
|
+
ctx?.logger?.info?.(`import-leads: aborted via signal; importIds=${importIds.join(",")}`);
|
|
2729
|
+
} else if (err?.error === true) {
|
|
2730
|
+
if (err.code === "FORBIDDEN") {
|
|
2731
|
+
throw client.makeError("IMPORT_ADMIN_REQUIRED", err.message || "Insufficient permissions for /imports", "This tool requires admin role on the Leadbay account. Ask the account owner.", err._meta?.endpoint);
|
|
2732
|
+
}
|
|
2733
|
+
if (err.code === "BILLING_SUSPENDED") {
|
|
2734
|
+
throw client.makeError("IMPORT_BILLING_REQUIRED", err.message || "Active billing required for imports", "Upgrade at https://app.leadbay.ai/billing, then retry.", err._meta?.endpoint);
|
|
2735
|
+
}
|
|
2736
|
+
throw err;
|
|
2737
|
+
} else {
|
|
2738
|
+
throw err;
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
for (const m of lookup.malformed) {
|
|
2742
|
+
notImported.set(-1 - notImported.size, { domain: m, reason: "malformed" });
|
|
2743
|
+
}
|
|
2744
|
+
const leads = [];
|
|
2745
|
+
const not_imported = [];
|
|
2746
|
+
if (dryRun) {
|
|
2747
|
+
for (const inp of lookup.validInputs) {
|
|
2748
|
+
not_imported.push({ domain: inp.domain, reason: "dry_run" });
|
|
2749
|
+
}
|
|
2750
|
+
} else {
|
|
2751
|
+
for (const inp of lookup.validInputs) {
|
|
2752
|
+
const m = matched.get(inp.index);
|
|
2753
|
+
if (m) {
|
|
2754
|
+
leads.push(m);
|
|
2755
|
+
continue;
|
|
2756
|
+
}
|
|
2757
|
+
const ni = notImported.get(inp.index);
|
|
2758
|
+
if (ni) {
|
|
2759
|
+
not_imported.push(ni);
|
|
2760
|
+
continue;
|
|
2761
|
+
}
|
|
2762
|
+
not_imported.push({ domain: inp.domain, reason: "internal_error" });
|
|
2763
|
+
}
|
|
2764
|
+
}
|
|
2765
|
+
for (const [k, v] of notImported) {
|
|
2766
|
+
if (k < 0)
|
|
2767
|
+
not_imported.push(v);
|
|
2768
|
+
}
|
|
2769
|
+
return {
|
|
2770
|
+
leads,
|
|
2771
|
+
not_imported,
|
|
2772
|
+
importIds,
|
|
2773
|
+
region: client.region,
|
|
2774
|
+
cancelled: cancelled || void 0,
|
|
2775
|
+
dry_run: dryRun || void 0,
|
|
2776
|
+
_meta: client.lastMeta ?? {
|
|
2777
|
+
region: client.region,
|
|
2778
|
+
endpoint: "POST /imports",
|
|
2779
|
+
latency_ms: null,
|
|
2780
|
+
retry_after: null
|
|
2781
|
+
}
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
};
|
|
2785
|
+
|
|
2193
2786
|
// ../core/dist/composite/enrich-titles.js
|
|
2194
2787
|
var DEFAULT_CANDIDATE_COUNT = 25;
|
|
2195
2788
|
var enrichTitles = {
|
|
@@ -2421,7 +3014,7 @@ import { mkdir as mkdirAsync, lstat, open as fsOpen, readFile, rename, stat, unl
|
|
|
2421
3014
|
import { constants as fsConstants } from "fs";
|
|
2422
3015
|
import { dirname, resolve as resolvePath } from "path";
|
|
2423
3016
|
import { homedir, platform } from "os";
|
|
2424
|
-
import { createHash, randomUUID } from "crypto";
|
|
3017
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
2425
3018
|
var DEFAULT_IDEMPOTENCY_WINDOW_MS = 5 * 60 * 1e3;
|
|
2426
3019
|
var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2427
3020
|
var UUIDV4_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
@@ -2675,7 +3268,7 @@ var LocalBulkStore = class {
|
|
|
2675
3268
|
};
|
|
2676
3269
|
}
|
|
2677
3270
|
const record = {
|
|
2678
|
-
bulk_id:
|
|
3271
|
+
bulk_id: randomUUID2(),
|
|
2679
3272
|
launched_at: new Date(nowMs).toISOString(),
|
|
2680
3273
|
lead_ids,
|
|
2681
3274
|
titles,
|
|
@@ -3205,14 +3798,14 @@ var refinePrompt = {
|
|
|
3205
3798
|
would_call: {
|
|
3206
3799
|
method: "POST",
|
|
3207
3800
|
path: `/organizations/${orgId}/user_prompt`,
|
|
3208
|
-
body: {
|
|
3801
|
+
body: { user_prompt: params.prompt }
|
|
3209
3802
|
}
|
|
3210
3803
|
};
|
|
3211
3804
|
}
|
|
3212
3805
|
const postedAt = Date.now();
|
|
3213
3806
|
const STALE_GUARD_MS = 5e3;
|
|
3214
3807
|
await client.requestVoid("POST", `/organizations/${orgId}/user_prompt`, {
|
|
3215
|
-
|
|
3808
|
+
user_prompt: params.prompt
|
|
3216
3809
|
});
|
|
3217
3810
|
client.invalidateMe();
|
|
3218
3811
|
const attempts = params.clarification_poll_attempts ?? DEFAULT_POLL_ATTEMPTS;
|
|
@@ -3521,7 +4114,8 @@ var compositeWriteTools = [
|
|
|
3521
4114
|
adjustAudience,
|
|
3522
4115
|
refinePrompt,
|
|
3523
4116
|
answerClarification,
|
|
3524
|
-
reportOutreach
|
|
4117
|
+
reportOutreach,
|
|
4118
|
+
importLeads
|
|
3525
4119
|
];
|
|
3526
4120
|
var compositeTools = [
|
|
3527
4121
|
...compositeReadTools,
|
|
@@ -3532,6 +4126,7 @@ var tools = [...compositeTools, ...granularTools];
|
|
|
3532
4126
|
export {
|
|
3533
4127
|
REGIONS,
|
|
3534
4128
|
createClient,
|
|
4129
|
+
formatLoginError,
|
|
3535
4130
|
resolveRegion,
|
|
3536
4131
|
getMockJournal,
|
|
3537
4132
|
clearMockJournal,
|
|
@@ -3582,6 +4177,7 @@ export {
|
|
|
3582
4177
|
recallOrderedTitles,
|
|
3583
4178
|
accountStatus,
|
|
3584
4179
|
bulkQualifyLeads,
|
|
4180
|
+
importLeads,
|
|
3585
4181
|
enrichTitles,
|
|
3586
4182
|
isValidBulkId,
|
|
3587
4183
|
LocalBulkStore,
|