@leadbay/mcp 0.2.2 → 0.4.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 +91 -0
- package/MIGRATION.md +72 -0
- package/README.md +74 -23
- package/dist/bin.js +358 -75
- package/dist/{chunk-FJBO2MY2.js → chunk-O2UOXRZO.js} +778 -12
- package/dist/{dist-FENQ2I7R.js → dist-RONMQBYU.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,680 @@ 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 MAX_COLUMN_NAME_LEN = 128;
|
|
2290
|
+
var RESERVED_COLUMN_RE = /^mcp_row_id$/i;
|
|
2291
|
+
var PUBLIC_MAILBOX_DOMAINS = /* @__PURE__ */ new Set([
|
|
2292
|
+
"gmail.com",
|
|
2293
|
+
"googlemail.com",
|
|
2294
|
+
"yahoo.com",
|
|
2295
|
+
"ymail.com",
|
|
2296
|
+
"outlook.com",
|
|
2297
|
+
"hotmail.com",
|
|
2298
|
+
"live.com",
|
|
2299
|
+
"icloud.com",
|
|
2300
|
+
"me.com",
|
|
2301
|
+
"mac.com",
|
|
2302
|
+
"aol.com",
|
|
2303
|
+
"proton.me",
|
|
2304
|
+
"protonmail.com",
|
|
2305
|
+
"tutanota.com",
|
|
2306
|
+
"gmx.com",
|
|
2307
|
+
"gmx.net",
|
|
2308
|
+
"gmx.de",
|
|
2309
|
+
"mail.com",
|
|
2310
|
+
"yandex.com",
|
|
2311
|
+
"yandex.ru",
|
|
2312
|
+
"qq.com",
|
|
2313
|
+
"163.com",
|
|
2314
|
+
"126.com"
|
|
2315
|
+
]);
|
|
2316
|
+
function normalizeDomain(input) {
|
|
2317
|
+
if (!input || typeof input !== "string")
|
|
2318
|
+
return null;
|
|
2319
|
+
let v = input.trim().toLowerCase();
|
|
2320
|
+
if (!v)
|
|
2321
|
+
return null;
|
|
2322
|
+
v = v.replace(/^https?:\/\//, "");
|
|
2323
|
+
v = v.replace(/^www\./, "");
|
|
2324
|
+
v = v.split("/")[0].split("?")[0].split("#")[0];
|
|
2325
|
+
v = v.replace(/\.+$/, "");
|
|
2326
|
+
if (!v)
|
|
2327
|
+
return null;
|
|
2328
|
+
if (/\s/.test(v))
|
|
2329
|
+
return null;
|
|
2330
|
+
if (!v.includes("."))
|
|
2331
|
+
return null;
|
|
2332
|
+
if (v.startsWith(".") || v.endsWith("."))
|
|
2333
|
+
return null;
|
|
2334
|
+
const parts = v.split(".");
|
|
2335
|
+
if (parts.length < 2)
|
|
2336
|
+
return null;
|
|
2337
|
+
if (parts.some((p) => p.length === 0))
|
|
2338
|
+
return null;
|
|
2339
|
+
const tld = parts[parts.length - 1];
|
|
2340
|
+
if (!/^[a-z]{2,}$/.test(tld) && !tld.startsWith("xn--"))
|
|
2341
|
+
return null;
|
|
2342
|
+
if (!/^[a-z0-9-]+$/.test(parts[parts.length - 2]))
|
|
2343
|
+
return null;
|
|
2344
|
+
return v;
|
|
2345
|
+
}
|
|
2346
|
+
function escapeCsvCell(raw) {
|
|
2347
|
+
if (raw == null)
|
|
2348
|
+
return "";
|
|
2349
|
+
let s = String(raw);
|
|
2350
|
+
const trimmed = s.replace(/^[\s\r\n\t]+/, "");
|
|
2351
|
+
if (trimmed.length > 0) {
|
|
2352
|
+
const first = trimmed[0];
|
|
2353
|
+
if (first === "=" || first === "+" || first === "-" || first === "@") {
|
|
2354
|
+
s = "'" + s;
|
|
2355
|
+
}
|
|
2356
|
+
}
|
|
2357
|
+
if (/[",\n\r]/.test(s)) {
|
|
2358
|
+
s = '"' + s.replace(/"/g, '""') + '"';
|
|
2359
|
+
}
|
|
2360
|
+
return s;
|
|
2361
|
+
}
|
|
2362
|
+
function synthesizeCsv(header, rows) {
|
|
2363
|
+
const headerLine = header.map(escapeCsvCell).join(",");
|
|
2364
|
+
const dataLines = rows.map((r) => header.map((col) => escapeCsvCell(r[col] ?? "")).join(","));
|
|
2365
|
+
return [headerLine, ...dataLines].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 validateColumnName(client, name, path) {
|
|
2434
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
2435
|
+
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");
|
|
2436
|
+
}
|
|
2437
|
+
if (name.length > MAX_COLUMN_NAME_LEN) {
|
|
2438
|
+
throw client.makeError("IMPORT_INVALID_COLUMN_NAME", `Column name at ${path} exceeds ${MAX_COLUMN_NAME_LEN} chars`, `Shorten the column name to ${MAX_COLUMN_NAME_LEN} chars or fewer.`, "POST /imports");
|
|
2439
|
+
}
|
|
2440
|
+
if (/[\x00-\x1F\x7F]/.test(name)) {
|
|
2441
|
+
throw client.makeError("IMPORT_INVALID_COLUMN_NAME", `Column name at ${path} contains control characters`, `Strip control characters (e.g. \\n, \\t, \\x00) from column names.`, "POST /imports");
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
2444
|
+
function coerceCell(client, v, path) {
|
|
2445
|
+
if (v == null)
|
|
2446
|
+
return "";
|
|
2447
|
+
if (typeof v === "string")
|
|
2448
|
+
return v;
|
|
2449
|
+
if (typeof v === "number" || typeof v === "boolean")
|
|
2450
|
+
return String(v);
|
|
2451
|
+
throw client.makeError("IMPORT_INVALID_CELL_TYPE", `Cell at ${path} is ${Array.isArray(v) ? "an array" : typeof v}, expected string|number|boolean|null`, `Convert the value to a string before passing.`, "POST /imports");
|
|
2452
|
+
}
|
|
2453
|
+
function prepareDomainsMode(client, inputs) {
|
|
2454
|
+
const validInputs = [];
|
|
2455
|
+
const malformedDomains = [];
|
|
2456
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
2457
|
+
const byRowId = /* @__PURE__ */ new Map();
|
|
2458
|
+
for (const inp of inputs) {
|
|
2459
|
+
const norm = normalizeDomain(inp?.domain ?? "");
|
|
2460
|
+
if (!norm) {
|
|
2461
|
+
malformedDomains.push(inp?.domain ?? "");
|
|
2462
|
+
continue;
|
|
2463
|
+
}
|
|
2464
|
+
if (byDomain.has(norm))
|
|
2465
|
+
continue;
|
|
2466
|
+
const rowId = randomUUID();
|
|
2467
|
+
const idx = validInputs.length;
|
|
2468
|
+
const name = inp.name?.trim() || norm;
|
|
2469
|
+
validInputs.push({
|
|
2470
|
+
index: idx,
|
|
2471
|
+
rowId,
|
|
2472
|
+
row: { MCP_ROW_ID: rowId, LEAD_NAME: name, LEAD_WEBSITE: norm },
|
|
2473
|
+
domain: norm,
|
|
2474
|
+
outputDomain: norm
|
|
2475
|
+
});
|
|
2476
|
+
byDomain.set(norm, idx);
|
|
2477
|
+
byRowId.set(rowId, idx);
|
|
2478
|
+
}
|
|
2479
|
+
return {
|
|
2480
|
+
mode: "domains",
|
|
2481
|
+
validInputs,
|
|
2482
|
+
malformedDomains,
|
|
2483
|
+
byDomain,
|
|
2484
|
+
byRowId,
|
|
2485
|
+
header: ["MCP_ROW_ID", "LEAD_NAME", "LEAD_WEBSITE"],
|
|
2486
|
+
mappings: {
|
|
2487
|
+
fields: { LEAD_NAME: "LEAD_NAME", LEAD_WEBSITE: "LEAD_WEBSITE" },
|
|
2488
|
+
statuses: {},
|
|
2489
|
+
default_status: null
|
|
2490
|
+
}
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
function prepareRecordsMode(client, records, mappings) {
|
|
2494
|
+
if (!mappings || !mappings.fields || typeof mappings.fields !== "object") {
|
|
2495
|
+
throw client.makeError("IMPORT_MAPPING_REQUIRED", "records[] requires a mappings.fields object", "Pass `mappings: { fields: { CsvColumn: 'LEAD_NAME', ... } }`.", "POST /imports");
|
|
2496
|
+
}
|
|
2497
|
+
const fieldEntries = Object.entries(mappings.fields);
|
|
2498
|
+
if (fieldEntries.length === 0) {
|
|
2499
|
+
throw client.makeError("IMPORT_MAPPING_REQUIRED", "mappings.fields must contain at least one column \u2192 CRM field entry", "Map at least one CSV column to LEAD_NAME or LEAD_WEBSITE.", "POST /imports");
|
|
2500
|
+
}
|
|
2501
|
+
const targets = new Set(fieldEntries.map(([, v]) => v));
|
|
2502
|
+
if (!targets.has("LEAD_NAME") && !targets.has("LEAD_WEBSITE")) {
|
|
2503
|
+
throw client.makeError("IMPORT_MAPPING_NO_RESOLVER", "mappings.fields must include LEAD_NAME or LEAD_WEBSITE", "The wizard needs at least one of those fields to match a lead. Map a CSV column to one of them.", "POST /imports");
|
|
2504
|
+
}
|
|
2505
|
+
for (const [colName] of fieldEntries) {
|
|
2506
|
+
validateColumnName(client, colName, `mappings.fields[${JSON.stringify(colName)}]`);
|
|
2507
|
+
if (RESERVED_COLUMN_RE.test(colName)) {
|
|
2508
|
+
throw client.makeError("IMPORT_RESERVED_COLUMN", `mappings.fields key '${colName}' collides with reserved synthetic column MCP_ROW_ID`, `Rename the column. MCP_ROW_ID (any case) is reserved for tool-internal reconciliation.`, "POST /imports");
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
const headerSet = /* @__PURE__ */ new Set();
|
|
2512
|
+
const coercedRecords = [];
|
|
2513
|
+
records.forEach((rec, i) => {
|
|
2514
|
+
if (rec == null || typeof rec !== "object" || Array.isArray(rec)) {
|
|
2515
|
+
throw client.makeError("IMPORT_INVALID_CELL_TYPE", `records[${i}] must be a plain object`, `Pass each record as { ColumnName: value, ... }.`, "POST /imports");
|
|
2516
|
+
}
|
|
2517
|
+
const out = {};
|
|
2518
|
+
for (const [k, v] of Object.entries(rec)) {
|
|
2519
|
+
validateColumnName(client, k, `records[${i}] key`);
|
|
2520
|
+
if (RESERVED_COLUMN_RE.test(k)) {
|
|
2521
|
+
throw client.makeError("IMPORT_RESERVED_COLUMN", `records[${i}] key '${k}' collides with reserved synthetic column MCP_ROW_ID`, `Rename the column in your records (any case variant of MCP_ROW_ID is reserved).`, "POST /imports");
|
|
2522
|
+
}
|
|
2523
|
+
out[k] = coerceCell(client, v, `records[${i}].${k}`);
|
|
2524
|
+
headerSet.add(k);
|
|
2525
|
+
}
|
|
2526
|
+
coercedRecords.push(out);
|
|
2527
|
+
});
|
|
2528
|
+
for (const [colName] of fieldEntries) {
|
|
2529
|
+
if (!headerSet.has(colName)) {
|
|
2530
|
+
throw client.makeError("IMPORT_MAPPING_KEY_UNKNOWN", `mappings.fields key '${colName}' is not present in any record`, `Add a value for '${colName}' to at least one record, or remove it from mappings.`, "POST /imports");
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
const userKeys = [...headerSet].sort();
|
|
2534
|
+
const header = ["MCP_ROW_ID", ...userKeys];
|
|
2535
|
+
const websiteCol = fieldEntries.find(([, v]) => v === "LEAD_WEBSITE")?.[0];
|
|
2536
|
+
const validInputs = [];
|
|
2537
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
2538
|
+
const byRowId = /* @__PURE__ */ new Map();
|
|
2539
|
+
coercedRecords.forEach((row) => {
|
|
2540
|
+
const rowId = randomUUID();
|
|
2541
|
+
const idx = validInputs.length;
|
|
2542
|
+
let normDomain = null;
|
|
2543
|
+
if (websiteCol) {
|
|
2544
|
+
normDomain = normalizeDomain(row[websiteCol] ?? "");
|
|
2545
|
+
}
|
|
2546
|
+
const fullRow = { MCP_ROW_ID: rowId, ...row };
|
|
2547
|
+
validInputs.push({
|
|
2548
|
+
index: idx,
|
|
2549
|
+
rowId,
|
|
2550
|
+
row: fullRow,
|
|
2551
|
+
domain: normDomain,
|
|
2552
|
+
outputDomain: normDomain ?? void 0
|
|
2553
|
+
});
|
|
2554
|
+
byRowId.set(rowId, idx);
|
|
2555
|
+
if (normDomain && !byDomain.has(normDomain))
|
|
2556
|
+
byDomain.set(normDomain, idx);
|
|
2557
|
+
});
|
|
2558
|
+
return {
|
|
2559
|
+
mode: "records",
|
|
2560
|
+
validInputs,
|
|
2561
|
+
malformedDomains: [],
|
|
2562
|
+
byDomain,
|
|
2563
|
+
byRowId,
|
|
2564
|
+
header,
|
|
2565
|
+
mappings: {
|
|
2566
|
+
fields: { ...mappings.fields },
|
|
2567
|
+
statuses: mappings.statuses ?? {},
|
|
2568
|
+
default_status: mappings.default_status ?? null
|
|
2569
|
+
}
|
|
2570
|
+
};
|
|
2571
|
+
}
|
|
2572
|
+
async function pollUntil(fn, done, budgetMs, signal, ctx, label) {
|
|
2573
|
+
const deadline = Date.now() + budgetMs;
|
|
2574
|
+
let last;
|
|
2575
|
+
while (true) {
|
|
2576
|
+
checkAborted(signal);
|
|
2577
|
+
last = await fn();
|
|
2578
|
+
if (done(last))
|
|
2579
|
+
return last;
|
|
2580
|
+
if (Date.now() >= deadline) {
|
|
2581
|
+
ctx?.logger?.warn?.(`import-leads: ${label} budget exhausted (${budgetMs}ms)`);
|
|
2582
|
+
return last;
|
|
2583
|
+
}
|
|
2584
|
+
await sleepWithAbort(POLL_INTERVAL_MS, signal);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
async function pollPreprocess(client, importId, budgetMs, ctx, signal) {
|
|
2588
|
+
const result = await pollUntil(() => client.request("GET", `/imports/${importId}`), (r) => Boolean(r.pre_processing?.finished), budgetMs, signal, ctx, "preprocess");
|
|
2589
|
+
if (!result.pre_processing?.finished) {
|
|
2590
|
+
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}`);
|
|
2591
|
+
}
|
|
2592
|
+
if (result.pre_processing.error) {
|
|
2593
|
+
throw client.makeError("IMPORT_PREPROCESS_FAILED", `Preprocess failed: ${result.pre_processing.error}`, `Check the input domains. importId=${importId} for backend debugging.`, `GET /imports/${importId}`);
|
|
2594
|
+
}
|
|
2595
|
+
return result;
|
|
2596
|
+
}
|
|
2597
|
+
async function pollProcess(client, importId, budgetMs, ctx, signal) {
|
|
2598
|
+
const result = await pollUntil(() => client.request("GET", `/imports/${importId}`), (r) => Boolean(r.processing?.finished), budgetMs, signal, ctx, "process");
|
|
2599
|
+
if (!result.processing?.finished) {
|
|
2600
|
+
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}`);
|
|
2601
|
+
}
|
|
2602
|
+
if (result.processing.error != null) {
|
|
2603
|
+
throw client.makeError("IMPORT_PROCESSING_FAILED", `Backend processing failed: ${result.processing.error}`, `importId=${importId}.`, `GET /imports/${importId}`);
|
|
2604
|
+
}
|
|
2605
|
+
return result;
|
|
2606
|
+
}
|
|
2607
|
+
async function pollRecordsToTerminal(client, importId, budgetMs, expectedRowCount, ctx, signal) {
|
|
2608
|
+
const deadline = Date.now() + budgetMs;
|
|
2609
|
+
const maxPagesPerPoll = Math.max(2, Math.ceil(expectedRowCount / 100) * 2 + 4);
|
|
2610
|
+
let stableCounts = 0;
|
|
2611
|
+
let lastSnapshot = null;
|
|
2612
|
+
while (true) {
|
|
2613
|
+
checkAborted(signal);
|
|
2614
|
+
let total = 0;
|
|
2615
|
+
let transient = 0;
|
|
2616
|
+
let pagesFetched = 0;
|
|
2617
|
+
let exhaustedPagination = false;
|
|
2618
|
+
const records = [];
|
|
2619
|
+
for (let page = 0; page < maxPagesPerPoll; page++) {
|
|
2620
|
+
checkAborted(signal);
|
|
2621
|
+
const qs = `count=100&page=${page}&automatic_match=true&manual_match=true&no_match=true&matching=true&importing=true&imported=true`;
|
|
2622
|
+
const res = await client.request("GET", `/imports/${importId}/records?${qs}`);
|
|
2623
|
+
pagesFetched++;
|
|
2624
|
+
records.push(...res.items);
|
|
2625
|
+
total = res.pagination.total ?? records.length;
|
|
2626
|
+
for (const r of res.items) {
|
|
2627
|
+
const status = (r.status ?? "").toString().toUpperCase();
|
|
2628
|
+
const matchType = (r.match_type ?? r.matchType ?? "").toString().toUpperCase();
|
|
2629
|
+
const isTerminal = matchType === "NO_MATCH" || status === "IMPORTED";
|
|
2630
|
+
if (!isTerminal)
|
|
2631
|
+
transient++;
|
|
2632
|
+
}
|
|
2633
|
+
const totalPages = res.pagination.pages ?? 0;
|
|
2634
|
+
if (page + 1 >= totalPages) {
|
|
2635
|
+
exhaustedPagination = true;
|
|
2636
|
+
break;
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
if (!exhaustedPagination) {
|
|
2640
|
+
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`);
|
|
2641
|
+
}
|
|
2642
|
+
const snapshot = { total, transient };
|
|
2643
|
+
const settled = transient === 0;
|
|
2644
|
+
const stableVsLast = lastSnapshot != null && lastSnapshot.total === snapshot.total && lastSnapshot.transient === snapshot.transient;
|
|
2645
|
+
if (settled && stableVsLast) {
|
|
2646
|
+
stableCounts++;
|
|
2647
|
+
} else if (settled) {
|
|
2648
|
+
stableCounts = 1;
|
|
2649
|
+
} else {
|
|
2650
|
+
stableCounts = 0;
|
|
2651
|
+
}
|
|
2652
|
+
lastSnapshot = snapshot;
|
|
2653
|
+
if (settled && stableCounts >= STABILIZATION_POLLS) {
|
|
2654
|
+
return records;
|
|
2655
|
+
}
|
|
2656
|
+
if (Date.now() >= deadline) {
|
|
2657
|
+
ctx?.logger?.warn?.(`import-leads: records did not stabilize (transient=${transient}, total=${total}); returning best-effort`);
|
|
2658
|
+
throw client.makeError("IMPORT_NOT_TERMINAL", `Backend hasn't fully settled records within ${budgetMs}ms`, `Retry leadbay_import_leads with the same input in 30s, or split the batch. importId=${importId}.`, `GET /imports/${importId}/records`);
|
|
2659
|
+
}
|
|
2660
|
+
await sleepWithAbort(POLL_INTERVAL_MS, signal);
|
|
2661
|
+
}
|
|
2662
|
+
}
|
|
2663
|
+
async function runOneChunk(client, chunk, chunkIdx, totalChunks, header, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal, onImportId) {
|
|
2664
|
+
const csv = synthesizeCsv(header, chunk.map((c) => c.row));
|
|
2665
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2666
|
+
const fileName = `mcp-import-${ts}-${chunkIdx}.csv`;
|
|
2667
|
+
ctx?.logger?.info?.(`import-leads: uploading chunk ${chunkIdx + 1}/${totalChunks} (${chunk.length} rows, ${csv.length}B)`);
|
|
2668
|
+
const upload = await client.requestRawBinary("POST", `/imports?file_name=${encodeURIComponent(fileName)}`, "text/csv", csv);
|
|
2669
|
+
const importId = upload.id;
|
|
2670
|
+
onImportId(importId);
|
|
2671
|
+
const phaseBudget = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
2672
|
+
await pollPreprocess(client, importId, phaseBudget, ctx, signal);
|
|
2673
|
+
ctx?.logger?.info?.(`import-leads: preprocess done for importId=${importId}`);
|
|
2674
|
+
if (dryRun) {
|
|
2675
|
+
return { importId, records: [] };
|
|
2676
|
+
}
|
|
2677
|
+
await client.requestVoid("POST", `/imports/${importId}/update_mappings`, mappings);
|
|
2678
|
+
ctx?.logger?.info?.(`import-leads: mappings committed for importId=${importId}`);
|
|
2679
|
+
const phaseBudget2 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
2680
|
+
await pollProcess(client, importId, phaseBudget2, ctx, signal);
|
|
2681
|
+
ctx?.logger?.info?.(`import-leads: process done for importId=${importId}`);
|
|
2682
|
+
const phaseBudget3 = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
2683
|
+
const records = await pollRecordsToTerminal(client, importId, phaseBudget3, chunk.length, ctx, signal);
|
|
2684
|
+
ctx?.logger?.info?.(`import-leads: ${records.length} records terminal for importId=${importId}`);
|
|
2685
|
+
return { importId, records };
|
|
2686
|
+
}
|
|
2687
|
+
function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
2688
|
+
const seenInputIndex = /* @__PURE__ */ new Set();
|
|
2689
|
+
const sortedRecords = [...chunk.records].sort((a, b) => {
|
|
2690
|
+
const aHasLead = a.lead?.id ? 0 : 1;
|
|
2691
|
+
const bHasLead = b.lead?.id ? 0 : 1;
|
|
2692
|
+
return aHasLead - bHasLead;
|
|
2693
|
+
});
|
|
2694
|
+
for (const rec of sortedRecords) {
|
|
2695
|
+
let inputIdx;
|
|
2696
|
+
const rowIdCell = readCell(rec, "MCP_ROW_ID");
|
|
2697
|
+
if (rowIdCell && prep.byRowId.has(rowIdCell)) {
|
|
2698
|
+
inputIdx = prep.byRowId.get(rowIdCell);
|
|
2699
|
+
}
|
|
2700
|
+
if (inputIdx === void 0) {
|
|
2701
|
+
const websiteCell = readCell(rec, "LEAD_WEBSITE");
|
|
2702
|
+
if (websiteCell) {
|
|
2703
|
+
const norm = normalizeDomain(websiteCell);
|
|
2704
|
+
if (norm && prep.byDomain.has(norm)) {
|
|
2705
|
+
inputIdx = prep.byDomain.get(norm);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
if (inputIdx === void 0 && rec.lead?.website) {
|
|
2710
|
+
const norm = normalizeDomain(rec.lead.website);
|
|
2711
|
+
if (norm && prep.byDomain.has(norm)) {
|
|
2712
|
+
inputIdx = prep.byDomain.get(norm);
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
if (inputIdx === void 0)
|
|
2716
|
+
continue;
|
|
2717
|
+
if (seenInputIndex.has(inputIdx)) {
|
|
2718
|
+
if (!matched.has(inputIdx) && !notImported.has(inputIdx)) {
|
|
2719
|
+
const inp2 = prep.validInputs[inputIdx];
|
|
2720
|
+
notImported.set(inputIdx, { domain: inp2.outputDomain, reason: "ambiguous" });
|
|
2721
|
+
}
|
|
2722
|
+
continue;
|
|
2723
|
+
}
|
|
2724
|
+
seenInputIndex.add(inputIdx);
|
|
2725
|
+
const inp = prep.validInputs[inputIdx];
|
|
2726
|
+
const matchType = (rec.match_type ?? rec.matchType ?? "").toString();
|
|
2727
|
+
if (rec.lead?.id) {
|
|
2728
|
+
matched.set(inputIdx, {
|
|
2729
|
+
domain: inp.outputDomain,
|
|
2730
|
+
leadId: rec.lead.id,
|
|
2731
|
+
name: rec.lead.name ?? null
|
|
2732
|
+
});
|
|
2733
|
+
} else if (matchType === "NO_MATCH") {
|
|
2734
|
+
const reason = inp.domain && PUBLIC_MAILBOX_DOMAINS.has(inp.domain) ? "no_match" : "uncrawled";
|
|
2735
|
+
notImported.set(inputIdx, { domain: inp.outputDomain, reason });
|
|
2736
|
+
} else {
|
|
2737
|
+
notImported.set(inputIdx, { domain: inp.outputDomain, reason: "internal_error" });
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
var importLeads = {
|
|
2742
|
+
name: "leadbay_import_leads",
|
|
2743
|
+
description: "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.\n\nTWO MODES:\n 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. Output: { leads: [{domain, leadId, name}], not_imported: [{domain, reason}], importIds, _meta }.\n B) Custom records + mapping \u2014 pass `records: [{Col1, Col2, ...}]` plus `mappings.fields: {Col1: 'LEAD_NAME', Col2: 'LEAD_WEBSITE', ...}`. The tool synthesizes a CSV from the union of record keys (deterministic order) and POSTs the caller-supplied mapping to the wizard. mappings.fields must include LEAD_NAME or LEAD_WEBSITE (the resolver needs at least one). Output: { leads: [{rowId, domain?, leadId, name}], not_imported: [{rowId, domain?, reason}], importIds, _meta }. `rowId` round-trips your input order.\n\nPass exactly one of `domains` / `records`. Reserved column MCP_ROW_ID (any case) cannot appear in records or mappings \u2014 the tool injects it for stable reconciliation.\n\n\u26A0\uFE0F MUTATES USER STATE. 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\nWhen to use: you have a list of company domains from another system (CRM, analytics, email correspondents) and need stable Leadbay leadIds; or you have CRM-shaped rows with custom columns (sector, location, status, etc.) and want to drive the wizard with explicit field mappings.\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.",
|
|
2744
|
+
write: true,
|
|
2745
|
+
version: "0.2.0",
|
|
2746
|
+
inputSchema: {
|
|
2747
|
+
type: "object",
|
|
2748
|
+
properties: {
|
|
2749
|
+
domains: {
|
|
2750
|
+
type: "array",
|
|
2751
|
+
description: "Mode A: list of company domains to map to Leadbay leadIds. Mutually exclusive with `records`.",
|
|
2752
|
+
items: {
|
|
2753
|
+
type: "object",
|
|
2754
|
+
properties: {
|
|
2755
|
+
domain: {
|
|
2756
|
+
type: "string",
|
|
2757
|
+
description: "Company domain (e.g. 'apple.com'). Protocol/path are stripped."
|
|
2758
|
+
},
|
|
2759
|
+
name: {
|
|
2760
|
+
type: "string",
|
|
2761
|
+
description: "Optional display name override; defaults to the domain."
|
|
2762
|
+
}
|
|
2763
|
+
},
|
|
2764
|
+
required: ["domain"]
|
|
2765
|
+
}
|
|
2766
|
+
},
|
|
2767
|
+
records: {
|
|
2768
|
+
type: "array",
|
|
2769
|
+
description: "Mode B: arbitrary CSV-shaped rows. Each record is an object whose keys are column names and values are scalar (string/number/boolean/null). Mutually exclusive with `domains`. Must be accompanied by `mappings.fields`. The tool synthesizes a CSV from the union of all record keys.",
|
|
2770
|
+
items: { type: "object" }
|
|
2771
|
+
},
|
|
2772
|
+
mappings: {
|
|
2773
|
+
type: "object",
|
|
2774
|
+
description: "Mode B: how each CSV column maps to Leadbay's CRM field schema. Required when `records` is supplied; ignored otherwise.",
|
|
2775
|
+
properties: {
|
|
2776
|
+
fields: {
|
|
2777
|
+
type: "object",
|
|
2778
|
+
description: "Object whose keys are CSV column names (matching keys in `records`) and whose values are Leadbay CRM field types (LEAD_NAME, LEAD_WEBSITE, LEAD_STATUS, LEAD_LOCATION, LEAD_SECTOR, LEAD_SIZE, CRM_ID, LEADBAY_ID, EMAIL, DEAL_CRM_ID, CONTACT_TITLE, LEAD_STATUS_DATE). At least one entry must target LEAD_NAME or LEAD_WEBSITE \u2014 the wizard needs that to find leads."
|
|
2779
|
+
},
|
|
2780
|
+
statuses: {
|
|
2781
|
+
type: "object",
|
|
2782
|
+
description: "Optional status string mapping (rarely needed). Defaults to {}."
|
|
2783
|
+
},
|
|
2784
|
+
default_status: {
|
|
2785
|
+
type: ["string", "null"],
|
|
2786
|
+
description: "Optional default status. Defaults to null."
|
|
2787
|
+
}
|
|
2788
|
+
},
|
|
2789
|
+
required: ["fields"]
|
|
2790
|
+
},
|
|
2791
|
+
dry_run: {
|
|
2792
|
+
type: "boolean",
|
|
2793
|
+
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 input format / wizard reachability without polluting the CRM."
|
|
2794
|
+
},
|
|
2795
|
+
per_phase_budget_ms: {
|
|
2796
|
+
type: "number",
|
|
2797
|
+
description: `Single poll-loop cap (default ${DEFAULT_PER_PHASE_BUDGET_MS}ms).`
|
|
2798
|
+
},
|
|
2799
|
+
total_budget_ms: {
|
|
2800
|
+
type: "number",
|
|
2801
|
+
description: `Overall cap across all phases (default ${DEFAULT_TOTAL_BUDGET_MS2}ms).`
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
// Neither field is "required" at the schema level; xor + presence is
|
|
2805
|
+
// enforced in execute() so we can produce specific error codes.
|
|
2806
|
+
},
|
|
2807
|
+
execute: async (client, params, ctx) => {
|
|
2808
|
+
const signal = ctx?.signal;
|
|
2809
|
+
const dryRun = Boolean(params.dry_run);
|
|
2810
|
+
const perPhaseBudget = params.per_phase_budget_ms ?? DEFAULT_PER_PHASE_BUDGET_MS;
|
|
2811
|
+
const totalBudget = params.total_budget_ms ?? DEFAULT_TOTAL_BUDGET_MS2;
|
|
2812
|
+
const totalDeadline = Date.now() + totalBudget;
|
|
2813
|
+
const hasDomains = Array.isArray(params.domains) && params.domains.length > 0;
|
|
2814
|
+
const hasRecords = Array.isArray(params.records) && params.records.length > 0;
|
|
2815
|
+
if (hasDomains && hasRecords) {
|
|
2816
|
+
throw client.makeError("IMPORT_INPUT_CONFLICT", "Pass exactly one of `domains` or `records`, not both", "Use `domains` for the simple shortcut, or `records`+`mappings` for arbitrary CSV input.", "POST /imports");
|
|
2817
|
+
}
|
|
2818
|
+
if (!hasDomains && !hasRecords) {
|
|
2819
|
+
throw client.makeError("IMPORT_EMPTY_INPUT", "domains[] or records[] must contain at least one entry", "Pass at least one entry. See the tool description for the two input modes.", "POST /imports");
|
|
2820
|
+
}
|
|
2821
|
+
const me = await client.resolveMe();
|
|
2822
|
+
if (!me.admin) {
|
|
2823
|
+
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");
|
|
2824
|
+
}
|
|
2825
|
+
const prep = hasDomains ? prepareDomainsMode(client, params.domains) : prepareRecordsMode(client, params.records, params.mappings);
|
|
2826
|
+
if (prep.validInputs.length === 0) {
|
|
2827
|
+
const not_imported2 = prep.malformedDomains.map((d) => ({
|
|
2828
|
+
domain: d,
|
|
2829
|
+
reason: "malformed"
|
|
2830
|
+
}));
|
|
2831
|
+
return {
|
|
2832
|
+
leads: [],
|
|
2833
|
+
not_imported: not_imported2,
|
|
2834
|
+
importIds: [],
|
|
2835
|
+
region: client.region,
|
|
2836
|
+
dry_run: dryRun || void 0,
|
|
2837
|
+
_meta: client.lastMeta ?? {
|
|
2838
|
+
region: client.region,
|
|
2839
|
+
endpoint: "POST /imports",
|
|
2840
|
+
latency_ms: null,
|
|
2841
|
+
retry_after: null
|
|
2842
|
+
}
|
|
2843
|
+
};
|
|
2844
|
+
}
|
|
2845
|
+
const chunks = chunkAt100(prep.validInputs);
|
|
2846
|
+
ctx?.logger?.info?.(`import-leads(${prep.mode}): ${prep.validInputs.length} rows \u2192 ${chunks.length} chunk(s); dry_run=${dryRun}, totalBudgetMs=${totalBudget}`);
|
|
2847
|
+
const importIds = [];
|
|
2848
|
+
const matched = /* @__PURE__ */ new Map();
|
|
2849
|
+
const notImported = /* @__PURE__ */ new Map();
|
|
2850
|
+
let cancelled = false;
|
|
2851
|
+
const recordImportId = (id) => {
|
|
2852
|
+
if (!importIds.includes(id))
|
|
2853
|
+
importIds.push(id);
|
|
2854
|
+
};
|
|
2855
|
+
try {
|
|
2856
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2857
|
+
const chunk = chunks[i];
|
|
2858
|
+
const out = await runOneChunk(client, chunk, i, chunks.length, prep.header, prep.mappings, dryRun, perPhaseBudget, totalDeadline, ctx, signal, recordImportId);
|
|
2859
|
+
if (!dryRun) {
|
|
2860
|
+
reconcileOneChunk(prep, out, matched, notImported);
|
|
2861
|
+
}
|
|
2862
|
+
}
|
|
2863
|
+
} catch (err) {
|
|
2864
|
+
if (err?.name === "AbortError") {
|
|
2865
|
+
cancelled = true;
|
|
2866
|
+
ctx?.logger?.info?.(`import-leads: aborted via signal; importIds=${importIds.join(",")}`);
|
|
2867
|
+
} else if (err?.error === true) {
|
|
2868
|
+
if (err.code === "FORBIDDEN") {
|
|
2869
|
+
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);
|
|
2870
|
+
}
|
|
2871
|
+
if (err.code === "BILLING_SUSPENDED") {
|
|
2872
|
+
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);
|
|
2873
|
+
}
|
|
2874
|
+
throw err;
|
|
2875
|
+
} else {
|
|
2876
|
+
throw err;
|
|
2877
|
+
}
|
|
2878
|
+
}
|
|
2879
|
+
const leads = [];
|
|
2880
|
+
const not_imported = [];
|
|
2881
|
+
if (dryRun) {
|
|
2882
|
+
for (const inp of prep.validInputs) {
|
|
2883
|
+
if (prep.mode === "domains") {
|
|
2884
|
+
not_imported.push({ domain: inp.outputDomain, reason: "dry_run" });
|
|
2885
|
+
} else {
|
|
2886
|
+
const entry = { rowId: inp.rowId, reason: "dry_run" };
|
|
2887
|
+
if (inp.outputDomain)
|
|
2888
|
+
entry.domain = inp.outputDomain;
|
|
2889
|
+
not_imported.push(entry);
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
} else {
|
|
2893
|
+
for (const inp of prep.validInputs) {
|
|
2894
|
+
const m = matched.get(inp.index);
|
|
2895
|
+
if (m) {
|
|
2896
|
+
if (prep.mode === "domains") {
|
|
2897
|
+
leads.push({
|
|
2898
|
+
domain: inp.outputDomain,
|
|
2899
|
+
leadId: m.leadId,
|
|
2900
|
+
name: m.name
|
|
2901
|
+
});
|
|
2902
|
+
} else {
|
|
2903
|
+
const e = {
|
|
2904
|
+
rowId: inp.rowId,
|
|
2905
|
+
leadId: m.leadId,
|
|
2906
|
+
name: m.name
|
|
2907
|
+
};
|
|
2908
|
+
if (m.domain ?? inp.outputDomain)
|
|
2909
|
+
e.domain = m.domain ?? inp.outputDomain;
|
|
2910
|
+
leads.push(e);
|
|
2911
|
+
}
|
|
2912
|
+
continue;
|
|
2913
|
+
}
|
|
2914
|
+
const ni = notImported.get(inp.index);
|
|
2915
|
+
if (ni) {
|
|
2916
|
+
if (prep.mode === "domains") {
|
|
2917
|
+
not_imported.push({ domain: inp.outputDomain, reason: ni.reason });
|
|
2918
|
+
} else {
|
|
2919
|
+
const e = { rowId: inp.rowId, reason: ni.reason };
|
|
2920
|
+
if (ni.domain ?? inp.outputDomain)
|
|
2921
|
+
e.domain = ni.domain ?? inp.outputDomain;
|
|
2922
|
+
not_imported.push(e);
|
|
2923
|
+
}
|
|
2924
|
+
continue;
|
|
2925
|
+
}
|
|
2926
|
+
if (prep.mode === "domains") {
|
|
2927
|
+
not_imported.push({ domain: inp.outputDomain, reason: "internal_error" });
|
|
2928
|
+
} else {
|
|
2929
|
+
const e = { rowId: inp.rowId, reason: "internal_error" };
|
|
2930
|
+
if (inp.outputDomain)
|
|
2931
|
+
e.domain = inp.outputDomain;
|
|
2932
|
+
not_imported.push(e);
|
|
2933
|
+
}
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2936
|
+
for (const m of prep.malformedDomains) {
|
|
2937
|
+
not_imported.push({ domain: m, reason: "malformed" });
|
|
2938
|
+
}
|
|
2939
|
+
return {
|
|
2940
|
+
leads,
|
|
2941
|
+
not_imported,
|
|
2942
|
+
importIds,
|
|
2943
|
+
region: client.region,
|
|
2944
|
+
cancelled: cancelled || void 0,
|
|
2945
|
+
dry_run: dryRun || void 0,
|
|
2946
|
+
_meta: client.lastMeta ?? {
|
|
2947
|
+
region: client.region,
|
|
2948
|
+
endpoint: "POST /imports",
|
|
2949
|
+
latency_ms: null,
|
|
2950
|
+
retry_after: null
|
|
2951
|
+
}
|
|
2952
|
+
};
|
|
2953
|
+
}
|
|
2954
|
+
};
|
|
2955
|
+
|
|
2193
2956
|
// ../core/dist/composite/enrich-titles.js
|
|
2194
2957
|
var DEFAULT_CANDIDATE_COUNT = 25;
|
|
2195
2958
|
var enrichTitles = {
|
|
@@ -2421,7 +3184,7 @@ import { mkdir as mkdirAsync, lstat, open as fsOpen, readFile, rename, stat, unl
|
|
|
2421
3184
|
import { constants as fsConstants } from "fs";
|
|
2422
3185
|
import { dirname, resolve as resolvePath } from "path";
|
|
2423
3186
|
import { homedir, platform } from "os";
|
|
2424
|
-
import { createHash, randomUUID } from "crypto";
|
|
3187
|
+
import { createHash, randomUUID as randomUUID2 } from "crypto";
|
|
2425
3188
|
var DEFAULT_IDEMPOTENCY_WINDOW_MS = 5 * 60 * 1e3;
|
|
2426
3189
|
var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2427
3190
|
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 +3438,7 @@ var LocalBulkStore = class {
|
|
|
2675
3438
|
};
|
|
2676
3439
|
}
|
|
2677
3440
|
const record = {
|
|
2678
|
-
bulk_id:
|
|
3441
|
+
bulk_id: randomUUID2(),
|
|
2679
3442
|
launched_at: new Date(nowMs).toISOString(),
|
|
2680
3443
|
lead_ids,
|
|
2681
3444
|
titles,
|
|
@@ -3205,14 +3968,14 @@ var refinePrompt = {
|
|
|
3205
3968
|
would_call: {
|
|
3206
3969
|
method: "POST",
|
|
3207
3970
|
path: `/organizations/${orgId}/user_prompt`,
|
|
3208
|
-
body: {
|
|
3971
|
+
body: { user_prompt: params.prompt }
|
|
3209
3972
|
}
|
|
3210
3973
|
};
|
|
3211
3974
|
}
|
|
3212
3975
|
const postedAt = Date.now();
|
|
3213
3976
|
const STALE_GUARD_MS = 5e3;
|
|
3214
3977
|
await client.requestVoid("POST", `/organizations/${orgId}/user_prompt`, {
|
|
3215
|
-
|
|
3978
|
+
user_prompt: params.prompt
|
|
3216
3979
|
});
|
|
3217
3980
|
client.invalidateMe();
|
|
3218
3981
|
const attempts = params.clarification_poll_attempts ?? DEFAULT_POLL_ATTEMPTS;
|
|
@@ -3521,7 +4284,8 @@ var compositeWriteTools = [
|
|
|
3521
4284
|
adjustAudience,
|
|
3522
4285
|
refinePrompt,
|
|
3523
4286
|
answerClarification,
|
|
3524
|
-
reportOutreach
|
|
4287
|
+
reportOutreach,
|
|
4288
|
+
importLeads
|
|
3525
4289
|
];
|
|
3526
4290
|
var compositeTools = [
|
|
3527
4291
|
...compositeReadTools,
|
|
@@ -3532,6 +4296,7 @@ var tools = [...compositeTools, ...granularTools];
|
|
|
3532
4296
|
export {
|
|
3533
4297
|
REGIONS,
|
|
3534
4298
|
createClient,
|
|
4299
|
+
formatLoginError,
|
|
3535
4300
|
resolveRegion,
|
|
3536
4301
|
getMockJournal,
|
|
3537
4302
|
clearMockJournal,
|
|
@@ -3582,6 +4347,7 @@ export {
|
|
|
3582
4347
|
recallOrderedTitles,
|
|
3583
4348
|
accountStatus,
|
|
3584
4349
|
bulkQualifyLeads,
|
|
4350
|
+
importLeads,
|
|
3585
4351
|
enrichTitles,
|
|
3586
4352
|
isValidBulkId,
|
|
3587
4353
|
LocalBulkStore,
|