@leadbay/mcp 0.6.2 → 0.6.3
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
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
# Changelog — @leadbay/mcp
|
|
2
2
|
|
|
3
|
-
## 0.6.
|
|
3
|
+
## 0.6.3 — 2026-05-12
|
|
4
|
+
|
|
5
|
+
**Async import schema fix**: `leadbay_import_leads` now declares both its legacy blocking result shape and its async kickoff shape (`{status: "running", handle_id, importIds, progress}`) in `outputSchema`, so Claude Desktop and other MCP SDK clients accept the fast handle response instead of rejecting `structuredContent`.
|
|
6
|
+
|
|
7
|
+
**Async qualification schema fix**: `leadbay_bulk_qualify_leads` now also declares its async kickoff shape (`{status: "running", handle_id, qualify_id, ...}`), matching the `wait_for_completion:false` behavior added for short MCP client transport timeouts.
|
|
8
|
+
|
|
9
|
+
## 0.6.2 — 2026-05-12
|
|
4
10
|
|
|
5
11
|
**MCPB install fix**: desktop extension bundles now use the current `manifest_version` field and remove unsupported manifest keys (`user_config.*.enum` and an internal note field) so Claude Desktop can preview and install the MCPB.
|
|
6
12
|
|
package/dist/bin.js
CHANGED
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
granularReadTools,
|
|
9
9
|
granularWriteTools,
|
|
10
10
|
resolveRegion
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-QAOJARMK.js";
|
|
12
12
|
|
|
13
13
|
// src/bin.ts
|
|
14
14
|
import { realpathSync } from "fs";
|
|
@@ -587,7 +587,7 @@ function buildServer(client, opts = {}) {
|
|
|
587
587
|
|
|
588
588
|
// src/bin.ts
|
|
589
589
|
import { createRequire } from "module";
|
|
590
|
-
var VERSION = "0.6.
|
|
590
|
+
var VERSION = "0.6.3";
|
|
591
591
|
var HELP = `
|
|
592
592
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
593
593
|
|
|
@@ -876,7 +876,7 @@ async function runLogin(args) {
|
|
|
876
876
|
let result;
|
|
877
877
|
try {
|
|
878
878
|
if (pinnedRegion && !allowFallback) {
|
|
879
|
-
const { REGIONS } = await import("./dist-
|
|
879
|
+
const { REGIONS } = await import("./dist-KHXRELXF.js");
|
|
880
880
|
const baseUrl = REGIONS[pinnedRegion];
|
|
881
881
|
const c = createClient({ region: pinnedRegion });
|
|
882
882
|
const token = await loginAt(baseUrl, email, password);
|
|
@@ -1368,7 +1368,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
|
|
|
1368
1368
|
let region;
|
|
1369
1369
|
try {
|
|
1370
1370
|
if (pinnedRegion && !allowFallback) {
|
|
1371
|
-
const { REGIONS } = await import("./dist-
|
|
1371
|
+
const { REGIONS } = await import("./dist-KHXRELXF.js");
|
|
1372
1372
|
const baseUrl = REGIONS[pinnedRegion];
|
|
1373
1373
|
token = await loginAt(baseUrl, email, password);
|
|
1374
1374
|
region = pinnedRegion;
|
|
@@ -1871,7 +1871,10 @@ async function refreshLeadStates(client, leadIds, questionOrder) {
|
|
|
1871
1871
|
}
|
|
1872
1872
|
|
|
1873
1873
|
// ../core/dist/composite/import-leads.js
|
|
1874
|
-
import { randomUUID } from "crypto";
|
|
1874
|
+
import { createHash as createHash2, randomUUID } from "crypto";
|
|
1875
|
+
function isImportLeadsRunningResult(result) {
|
|
1876
|
+
return "status" in result && result.status === "running";
|
|
1877
|
+
}
|
|
1875
1878
|
var CHUNK_SIZE = 100;
|
|
1876
1879
|
var POLL_INTERVAL_MS2 = 2e3;
|
|
1877
1880
|
var DEFAULT_PER_PHASE_BUDGET_MS = 6e4;
|
|
@@ -1972,6 +1975,29 @@ function chunkAt100(items) {
|
|
|
1972
1975
|
}
|
|
1973
1976
|
return chunks;
|
|
1974
1977
|
}
|
|
1978
|
+
function stableStringify(value) {
|
|
1979
|
+
if (value === null || typeof value !== "object")
|
|
1980
|
+
return JSON.stringify(value);
|
|
1981
|
+
if (Array.isArray(value))
|
|
1982
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
1983
|
+
const obj = value;
|
|
1984
|
+
return `{${Object.keys(obj).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`).join(",")}}`;
|
|
1985
|
+
}
|
|
1986
|
+
function importFingerprint(params, prep) {
|
|
1987
|
+
const payload = {
|
|
1988
|
+
mode: prep.mode,
|
|
1989
|
+
rows: prep.validInputs.map((i) => ({
|
|
1990
|
+
domain: i.domain,
|
|
1991
|
+
outputDomain: i.outputDomain,
|
|
1992
|
+
row: i.row
|
|
1993
|
+
})),
|
|
1994
|
+
malformed: prep.malformedDomains,
|
|
1995
|
+
header: prep.header,
|
|
1996
|
+
mappings: prep.mappings,
|
|
1997
|
+
dry_run: Boolean(params.dry_run)
|
|
1998
|
+
};
|
|
1999
|
+
return createHash2("sha256").update(stableStringify(payload)).digest("hex");
|
|
2000
|
+
}
|
|
1975
2001
|
function checkAborted(signal) {
|
|
1976
2002
|
if (signal?.aborted) {
|
|
1977
2003
|
throw Object.assign(new Error("aborted"), { name: "AbortError" });
|
|
@@ -2339,6 +2365,10 @@ async function pollRecordsToTerminal(client, importId, budgetMs, expectedRowCoun
|
|
|
2339
2365
|
}
|
|
2340
2366
|
}
|
|
2341
2367
|
async function runOneChunk(client, chunk, chunkIdx, totalChunks, header, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal, onImportId) {
|
|
2368
|
+
const upload = await uploadOneChunk(client, chunk, chunkIdx, totalChunks, header, ctx, onImportId);
|
|
2369
|
+
return completeUploadedChunk(client, upload, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal);
|
|
2370
|
+
}
|
|
2371
|
+
async function uploadOneChunk(client, chunk, chunkIdx, totalChunks, header, ctx, onImportId) {
|
|
2342
2372
|
const csv = synthesizeCsv(header, chunk.map((c) => c.row));
|
|
2343
2373
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2344
2374
|
const fileName = `mcp-import-${ts}-${chunkIdx}.csv`;
|
|
@@ -2346,6 +2376,10 @@ async function runOneChunk(client, chunk, chunkIdx, totalChunks, header, mapping
|
|
|
2346
2376
|
const upload = await client.requestRawBinary("POST", `/imports?file_name=${encodeURIComponent(fileName)}`, "text/csv", csv);
|
|
2347
2377
|
const importId = upload.id;
|
|
2348
2378
|
onImportId(importId);
|
|
2379
|
+
return { importId, chunk, chunkIdx, totalChunks };
|
|
2380
|
+
}
|
|
2381
|
+
async function completeUploadedChunk(client, upload, mappings, dryRun, perPhaseBudgetMs, totalDeadline, ctx, signal) {
|
|
2382
|
+
const { importId, chunk } = upload;
|
|
2349
2383
|
const phaseBudget = Math.min(perPhaseBudgetMs, Math.max(1, totalDeadline - Date.now()));
|
|
2350
2384
|
await pollPreprocess(client, importId, phaseBudget, ctx, signal);
|
|
2351
2385
|
ctx?.logger?.info?.(`import-leads: preprocess done for importId=${importId}`);
|
|
@@ -2416,6 +2450,82 @@ function reconcileOneChunk(prep, chunk, matched, notImported) {
|
|
|
2416
2450
|
}
|
|
2417
2451
|
}
|
|
2418
2452
|
}
|
|
2453
|
+
function buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled) {
|
|
2454
|
+
const leads = [];
|
|
2455
|
+
const not_imported = [];
|
|
2456
|
+
if (dryRun) {
|
|
2457
|
+
for (const inp of prep.validInputs) {
|
|
2458
|
+
if (prep.mode === "domains") {
|
|
2459
|
+
not_imported.push({ domain: inp.outputDomain, reason: "dry_run" });
|
|
2460
|
+
} else {
|
|
2461
|
+
const entry = { rowId: inp.rowId, reason: "dry_run" };
|
|
2462
|
+
if (inp.outputDomain)
|
|
2463
|
+
entry.domain = inp.outputDomain;
|
|
2464
|
+
not_imported.push(entry);
|
|
2465
|
+
}
|
|
2466
|
+
}
|
|
2467
|
+
} else {
|
|
2468
|
+
for (const inp of prep.validInputs) {
|
|
2469
|
+
const m = matched.get(inp.index);
|
|
2470
|
+
if (m) {
|
|
2471
|
+
if (prep.mode === "domains") {
|
|
2472
|
+
leads.push({
|
|
2473
|
+
domain: inp.outputDomain,
|
|
2474
|
+
leadId: m.leadId,
|
|
2475
|
+
name: m.name
|
|
2476
|
+
});
|
|
2477
|
+
} else {
|
|
2478
|
+
const e = {
|
|
2479
|
+
rowId: inp.rowId,
|
|
2480
|
+
leadId: m.leadId,
|
|
2481
|
+
name: m.name
|
|
2482
|
+
};
|
|
2483
|
+
if (m.domain ?? inp.outputDomain)
|
|
2484
|
+
e.domain = m.domain ?? inp.outputDomain;
|
|
2485
|
+
leads.push(e);
|
|
2486
|
+
}
|
|
2487
|
+
continue;
|
|
2488
|
+
}
|
|
2489
|
+
const ni = notImported.get(inp.index);
|
|
2490
|
+
if (ni) {
|
|
2491
|
+
if (prep.mode === "domains") {
|
|
2492
|
+
not_imported.push({ domain: inp.outputDomain, reason: ni.reason });
|
|
2493
|
+
} else {
|
|
2494
|
+
const e = { rowId: inp.rowId, reason: ni.reason };
|
|
2495
|
+
if (ni.domain ?? inp.outputDomain)
|
|
2496
|
+
e.domain = ni.domain ?? inp.outputDomain;
|
|
2497
|
+
not_imported.push(e);
|
|
2498
|
+
}
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
if (prep.mode === "domains") {
|
|
2502
|
+
not_imported.push({ domain: inp.outputDomain, reason: "internal_error" });
|
|
2503
|
+
} else {
|
|
2504
|
+
const e = { rowId: inp.rowId, reason: "internal_error" };
|
|
2505
|
+
if (inp.outputDomain)
|
|
2506
|
+
e.domain = inp.outputDomain;
|
|
2507
|
+
not_imported.push(e);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
for (const m of prep.malformedDomains) {
|
|
2512
|
+
not_imported.push({ domain: m, reason: "malformed" });
|
|
2513
|
+
}
|
|
2514
|
+
return {
|
|
2515
|
+
leads,
|
|
2516
|
+
not_imported,
|
|
2517
|
+
importIds,
|
|
2518
|
+
region: client.region,
|
|
2519
|
+
cancelled: cancelled || void 0,
|
|
2520
|
+
dry_run: dryRun || void 0,
|
|
2521
|
+
_meta: client.lastMeta ?? {
|
|
2522
|
+
region: client.region,
|
|
2523
|
+
endpoint: "POST /imports",
|
|
2524
|
+
latency_ms: null,
|
|
2525
|
+
retry_after: null
|
|
2526
|
+
}
|
|
2527
|
+
};
|
|
2528
|
+
}
|
|
2419
2529
|
var importLeads = {
|
|
2420
2530
|
name: "leadbay_import_leads",
|
|
2421
2531
|
annotations: {
|
|
@@ -2428,7 +2538,7 @@ var importLeads = {
|
|
|
2428
2538
|
idempotentHint: true,
|
|
2429
2539
|
openWorldHint: true
|
|
2430
2540
|
},
|
|
2431
|
-
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\n\u2139\uFE0F Monitor-tab membership: imported leads are NOT auto-promoted to the user's Monitor view. Lens-scoring decides \u2014 only above-threshold leads get `in_monitor: true` server-side.\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\nCustom fields: pass org-defined custom field mappings as 'CUSTOM.<id>' (raw wire format) in `mappings.fields`, OR use the ergonomic `mappings.custom_fields` shorthand: `{ColName: 8}` (numeric id) or `{ColName: 'priority_test'}` (field name). Discover available custom fields via leadbay_list_mappable_fields.\n\nRequires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role on the Leadbay account; active billing.",
|
|
2541
|
+
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. For MCP clients with short transport timeouts, pass `wait_for_completion:false` to return quickly with `{status:'running', handle_id}`; poll `leadbay_import_status` with that handle for progress and the final `{leads, not_imported, importIds}` result.\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\n\u2139\uFE0F Monitor-tab membership: imported leads are NOT auto-promoted to the user's Monitor view. Lens-scoring decides \u2014 only above-threshold leads get `in_monitor: true` server-side.\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\nCustom fields: pass org-defined custom field mappings as 'CUSTOM.<id>' (raw wire format) in `mappings.fields`, OR use the ergonomic `mappings.custom_fields` shorthand: `{ColName: 8}` (numeric id) or `{ColName: 'priority_test'}` (field name). Discover available custom fields via leadbay_list_mappable_fields.\n\nRequires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role on the Leadbay account; active billing.",
|
|
2432
2542
|
write: true,
|
|
2433
2543
|
version: "0.3.0",
|
|
2434
2544
|
inputSchema: {
|
|
@@ -2494,6 +2604,10 @@ var importLeads = {
|
|
|
2494
2604
|
total_budget_ms: {
|
|
2495
2605
|
type: "number",
|
|
2496
2606
|
description: `Overall cap across all phases (default ${DEFAULT_TOTAL_BUDGET_MS}ms).`
|
|
2607
|
+
},
|
|
2608
|
+
wait_for_completion: {
|
|
2609
|
+
type: "boolean",
|
|
2610
|
+
description: "When false, validate and enqueue the import in the background, then return `{status:'running', handle_id}` immediately. Poll leadbay_import_status(handle_id). Default is true for 0.6.x backwards compatibility."
|
|
2497
2611
|
}
|
|
2498
2612
|
},
|
|
2499
2613
|
// Neither field is "required" at the schema level; xor + presence is
|
|
@@ -2508,6 +2622,18 @@ var importLeads = {
|
|
|
2508
2622
|
description: "Imported leads. Domains mode: [{domain, leadId, name}]. Records mode: [{rowId, domain?, leadId, name}].",
|
|
2509
2623
|
items: { type: "object" }
|
|
2510
2624
|
},
|
|
2625
|
+
status: {
|
|
2626
|
+
type: "string",
|
|
2627
|
+
description: "`running` when wait_for_completion=false; absent on the legacy blocking result."
|
|
2628
|
+
},
|
|
2629
|
+
handle_id: {
|
|
2630
|
+
type: "string",
|
|
2631
|
+
description: "Persisted UUID handle to pass to leadbay_import_status."
|
|
2632
|
+
},
|
|
2633
|
+
progress: {
|
|
2634
|
+
type: "object",
|
|
2635
|
+
description: "Current async import progress when wait_for_completion=false."
|
|
2636
|
+
},
|
|
2511
2637
|
not_imported: {
|
|
2512
2638
|
type: "array",
|
|
2513
2639
|
description: "Inputs that did NOT yield a leadId. Each entry has a `reason` ('malformed', 'NO_MATCH', 'TIMEOUT', etc.) plus the input echo.",
|
|
@@ -2529,7 +2655,11 @@ var importLeads = {
|
|
|
2529
2655
|
},
|
|
2530
2656
|
_meta: { type: "object" }
|
|
2531
2657
|
},
|
|
2532
|
-
required: ["
|
|
2658
|
+
required: ["importIds", "region", "_meta"],
|
|
2659
|
+
anyOf: [
|
|
2660
|
+
{ required: ["leads", "not_imported", "importIds", "region", "_meta"] },
|
|
2661
|
+
{ required: ["status", "handle_id", "importIds", "progress", "region", "_meta"] }
|
|
2662
|
+
]
|
|
2533
2663
|
},
|
|
2534
2664
|
execute: async (client, params, ctx) => {
|
|
2535
2665
|
const signal = ctx?.signal;
|
|
@@ -2537,6 +2667,7 @@ var importLeads = {
|
|
|
2537
2667
|
const perPhaseBudget = params.per_phase_budget_ms ?? DEFAULT_PER_PHASE_BUDGET_MS;
|
|
2538
2668
|
const totalBudget = params.total_budget_ms ?? DEFAULT_TOTAL_BUDGET_MS;
|
|
2539
2669
|
const totalDeadline = Date.now() + totalBudget;
|
|
2670
|
+
const waitForCompletion = params.wait_for_completion ?? true;
|
|
2540
2671
|
const hasDomains = Array.isArray(params.domains) && params.domains.length > 0;
|
|
2541
2672
|
const hasRecords = Array.isArray(params.records) && params.records.length > 0;
|
|
2542
2673
|
if (hasDomains && hasRecords) {
|
|
@@ -2563,13 +2694,13 @@ var importLeads = {
|
|
|
2563
2694
|
}
|
|
2564
2695
|
const prep = hasDomains ? prepareDomainsMode(client, params.domains) : prepareRecordsMode(client, params.records, params.mappings, customFieldCatalog);
|
|
2565
2696
|
if (prep.validInputs.length === 0) {
|
|
2566
|
-
const
|
|
2697
|
+
const not_imported = prep.malformedDomains.map((d) => ({
|
|
2567
2698
|
domain: d,
|
|
2568
2699
|
reason: "malformed"
|
|
2569
2700
|
}));
|
|
2570
2701
|
return {
|
|
2571
2702
|
leads: [],
|
|
2572
|
-
not_imported
|
|
2703
|
+
not_imported,
|
|
2573
2704
|
importIds: [],
|
|
2574
2705
|
region: client.region,
|
|
2575
2706
|
dry_run: dryRun || void 0,
|
|
@@ -2582,6 +2713,67 @@ var importLeads = {
|
|
|
2582
2713
|
};
|
|
2583
2714
|
}
|
|
2584
2715
|
const chunks = chunkAt100(prep.validInputs);
|
|
2716
|
+
if (!waitForCompletion) {
|
|
2717
|
+
if (!ctx?.bulkTracker) {
|
|
2718
|
+
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_import_leads wait_for_completion=false needs a BulkTracker so the handle survives restart.", "");
|
|
2719
|
+
}
|
|
2720
|
+
const reservation = await ctx.bulkTracker.findOrCreatePendingImport({
|
|
2721
|
+
import_fingerprint: importFingerprint(params, prep),
|
|
2722
|
+
mode: prep.mode,
|
|
2723
|
+
dry_run: dryRun,
|
|
2724
|
+
records_total: prep.validInputs.length
|
|
2725
|
+
});
|
|
2726
|
+
const importIds2 = [...reservation.record.import_ids];
|
|
2727
|
+
const uploadedChunks = [];
|
|
2728
|
+
if (!reservation.reused || reservation.record.import_ids.length === 0) {
|
|
2729
|
+
try {
|
|
2730
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
2731
|
+
const upload = await uploadOneChunk(client, chunks[i], i, chunks.length, prep.header, ctx, (id) => {
|
|
2732
|
+
if (!importIds2.includes(id))
|
|
2733
|
+
importIds2.push(id);
|
|
2734
|
+
});
|
|
2735
|
+
uploadedChunks.push(upload);
|
|
2736
|
+
await ctx.bulkTracker.setImportIds(reservation.record.bulk_id, importIds2);
|
|
2737
|
+
}
|
|
2738
|
+
await ctx.bulkTracker.setImportProgress(reservation.record.bulk_id, {
|
|
2739
|
+
phase: "preprocess",
|
|
2740
|
+
records_processed: 0,
|
|
2741
|
+
records_total: prep.validInputs.length
|
|
2742
|
+
});
|
|
2743
|
+
} catch (err) {
|
|
2744
|
+
await ctx.bulkTracker.markImportFailed(reservation.record.bulk_id, err?.message ?? err?.code ?? "unknown");
|
|
2745
|
+
throw err;
|
|
2746
|
+
}
|
|
2747
|
+
}
|
|
2748
|
+
if (uploadedChunks.length > 0) {
|
|
2749
|
+
void runImportInBackground(client, prep, uploadedChunks, {
|
|
2750
|
+
dryRun,
|
|
2751
|
+
perPhaseBudget,
|
|
2752
|
+
totalBudget
|
|
2753
|
+
}, ctx, reservation.record.bulk_id);
|
|
2754
|
+
}
|
|
2755
|
+
return {
|
|
2756
|
+
status: "running",
|
|
2757
|
+
handle_id: reservation.record.bulk_id,
|
|
2758
|
+
importIds: importIds2,
|
|
2759
|
+
progress: {
|
|
2760
|
+
phase: reservation.record.status === "complete" ? "complete" : importIds2.length > 0 ? "preprocess" : "queued",
|
|
2761
|
+
records_processed: reservation.record.status === "complete" ? reservation.record.records_total : 0,
|
|
2762
|
+
records_total: reservation.record.records_total
|
|
2763
|
+
},
|
|
2764
|
+
region: client.region,
|
|
2765
|
+
...reservation.reused ? {
|
|
2766
|
+
reused: true,
|
|
2767
|
+
seconds_since_original: reservation.seconds_since_original
|
|
2768
|
+
} : {},
|
|
2769
|
+
_meta: client.lastMeta ?? {
|
|
2770
|
+
region: client.region,
|
|
2771
|
+
endpoint: "POST /imports",
|
|
2772
|
+
latency_ms: null,
|
|
2773
|
+
retry_after: null
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2585
2777
|
ctx?.logger?.info?.(`import-leads(${prep.mode}): ${prep.validInputs.length} rows \u2192 ${chunks.length} chunk(s); dry_run=${dryRun}, totalBudgetMs=${totalBudget}`);
|
|
2586
2778
|
const importIds = [];
|
|
2587
2779
|
const matched = /* @__PURE__ */ new Map();
|
|
@@ -2615,82 +2807,45 @@ var importLeads = {
|
|
|
2615
2807
|
throw err;
|
|
2616
2808
|
}
|
|
2617
2809
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
rowId: inp.rowId,
|
|
2644
|
-
leadId: m.leadId,
|
|
2645
|
-
name: m.name
|
|
2646
|
-
};
|
|
2647
|
-
if (m.domain ?? inp.outputDomain)
|
|
2648
|
-
e.domain = m.domain ?? inp.outputDomain;
|
|
2649
|
-
leads.push(e);
|
|
2650
|
-
}
|
|
2651
|
-
continue;
|
|
2652
|
-
}
|
|
2653
|
-
const ni = notImported.get(inp.index);
|
|
2654
|
-
if (ni) {
|
|
2655
|
-
if (prep.mode === "domains") {
|
|
2656
|
-
not_imported.push({ domain: inp.outputDomain, reason: ni.reason });
|
|
2657
|
-
} else {
|
|
2658
|
-
const e = { rowId: inp.rowId, reason: ni.reason };
|
|
2659
|
-
if (ni.domain ?? inp.outputDomain)
|
|
2660
|
-
e.domain = ni.domain ?? inp.outputDomain;
|
|
2661
|
-
not_imported.push(e);
|
|
2810
|
+
return buildImportLeadsResult(client, prep, importIds, matched, notImported, dryRun, cancelled);
|
|
2811
|
+
}
|
|
2812
|
+
};
|
|
2813
|
+
async function runImportInBackground(client, prep, uploadedChunks, opts, ctx, handleId) {
|
|
2814
|
+
const tracker = ctx.bulkTracker;
|
|
2815
|
+
if (!tracker)
|
|
2816
|
+
return;
|
|
2817
|
+
void tracker.setImportProgress(handleId, {
|
|
2818
|
+
phase: "preprocess",
|
|
2819
|
+
records_processed: 0,
|
|
2820
|
+
records_total: prep.validInputs.length
|
|
2821
|
+
}).catch(() => {
|
|
2822
|
+
});
|
|
2823
|
+
setTimeout(() => {
|
|
2824
|
+
void (async () => {
|
|
2825
|
+
const bgCtx = { logger: ctx.logger, bulkTracker: tracker };
|
|
2826
|
+
const importIds = uploadedChunks.map((chunk) => chunk.importId);
|
|
2827
|
+
const matched = /* @__PURE__ */ new Map();
|
|
2828
|
+
const notImported = /* @__PURE__ */ new Map();
|
|
2829
|
+
try {
|
|
2830
|
+
const totalDeadline = Date.now() + opts.totalBudget;
|
|
2831
|
+
for (const upload of uploadedChunks) {
|
|
2832
|
+
const out = await completeUploadedChunk(client, upload, prep.mappings, opts.dryRun, opts.perPhaseBudget, totalDeadline, bgCtx, void 0);
|
|
2833
|
+
if (!opts.dryRun) {
|
|
2834
|
+
reconcileOneChunk(prep, out, matched, notImported);
|
|
2662
2835
|
}
|
|
2663
|
-
continue;
|
|
2664
|
-
}
|
|
2665
|
-
if (prep.mode === "domains") {
|
|
2666
|
-
not_imported.push({ domain: inp.outputDomain, reason: "internal_error" });
|
|
2667
|
-
} else {
|
|
2668
|
-
const e = { rowId: inp.rowId, reason: "internal_error" };
|
|
2669
|
-
if (inp.outputDomain)
|
|
2670
|
-
e.domain = inp.outputDomain;
|
|
2671
|
-
not_imported.push(e);
|
|
2672
2836
|
}
|
|
2837
|
+
const result = buildImportLeadsResult(client, prep, importIds, matched, notImported, opts.dryRun, false);
|
|
2838
|
+
await tracker.markImportComplete(handleId, {
|
|
2839
|
+
leads: result.leads,
|
|
2840
|
+
not_imported: result.not_imported,
|
|
2841
|
+
importIds: result.importIds
|
|
2842
|
+
});
|
|
2843
|
+
} catch (err) {
|
|
2844
|
+
await tracker.markImportFailed(handleId, err?.message ?? err?.code ?? "unknown");
|
|
2673
2845
|
}
|
|
2674
|
-
}
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
}
|
|
2678
|
-
return {
|
|
2679
|
-
leads,
|
|
2680
|
-
not_imported,
|
|
2681
|
-
importIds,
|
|
2682
|
-
region: client.region,
|
|
2683
|
-
cancelled: cancelled || void 0,
|
|
2684
|
-
dry_run: dryRun || void 0,
|
|
2685
|
-
_meta: client.lastMeta ?? {
|
|
2686
|
-
region: client.region,
|
|
2687
|
-
endpoint: "POST /imports",
|
|
2688
|
-
latency_ms: null,
|
|
2689
|
-
retry_after: null
|
|
2690
|
-
}
|
|
2691
|
-
};
|
|
2692
|
-
}
|
|
2693
|
-
};
|
|
2846
|
+
})();
|
|
2847
|
+
}, 0);
|
|
2848
|
+
}
|
|
2694
2849
|
|
|
2695
2850
|
// ../core/dist/tools/list-mappable-fields.js
|
|
2696
2851
|
var STANDARD_FIELDS = [
|
|
@@ -4490,7 +4645,7 @@ var bulkQualifyLeads = {
|
|
|
4490
4645
|
idempotentHint: true,
|
|
4491
4646
|
openWorldHint: true
|
|
4492
4647
|
},
|
|
4493
|
-
description: "Pick the next N unqualified leads in the active lens and qualify them (run AI rescore + web fetch),
|
|
4648
|
+
description: "Pick the next N unqualified leads in the active lens and qualify them (run AI rescore + web fetch). Pass `wait_for_completion:false` to return quickly with `{status:'running', qualify_id}`; poll leadbay_qualify_status with that id. With wait_for_completion omitted/true, the legacy behavior polls until the answers are populated or a budget is exhausted. Already-qualified leads (those with a non-null ai_agent_lead_score) are silently no-ops on the backend, so this composite paginates past them to find fresh candidates. On 429 mid-fanout, stops launching but keeps polling already-launched leads. Context: Leadbay auto-qualifies roughly the top 10 of each daily batch. Leads below the top ~10 are NOT worse \u2014 the system is saving resources. This tool is how the agent spends more resources to go deeper on promising-looking leads the user hasn't had time to surface yet. When to use: when the user wants more qualified leads than what's currently shown, or when a lead looks promising in leadbay_pull_leads but has an empty qualification_summary. When NOT to use: to qualify a single specific lead \u2014 that's leadbay_qualify_lead (granular, advanced).",
|
|
4494
4649
|
inputSchema: {
|
|
4495
4650
|
type: "object",
|
|
4496
4651
|
properties: {
|
|
@@ -4514,6 +4669,10 @@ var bulkQualifyLeads = {
|
|
|
4514
4669
|
total_budget_ms: {
|
|
4515
4670
|
type: "number",
|
|
4516
4671
|
description: `Total polling budget in ms (default ${DEFAULT_TOTAL_BUDGET_MS2})`
|
|
4672
|
+
},
|
|
4673
|
+
wait_for_completion: {
|
|
4674
|
+
type: "boolean",
|
|
4675
|
+
description: "When false, launch qualification and return `{status:'running', qualify_id}` immediately. Poll leadbay_qualify_status. Default is true for 0.6.x backwards compatibility."
|
|
4517
4676
|
}
|
|
4518
4677
|
},
|
|
4519
4678
|
additionalProperties: false
|
|
@@ -4526,6 +4685,14 @@ var bulkQualifyLeads = {
|
|
|
4526
4685
|
description: "Leads whose qualification finished within budget. Each entry: lead_id, qualification_summary{answered,total,avg_qualification_boost}, signals_count.",
|
|
4527
4686
|
items: { type: "object" }
|
|
4528
4687
|
},
|
|
4688
|
+
status: {
|
|
4689
|
+
type: "string",
|
|
4690
|
+
description: "`running` when wait_for_completion=false; absent on the legacy blocking result."
|
|
4691
|
+
},
|
|
4692
|
+
handle_id: { type: "string", description: "Alias of qualify_id for handle-oriented callers." },
|
|
4693
|
+
qualify_id: { type: "string", description: "UUIDv4 to poll via leadbay_qualify_status." },
|
|
4694
|
+
lead_ids: { type: "array", items: { type: "string" } },
|
|
4695
|
+
launched_count: { type: "number" },
|
|
4529
4696
|
still_running: {
|
|
4530
4697
|
type: "array",
|
|
4531
4698
|
description: "Leads launched but whose qualification did not complete within budget. Re-poll via leadbay_qualify_status with the bulk_id (when present).",
|
|
@@ -4556,13 +4723,30 @@ var bulkQualifyLeads = {
|
|
|
4556
4723
|
properties: { region: { type: "string" } }
|
|
4557
4724
|
}
|
|
4558
4725
|
},
|
|
4559
|
-
required: ["
|
|
4726
|
+
required: ["failed", "quota_exceeded"],
|
|
4727
|
+
anyOf: [
|
|
4728
|
+
{ required: ["qualified", "still_running", "failed", "quota_exceeded"] },
|
|
4729
|
+
{
|
|
4730
|
+
required: [
|
|
4731
|
+
"status",
|
|
4732
|
+
"handle_id",
|
|
4733
|
+
"qualify_id",
|
|
4734
|
+
"lead_ids",
|
|
4735
|
+
"launched_count",
|
|
4736
|
+
"failed",
|
|
4737
|
+
"quota_exceeded",
|
|
4738
|
+
"lens_id",
|
|
4739
|
+
"_meta"
|
|
4740
|
+
]
|
|
4741
|
+
}
|
|
4742
|
+
]
|
|
4560
4743
|
},
|
|
4561
4744
|
execute: async (client, params, ctx) => {
|
|
4562
4745
|
const wantCount = Math.min(params.count ?? DEFAULT_COUNT, MAX_COUNT);
|
|
4563
4746
|
const perLeadBudget = params.per_lead_budget_ms ?? DEFAULT_PER_LEAD_BUDGET_MS;
|
|
4564
4747
|
const totalBudget = params.total_budget_ms ?? DEFAULT_TOTAL_BUDGET_MS2;
|
|
4565
4748
|
const totalDeadline = Date.now() + totalBudget;
|
|
4749
|
+
const waitForCompletion = params.wait_for_completion ?? true;
|
|
4566
4750
|
let candidates;
|
|
4567
4751
|
let exhausted = false;
|
|
4568
4752
|
let totalUnqualifiedFound = 0;
|
|
@@ -4605,6 +4789,58 @@ var bulkQualifyLeads = {
|
|
|
4605
4789
|
message: "No unqualified leads found in this lens \u2014 either all leads have been qualified, or the wishlist is still computing (check leadbay_account_status for computing_wishlist)."
|
|
4606
4790
|
};
|
|
4607
4791
|
}
|
|
4792
|
+
if (!waitForCompletion) {
|
|
4793
|
+
if (!ctx?.bulkTracker) {
|
|
4794
|
+
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_bulk_qualify_leads wait_for_completion=false needs a BulkTracker so qualify_id survives restart.", "");
|
|
4795
|
+
}
|
|
4796
|
+
const reservation = await ctx.bulkTracker.findOrCreatePendingQualify({
|
|
4797
|
+
lead_ids: candidates,
|
|
4798
|
+
import_ids: [],
|
|
4799
|
+
lens_id: lensId,
|
|
4800
|
+
mapping_fingerprint: "bulk_qualify_leads",
|
|
4801
|
+
per_lead_budget_ms: perLeadBudget,
|
|
4802
|
+
total_budget_ms: totalBudget
|
|
4803
|
+
});
|
|
4804
|
+
const launched2 = [];
|
|
4805
|
+
const failed2 = [];
|
|
4806
|
+
let quotaExceeded2 = false;
|
|
4807
|
+
if (!reservation.reused) {
|
|
4808
|
+
for (const leadId of candidates) {
|
|
4809
|
+
if (quotaExceeded2)
|
|
4810
|
+
break;
|
|
4811
|
+
try {
|
|
4812
|
+
await client.requestVoid("POST", `/leads/${leadId}/web_fetch?force_fetch=false`);
|
|
4813
|
+
launched2.push(leadId);
|
|
4814
|
+
} catch (err) {
|
|
4815
|
+
if (err?.code === "QUOTA_EXCEEDED") {
|
|
4816
|
+
quotaExceeded2 = true;
|
|
4817
|
+
} else if (err?.code === "NOT_FOUND") {
|
|
4818
|
+
failed2.push({ lead_id: leadId, error: "lead not found" });
|
|
4819
|
+
} else {
|
|
4820
|
+
failed2.push({
|
|
4821
|
+
lead_id: leadId,
|
|
4822
|
+
error: err?.message ?? err?.code ?? "unknown"
|
|
4823
|
+
});
|
|
4824
|
+
}
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
if (failed2.length === candidates.length || launched2.length > 0 || quotaExceeded2) {
|
|
4828
|
+
await ctx.bulkTracker.markLaunched(reservation.record.bulk_id);
|
|
4829
|
+
}
|
|
4830
|
+
}
|
|
4831
|
+
const out = {
|
|
4832
|
+
status: "running",
|
|
4833
|
+
handle_id: reservation.record.bulk_id,
|
|
4834
|
+
qualify_id: reservation.record.bulk_id,
|
|
4835
|
+
lead_ids: candidates,
|
|
4836
|
+
launched_count: reservation.reused ? reservation.record.lead_ids.length : launched2.length,
|
|
4837
|
+
failed: failed2,
|
|
4838
|
+
quota_exceeded: quotaExceeded2,
|
|
4839
|
+
lens_id: lensId,
|
|
4840
|
+
_meta: { region: client.region }
|
|
4841
|
+
};
|
|
4842
|
+
return out;
|
|
4843
|
+
}
|
|
4608
4844
|
const launched = [];
|
|
4609
4845
|
const failed = [];
|
|
4610
4846
|
let quotaExceeded = false;
|
|
@@ -4799,7 +5035,7 @@ var importAndQualify = {
|
|
|
4799
5035
|
idempotentHint: true,
|
|
4800
5036
|
openWorldHint: true
|
|
4801
5037
|
},
|
|
4802
|
-
description: `Composite: import a list of leads (CSV-shaped records OR a list of domains), then trigger Leadbay's AI qualification (web research + per-question scoring) on every imported leadId, and return both the import outcome and the per-lead qualification answers \u2014 in one call. Honours a total wall-clock budget; when the budget is exhausted before all leads finish qualifying, returns a \`qualify_id\` UUID handle that survives MCP restart and can be passed to leadbay_qualify_status to retrieve the rest of the answers later.
|
|
5038
|
+
description: `Composite: import a list of leads (CSV-shaped records OR a list of domains), then trigger Leadbay's AI qualification (web research + per-question scoring) on every imported leadId, and return both the import outcome and the per-lead qualification answers \u2014 in one call. For MCP clients with short transport timeouts, pass \`wait_for_completion:false\`; the tool returns quickly with an import \`handle_id\` that can be polled with leadbay_import_status before continuing qualification. Honours a total wall-clock budget; when the budget is exhausted before all leads finish qualifying, returns a \`qualify_id\` UUID handle that survives MCP restart and can be passed to leadbay_qualify_status to retrieve the rest of the answers later.
|
|
4803
5039
|
|
|
4804
5040
|
Inputs:
|
|
4805
5041
|
- \`domains\`: list of \`{domain, name?}\` (Mode A) \u2014 mutually exclusive with \`records\`.
|
|
@@ -4887,6 +5123,10 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
4887
5123
|
type: "number",
|
|
4888
5124
|
description: `Per-phase budget for the import wizard (default ${DEFAULT_PER_PHASE_BUDGET_MS2}); mirrors leadbay_import_leads.`
|
|
4889
5125
|
},
|
|
5126
|
+
wait_for_completion: {
|
|
5127
|
+
type: "boolean",
|
|
5128
|
+
description: "When false, enqueue the import phase and return `{kind:'result', status:'running', handle_id}` immediately. Poll leadbay_import_status. Default is true for 0.6.x backwards compatibility."
|
|
5129
|
+
},
|
|
4890
5130
|
lensId: {
|
|
4891
5131
|
type: "number",
|
|
4892
5132
|
description: "Lens id (escape hatch \u2014 defaults to active)."
|
|
@@ -4909,6 +5149,14 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
4909
5149
|
type: "string",
|
|
4910
5150
|
description: "'result' (full flow) or 'preview' (dry_run='preview' mapping diagnostics)."
|
|
4911
5151
|
},
|
|
5152
|
+
status: {
|
|
5153
|
+
type: "string",
|
|
5154
|
+
description: "`running` when wait_for_completion=false."
|
|
5155
|
+
},
|
|
5156
|
+
handle_id: {
|
|
5157
|
+
type: "string",
|
|
5158
|
+
description: "Import handle to pass to leadbay_import_status when wait_for_completion=false."
|
|
5159
|
+
},
|
|
4912
5160
|
// preview-shape keys
|
|
4913
5161
|
mapping_hints: {
|
|
4914
5162
|
type: "array",
|
|
@@ -5015,19 +5263,76 @@ Requires: LEADBAY_MCP_WRITE=1 (MCP) or exposeWrite=true (OpenClaw); admin role;
|
|
|
5015
5263
|
if (!ctx?.bulkTracker) {
|
|
5016
5264
|
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_import_and_qualify needs a BulkTracker (qualify_id persistence). Upgrade to @leadbay/mcp \u22650.5.0 or set LEADBAY_BULK_STORE_ALLOW_MEMORY=1.", "");
|
|
5017
5265
|
}
|
|
5266
|
+
if (params.wait_for_completion === false) {
|
|
5267
|
+
const queued = await importLeads.execute(client, {
|
|
5268
|
+
domains: params.domains,
|
|
5269
|
+
records: params.records,
|
|
5270
|
+
mappings: params.mappings,
|
|
5271
|
+
per_phase_budget_ms: perPhaseBudget,
|
|
5272
|
+
total_budget_ms: totalBudget,
|
|
5273
|
+
...params.dry_run === true ? { dry_run: true } : {},
|
|
5274
|
+
wait_for_completion: false
|
|
5275
|
+
}, ctx);
|
|
5276
|
+
if (!isImportLeadsRunningResult(queued)) {
|
|
5277
|
+
return {
|
|
5278
|
+
kind: "result",
|
|
5279
|
+
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
5280
|
+
qualify_id: null,
|
|
5281
|
+
import_ids: queued.importIds,
|
|
5282
|
+
imported: queued.leads.map((l) => ({
|
|
5283
|
+
leadId: l.leadId,
|
|
5284
|
+
...l.domain ? { domain: l.domain } : {},
|
|
5285
|
+
name: l.name ?? null,
|
|
5286
|
+
...l.rowId ? { rowId: l.rowId } : {}
|
|
5287
|
+
})),
|
|
5288
|
+
not_imported: queued.not_imported.map(toNotImportedEntry),
|
|
5289
|
+
qualified: [],
|
|
5290
|
+
still_running: [],
|
|
5291
|
+
failed: [],
|
|
5292
|
+
quota_exceeded: false,
|
|
5293
|
+
skipped_already_qualified: [],
|
|
5294
|
+
not_in_lens: [],
|
|
5295
|
+
region: client.region,
|
|
5296
|
+
_meta: queued._meta
|
|
5297
|
+
};
|
|
5298
|
+
}
|
|
5299
|
+
return {
|
|
5300
|
+
kind: "result",
|
|
5301
|
+
status: "running",
|
|
5302
|
+
handle_id: queued.handle_id,
|
|
5303
|
+
...chosenBudgets ? { chosen_budgets: chosenBudgets } : {},
|
|
5304
|
+
qualify_id: null,
|
|
5305
|
+
import_ids: queued.importIds,
|
|
5306
|
+
imported: [],
|
|
5307
|
+
not_imported: [],
|
|
5308
|
+
qualified: [],
|
|
5309
|
+
still_running: [],
|
|
5310
|
+
failed: [],
|
|
5311
|
+
quota_exceeded: false,
|
|
5312
|
+
skipped_already_qualified: [],
|
|
5313
|
+
not_in_lens: [],
|
|
5314
|
+
region: client.region,
|
|
5315
|
+
_meta: queued._meta
|
|
5316
|
+
};
|
|
5317
|
+
}
|
|
5018
5318
|
ctx?.progress?.({
|
|
5019
5319
|
progress: 1,
|
|
5020
5320
|
total: 3,
|
|
5021
5321
|
message: "Importing leads (phase 1/3 \u2014 preprocess + commit)"
|
|
5022
5322
|
});
|
|
5023
|
-
const
|
|
5323
|
+
const importResultRaw = await importLeads.execute(client, {
|
|
5024
5324
|
domains: params.domains,
|
|
5025
5325
|
records: params.records,
|
|
5026
5326
|
mappings: params.mappings,
|
|
5027
5327
|
per_phase_budget_ms: perPhaseBudget,
|
|
5028
5328
|
total_budget_ms: totalBudget,
|
|
5029
|
-
...params.dry_run === true ? { dry_run: true } : {}
|
|
5329
|
+
...params.dry_run === true ? { dry_run: true } : {},
|
|
5330
|
+
wait_for_completion: true
|
|
5030
5331
|
}, ctx);
|
|
5332
|
+
if (isImportLeadsRunningResult(importResultRaw)) {
|
|
5333
|
+
throw client.makeError("IMPORT_ASYNC_UNEXPECTED", "Import returned an async handle while import_and_qualify was waiting for completion", "Retry with wait_for_completion=false and poll leadbay_import_status, or retry the blocking call.", "POST /imports");
|
|
5334
|
+
}
|
|
5335
|
+
const importResult = importResultRaw;
|
|
5031
5336
|
if (importResult.cancelled) {
|
|
5032
5337
|
return {
|
|
5033
5338
|
kind: "result",
|
|
@@ -5320,7 +5625,7 @@ import { mkdir as mkdirAsync, lstat, open as fsOpen, readFile, rename, stat, unl
|
|
|
5320
5625
|
import { constants as fsConstants } from "fs";
|
|
5321
5626
|
import { dirname, resolve as resolvePath } from "path";
|
|
5322
5627
|
import { homedir, platform } from "os";
|
|
5323
|
-
import { createHash as
|
|
5628
|
+
import { createHash as createHash3, randomUUID as randomUUID2 } from "crypto";
|
|
5324
5629
|
var DEFAULT_IDEMPOTENCY_WINDOW_MS = 5 * 60 * 1e3;
|
|
5325
5630
|
var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
5326
5631
|
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;
|
|
@@ -5335,7 +5640,7 @@ function computeIdempotencyKey(args) {
|
|
|
5335
5640
|
args.phone ? "p1" : "p0",
|
|
5336
5641
|
`l${args.lens_id}`
|
|
5337
5642
|
];
|
|
5338
|
-
return
|
|
5643
|
+
return createHash3("sha256").update(parts.join("|")).digest("hex");
|
|
5339
5644
|
}
|
|
5340
5645
|
function computeQualifyIdempotencyKey(args) {
|
|
5341
5646
|
const parts = [
|
|
@@ -5345,7 +5650,16 @@ function computeQualifyIdempotencyKey(args) {
|
|
|
5345
5650
|
`l${args.lens_id}`,
|
|
5346
5651
|
args.mapping_fingerprint
|
|
5347
5652
|
];
|
|
5348
|
-
return
|
|
5653
|
+
return createHash3("sha256").update(parts.join("|")).digest("hex");
|
|
5654
|
+
}
|
|
5655
|
+
function computeImportIdempotencyKey(args) {
|
|
5656
|
+
const parts = [
|
|
5657
|
+
"import",
|
|
5658
|
+
args.mode,
|
|
5659
|
+
args.dry_run ? "dry1" : "dry0",
|
|
5660
|
+
args.import_fingerprint
|
|
5661
|
+
];
|
|
5662
|
+
return createHash3("sha256").update(parts.join("|")).digest("hex");
|
|
5349
5663
|
}
|
|
5350
5664
|
function normalizeLaunchInputs(args) {
|
|
5351
5665
|
return {
|
|
@@ -5483,7 +5797,7 @@ var LocalBulkStore = class {
|
|
|
5483
5797
|
throw new Error("missing launched_at");
|
|
5484
5798
|
if (!Array.isArray(r.lead_ids) || !r.lead_ids.every((x) => typeof x === "string"))
|
|
5485
5799
|
throw new Error("invalid lead_ids");
|
|
5486
|
-
if (r.status !== "pending" && r.status !== "launched" && r.status !== "failed")
|
|
5800
|
+
if (r.status !== "pending" && r.status !== "launched" && r.status !== "complete" && r.status !== "failed" && r.status !== "cancelled")
|
|
5487
5801
|
throw new Error("invalid status");
|
|
5488
5802
|
if (typeof r.idempotency_key !== "string")
|
|
5489
5803
|
throw new Error("invalid idempotency_key");
|
|
@@ -5510,6 +5824,52 @@ var LocalBulkStore = class {
|
|
|
5510
5824
|
out.total_budget_ms = r.total_budget_ms;
|
|
5511
5825
|
return out;
|
|
5512
5826
|
}
|
|
5827
|
+
if (kind === "import") {
|
|
5828
|
+
if (!Array.isArray(r.import_ids) || !r.import_ids.every((x) => typeof x === "string"))
|
|
5829
|
+
throw new Error("invalid import_ids");
|
|
5830
|
+
if (r.mode !== "domains" && r.mode !== "records")
|
|
5831
|
+
throw new Error("invalid mode");
|
|
5832
|
+
if (typeof r.dry_run !== "boolean")
|
|
5833
|
+
throw new Error("invalid dry_run");
|
|
5834
|
+
if (typeof r.records_total !== "number")
|
|
5835
|
+
throw new Error("invalid records_total");
|
|
5836
|
+
const out = {
|
|
5837
|
+
kind: "import",
|
|
5838
|
+
bulk_id: r.bulk_id,
|
|
5839
|
+
launched_at: r.launched_at,
|
|
5840
|
+
lead_ids: r.lead_ids,
|
|
5841
|
+
import_ids: r.import_ids,
|
|
5842
|
+
mode: r.mode,
|
|
5843
|
+
dry_run: r.dry_run,
|
|
5844
|
+
records_total: r.records_total,
|
|
5845
|
+
status: r.status,
|
|
5846
|
+
idempotency_key: r.idempotency_key,
|
|
5847
|
+
durability: this.backend
|
|
5848
|
+
};
|
|
5849
|
+
if (r.result && typeof r.result === "object") {
|
|
5850
|
+
const result = r.result;
|
|
5851
|
+
if (Array.isArray(result.leads) && Array.isArray(result.not_imported) && Array.isArray(result.importIds) && result.importIds.every((x) => typeof x === "string")) {
|
|
5852
|
+
out.result = {
|
|
5853
|
+
leads: result.leads,
|
|
5854
|
+
not_imported: result.not_imported,
|
|
5855
|
+
importIds: result.importIds
|
|
5856
|
+
};
|
|
5857
|
+
}
|
|
5858
|
+
}
|
|
5859
|
+
if (r.progress && typeof r.progress === "object") {
|
|
5860
|
+
const p = r.progress;
|
|
5861
|
+
if (typeof p.phase === "string" && typeof p.records_processed === "number" && typeof p.records_total === "number") {
|
|
5862
|
+
out.progress = {
|
|
5863
|
+
phase: p.phase,
|
|
5864
|
+
records_processed: p.records_processed,
|
|
5865
|
+
records_total: p.records_total
|
|
5866
|
+
};
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5869
|
+
if (typeof r.error === "string")
|
|
5870
|
+
out.error = r.error;
|
|
5871
|
+
return out;
|
|
5872
|
+
}
|
|
5513
5873
|
if (kind === "enrich") {
|
|
5514
5874
|
if (!Array.isArray(r.titles) || !r.titles.every((x) => typeof x === "string"))
|
|
5515
5875
|
throw new Error("invalid titles");
|
|
@@ -5673,10 +6033,119 @@ var LocalBulkStore = class {
|
|
|
5673
6033
|
return { record, reused: false };
|
|
5674
6034
|
});
|
|
5675
6035
|
}
|
|
6036
|
+
async findOrCreatePendingImport(args) {
|
|
6037
|
+
const idempotency_key = computeImportIdempotencyKey({
|
|
6038
|
+
import_fingerprint: args.import_fingerprint,
|
|
6039
|
+
mode: args.mode,
|
|
6040
|
+
dry_run: args.dry_run
|
|
6041
|
+
});
|
|
6042
|
+
const window = args.idempotency_window_ms ?? DEFAULT_IDEMPOTENCY_WINDOW_MS;
|
|
6043
|
+
return this.mutex.run(async () => {
|
|
6044
|
+
const all = this.prune(await this.readAll());
|
|
6045
|
+
const nowMs = this.now();
|
|
6046
|
+
const existing = all.find((r) => r.kind === "import" && r.idempotency_key === idempotency_key && r.status !== "failed" && r.status !== "cancelled" && nowMs - Date.parse(r.launched_at) < window);
|
|
6047
|
+
if (existing) {
|
|
6048
|
+
this.logger?.info?.(`bulk.reused kind=import bulk_id=${existing.bulk_id} seconds_since_original=${Math.round((nowMs - Date.parse(existing.launched_at)) / 1e3)}`);
|
|
6049
|
+
return {
|
|
6050
|
+
record: existing,
|
|
6051
|
+
reused: true,
|
|
6052
|
+
seconds_since_original: Math.round((nowMs - Date.parse(existing.launched_at)) / 1e3)
|
|
6053
|
+
};
|
|
6054
|
+
}
|
|
6055
|
+
const record = {
|
|
6056
|
+
kind: "import",
|
|
6057
|
+
bulk_id: randomUUID2(),
|
|
6058
|
+
launched_at: new Date(nowMs).toISOString(),
|
|
6059
|
+
lead_ids: [],
|
|
6060
|
+
import_ids: [],
|
|
6061
|
+
mode: args.mode,
|
|
6062
|
+
dry_run: args.dry_run,
|
|
6063
|
+
records_total: args.records_total,
|
|
6064
|
+
progress: {
|
|
6065
|
+
phase: "queued",
|
|
6066
|
+
records_processed: 0,
|
|
6067
|
+
records_total: args.records_total
|
|
6068
|
+
},
|
|
6069
|
+
status: "pending",
|
|
6070
|
+
idempotency_key,
|
|
6071
|
+
durability: this.backend
|
|
6072
|
+
};
|
|
6073
|
+
all.push(record);
|
|
6074
|
+
await this.writeAll(all);
|
|
6075
|
+
this.logger?.info?.(`bulk.registered kind=import bulk_id=${record.bulk_id} mode=${record.mode} records_total=${record.records_total} durability=${record.durability}`);
|
|
6076
|
+
return { record, reused: false };
|
|
6077
|
+
});
|
|
6078
|
+
}
|
|
5676
6079
|
async getQualify(bulk_id) {
|
|
5677
6080
|
const r = await this.get(bulk_id);
|
|
5678
6081
|
return r && r.kind === "qualify" ? r : void 0;
|
|
5679
6082
|
}
|
|
6083
|
+
async getImport(bulk_id) {
|
|
6084
|
+
const r = await this.get(bulk_id);
|
|
6085
|
+
return r && r.kind === "import" ? r : void 0;
|
|
6086
|
+
}
|
|
6087
|
+
async setImportIds(bulk_id, import_ids) {
|
|
6088
|
+
return this.mutex.run(async () => {
|
|
6089
|
+
const all = this.prune(await this.readAll());
|
|
6090
|
+
const idx = all.findIndex((r) => r.bulk_id === bulk_id && r.kind === "import");
|
|
6091
|
+
if (idx < 0)
|
|
6092
|
+
throw new Error(`import bulk_id not found: ${bulk_id}`);
|
|
6093
|
+
const record = all[idx];
|
|
6094
|
+
all[idx] = {
|
|
6095
|
+
...record,
|
|
6096
|
+
import_ids: [...new Set(import_ids)].sort(),
|
|
6097
|
+
status: record.status === "pending" ? "launched" : record.status
|
|
6098
|
+
};
|
|
6099
|
+
await this.writeAll(all);
|
|
6100
|
+
return all[idx];
|
|
6101
|
+
});
|
|
6102
|
+
}
|
|
6103
|
+
async setImportProgress(bulk_id, progress) {
|
|
6104
|
+
return this.mutex.run(async () => {
|
|
6105
|
+
const all = this.prune(await this.readAll());
|
|
6106
|
+
const idx = all.findIndex((r) => r.bulk_id === bulk_id && r.kind === "import");
|
|
6107
|
+
if (idx < 0)
|
|
6108
|
+
throw new Error(`import bulk_id not found: ${bulk_id}`);
|
|
6109
|
+
const record = all[idx];
|
|
6110
|
+
all[idx] = { ...record, progress };
|
|
6111
|
+
await this.writeAll(all);
|
|
6112
|
+
return all[idx];
|
|
6113
|
+
});
|
|
6114
|
+
}
|
|
6115
|
+
async markImportComplete(bulk_id, result) {
|
|
6116
|
+
return this.mutex.run(async () => {
|
|
6117
|
+
const all = this.prune(await this.readAll());
|
|
6118
|
+
const idx = all.findIndex((r) => r.bulk_id === bulk_id && r.kind === "import");
|
|
6119
|
+
if (idx < 0)
|
|
6120
|
+
throw new Error(`import bulk_id not found: ${bulk_id}`);
|
|
6121
|
+
const record = all[idx];
|
|
6122
|
+
all[idx] = {
|
|
6123
|
+
...record,
|
|
6124
|
+
import_ids: [...new Set(result.importIds)].sort(),
|
|
6125
|
+
result,
|
|
6126
|
+
progress: {
|
|
6127
|
+
phase: "complete",
|
|
6128
|
+
records_processed: record.records_total,
|
|
6129
|
+
records_total: record.records_total
|
|
6130
|
+
},
|
|
6131
|
+
status: "complete"
|
|
6132
|
+
};
|
|
6133
|
+
await this.writeAll(all);
|
|
6134
|
+
this.logger?.info?.(`bulk.import_complete bulk_id=${bulk_id}`);
|
|
6135
|
+
return all[idx];
|
|
6136
|
+
});
|
|
6137
|
+
}
|
|
6138
|
+
async markImportFailed(bulk_id, error) {
|
|
6139
|
+
return this.mutex.run(async () => {
|
|
6140
|
+
const all = this.prune(await this.readAll());
|
|
6141
|
+
const idx = all.findIndex((r) => r.bulk_id === bulk_id && r.kind === "import");
|
|
6142
|
+
if (idx < 0)
|
|
6143
|
+
return;
|
|
6144
|
+
all[idx] = { ...all[idx], status: "failed", error };
|
|
6145
|
+
await this.writeAll(all);
|
|
6146
|
+
this.logger?.info?.(`bulk.import_failed bulk_id=${bulk_id}`);
|
|
6147
|
+
});
|
|
6148
|
+
}
|
|
5680
6149
|
async markLaunched(bulk_id) {
|
|
5681
6150
|
return this.mutex.run(async () => {
|
|
5682
6151
|
const all = this.prune(await this.readAll());
|
|
@@ -5770,6 +6239,200 @@ async function createDefaultBulkStore(opts = {}) {
|
|
|
5770
6239
|
}
|
|
5771
6240
|
}
|
|
5772
6241
|
|
|
6242
|
+
// ../core/dist/composite/import-status.js
|
|
6243
|
+
function summarizeImports(imports, dryRun) {
|
|
6244
|
+
let recordsTotal = 0;
|
|
6245
|
+
let recordsProcessed = 0;
|
|
6246
|
+
let hasPreprocess = false;
|
|
6247
|
+
let hasProcess = false;
|
|
6248
|
+
let hasFailed = false;
|
|
6249
|
+
for (const imp of imports) {
|
|
6250
|
+
recordsTotal += Number(imp.total_records ?? 0);
|
|
6251
|
+
recordsProcessed += Number(imp.imported_records ?? 0);
|
|
6252
|
+
if (!imp.pre_processing?.finished) {
|
|
6253
|
+
hasPreprocess = true;
|
|
6254
|
+
continue;
|
|
6255
|
+
}
|
|
6256
|
+
if (imp.pre_processing?.error) {
|
|
6257
|
+
hasFailed = true;
|
|
6258
|
+
continue;
|
|
6259
|
+
}
|
|
6260
|
+
if (dryRun === true) {
|
|
6261
|
+
continue;
|
|
6262
|
+
}
|
|
6263
|
+
if (!imp.processing?.finished) {
|
|
6264
|
+
if (dryRun === false || imp.processing != null)
|
|
6265
|
+
hasProcess = true;
|
|
6266
|
+
continue;
|
|
6267
|
+
}
|
|
6268
|
+
if (imp.processing?.error) {
|
|
6269
|
+
hasFailed = true;
|
|
6270
|
+
}
|
|
6271
|
+
}
|
|
6272
|
+
const phase = hasFailed ? "failed" : hasPreprocess ? "preprocess" : hasProcess ? "process" : imports.length > 0 ? "complete" : "queued";
|
|
6273
|
+
return {
|
|
6274
|
+
phase,
|
|
6275
|
+
records_processed: recordsProcessed,
|
|
6276
|
+
records_total: recordsTotal
|
|
6277
|
+
};
|
|
6278
|
+
}
|
|
6279
|
+
var importStatus = {
|
|
6280
|
+
name: "leadbay_import_status",
|
|
6281
|
+
annotations: {
|
|
6282
|
+
title: "Poll import status",
|
|
6283
|
+
readOnlyHint: true,
|
|
6284
|
+
destructiveHint: false,
|
|
6285
|
+
idempotentHint: true,
|
|
6286
|
+
openWorldHint: true
|
|
6287
|
+
},
|
|
6288
|
+
description: "Retrieve the current state of an async lead import. Pass `handle_id` returned by leadbay_import_leads({wait_for_completion:false}), or pass legacy `importIds[]` to inspect backend wizard rows. This status call performs a single refresh pass and never polls in a loop.\nWhen to use: after leadbay_import_leads or leadbay_import_and_qualify returns `{status:'running', handle_id}` for the import phase, call this tool later to retrieve progress or the final import result without re-running the import.\nWhen NOT to use: for qualification handles returned as `qualify_id` \u2014 use leadbay_qualify_status for those; or when you still want the legacy blocking behavior from leadbay_import_leads with wait_for_completion=true.",
|
|
6289
|
+
inputSchema: {
|
|
6290
|
+
type: "object",
|
|
6291
|
+
properties: {
|
|
6292
|
+
handle_id: {
|
|
6293
|
+
type: "string",
|
|
6294
|
+
description: "UUIDv4 handle returned by leadbay_import_leads when wait_for_completion=false."
|
|
6295
|
+
},
|
|
6296
|
+
importIds: {
|
|
6297
|
+
type: "array",
|
|
6298
|
+
description: "Legacy backend file-import ids to inspect directly.",
|
|
6299
|
+
items: { type: "string" }
|
|
6300
|
+
}
|
|
6301
|
+
},
|
|
6302
|
+
additionalProperties: false
|
|
6303
|
+
},
|
|
6304
|
+
outputSchema: {
|
|
6305
|
+
type: "object",
|
|
6306
|
+
properties: {
|
|
6307
|
+
status: { type: "string", description: "running, complete, or failed." },
|
|
6308
|
+
handle_id: { type: "string" },
|
|
6309
|
+
importIds: { type: "array", items: { type: "string" } },
|
|
6310
|
+
progress: { type: "object" },
|
|
6311
|
+
result: {
|
|
6312
|
+
type: "object",
|
|
6313
|
+
description: "Final import result when the handle has completed in this MCP instance."
|
|
6314
|
+
},
|
|
6315
|
+
error: { type: "string" },
|
|
6316
|
+
region: { type: "string" },
|
|
6317
|
+
_meta: { type: "object" }
|
|
6318
|
+
},
|
|
6319
|
+
required: ["status", "importIds", "progress", "region", "_meta"]
|
|
6320
|
+
},
|
|
6321
|
+
execute: async (client, params, ctx) => {
|
|
6322
|
+
let handleId = params.handle_id;
|
|
6323
|
+
let importIds = params.importIds ?? [];
|
|
6324
|
+
let handleDryRun;
|
|
6325
|
+
if (handleId) {
|
|
6326
|
+
if (!isValidBulkId(handleId)) {
|
|
6327
|
+
throw client.makeError("BULK_INVALID_ID", "handle_id is not a valid UUIDv4", "Pass the handle_id returned by leadbay_import_leads verbatim.", "");
|
|
6328
|
+
}
|
|
6329
|
+
if (!ctx?.bulkTracker) {
|
|
6330
|
+
throw client.makeError("BULK_TRACKER_UNAVAILABLE", "No BulkTracker configured on this MCP instance", "leadbay_import_status needs a BulkTracker to resolve handle_id. Pass importIds[] directly as a fallback.", "");
|
|
6331
|
+
}
|
|
6332
|
+
const record = await ctx.bulkTracker.getImport(handleId);
|
|
6333
|
+
if (!record) {
|
|
6334
|
+
const any = await ctx.bulkTracker.get(handleId);
|
|
6335
|
+
if (any && any.kind !== "import") {
|
|
6336
|
+
throw client.makeError("BULK_WRONG_KIND", "This handle was not created by leadbay_import_leads", "Use leadbay_qualify_status for qualify ids or leadbay_bulk_enrich_status for enrich ids.", "");
|
|
6337
|
+
}
|
|
6338
|
+
throw client.makeError("BULK_NOT_FOUND", "No import record for that handle_id", "It may have expired (30-day TTL) or the MCP process was restarted without persistence.", "");
|
|
6339
|
+
}
|
|
6340
|
+
importIds = record.import_ids;
|
|
6341
|
+
handleDryRun = record.dry_run;
|
|
6342
|
+
if (record.status === "complete" && record.result) {
|
|
6343
|
+
return {
|
|
6344
|
+
status: "complete",
|
|
6345
|
+
handle_id: handleId,
|
|
6346
|
+
importIds,
|
|
6347
|
+
progress: record.progress ?? {
|
|
6348
|
+
phase: "complete",
|
|
6349
|
+
records_processed: record.records_total,
|
|
6350
|
+
records_total: record.records_total
|
|
6351
|
+
},
|
|
6352
|
+
result: record.result,
|
|
6353
|
+
region: client.region,
|
|
6354
|
+
_meta: client.lastMeta ?? {
|
|
6355
|
+
region: client.region,
|
|
6356
|
+
endpoint: "bulk-store",
|
|
6357
|
+
latency_ms: null,
|
|
6358
|
+
retry_after: null
|
|
6359
|
+
}
|
|
6360
|
+
};
|
|
6361
|
+
}
|
|
6362
|
+
if (record.status === "failed") {
|
|
6363
|
+
return {
|
|
6364
|
+
status: "failed",
|
|
6365
|
+
handle_id: handleId,
|
|
6366
|
+
importIds,
|
|
6367
|
+
progress: record.progress ?? {
|
|
6368
|
+
phase: "failed",
|
|
6369
|
+
records_processed: 0,
|
|
6370
|
+
records_total: record.records_total
|
|
6371
|
+
},
|
|
6372
|
+
error: record.error ?? "import failed",
|
|
6373
|
+
region: client.region,
|
|
6374
|
+
_meta: client.lastMeta ?? {
|
|
6375
|
+
region: client.region,
|
|
6376
|
+
endpoint: "bulk-store",
|
|
6377
|
+
latency_ms: null,
|
|
6378
|
+
retry_after: null
|
|
6379
|
+
}
|
|
6380
|
+
};
|
|
6381
|
+
}
|
|
6382
|
+
if (importIds.length === 0) {
|
|
6383
|
+
return {
|
|
6384
|
+
status: "running",
|
|
6385
|
+
handle_id: handleId,
|
|
6386
|
+
importIds,
|
|
6387
|
+
progress: record.progress ?? {
|
|
6388
|
+
phase: "queued",
|
|
6389
|
+
records_processed: 0,
|
|
6390
|
+
records_total: record.records_total
|
|
6391
|
+
},
|
|
6392
|
+
region: client.region,
|
|
6393
|
+
_meta: client.lastMeta ?? {
|
|
6394
|
+
region: client.region,
|
|
6395
|
+
endpoint: "bulk-store",
|
|
6396
|
+
latency_ms: null,
|
|
6397
|
+
retry_after: null
|
|
6398
|
+
}
|
|
6399
|
+
};
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
if (importIds.length === 0) {
|
|
6403
|
+
throw client.makeError("IMPORT_STATUS_INPUT_REQUIRED", "Pass either handle_id or importIds[]", "Call leadbay_import_leads with wait_for_completion=false first, then pass its handle_id.", "");
|
|
6404
|
+
}
|
|
6405
|
+
const imports = await Promise.all(importIds.map((id) => client.request("GET", `/imports/${id}`)));
|
|
6406
|
+
const progress = summarizeImports(imports, handleDryRun);
|
|
6407
|
+
const failed = imports.find((i) => i.pre_processing?.error || i.processing?.error);
|
|
6408
|
+
const complete = imports.every((i) => {
|
|
6409
|
+
if (i.pre_processing?.error || i.processing?.error)
|
|
6410
|
+
return false;
|
|
6411
|
+
if (handleDryRun === true)
|
|
6412
|
+
return Boolean(i.pre_processing?.finished);
|
|
6413
|
+
if (handleDryRun === false)
|
|
6414
|
+
return Boolean(i.processing?.finished);
|
|
6415
|
+
return Boolean(i.processing?.finished || i.pre_processing?.finished && !i.processing);
|
|
6416
|
+
});
|
|
6417
|
+
return {
|
|
6418
|
+
status: failed ? "failed" : complete ? "complete" : "running",
|
|
6419
|
+
...handleId ? { handle_id: handleId } : {},
|
|
6420
|
+
importIds,
|
|
6421
|
+
progress,
|
|
6422
|
+
...failed ? {
|
|
6423
|
+
error: failed.pre_processing?.error ?? failed.processing?.error ?? "import failed"
|
|
6424
|
+
} : {},
|
|
6425
|
+
region: client.region,
|
|
6426
|
+
_meta: client.lastMeta ?? {
|
|
6427
|
+
region: client.region,
|
|
6428
|
+
endpoint: "GET /imports/<id>",
|
|
6429
|
+
latency_ms: null,
|
|
6430
|
+
retry_after: null
|
|
6431
|
+
}
|
|
6432
|
+
};
|
|
6433
|
+
}
|
|
6434
|
+
};
|
|
6435
|
+
|
|
5773
6436
|
// ../core/dist/composite/qualify-status.js
|
|
5774
6437
|
var qualifyStatus = {
|
|
5775
6438
|
name: "leadbay_qualify_status",
|
|
@@ -5865,7 +6528,8 @@ var qualifyStatus = {
|
|
|
5865
6528
|
if (!record) {
|
|
5866
6529
|
const any = await ctx.bulkTracker.get(params.qualify_id);
|
|
5867
6530
|
if (any && any.kind !== "qualify") {
|
|
5868
|
-
|
|
6531
|
+
const hint = any.kind === "import" ? "Call leadbay_import_status with this id instead." : "Call leadbay_bulk_enrich_status with this id instead.";
|
|
6532
|
+
throw client.makeError("BULK_WRONG_KIND", `This bulk_id was created by ${any.kind}, not leadbay_import_and_qualify`, hint, "");
|
|
5869
6533
|
}
|
|
5870
6534
|
throw client.makeError("BULK_NOT_FOUND", "No qualify record for that qualify_id", "It may have expired (30-day TTL) or the MCP process was restarted without persistence. Re-launch via leadbay_import_and_qualify.", "");
|
|
5871
6535
|
}
|
|
@@ -5927,7 +6591,7 @@ var qualifyStatus = {
|
|
|
5927
6591
|
const out = {
|
|
5928
6592
|
qualify_id: record.bulk_id,
|
|
5929
6593
|
launched_at: record.launched_at,
|
|
5930
|
-
status: record.status,
|
|
6594
|
+
status: record.status === "complete" ? "launched" : record.status,
|
|
5931
6595
|
import_ids: record.import_ids,
|
|
5932
6596
|
lens_id: record.lens_id,
|
|
5933
6597
|
lead_ids: record.lead_ids,
|
|
@@ -6429,12 +7093,12 @@ var bulkEnrichStatus = {
|
|
|
6429
7093
|
hint: "The record may have aged out (30-day TTL) or the MCP process was restarted without persistence. Launch a new enrichment via leadbay_enrich_titles."
|
|
6430
7094
|
};
|
|
6431
7095
|
}
|
|
6432
|
-
if (record.kind
|
|
7096
|
+
if (record.kind !== "enrich") {
|
|
6433
7097
|
return {
|
|
6434
7098
|
error: true,
|
|
6435
7099
|
code: "BULK_WRONG_KIND",
|
|
6436
|
-
message:
|
|
6437
|
-
hint: "Call leadbay_qualify_status with this id instead.",
|
|
7100
|
+
message: `This bulk_id was created by ${record.kind === "qualify" ? "leadbay_import_and_qualify" : "leadbay_import_leads"}, not leadbay_enrich_titles.`,
|
|
7101
|
+
hint: record.kind === "qualify" ? "Call leadbay_qualify_status with this id instead." : "Call leadbay_import_status with this id instead.",
|
|
6438
7102
|
bulk_id: record.bulk_id
|
|
6439
7103
|
};
|
|
6440
7104
|
}
|
|
@@ -7438,6 +8102,7 @@ var compositeReadTools = [
|
|
|
7438
8102
|
accountStatus,
|
|
7439
8103
|
bulkEnrichStatus,
|
|
7440
8104
|
qualifyStatus,
|
|
8105
|
+
importStatus,
|
|
7441
8106
|
// listMappableFields is granular-shaped but the import composites depend on
|
|
7442
8107
|
// it for discoverability; expose it always-on so agents can find custom fields
|
|
7443
8108
|
// without needing LEADBAY_MCP_ADVANCED=1.
|
|
@@ -7523,6 +8188,7 @@ export {
|
|
|
7523
8188
|
LocalBulkStore,
|
|
7524
8189
|
InMemoryBulkStore,
|
|
7525
8190
|
createDefaultBulkStore,
|
|
8191
|
+
importStatus,
|
|
7526
8192
|
qualifyStatus,
|
|
7527
8193
|
enrichTitles,
|
|
7528
8194
|
bulkEnrichStatus,
|
|
@@ -47,6 +47,7 @@ import {
|
|
|
47
47
|
granularWriteTools,
|
|
48
48
|
importAndQualify,
|
|
49
49
|
importLeads,
|
|
50
|
+
importStatus,
|
|
50
51
|
isValidBulkId,
|
|
51
52
|
launchBulkEnrichment,
|
|
52
53
|
listLenses,
|
|
@@ -74,7 +75,7 @@ import {
|
|
|
74
75
|
tools,
|
|
75
76
|
updateLens,
|
|
76
77
|
updateLensFilter
|
|
77
|
-
} from "./chunk-
|
|
78
|
+
} from "./chunk-QAOJARMK.js";
|
|
78
79
|
export {
|
|
79
80
|
InMemoryBulkStore,
|
|
80
81
|
LeadbayClient,
|
|
@@ -123,6 +124,7 @@ export {
|
|
|
123
124
|
granularWriteTools,
|
|
124
125
|
importAndQualify,
|
|
125
126
|
importLeads,
|
|
127
|
+
importStatus,
|
|
126
128
|
isValidBulkId,
|
|
127
129
|
launchBulkEnrichment,
|
|
128
130
|
listLenses,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leadbay/mcp",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"mcpName": "io.github.leadbay/leadbay-mcp",
|
|
5
5
|
"description": "Model Context Protocol (MCP) server for Leadbay — AI lead discovery, qualification, and enrichment for Claude Desktop, Cursor, and Claude Code.",
|
|
6
6
|
"type": "module",
|