@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.
@@ -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
- throw new Error(`Leadbay login failed in both regions (us, fr). Last response: ${JSON.stringify(lastErr)}`);
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: { prompt: params.prompt }
1409
+ body: { user_prompt: params.prompt }
1326
1410
  }
1327
1411
  };
1328
1412
  }
1329
1413
  await client.requestVoid("POST", `/organizations/${orgId}/user_prompt`, {
1330
- prompt: params.prompt
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: randomUUID(),
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: { prompt: params.prompt }
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
- prompt: params.prompt
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,