@leadbay/mcp 0.2.1 → 0.2.2
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 +12 -0
- package/README.md +6 -6
- package/dist/bin.js +25 -13
- package/dist/{chunk-BGJ6JWIO.js → chunk-FJBO2MY2.js} +610 -18
- package/dist/{dist-PIXZN6N4.js → dist-FENQ2I7R.js} +11 -1
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog — @leadbay/mcp
|
|
2
2
|
|
|
3
|
+
## 0.2.2 — 2026-04-21
|
|
4
|
+
|
|
5
|
+
Bug fix + contract correction + mental-model docs release.
|
|
6
|
+
|
|
7
|
+
- **Fix [product#3504](https://github.com/leadbay/product/issues/3504)**: `npx -y @leadbay/mcp` no longer exits silently on Node 25. The `isEntrypoint` check now resolves both sides through `realpathSync`, so the npx shim symlink path matches the real `dist/bin.js`. Previously `main()` never ran under npx and the MCP host saw a dead connection with no diagnostic.
|
|
8
|
+
- Replaced stale `app.leadbay.ai` URLs in error strings (NOT_AUTHENTICATED, AUTH_INVALID, BILLING_SUSPENDED, PERMISSION_DENIED) and CLI help text with runnable commands (`leadbay-mcp install`, `leadbay-mcp login`) or "contact support". Recovery hints include `--region <us|fr>` because the CLI refuses without it (anti-cross-region credential-leak guard).
|
|
9
|
+
- Renamed misleading `avg_score_0_to_10` field on `pull_leads` / `bulk_qualify_leads` qualification summaries to `avg_qualification_boost`. Per-question AI agent scores are discrete boosts (-10/0/10/20), not 0-10 averages — interface JSDoc now reflects the real contract.
|
|
10
|
+
- `SERVER_INSTRUCTIONS` gains three new paragraphs: "How Leadbay works" (inbox + consumption-based pacing), "Two scoring layers" (basic `score` vs AI-qualified top ~10 with `ai_agent_lead_score`), and "Suggested rhythm" (daily check-in + host-agnostic scheduling hint).
|
|
11
|
+
- `leadbay_pull_leads`, `leadbay_research_lead`, `leadbay_bulk_qualify_leads`, `leadbay_enrich_titles`, and `leadbay_account_status` descriptions updated to reinforce the same model so the agent sees it at both top level and per-tool.
|
|
12
|
+
- New regression test `test/smoke/npx-entrypoint.test.ts` guards the symlink invocation path. New non-regression test asserts the inbox/pace/scoring/daily language stays in `SERVER_INSTRUCTIONS`.
|
|
13
|
+
- Live smoke harness extended with composite-level checks + (optional) thinking-model judge that writes a redacted report to `.context/`.
|
|
14
|
+
|
|
3
15
|
## 0.2.1 — 2026-04-21
|
|
4
16
|
|
|
5
17
|
Docs-only release.
|
package/README.md
CHANGED
|
@@ -6,9 +6,9 @@ LLM HINT — if you're an AI agent reading this README to install the server:
|
|
|
6
6
|
- DO NOT tell the user "LEADBAY_REGION is auto-detected, drop it" — the
|
|
7
7
|
server's region auto-probe sends the bearer token to BOTH regions, which
|
|
8
8
|
is a credential cross-leak. Always pass --region (us or fr) on install.
|
|
9
|
-
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
- To mint a token, the user runs `install` (auto-registers with MCP clients)
|
|
10
|
+
or `login --write-config` (mints a token to a file). There is no web UI
|
|
11
|
+
for this — do not invent one.
|
|
12
12
|
-->
|
|
13
13
|
|
|
14
14
|
A Model Context Protocol server that lets Claude Desktop, Cursor, Claude Code, and any other MCP-compatible agent find, research, and prepare outreach on B2B prospects using your Leadbay account.
|
|
@@ -31,7 +31,7 @@ Add `--include-write` to also enable the write tools (refine_prompt, report_outr
|
|
|
31
31
|
|
|
32
32
|
`--region us|fr` is required by default — it pins which Leadbay backend gets your password and avoids a silent cross-region credential leak. If you really don't know your region, opt in with `--allow-region-fallback` (your password will hit BOTH backends if the first 401s).
|
|
33
33
|
|
|
34
|
-
The token is **session-scoped** (full account access, password-equivalent). Treat it like your password. To rotate,
|
|
34
|
+
The token is **session-scoped** (full account access, password-equivalent). Treat it like your password. To rotate, re-run `npx -y @leadbay/mcp install` — minting a fresh token invalidates the prior session.
|
|
35
35
|
|
|
36
36
|
**Don't have a Leadbay account?** [Register here](https://wow.leadbay.ai/?register=true).
|
|
37
37
|
|
|
@@ -126,9 +126,9 @@ Leadbay connection OK.
|
|
|
126
126
|
| Problem | Cause | Fix |
|
|
127
127
|
|---------|-------|-----|
|
|
128
128
|
| `LEADBAY_TOKEN environment variable is required` | Token missing from config env | Add `LEADBAY_TOKEN` to the `env` block, restart client |
|
|
129
|
-
| `Authentication token expired or invalid` | Token revoked or wrong region | Re-
|
|
129
|
+
| `Authentication token expired or invalid` | Token revoked or wrong region | Re-mint a token: `npx -y @leadbay/mcp install --email <you> --region <us\|fr>`; verify `LEADBAY_REGION` |
|
|
130
130
|
| `Leadbay doctor: could not reach any Leadbay region` | Wrong region OR network blocked | Run `doctor` with `LEADBAY_REGION=fr` to auto-probe. Check `https://api-us.leadbay.app` reachable. |
|
|
131
|
-
| `No enrichment credits remaining` | Out of quota |
|
|
131
|
+
| `No enrichment credits remaining` | Out of quota | Contact Leadbay support to extend quota |
|
|
132
132
|
| Claude Desktop "loading forever" on first use | `npx` cold-start fetching the package | First run takes ~10s. Prefer `npm install -g @leadbay/mcp` for faster startup. |
|
|
133
133
|
| Claude Desktop doesn't show Leadbay tools | Server crashed at startup | Check `~/Library/Logs/Claude/mcp*.log` (macOS) or `%APPDATA%\Claude\logs\mcp*.log` (Windows). |
|
|
134
134
|
|
package/dist/bin.js
CHANGED
|
@@ -3,12 +3,15 @@ import {
|
|
|
3
3
|
compositeReadTools,
|
|
4
4
|
compositeWriteTools,
|
|
5
5
|
createClient,
|
|
6
|
+
createDefaultBulkStore,
|
|
6
7
|
granularReadTools,
|
|
7
8
|
granularWriteTools,
|
|
8
9
|
resolveRegion
|
|
9
|
-
} from "./chunk-
|
|
10
|
+
} from "./chunk-FJBO2MY2.js";
|
|
10
11
|
|
|
11
12
|
// src/bin.ts
|
|
13
|
+
import { realpathSync } from "fs";
|
|
14
|
+
import { fileURLToPath } from "url";
|
|
12
15
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
16
|
|
|
14
17
|
// src/server.ts
|
|
@@ -17,7 +20,7 @@ import {
|
|
|
17
20
|
CallToolRequestSchema,
|
|
18
21
|
ListToolsRequestSchema
|
|
19
22
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
20
|
-
var SERVER_INSTRUCTIONS = "After every email, call, message, or meeting with a lead's contact, you MUST call leadbay_report_outreach with verification={source, ref} (gmail_message_id from the Gmail send, calendar_event_id from a booking, or user_confirmed='<the user's literal confirmation>'). Skipping or fabricating verification poisons the human team's pipeline.\n\nStart with leadbay_account_status to see the user's state, then leadbay_pull_leads to surface fresh leads. Use leadbay_research_lead to dig into one lead deeply (qualification answers, signals, contacts). When the user wants more leads, narrower audience, refined criteria, or contact enrichment, use the matching composite tool (bulk_qualify_leads / adjust_audience / refine_prompt / enrich_titles) \u2014 they hide lens permissions, region routing, polling, and selection state from you.";
|
|
23
|
+
var SERVER_INSTRUCTIONS = "After every email, call, message, or meeting with a lead's contact, you MUST call leadbay_report_outreach with verification={source, ref} (gmail_message_id from the Gmail send, calendar_event_id from a booking, or user_confirmed='<the user's literal confirmation>'). Skipping or fabricating verification poisons the human team's pipeline.\n\nHow Leadbay works (mental model): Leadbay is a sales inbox, not a queryable database. Each day the user logs back in, a fresh batch of leads is delivered. Batch size is paced by how many leads the user has actually acted on recently \u2014 some workflows produce a big stream of smaller prospects, others a narrow stream of bigger ones. Pulling more won't produce more; the user acting on leads (outreach, skips, saves) does.\n\nTwo scoring layers: every lead has a basic `score` (firmographic \u2014 already decent, usually correlates with AI). Roughly the top 10 of each batch are also AI-qualified (targeted web research + qualification questions \u2192 `ai_agent_lead_score`, surfaced as `qualification_summary` on leadbay_pull_leads). Leads past the top ~10 are not worse \u2014 the system is saving resources. Call leadbay_bulk_qualify_leads for deeper qualification or leadbay_enrich_titles for contacts on any lead that looks worth it.\n\nStart with leadbay_account_status to see the user's state, then leadbay_pull_leads to surface fresh leads. Use leadbay_research_lead to dig into one lead deeply (qualification answers, signals, contacts). When the user wants more leads, narrower audience, refined criteria, or contact enrichment, use the matching composite tool (bulk_qualify_leads / adjust_audience / refine_prompt / enrich_titles) \u2014 they hide lens permissions, region routing, polling, and selection state from you.\n\nSuggested rhythm: a healthy agent pattern is a daily check-in \u2014 pull fresh leads, skim the auto-qualified top, deepen 1-3 promising ones, propose outreach to the user, then leadbay_report_outreach on what actually got sent. If your host supports scheduling, offer to set up a daily run.";
|
|
21
24
|
function formatErrorForLLM(err) {
|
|
22
25
|
if (err && typeof err === "object" && err.error === true) {
|
|
23
26
|
const parts = [`${err.message}.`, err.hint];
|
|
@@ -85,7 +88,10 @@ function buildServer(client, opts = {}) {
|
|
|
85
88
|
}
|
|
86
89
|
const args = req.params.arguments ?? {};
|
|
87
90
|
try {
|
|
88
|
-
const result = await tool.execute(client, args, {
|
|
91
|
+
const result = await tool.execute(client, args, {
|
|
92
|
+
logger: opts.logger,
|
|
93
|
+
bulkTracker: opts.bulkTracker
|
|
94
|
+
});
|
|
89
95
|
if (result && typeof result === "object" && result.error === true) {
|
|
90
96
|
return {
|
|
91
97
|
content: [
|
|
@@ -113,7 +119,7 @@ function buildServer(client, opts = {}) {
|
|
|
113
119
|
|
|
114
120
|
// src/bin.ts
|
|
115
121
|
import { createRequire } from "module";
|
|
116
|
-
var VERSION = "0.2.
|
|
122
|
+
var VERSION = "0.2.2";
|
|
117
123
|
var HELP = `
|
|
118
124
|
leadbay-mcp ${VERSION} \u2014 Leadbay Model Context Protocol server
|
|
119
125
|
|
|
@@ -132,7 +138,7 @@ USAGE
|
|
|
132
138
|
leadbay-mcp --help Print this help
|
|
133
139
|
|
|
134
140
|
ENV VARS
|
|
135
|
-
LEADBAY_TOKEN (required) Bearer token
|
|
141
|
+
LEADBAY_TOKEN (required) Bearer token (run \`leadbay-mcp install\` to mint one).
|
|
136
142
|
LEADBAY_REGION (optional) "us" or "fr". Auto-detected from /users/me if unset.
|
|
137
143
|
LEADBAY_BASE_URL (optional) Override API base URL (for staging/dev).
|
|
138
144
|
LEADBAY_MCP_ADVANCED (optional) Set to "1" to expose granular API tools alongside
|
|
@@ -189,7 +195,7 @@ function parseLogLevel(raw) {
|
|
|
189
195
|
}
|
|
190
196
|
function exitWithTokenError() {
|
|
191
197
|
process.stderr.write(
|
|
192
|
-
"leadbay-mcp: LEADBAY_TOKEN environment variable is required.\n 1.
|
|
198
|
+
"leadbay-mcp: LEADBAY_TOKEN environment variable is required.\n 1. Run: npx -y @leadbay/mcp install --email <you> --region <us|fr>\n 2. Set it in your MCP client config (e.g. claude_desktop_config.json).\n\nRun `leadbay-mcp --help` for the full config template.\n"
|
|
193
199
|
);
|
|
194
200
|
process.exit(1);
|
|
195
201
|
}
|
|
@@ -326,7 +332,7 @@ async function runLogin(args) {
|
|
|
326
332
|
let result;
|
|
327
333
|
try {
|
|
328
334
|
if (pinnedRegion && !allowFallback) {
|
|
329
|
-
const { REGIONS } = await import("./dist-
|
|
335
|
+
const { REGIONS } = await import("./dist-FENQ2I7R.js");
|
|
330
336
|
const baseUrl = REGIONS[pinnedRegion];
|
|
331
337
|
const c = createClient({ region: pinnedRegion });
|
|
332
338
|
const token = await loginAt(baseUrl, email, password);
|
|
@@ -669,7 +675,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
|
|
|
669
675
|
let region;
|
|
670
676
|
try {
|
|
671
677
|
if (pinnedRegion && !allowFallback) {
|
|
672
|
-
const { REGIONS } = await import("./dist-
|
|
678
|
+
const { REGIONS } = await import("./dist-FENQ2I7R.js");
|
|
673
679
|
const baseUrl = REGIONS[pinnedRegion];
|
|
674
680
|
token = await loginAt(baseUrl, email, password);
|
|
675
681
|
region = pinnedRegion;
|
|
@@ -721,7 +727,7 @@ leadbay-mcp install \u2014 detected MCP clients on this machine:
|
|
|
721
727
|
The token was written into client config files but never printed to your terminal.
|
|
722
728
|
Verify with: LEADBAY_TOKEN=$(...) npx -y @leadbay/mcp@0.2 doctor
|
|
723
729
|
Restart your MCP client(s) to pick up the new server.
|
|
724
|
-
If you ever leak the token,
|
|
730
|
+
If you ever leak the token, run \`leadbay-mcp login --email <you> --region <us|fr>\` to mint a fresh one (which invalidates the prior session).
|
|
725
731
|
`
|
|
726
732
|
);
|
|
727
733
|
return anyOk ? 0 : 1;
|
|
@@ -795,10 +801,16 @@ async function main() {
|
|
|
795
801
|
const client = await resolveClientFromEnv(logger);
|
|
796
802
|
const includeAdvanced = process.env.LEADBAY_MCP_ADVANCED === "1";
|
|
797
803
|
const includeWrite = process.env.LEADBAY_MCP_WRITE === "1";
|
|
798
|
-
const
|
|
804
|
+
const bulkTracker = await createDefaultBulkStore({ logger });
|
|
805
|
+
const server = buildServer(client, {
|
|
806
|
+
includeAdvanced,
|
|
807
|
+
includeWrite,
|
|
808
|
+
logger,
|
|
809
|
+
bulkTracker
|
|
810
|
+
});
|
|
799
811
|
const transport = new StdioServerTransport();
|
|
800
812
|
logger.info?.(
|
|
801
|
-
`Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl})`
|
|
813
|
+
`Starting MCP server v${VERSION} (advanced=${includeAdvanced}, write=${includeWrite}, baseUrl=${client.baseUrl}, bulk_store=${bulkTracker.durability})`
|
|
802
814
|
);
|
|
803
815
|
await server.connect(transport);
|
|
804
816
|
}
|
|
@@ -806,8 +818,8 @@ var isEntrypoint = (() => {
|
|
|
806
818
|
try {
|
|
807
819
|
const entry = process.argv[1];
|
|
808
820
|
if (!entry) return false;
|
|
809
|
-
const
|
|
810
|
-
return
|
|
821
|
+
const self = fileURLToPath(import.meta.url);
|
|
822
|
+
return realpathSync(self) === realpathSync(entry);
|
|
811
823
|
} catch {
|
|
812
824
|
return false;
|
|
813
825
|
}
|
|
@@ -240,7 +240,7 @@ var LeadbayClient = class {
|
|
|
240
240
|
return this.mockRequest(method, path, body);
|
|
241
241
|
}
|
|
242
242
|
if (!this.token) {
|
|
243
|
-
throw this.makeError("NOT_AUTHENTICATED", "Not logged in to Leadbay", "Set LEADBAY_TOKEN
|
|
243
|
+
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);
|
|
244
244
|
}
|
|
245
245
|
await this.acquireSemaphore();
|
|
246
246
|
try {
|
|
@@ -275,7 +275,7 @@ var LeadbayClient = class {
|
|
|
275
275
|
return;
|
|
276
276
|
}
|
|
277
277
|
if (!this.token) {
|
|
278
|
-
throw this.makeError("NOT_AUTHENTICATED", "Not logged in to Leadbay", "Set LEADBAY_TOKEN
|
|
278
|
+
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);
|
|
279
279
|
}
|
|
280
280
|
await this.acquireSemaphore();
|
|
281
281
|
try {
|
|
@@ -332,7 +332,7 @@ var LeadbayClient = class {
|
|
|
332
332
|
}
|
|
333
333
|
const retryAfter = parseRetryAfter(headers["retry-after"]);
|
|
334
334
|
if (status === 401) {
|
|
335
|
-
return this.makeError("AUTH_EXPIRED", "Authentication token expired or invalid", "Your LEADBAY_TOKEN is no longer valid. Regenerate
|
|
335
|
+
return this.makeError("AUTH_EXPIRED", "Authentication token expired or invalid", "Your LEADBAY_TOKEN is no longer valid. Regenerate it: npx -y @leadbay/mcp login --email <you> --region <us|fr>, then restart your MCP client.", endpoint);
|
|
336
336
|
}
|
|
337
337
|
if (status === 429 || status === 402 || parsed?.error === "quota_exceeded" || parsed?.error?.code === "quota_exceeded") {
|
|
338
338
|
return this.makeError("QUOTA_EXCEEDED", retryAfter ? `Quota exceeded \u2014 retry in ${retryAfter}s` : "Quota exceeded", retryAfter ? `Wait ${retryAfter}s before retrying. Check leadbay_get_quota to see which resource window was hit.` : "Wait, then retry. Check leadbay_get_quota to see which resource window (daily/weekly/monthly) was hit.", endpoint, retryAfter);
|
|
@@ -340,9 +340,9 @@ var LeadbayClient = class {
|
|
|
340
340
|
if (status === 403) {
|
|
341
341
|
const msg = parsed?.message || parsed?.error || parsed?.error?.message || "";
|
|
342
342
|
if (typeof msg === "string" && (msg.includes("suspend") || msg.includes("billing"))) {
|
|
343
|
-
return this.makeError("BILLING_SUSPENDED", "Account billing is suspended", "Your Leadbay account billing is suspended.
|
|
343
|
+
return this.makeError("BILLING_SUSPENDED", "Account billing is suspended", "Your Leadbay account billing is suspended. Contact Leadbay support.", endpoint);
|
|
344
344
|
}
|
|
345
|
-
return this.makeError("FORBIDDEN", "Insufficient permissions", "Your token does not have access to this resource.
|
|
345
|
+
return this.makeError("FORBIDDEN", "Insufficient permissions", "Your token does not have access to this resource. Contact Leadbay support to verify account permissions.", endpoint);
|
|
346
346
|
}
|
|
347
347
|
if (status === 404) {
|
|
348
348
|
return this.makeError("NOT_FOUND", parsed?.message || parsed?.error?.message || "Resource not found", "Verify the ID is correct", endpoint);
|
|
@@ -754,7 +754,7 @@ var getTasteProfile = {
|
|
|
754
754
|
question: q.question
|
|
755
755
|
})),
|
|
756
756
|
...isEmpty ? {
|
|
757
|
-
hint: "No taste profile configured yet.
|
|
757
|
+
hint: "No taste profile configured yet. Use leadbay_refine_prompt or contact Leadbay support to set one up for better lead matching."
|
|
758
758
|
} : {}
|
|
759
759
|
};
|
|
760
760
|
}
|
|
@@ -827,7 +827,7 @@ var enrichContacts = {
|
|
|
827
827
|
const me = await client.request("GET", "/users/me");
|
|
828
828
|
creditsRemaining = me.organization.billing?.ai_credits ?? null;
|
|
829
829
|
if (creditsRemaining !== null && creditsRemaining <= 0) {
|
|
830
|
-
throw client.makeError("QUOTA_EXCEEDED", "No enrichment credits remaining", "
|
|
830
|
+
throw client.makeError("QUOTA_EXCEEDED", "No enrichment credits remaining", "Contact Leadbay support to extend your credit quota");
|
|
831
831
|
}
|
|
832
832
|
} catch (e) {
|
|
833
833
|
if (e?.code === "QUOTA_EXCEEDED")
|
|
@@ -1664,11 +1664,11 @@ function summarise(responses) {
|
|
|
1664
1664
|
if (excerpt && excerpt.length > 200) {
|
|
1665
1665
|
excerpt = excerpt.slice(0, 197) + "...";
|
|
1666
1666
|
}
|
|
1667
|
-
return { answered, total,
|
|
1667
|
+
return { answered, total, avg_qualification_boost: avg, best_response_excerpt: excerpt };
|
|
1668
1668
|
}
|
|
1669
1669
|
var pullLeads = {
|
|
1670
1670
|
name: "leadbay_pull_leads",
|
|
1671
|
-
description: "Pull up new leads from the user's last-active lens
|
|
1671
|
+
description: "Pull up new leads from the user's last-active lens \u2014 the canonical 'show me today's prospects' tool. Leadbay works like an inbox: each time the user logs back in, a fresh batch is delivered, paced by how many leads they've actually acted on recently. Pulling more won't produce more; user outreach/skips/saves does. Each returned lead carries a one-line qualification_summary built from leadbay_ai_agent_responses, plus the rich tags / scores / recommended_contact_title / engagement counters / in-flight flags from the lead summary. Roughly the top 10 of the batch come pre-qualified (populated qualification_summary + ai_agent_lead_score); leads below the top ~10 carry only the basic firmographic `score` \u2014 not worse, just resource-saved by the system. Call leadbay_bulk_qualify_leads to deepen any of them on demand. When to use: as the agent's default opening move when the user wants to see leads, or as a daily check-in for what's new today. When NOT to use: when the user has named a specific lens \u2014 pass lensId to override the auto-resolution. Replaces the older leadbay_find_prospects (which is removed in v0.2.0).",
|
|
1672
1672
|
inputSchema: {
|
|
1673
1673
|
type: "object",
|
|
1674
1674
|
properties: {
|
|
@@ -1701,7 +1701,7 @@ var pullLeads = {
|
|
|
1701
1701
|
summary: {
|
|
1702
1702
|
answered: 0,
|
|
1703
1703
|
total: 0,
|
|
1704
|
-
|
|
1704
|
+
avg_qualification_boost: null,
|
|
1705
1705
|
best_response_excerpt: null
|
|
1706
1706
|
}
|
|
1707
1707
|
};
|
|
@@ -1783,7 +1783,7 @@ function reshapeWebFetchContent(content) {
|
|
|
1783
1783
|
}
|
|
1784
1784
|
var researchLead = {
|
|
1785
1785
|
name: "leadbay_research_lead",
|
|
1786
|
-
description: "Tell me everything decision-relevant about a single lead. Bundles the lens-scoped lead profile, the AI qualification answers (the agent's knowledge-base food), the structured web-research signals (with hot flags + sources), the enriched contacts, and the recent notes/epilogue/prospecting activity in one call. Order is deliberate: qualification first, then signals, then firmographics, then contacts, then engagement. When to use: when picking up a single lead from leadbay_pull_leads to decide whether to act on it. When NOT to use: across many leads at once \u2014 that's leadbay_pull_leads' job. (This composite supersedes the lower-level leadbay_get_lead_profile in agent flow; the granular tool stays available for fine-grained access.)",
|
|
1786
|
+
description: "Tell me everything decision-relevant about a single lead. Bundles the lens-scoped lead profile, the AI qualification answers (the agent's knowledge-base food), the structured web-research signals (with hot flags + sources), the enriched contacts, and the recent notes/epilogue/prospecting activity in one call. Order is deliberate: qualification first, then signals, then firmographics, then contacts, then engagement. Scoring has two layers: the basic `score` (firmographic, always present, already decent) and the AI qualification layer (`ai_agent_lead_score` + per-question answers + web_fetch signals). The AI layer is pre-populated for roughly the top 10 of each daily batch, and on-demand (via leadbay_bulk_qualify_leads) for anything below that. Combine both layers when judging a lead. When to use: when picking up a single lead from leadbay_pull_leads to decide whether to act on it. When NOT to use: across many leads at once \u2014 that's leadbay_pull_leads' job. (This composite supersedes the lower-level leadbay_get_lead_profile in agent flow; the granular tool stays available for fine-grained access.)",
|
|
1787
1787
|
inputSchema: {
|
|
1788
1788
|
type: "object",
|
|
1789
1789
|
properties: {
|
|
@@ -1997,7 +1997,7 @@ var recallOrderedTitles = {
|
|
|
1997
1997
|
// ../core/dist/composite/account-status.js
|
|
1998
1998
|
var accountStatus = {
|
|
1999
1999
|
name: "leadbay_account_status",
|
|
2000
|
-
description: "Show the user's account state \u2014 admin rights, language, last-active lens, current quota usage across daily/weekly/monthly windows for llm_completion / ai_rescore / web_fetch resources, and whether the org's intelligence is mid-regeneration. When to use: at the start of a session to know what the agent can/can't do, or after a 429 to explain to the user which resource window was exhausted and when it resets. When NOT to use: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating.",
|
|
2000
|
+
description: "Show the user's account state \u2014 admin rights, language, last-active lens, current quota usage across daily/weekly/monthly windows for llm_completion / ai_rescore / web_fetch resources, and whether the org's intelligence is mid-regeneration. Quota windows also hint at the user's consumption pace: heavy recent activity (ai_rescore / web_fetch near their window limits) is a signal that Leadbay will deliver a larger fresh batch next time the user logs back in, since batch size is paced by real consumption. When to use: at the start of a session to know what the agent can/can't do, or after a 429 to explain to the user which resource window was exhausted and when it resets. When NOT to use: as a pre-flight gate before bulk ops \u2014 operations themselves return 429; this tool is for context, not gating.",
|
|
2001
2001
|
inputSchema: { type: "object", properties: {} },
|
|
2002
2002
|
execute: async (client, _params, ctx) => {
|
|
2003
2003
|
const me = await client.resolveMe();
|
|
@@ -2042,7 +2042,7 @@ var DEFAULT_PER_LEAD_BUDGET_MS = 9e4;
|
|
|
2042
2042
|
var DEFAULT_TOTAL_BUDGET_MS = 5 * 6e4;
|
|
2043
2043
|
var bulkQualifyLeads = {
|
|
2044
2044
|
name: "leadbay_bulk_qualify_leads",
|
|
2045
|
-
description: "Pick the next N unqualified leads in the active lens and qualify them (run AI rescore + web fetch), polling 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. When to use: when the user wants more qualified leads than what's currently shown. When NOT to use: to qualify a single specific lead \u2014 that's leadbay_qualify_lead (granular, advanced).",
|
|
2045
|
+
description: "Pick the next N unqualified leads in the active lens and qualify them (run AI rescore + web fetch), polling 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).",
|
|
2046
2046
|
inputSchema: {
|
|
2047
2047
|
type: "object",
|
|
2048
2048
|
properties: {
|
|
@@ -2169,7 +2169,7 @@ var bulkQualifyLeads = {
|
|
|
2169
2169
|
qualification_summary: responses.length > 0 ? {
|
|
2170
2170
|
answered: responses.filter((r) => r.score != null).length,
|
|
2171
2171
|
total: responses.length,
|
|
2172
|
-
|
|
2172
|
+
avg_qualification_boost: avg
|
|
2173
2173
|
} : null,
|
|
2174
2174
|
signals_count: lastWf?.content ? Object.values(lastWf.content).reduce((acc, arr) => acc + (Array.isArray(arr) ? arr.length : 0), 0) : null,
|
|
2175
2175
|
_stillRunning: stillRunning
|
|
@@ -2194,7 +2194,7 @@ var bulkQualifyLeads = {
|
|
|
2194
2194
|
var DEFAULT_CANDIDATE_COUNT = 25;
|
|
2195
2195
|
var enrichTitles = {
|
|
2196
2196
|
name: "leadbay_enrich_titles",
|
|
2197
|
-
description: "Order contact enrichments by job title across many leads. Two modes: (A) NO titles param \u2014 returns the available titles + Leadbay's title_suggestions + auto_included_titles + a count of enrichable contacts, so the agent can ask the user which titles to enrich. (B) titles given \u2014 calls preview, then launches if there's anything enrichable. On 429 returns {status:'quota_exceeded'} cleanly. Selection lifecycle is wrapped in a try/finally so the user's selection is left clean even on error. When to use: as the agent's go-to enrichment entry point. When NOT to use: to enrich a single contact \u2014 that's leadbay_enrich_contacts (granular).",
|
|
2197
|
+
description: "Order contact enrichments by job title across many leads. Contacts are NOT returned by default with a lead (Leadbay keeps enrichment out-of-band to control cost); the agent requests them on demand via this tool when it's ready to actually reach out. Two modes: (A) NO titles param \u2014 returns the available titles + Leadbay's title_suggestions + auto_included_titles + a count of enrichable contacts, so the agent can ask the user which titles to enrich. (B) titles given \u2014 calls preview, then launches if there's anything enrichable. On 429 returns {status:'quota_exceeded'} cleanly. Selection lifecycle is wrapped in a try/finally so the user's selection is left clean even on error. When to use: as the agent's go-to enrichment entry point, immediately before proposing outreach. When NOT to use: to enrich a single contact \u2014 that's leadbay_enrich_contacts (granular). Speculatively, before the user has committed to outreaching \u2014 enrichment spends credits.",
|
|
2198
2198
|
inputSchema: {
|
|
2199
2199
|
type: "object",
|
|
2200
2200
|
properties: {
|
|
@@ -2235,9 +2235,11 @@ var enrichTitles = {
|
|
|
2235
2235
|
hint: "Set email:true (most common) or phone:true"
|
|
2236
2236
|
};
|
|
2237
2237
|
}
|
|
2238
|
+
const explicitLeadIds = params.leadIds && params.leadIds.length > 0;
|
|
2239
|
+
const selectionSource = explicitLeadIds ? "explicit" : "wishlist";
|
|
2240
|
+
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
2238
2241
|
let leadIds = params.leadIds;
|
|
2239
2242
|
if (!leadIds || leadIds.length === 0) {
|
|
2240
|
-
const lensId = params.lensId ?? await client.resolveDefaultLens();
|
|
2241
2243
|
const cnt = params.candidateCount ?? DEFAULT_CANDIDATE_COUNT;
|
|
2242
2244
|
const wish = await client.request("GET", `/lenses/${lensId}/leads/wishlist?count=${Math.min(cnt, 50)}&page=0`);
|
|
2243
2245
|
leadIds = wish.items.map((l) => l.id);
|
|
@@ -2311,9 +2313,53 @@ var enrichTitles = {
|
|
|
2311
2313
|
would_launch: { titles: params.titles, email, phone }
|
|
2312
2314
|
};
|
|
2313
2315
|
}
|
|
2316
|
+
const tracker = ctx?.bulkTracker;
|
|
2317
|
+
let bulkRecord;
|
|
2318
|
+
let bulkReused = false;
|
|
2319
|
+
let bulkSecondsSinceOriginal;
|
|
2320
|
+
if (tracker) {
|
|
2321
|
+
const res = await tracker.findOrCreatePending({
|
|
2322
|
+
lead_ids: leadIds,
|
|
2323
|
+
titles: params.titles,
|
|
2324
|
+
email,
|
|
2325
|
+
phone,
|
|
2326
|
+
lens_id: lensId,
|
|
2327
|
+
selection_source: selectionSource
|
|
2328
|
+
});
|
|
2329
|
+
bulkRecord = {
|
|
2330
|
+
bulk_id: res.record.bulk_id,
|
|
2331
|
+
launched_at: res.record.launched_at,
|
|
2332
|
+
durability: res.record.durability
|
|
2333
|
+
};
|
|
2334
|
+
bulkReused = res.reused;
|
|
2335
|
+
bulkSecondsSinceOriginal = res.seconds_since_original;
|
|
2336
|
+
if (bulkReused && res.record.status !== "failed") {
|
|
2337
|
+
return {
|
|
2338
|
+
mode: "already_launched",
|
|
2339
|
+
re_used: true,
|
|
2340
|
+
bulk_id: res.record.bulk_id,
|
|
2341
|
+
launched_at: res.record.launched_at,
|
|
2342
|
+
durability: res.record.durability,
|
|
2343
|
+
seconds_since_original_launch: bulkSecondsSinceOriginal ?? 0,
|
|
2344
|
+
titles: params.titles,
|
|
2345
|
+
email,
|
|
2346
|
+
phone,
|
|
2347
|
+
preview,
|
|
2348
|
+
message: `No new enrichment was ordered; quota not spent. An identical bulk was launched ${bulkSecondsSinceOriginal ?? 0}s ago. Poll leadbay_bulk_enrich_status with this bulk_id for results.`,
|
|
2349
|
+
next_action: "Call leadbay_bulk_enrich_status({bulk_id}) to check progress; include_contacts=true for the final read."
|
|
2350
|
+
};
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2314
2353
|
try {
|
|
2315
2354
|
await client.requestVoid("POST", "/leads/selection/enrichment/launch", { titles: params.titles, email, phone });
|
|
2316
2355
|
} catch (err) {
|
|
2356
|
+
if (bulkRecord && tracker) {
|
|
2357
|
+
try {
|
|
2358
|
+
await tracker.markFailed(bulkRecord.bulk_id);
|
|
2359
|
+
} catch (e) {
|
|
2360
|
+
ctx?.logger?.warn?.(`enrich_titles: tracker.markFailed failed: ${e?.message ?? e}`);
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2317
2363
|
if (err?.code === "QUOTA_EXCEEDED") {
|
|
2318
2364
|
return {
|
|
2319
2365
|
status: "quota_exceeded",
|
|
@@ -2324,6 +2370,26 @@ var enrichTitles = {
|
|
|
2324
2370
|
}
|
|
2325
2371
|
throw err;
|
|
2326
2372
|
}
|
|
2373
|
+
if (bulkRecord && tracker) {
|
|
2374
|
+
try {
|
|
2375
|
+
await tracker.markLaunched(bulkRecord.bulk_id);
|
|
2376
|
+
} catch (e) {
|
|
2377
|
+
ctx?.logger?.warn?.(`enrich_titles: tracker.markLaunched failed: ${e?.message ?? e}`);
|
|
2378
|
+
return {
|
|
2379
|
+
mode: "launched_tracker_pending",
|
|
2380
|
+
launched: true,
|
|
2381
|
+
preview,
|
|
2382
|
+
bulk_id: bulkRecord.bulk_id,
|
|
2383
|
+
launched_at: bulkRecord.launched_at,
|
|
2384
|
+
durability: bulkRecord.durability,
|
|
2385
|
+
titles: params.titles,
|
|
2386
|
+
email,
|
|
2387
|
+
phone,
|
|
2388
|
+
message: "Enrichment job launched on the backend, but the local tracker record could not be flipped to 'launched'. The bulk_id is still valid \u2014 leadbay_bulk_enrich_status will return status:'pending' until the tracker heals.",
|
|
2389
|
+
next_action: "Wait ~60s, then call leadbay_bulk_enrich_status({bulk_id}). If it persists, restart the MCP."
|
|
2390
|
+
};
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2327
2393
|
return {
|
|
2328
2394
|
mode: "launched",
|
|
2329
2395
|
preview,
|
|
@@ -2331,8 +2397,11 @@ var enrichTitles = {
|
|
|
2331
2397
|
titles: params.titles,
|
|
2332
2398
|
email,
|
|
2333
2399
|
phone,
|
|
2334
|
-
|
|
2335
|
-
|
|
2400
|
+
bulk_id: bulkRecord?.bulk_id,
|
|
2401
|
+
launched_at: bulkRecord?.launched_at,
|
|
2402
|
+
durability: bulkRecord?.durability,
|
|
2403
|
+
message: bulkRecord ? "Enrichment job launched. Backend has no server-side bulk_id yet; MCP minted a client-side bulk_id (persisted to disk by default) so you can poll via leadbay_bulk_enrich_status." : "Enrichment job launched. No bulk_id tracker configured \u2014 poll leadbay_get_contacts per lead after ~60s; contact.enrichment.done flips to true.",
|
|
2404
|
+
next_action: bulkRecord ? "Call leadbay_bulk_enrich_status({bulk_id}) after ~60s; pass include_contacts=true for the final read." : "Wait ~60s, then call leadbay_research_lead or leadbay_get_contacts on the leads you care about."
|
|
2336
2405
|
};
|
|
2337
2406
|
} finally {
|
|
2338
2407
|
try {
|
|
@@ -2347,6 +2416,523 @@ var enrichTitles = {
|
|
|
2347
2416
|
}
|
|
2348
2417
|
};
|
|
2349
2418
|
|
|
2419
|
+
// ../core/dist/jobs/bulk-store.js
|
|
2420
|
+
import { mkdir as mkdirAsync, lstat, open as fsOpen, readFile, rename, stat, unlink } from "fs/promises";
|
|
2421
|
+
import { constants as fsConstants } from "fs";
|
|
2422
|
+
import { dirname, resolve as resolvePath } from "path";
|
|
2423
|
+
import { homedir, platform } from "os";
|
|
2424
|
+
import { createHash, randomUUID } from "crypto";
|
|
2425
|
+
var DEFAULT_IDEMPOTENCY_WINDOW_MS = 5 * 60 * 1e3;
|
|
2426
|
+
var TTL_MS = 30 * 24 * 60 * 60 * 1e3;
|
|
2427
|
+
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;
|
|
2428
|
+
function isValidBulkId(v) {
|
|
2429
|
+
return typeof v === "string" && UUIDV4_RE.test(v);
|
|
2430
|
+
}
|
|
2431
|
+
function computeIdempotencyKey(args) {
|
|
2432
|
+
const parts = [
|
|
2433
|
+
[...args.lead_ids].sort().join(","),
|
|
2434
|
+
[...args.titles].sort().join(","),
|
|
2435
|
+
args.email ? "e1" : "e0",
|
|
2436
|
+
args.phone ? "p1" : "p0",
|
|
2437
|
+
`l${args.lens_id}`
|
|
2438
|
+
];
|
|
2439
|
+
return createHash("sha256").update(parts.join("|")).digest("hex");
|
|
2440
|
+
}
|
|
2441
|
+
function normalizeLaunchInputs(args) {
|
|
2442
|
+
return {
|
|
2443
|
+
lead_ids: [...new Set(args.lead_ids)].sort(),
|
|
2444
|
+
titles: [...new Set(args.titles)].sort()
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
var AsyncMutex = class {
|
|
2448
|
+
locked = false;
|
|
2449
|
+
queue = [];
|
|
2450
|
+
async lock() {
|
|
2451
|
+
if (!this.locked) {
|
|
2452
|
+
this.locked = true;
|
|
2453
|
+
return;
|
|
2454
|
+
}
|
|
2455
|
+
return new Promise((resolve) => {
|
|
2456
|
+
this.queue.push(() => {
|
|
2457
|
+
this.locked = true;
|
|
2458
|
+
resolve();
|
|
2459
|
+
});
|
|
2460
|
+
});
|
|
2461
|
+
}
|
|
2462
|
+
unlock() {
|
|
2463
|
+
this.locked = false;
|
|
2464
|
+
const next = this.queue.shift();
|
|
2465
|
+
if (next)
|
|
2466
|
+
next();
|
|
2467
|
+
}
|
|
2468
|
+
async run(fn) {
|
|
2469
|
+
await this.lock();
|
|
2470
|
+
try {
|
|
2471
|
+
return await fn();
|
|
2472
|
+
} finally {
|
|
2473
|
+
this.unlock();
|
|
2474
|
+
}
|
|
2475
|
+
}
|
|
2476
|
+
};
|
|
2477
|
+
var LocalBulkStore = class {
|
|
2478
|
+
backend;
|
|
2479
|
+
path;
|
|
2480
|
+
logger;
|
|
2481
|
+
allowUnsafePath;
|
|
2482
|
+
now;
|
|
2483
|
+
mutex = new AsyncMutex();
|
|
2484
|
+
memory = [];
|
|
2485
|
+
// Cached file resolution — computed lazily on first access.
|
|
2486
|
+
initialized = false;
|
|
2487
|
+
constructor(opts) {
|
|
2488
|
+
this.backend = opts.backend;
|
|
2489
|
+
this.logger = opts.logger;
|
|
2490
|
+
this.allowUnsafePath = !!opts.allowUnsafePath;
|
|
2491
|
+
this.now = opts.now ?? Date.now;
|
|
2492
|
+
if (this.backend === "file") {
|
|
2493
|
+
if (!opts.path) {
|
|
2494
|
+
throw new Error("LocalBulkStore: path is required when backend=file");
|
|
2495
|
+
}
|
|
2496
|
+
this.path = resolvePath(opts.path);
|
|
2497
|
+
this.validatePath(this.path);
|
|
2498
|
+
}
|
|
2499
|
+
}
|
|
2500
|
+
get durability() {
|
|
2501
|
+
return this.backend;
|
|
2502
|
+
}
|
|
2503
|
+
// Exposed for tests and ops tooling.
|
|
2504
|
+
get resolvedPath() {
|
|
2505
|
+
return this.path;
|
|
2506
|
+
}
|
|
2507
|
+
validatePath(p) {
|
|
2508
|
+
if (this.allowUnsafePath)
|
|
2509
|
+
return;
|
|
2510
|
+
const home = resolvePath(homedir());
|
|
2511
|
+
if (p !== home && !p.startsWith(home + "/") && !p.startsWith(home + "\\")) {
|
|
2512
|
+
throw new Error(`LocalBulkStore: path ${p} is outside $HOME (${home}). Set LEADBAY_BULK_STORE_PATH_UNSAFE=1 to override.`);
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
async ensureInitialized() {
|
|
2516
|
+
if (this.initialized || this.backend !== "file") {
|
|
2517
|
+
this.initialized = true;
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
const dir = dirname(this.path);
|
|
2521
|
+
await mkdirAsync(dir, { recursive: true, mode: 448 });
|
|
2522
|
+
try {
|
|
2523
|
+
const st = await lstat(this.path);
|
|
2524
|
+
if (st.isSymbolicLink()) {
|
|
2525
|
+
throw new Error(`LocalBulkStore: refusing to use ${this.path} \u2014 path is a symlink. Set LEADBAY_BULK_STORE_PATH_UNSAFE=1 to override.`);
|
|
2526
|
+
}
|
|
2527
|
+
} catch (err) {
|
|
2528
|
+
if (err?.code !== "ENOENT")
|
|
2529
|
+
throw err;
|
|
2530
|
+
}
|
|
2531
|
+
this.initialized = true;
|
|
2532
|
+
}
|
|
2533
|
+
// ─── Storage layer (file or memory) ──────────────────────────────────────
|
|
2534
|
+
async readAll() {
|
|
2535
|
+
if (this.backend === "memory")
|
|
2536
|
+
return [...this.memory];
|
|
2537
|
+
await this.ensureInitialized();
|
|
2538
|
+
let raw;
|
|
2539
|
+
try {
|
|
2540
|
+
raw = await readFile(this.path, "utf8");
|
|
2541
|
+
} catch (err) {
|
|
2542
|
+
if (err?.code === "ENOENT")
|
|
2543
|
+
return [];
|
|
2544
|
+
throw err;
|
|
2545
|
+
}
|
|
2546
|
+
let parsed;
|
|
2547
|
+
try {
|
|
2548
|
+
parsed = JSON.parse(raw);
|
|
2549
|
+
} catch (err) {
|
|
2550
|
+
this.logger?.warn?.(`bulk.record_dropped file_parse_failed ${err?.message ?? err}`);
|
|
2551
|
+
return [];
|
|
2552
|
+
}
|
|
2553
|
+
if (!Array.isArray(parsed)) {
|
|
2554
|
+
this.logger?.warn?.("bulk.record_dropped file_not_array");
|
|
2555
|
+
return [];
|
|
2556
|
+
}
|
|
2557
|
+
const out = [];
|
|
2558
|
+
for (const entry of parsed) {
|
|
2559
|
+
try {
|
|
2560
|
+
out.push(this.validateRecord(entry));
|
|
2561
|
+
} catch (err) {
|
|
2562
|
+
this.logger?.warn?.(`bulk.record_dropped invalid_record ${err?.message ?? err}`);
|
|
2563
|
+
}
|
|
2564
|
+
}
|
|
2565
|
+
return out;
|
|
2566
|
+
}
|
|
2567
|
+
validateRecord(raw) {
|
|
2568
|
+
if (!raw || typeof raw !== "object")
|
|
2569
|
+
throw new Error("not an object");
|
|
2570
|
+
const r = raw;
|
|
2571
|
+
if (!isValidBulkId(r.bulk_id))
|
|
2572
|
+
throw new Error("invalid bulk_id");
|
|
2573
|
+
if (typeof r.launched_at !== "string")
|
|
2574
|
+
throw new Error("missing launched_at");
|
|
2575
|
+
if (!Array.isArray(r.lead_ids) || !r.lead_ids.every((x) => typeof x === "string"))
|
|
2576
|
+
throw new Error("invalid lead_ids");
|
|
2577
|
+
if (!Array.isArray(r.titles) || !r.titles.every((x) => typeof x === "string"))
|
|
2578
|
+
throw new Error("invalid titles");
|
|
2579
|
+
if (typeof r.email !== "boolean")
|
|
2580
|
+
throw new Error("invalid email");
|
|
2581
|
+
if (typeof r.phone !== "boolean")
|
|
2582
|
+
throw new Error("invalid phone");
|
|
2583
|
+
if (typeof r.lens_id !== "number")
|
|
2584
|
+
throw new Error("invalid lens_id");
|
|
2585
|
+
if (r.selection_source !== "explicit" && r.selection_source !== "wishlist")
|
|
2586
|
+
throw new Error("invalid selection_source");
|
|
2587
|
+
if (r.status !== "pending" && r.status !== "launched" && r.status !== "failed")
|
|
2588
|
+
throw new Error("invalid status");
|
|
2589
|
+
if (typeof r.idempotency_key !== "string")
|
|
2590
|
+
throw new Error("invalid idempotency_key");
|
|
2591
|
+
return {
|
|
2592
|
+
bulk_id: r.bulk_id,
|
|
2593
|
+
launched_at: r.launched_at,
|
|
2594
|
+
lead_ids: r.lead_ids,
|
|
2595
|
+
titles: r.titles,
|
|
2596
|
+
email: r.email,
|
|
2597
|
+
phone: r.phone,
|
|
2598
|
+
lens_id: r.lens_id,
|
|
2599
|
+
selection_source: r.selection_source,
|
|
2600
|
+
status: r.status,
|
|
2601
|
+
idempotency_key: r.idempotency_key,
|
|
2602
|
+
durability: this.backend
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
async writeAll(records) {
|
|
2606
|
+
if (this.backend === "memory") {
|
|
2607
|
+
this.memory = records.map((r) => ({ ...r, durability: "memory" }));
|
|
2608
|
+
return;
|
|
2609
|
+
}
|
|
2610
|
+
await this.ensureInitialized();
|
|
2611
|
+
const payload = records.map((r) => ({ ...r, durability: "file" }));
|
|
2612
|
+
const json = JSON.stringify(payload, null, 2);
|
|
2613
|
+
const tmp = this.path + ".tmp";
|
|
2614
|
+
let fh = await openTmpFileExclusive(tmp);
|
|
2615
|
+
try {
|
|
2616
|
+
await fh.writeFile(json, { encoding: "utf8" });
|
|
2617
|
+
await fh.sync();
|
|
2618
|
+
} finally {
|
|
2619
|
+
await fh.close();
|
|
2620
|
+
}
|
|
2621
|
+
if (platform() === "win32") {
|
|
2622
|
+
try {
|
|
2623
|
+
await unlink(this.path);
|
|
2624
|
+
} catch (err) {
|
|
2625
|
+
if (err?.code !== "ENOENT")
|
|
2626
|
+
throw err;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
await rename(tmp, this.path);
|
|
2630
|
+
try {
|
|
2631
|
+
const dirFh = await fsOpen(dirname(this.path), "r");
|
|
2632
|
+
try {
|
|
2633
|
+
await dirFh.sync();
|
|
2634
|
+
} finally {
|
|
2635
|
+
await dirFh.close();
|
|
2636
|
+
}
|
|
2637
|
+
} catch {
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
// ─── TTL cleanup ─────────────────────────────────────────────────────────
|
|
2641
|
+
prune(records) {
|
|
2642
|
+
const cutoff = this.now() - TTL_MS;
|
|
2643
|
+
const kept = [];
|
|
2644
|
+
for (const r of records) {
|
|
2645
|
+
const launched = Date.parse(r.launched_at);
|
|
2646
|
+
if (Number.isFinite(launched) && launched >= cutoff) {
|
|
2647
|
+
kept.push(r);
|
|
2648
|
+
} else {
|
|
2649
|
+
this.logger?.info?.(`bulk.ttl_dropped bulk_id=${r.bulk_id} launched_at=${r.launched_at}`);
|
|
2650
|
+
}
|
|
2651
|
+
}
|
|
2652
|
+
return kept;
|
|
2653
|
+
}
|
|
2654
|
+
// ─── BulkTracker API ────────────────────────────────────────────────────
|
|
2655
|
+
async findOrCreatePending(args) {
|
|
2656
|
+
const { lead_ids, titles } = normalizeLaunchInputs(args);
|
|
2657
|
+
const idempotency_key = computeIdempotencyKey({
|
|
2658
|
+
lead_ids,
|
|
2659
|
+
titles,
|
|
2660
|
+
email: args.email,
|
|
2661
|
+
phone: args.phone,
|
|
2662
|
+
lens_id: args.lens_id
|
|
2663
|
+
});
|
|
2664
|
+
const window = args.idempotency_window_ms ?? DEFAULT_IDEMPOTENCY_WINDOW_MS;
|
|
2665
|
+
return this.mutex.run(async () => {
|
|
2666
|
+
const all = this.prune(await this.readAll());
|
|
2667
|
+
const nowMs = this.now();
|
|
2668
|
+
const existing = all.find((r) => r.idempotency_key === idempotency_key && r.status !== "failed" && nowMs - Date.parse(r.launched_at) < window);
|
|
2669
|
+
if (existing) {
|
|
2670
|
+
this.logger?.info?.(`bulk.reused bulk_id=${existing.bulk_id} seconds_since_original=${Math.round((nowMs - Date.parse(existing.launched_at)) / 1e3)}`);
|
|
2671
|
+
return {
|
|
2672
|
+
record: existing,
|
|
2673
|
+
reused: true,
|
|
2674
|
+
seconds_since_original: Math.round((nowMs - Date.parse(existing.launched_at)) / 1e3)
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
const record = {
|
|
2678
|
+
bulk_id: randomUUID(),
|
|
2679
|
+
launched_at: new Date(nowMs).toISOString(),
|
|
2680
|
+
lead_ids,
|
|
2681
|
+
titles,
|
|
2682
|
+
email: args.email,
|
|
2683
|
+
phone: args.phone,
|
|
2684
|
+
lens_id: args.lens_id,
|
|
2685
|
+
selection_source: args.selection_source,
|
|
2686
|
+
status: "pending",
|
|
2687
|
+
idempotency_key,
|
|
2688
|
+
durability: this.backend
|
|
2689
|
+
};
|
|
2690
|
+
all.push(record);
|
|
2691
|
+
await this.writeAll(all);
|
|
2692
|
+
this.logger?.info?.(`bulk.registered bulk_id=${record.bulk_id} lens_id=${record.lens_id} lead_count=${record.lead_ids.length} titles_count=${record.titles.length} durability=${record.durability}`);
|
|
2693
|
+
return { record, reused: false };
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
async markLaunched(bulk_id) {
|
|
2697
|
+
return this.mutex.run(async () => {
|
|
2698
|
+
const all = this.prune(await this.readAll());
|
|
2699
|
+
const idx = all.findIndex((r) => r.bulk_id === bulk_id);
|
|
2700
|
+
if (idx < 0) {
|
|
2701
|
+
throw new Error(`bulk_id not found: ${bulk_id}`);
|
|
2702
|
+
}
|
|
2703
|
+
all[idx] = { ...all[idx], status: "launched" };
|
|
2704
|
+
await this.writeAll(all);
|
|
2705
|
+
this.logger?.info?.(`bulk.launched bulk_id=${bulk_id}`);
|
|
2706
|
+
return all[idx];
|
|
2707
|
+
});
|
|
2708
|
+
}
|
|
2709
|
+
async markFailed(bulk_id) {
|
|
2710
|
+
return this.mutex.run(async () => {
|
|
2711
|
+
const all = this.prune(await this.readAll());
|
|
2712
|
+
const idx = all.findIndex((r) => r.bulk_id === bulk_id);
|
|
2713
|
+
if (idx < 0) {
|
|
2714
|
+
return;
|
|
2715
|
+
}
|
|
2716
|
+
all[idx] = { ...all[idx], status: "failed" };
|
|
2717
|
+
await this.writeAll(all);
|
|
2718
|
+
this.logger?.info?.(`bulk.launch_failed bulk_id=${bulk_id}`);
|
|
2719
|
+
});
|
|
2720
|
+
}
|
|
2721
|
+
async get(bulk_id) {
|
|
2722
|
+
return this.mutex.run(async () => {
|
|
2723
|
+
const all = this.prune(await this.readAll());
|
|
2724
|
+
return all.find((r) => r.bulk_id === bulk_id);
|
|
2725
|
+
});
|
|
2726
|
+
}
|
|
2727
|
+
async list() {
|
|
2728
|
+
return this.mutex.run(async () => {
|
|
2729
|
+
const all = this.prune(await this.readAll());
|
|
2730
|
+
return [...all].sort((a, b) => Date.parse(b.launched_at) - Date.parse(a.launched_at));
|
|
2731
|
+
});
|
|
2732
|
+
}
|
|
2733
|
+
};
|
|
2734
|
+
async function openTmpFileExclusive(path) {
|
|
2735
|
+
try {
|
|
2736
|
+
return await fsOpen(path, fsConstants.O_CREAT | fsConstants.O_WRONLY | fsConstants.O_EXCL, 384);
|
|
2737
|
+
} catch (err) {
|
|
2738
|
+
if (err?.code === "EEXIST") {
|
|
2739
|
+
await unlink(path).catch(() => {
|
|
2740
|
+
});
|
|
2741
|
+
return fsOpen(path, fsConstants.O_CREAT | fsConstants.O_WRONLY | fsConstants.O_EXCL, 384);
|
|
2742
|
+
}
|
|
2743
|
+
throw err;
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
var InMemoryBulkStore = class extends LocalBulkStore {
|
|
2747
|
+
constructor(opts = {}) {
|
|
2748
|
+
super({ backend: "memory", logger: opts.logger, now: opts.now });
|
|
2749
|
+
}
|
|
2750
|
+
};
|
|
2751
|
+
async function createDefaultBulkStore(opts = {}) {
|
|
2752
|
+
const env = opts.env ?? process.env;
|
|
2753
|
+
const allowMemory = env.LEADBAY_BULK_STORE_ALLOW_MEMORY === "1";
|
|
2754
|
+
const allowUnsafePath = env.LEADBAY_BULK_STORE_PATH_UNSAFE === "1";
|
|
2755
|
+
const path = env.LEADBAY_BULK_STORE_PATH ?? resolvePath(homedir(), ".leadbay", "bulks.json");
|
|
2756
|
+
try {
|
|
2757
|
+
const store = new LocalBulkStore({
|
|
2758
|
+
backend: "file",
|
|
2759
|
+
path,
|
|
2760
|
+
logger: opts.logger,
|
|
2761
|
+
allowUnsafePath
|
|
2762
|
+
});
|
|
2763
|
+
await store.ensureInitialized();
|
|
2764
|
+
await stat(dirname(path));
|
|
2765
|
+
return store;
|
|
2766
|
+
} catch (err) {
|
|
2767
|
+
if (!allowMemory) {
|
|
2768
|
+
const msg = `bulk store init failed at ${path}: ${err?.message ?? err}. Set LEADBAY_BULK_STORE_ALLOW_MEMORY=1 to fall back to in-memory (handles won't survive MCP restart), or set LEADBAY_BULK_STORE_PATH to a writable path.`;
|
|
2769
|
+
opts.logger?.error?.(msg);
|
|
2770
|
+
throw new Error(msg);
|
|
2771
|
+
}
|
|
2772
|
+
opts.logger?.warn?.(`bulk.fallback_memory path=${path} reason=${err?.message ?? err}`);
|
|
2773
|
+
return new LocalBulkStore({ backend: "memory", logger: opts.logger });
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
// ../core/dist/composite/bulk-enrich-status.js
|
|
2778
|
+
var STATUS_FETCH_CONCURRENCY = 5;
|
|
2779
|
+
async function pMap(items, fn, concurrency) {
|
|
2780
|
+
const out = new Array(items.length);
|
|
2781
|
+
let next = 0;
|
|
2782
|
+
async function worker() {
|
|
2783
|
+
while (true) {
|
|
2784
|
+
const i = next++;
|
|
2785
|
+
if (i >= items.length)
|
|
2786
|
+
return;
|
|
2787
|
+
out[i] = await fn(items[i], i);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()));
|
|
2791
|
+
return out;
|
|
2792
|
+
}
|
|
2793
|
+
var bulkEnrichStatus = {
|
|
2794
|
+
name: "leadbay_bulk_enrich_status",
|
|
2795
|
+
description: "Check status + per-lead contacts for a bulk enrichment you previously launched via leadbay_enrich_titles. Returns the bulk_id, progress per lead (done/total enrichable contacts), and overall progress. When include_contacts=true (opt-in), includes each contact's email/phone/job_title/enrichment.done. When to use: poll this after leadbay_enrich_titles returns a bulk_id. Default include_contacts=false for cheap status polls; set include_contacts=true once all_done flips for the final read. When NOT to use: as a substitute for leadbay_research_lead \u2014 that already includes enriched contacts for a single lead.",
|
|
2796
|
+
inputSchema: {
|
|
2797
|
+
type: "object",
|
|
2798
|
+
properties: {
|
|
2799
|
+
bulk_id: {
|
|
2800
|
+
type: "string",
|
|
2801
|
+
description: "UUIDv4 returned by leadbay_enrich_titles at launch time. Required."
|
|
2802
|
+
},
|
|
2803
|
+
include_contacts: {
|
|
2804
|
+
type: "boolean",
|
|
2805
|
+
description: "If true, return the full contact list per lead (email, phone, enrichment.done). Default false \u2014 cheap status polls."
|
|
2806
|
+
}
|
|
2807
|
+
},
|
|
2808
|
+
required: ["bulk_id"]
|
|
2809
|
+
},
|
|
2810
|
+
execute: async (client, params, ctx) => {
|
|
2811
|
+
if (!isValidBulkId(params.bulk_id)) {
|
|
2812
|
+
return {
|
|
2813
|
+
error: true,
|
|
2814
|
+
code: "BULK_INVALID_ID",
|
|
2815
|
+
message: "bulk_id is not a valid UUIDv4",
|
|
2816
|
+
hint: "Pass the bulk_id returned by leadbay_enrich_titles verbatim."
|
|
2817
|
+
};
|
|
2818
|
+
}
|
|
2819
|
+
if (!ctx?.bulkTracker) {
|
|
2820
|
+
return {
|
|
2821
|
+
error: true,
|
|
2822
|
+
code: "BULK_TRACKER_UNAVAILABLE",
|
|
2823
|
+
message: "No BulkTracker configured on this MCP instance",
|
|
2824
|
+
hint: "This composite requires a BulkTracker in ToolContext. Upgrade to @leadbay/mcp \u22650.3.0 or run with LEADBAY_BULK_STORE_ALLOW_MEMORY=1."
|
|
2825
|
+
};
|
|
2826
|
+
}
|
|
2827
|
+
const includeContacts = params.include_contacts ?? false;
|
|
2828
|
+
const startMs = Date.now();
|
|
2829
|
+
let record;
|
|
2830
|
+
try {
|
|
2831
|
+
record = await ctx.bulkTracker.get(params.bulk_id);
|
|
2832
|
+
} catch (err) {
|
|
2833
|
+
return {
|
|
2834
|
+
error: true,
|
|
2835
|
+
code: "BULK_STORE_UNAVAILABLE",
|
|
2836
|
+
message: `Bulk store read failed: ${err?.message ?? err}`,
|
|
2837
|
+
hint: "Check the file at $LEADBAY_BULK_STORE_PATH (default ~/.leadbay/bulks.json). Set LEADBAY_BULK_STORE_ALLOW_MEMORY=1 to fall back to in-memory storage on startup (handles won't survive restart)."
|
|
2838
|
+
};
|
|
2839
|
+
}
|
|
2840
|
+
if (!record) {
|
|
2841
|
+
return {
|
|
2842
|
+
error: true,
|
|
2843
|
+
code: "BULK_NOT_FOUND",
|
|
2844
|
+
message: "No bulk record for that bulk_id",
|
|
2845
|
+
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."
|
|
2846
|
+
};
|
|
2847
|
+
}
|
|
2848
|
+
if (record.status === "pending") {
|
|
2849
|
+
return {
|
|
2850
|
+
error: true,
|
|
2851
|
+
code: "BULK_PENDING",
|
|
2852
|
+
message: "Bulk is in 'pending' state \u2014 the launch is in flight or the MCP crashed between launch and ack.",
|
|
2853
|
+
hint: "Retry leadbay_bulk_enrich_status in a few seconds. If it persists >60s, relaunch via leadbay_enrich_titles.",
|
|
2854
|
+
bulk_id: record.bulk_id,
|
|
2855
|
+
launched_at: record.launched_at
|
|
2856
|
+
};
|
|
2857
|
+
}
|
|
2858
|
+
if (record.status === "failed") {
|
|
2859
|
+
return {
|
|
2860
|
+
error: true,
|
|
2861
|
+
code: "BULK_LAUNCH_FAILED",
|
|
2862
|
+
message: "The original /enrichment/launch POST failed; no backend enrichment was ordered.",
|
|
2863
|
+
hint: "Call leadbay_enrich_titles again \u2014 the failed record won't block a fresh launch.",
|
|
2864
|
+
bulk_id: record.bulk_id,
|
|
2865
|
+
launched_at: record.launched_at
|
|
2866
|
+
};
|
|
2867
|
+
}
|
|
2868
|
+
const results = await pMap(record.lead_ids, async (leadId) => {
|
|
2869
|
+
try {
|
|
2870
|
+
const out = await getContacts.execute(client, { leadId });
|
|
2871
|
+
const contacts = Array.isArray(out?.contacts) ? out.contacts : [];
|
|
2872
|
+
const enrichable = contacts.filter((c) => c && c.enrichment);
|
|
2873
|
+
const done = enrichable.filter((c) => c.enrichment?.done === true).length;
|
|
2874
|
+
const total = enrichable.length;
|
|
2875
|
+
return {
|
|
2876
|
+
kind: "ok",
|
|
2877
|
+
lead_id: leadId,
|
|
2878
|
+
done,
|
|
2879
|
+
total,
|
|
2880
|
+
contacts: includeContacts ? contacts : void 0
|
|
2881
|
+
};
|
|
2882
|
+
} catch (err) {
|
|
2883
|
+
return {
|
|
2884
|
+
kind: "fail",
|
|
2885
|
+
lead_id: leadId,
|
|
2886
|
+
code: err?.code ?? "UNKNOWN",
|
|
2887
|
+
retry_after: err?._meta?.retry_after
|
|
2888
|
+
};
|
|
2889
|
+
}
|
|
2890
|
+
}, STATUS_FETCH_CONCURRENCY);
|
|
2891
|
+
const leads = [];
|
|
2892
|
+
const partialFailures = [];
|
|
2893
|
+
let totalDone = 0;
|
|
2894
|
+
let totalAll = 0;
|
|
2895
|
+
for (const r of results) {
|
|
2896
|
+
if (r.kind === "fail") {
|
|
2897
|
+
partialFailures.push({
|
|
2898
|
+
lead_id: r.lead_id,
|
|
2899
|
+
code: r.code,
|
|
2900
|
+
...r.retry_after !== void 0 ? { retry_after: r.retry_after } : {}
|
|
2901
|
+
});
|
|
2902
|
+
continue;
|
|
2903
|
+
}
|
|
2904
|
+
leads.push({
|
|
2905
|
+
lead_id: r.lead_id,
|
|
2906
|
+
...r.contacts ? { contacts: r.contacts } : {},
|
|
2907
|
+
enrichment_progress: { done: r.done, total: r.total }
|
|
2908
|
+
});
|
|
2909
|
+
totalDone += r.done;
|
|
2910
|
+
totalAll += r.total;
|
|
2911
|
+
}
|
|
2912
|
+
const overallProgress = {
|
|
2913
|
+
done: totalDone,
|
|
2914
|
+
total: totalAll,
|
|
2915
|
+
done_ratio: totalAll === 0 ? 0 : totalDone / totalAll
|
|
2916
|
+
};
|
|
2917
|
+
const allDone = totalAll > 0 && totalDone === totalAll && partialFailures.length === 0;
|
|
2918
|
+
ctx?.logger?.info?.(`bulk.status_checked bulk_id=${record.bulk_id} done=${totalDone} total=${totalAll} wall_ms=${Date.now() - startMs}`);
|
|
2919
|
+
return {
|
|
2920
|
+
bulk_id: record.bulk_id,
|
|
2921
|
+
launched_at: record.launched_at,
|
|
2922
|
+
status: record.status,
|
|
2923
|
+
durability: record.durability,
|
|
2924
|
+
titles: record.titles,
|
|
2925
|
+
email: record.email,
|
|
2926
|
+
phone: record.phone,
|
|
2927
|
+
lens_id: record.lens_id,
|
|
2928
|
+
leads,
|
|
2929
|
+
overall_progress: overallProgress,
|
|
2930
|
+
all_done: allDone,
|
|
2931
|
+
...partialFailures.length > 0 ? { partial_failures: partialFailures } : {}
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
};
|
|
2935
|
+
|
|
2350
2936
|
// ../core/dist/composite/adjust-audience.js
|
|
2351
2937
|
function tokens(s) {
|
|
2352
2938
|
return s.toLowerCase().split(/[^\p{L}\p{N}]+/u).filter(Boolean);
|
|
@@ -2924,6 +3510,7 @@ var compositeReadTools = [
|
|
|
2924
3510
|
researchLead,
|
|
2925
3511
|
recallOrderedTitles,
|
|
2926
3512
|
accountStatus,
|
|
3513
|
+
bulkEnrichStatus,
|
|
2927
3514
|
// Keep the existing composites available too.
|
|
2928
3515
|
researchCompany,
|
|
2929
3516
|
prepareOutreach
|
|
@@ -2996,6 +3583,11 @@ export {
|
|
|
2996
3583
|
accountStatus,
|
|
2997
3584
|
bulkQualifyLeads,
|
|
2998
3585
|
enrichTitles,
|
|
3586
|
+
isValidBulkId,
|
|
3587
|
+
LocalBulkStore,
|
|
3588
|
+
InMemoryBulkStore,
|
|
3589
|
+
createDefaultBulkStore,
|
|
3590
|
+
bulkEnrichStatus,
|
|
2999
3591
|
adjustAudience,
|
|
3000
3592
|
refinePrompt,
|
|
3001
3593
|
answerClarification,
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
InMemoryBulkStore,
|
|
3
4
|
LeadbayClient,
|
|
5
|
+
LocalBulkStore,
|
|
4
6
|
REGIONS,
|
|
5
7
|
accountStatus,
|
|
6
8
|
addNote,
|
|
7
9
|
adjustAudience,
|
|
8
10
|
answerClarification,
|
|
11
|
+
bulkEnrichStatus,
|
|
9
12
|
bulkQualifyLeads,
|
|
10
13
|
clearMockJournal,
|
|
11
14
|
clearSelection,
|
|
@@ -14,6 +17,7 @@ import {
|
|
|
14
17
|
compositeTools,
|
|
15
18
|
compositeWriteTools,
|
|
16
19
|
createClient,
|
|
20
|
+
createDefaultBulkStore,
|
|
17
21
|
createLens,
|
|
18
22
|
createLensDraft,
|
|
19
23
|
deselectLeads,
|
|
@@ -40,6 +44,7 @@ import {
|
|
|
40
44
|
granularReadTools,
|
|
41
45
|
granularTools,
|
|
42
46
|
granularWriteTools,
|
|
47
|
+
isValidBulkId,
|
|
43
48
|
launchBulkEnrichment,
|
|
44
49
|
listLenses,
|
|
45
50
|
listSectors,
|
|
@@ -64,14 +69,17 @@ import {
|
|
|
64
69
|
tools,
|
|
65
70
|
updateLens,
|
|
66
71
|
updateLensFilter
|
|
67
|
-
} from "./chunk-
|
|
72
|
+
} from "./chunk-FJBO2MY2.js";
|
|
68
73
|
export {
|
|
74
|
+
InMemoryBulkStore,
|
|
69
75
|
LeadbayClient,
|
|
76
|
+
LocalBulkStore,
|
|
70
77
|
REGIONS,
|
|
71
78
|
accountStatus,
|
|
72
79
|
addNote,
|
|
73
80
|
adjustAudience,
|
|
74
81
|
answerClarification,
|
|
82
|
+
bulkEnrichStatus,
|
|
75
83
|
bulkQualifyLeads,
|
|
76
84
|
clearMockJournal,
|
|
77
85
|
clearSelection,
|
|
@@ -80,6 +88,7 @@ export {
|
|
|
80
88
|
compositeTools,
|
|
81
89
|
compositeWriteTools,
|
|
82
90
|
createClient,
|
|
91
|
+
createDefaultBulkStore,
|
|
83
92
|
createLens,
|
|
84
93
|
createLensDraft,
|
|
85
94
|
deselectLeads,
|
|
@@ -106,6 +115,7 @@ export {
|
|
|
106
115
|
granularReadTools,
|
|
107
116
|
granularTools,
|
|
108
117
|
granularWriteTools,
|
|
118
|
+
isValidBulkId,
|
|
109
119
|
launchBulkEnrichment,
|
|
110
120
|
listLenses,
|
|
111
121
|
listSectors,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leadbay/mcp",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Model Context Protocol (MCP) server for Leadbay — AI lead discovery, qualification, and enrichment for Claude Desktop, Cursor, and Claude Code.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@modelcontextprotocol/sdk": "1.29.0"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
+
"@anthropic-ai/sdk": "^0.40.0",
|
|
30
31
|
"@leadbay/core": "workspace:*"
|
|
31
32
|
},
|
|
32
33
|
"engines": {
|