diff --git a/packages/core/package.json b/packages/core/package.json index 5654481..10a747b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@leadbay/core", - "version": "0.2.0", + "version": "0.2.1", "private": true, "description": "Leadbay shared core: HTTP client and protocol-agnostic tool definitions. Consumed by @leadbay/leadclaw and @leadbay/mcp.", "type": "module", diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index b115ec4..12dc215 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -352,7 +352,7 @@ export class LeadbayClient { throw this.makeError( "NOT_AUTHENTICATED", "Not logged in to Leadbay", - "Set LEADBAY_TOKEN env var (obtain token at https://app.leadbay.ai/settings/api-tokens), or use the OpenClaw leadbay_login tool", + "Set LEADBAY_TOKEN in your MCP client config, or run: npx -y @leadbay/mcp install --email --region ", path ); } @@ -403,7 +403,7 @@ export class LeadbayClient { throw this.makeError( "NOT_AUTHENTICATED", "Not logged in to Leadbay", - "Set LEADBAY_TOKEN env var (obtain token at https://app.leadbay.ai/settings/api-tokens), or use the OpenClaw leadbay_login tool", + "Set LEADBAY_TOKEN in your MCP client config, or run: npx -y @leadbay/mcp install --email --region ", path ); } @@ -487,7 +487,7 @@ export class LeadbayClient { return this.makeError( "AUTH_EXPIRED", "Authentication token expired or invalid", - "Your LEADBAY_TOKEN is no longer valid. Regenerate at https://app.leadbay.ai/settings/api-tokens and restart.", + "Your LEADBAY_TOKEN is no longer valid. Regenerate it: npx -y @leadbay/mcp login --email --region , then restart your MCP client.", endpoint ); } @@ -519,14 +519,14 @@ export class LeadbayClient { return this.makeError( "BILLING_SUSPENDED", "Account billing is suspended", - "Your Leadbay account billing is suspended. Update at https://app.leadbay.ai", + "Your Leadbay account billing is suspended. Contact Leadbay support.", endpoint ); } return this.makeError( "FORBIDDEN", "Insufficient permissions", - "Your token does not have access to this resource. Check account permissions at https://app.leadbay.ai", + "Your token does not have access to this resource. Contact Leadbay support to verify account permissions.", endpoint ); } diff --git a/packages/core/src/composite/bulk-qualify-leads.ts b/packages/core/src/composite/bulk-qualify-leads.ts index 4ab9548..d04190b 100644 --- a/packages/core/src/composite/bulk-qualify-leads.ts +++ b/packages/core/src/composite/bulk-qualify-leads.ts @@ -26,7 +26,11 @@ interface QualResult { qualification_summary: { answered: number; total: number; - avg_score_0_to_10: number | null; + /** + * Average of per-question AI agent boost scores (each -10/0/10/20). + * NOT a 0-10 average. Negative = net negative signal across questions. + */ + avg_qualification_boost: number | null; } | null; signals_count: number | null; } @@ -220,7 +224,7 @@ export const bulkQualifyLeads: Tool = { ? { answered: responses.filter((r) => r.score != null).length, total: responses.length, - avg_score_0_to_10: avg, + avg_qualification_boost: avg, } : null, signals_count: lastWf?.content diff --git a/packages/core/src/composite/pull-leads.ts b/packages/core/src/composite/pull-leads.ts index 12e9f05..6e8d449 100644 --- a/packages/core/src/composite/pull-leads.ts +++ b/packages/core/src/composite/pull-leads.ts @@ -17,7 +17,11 @@ interface PullLeadsParams { interface QualificationSummary { answered: number; total: number; - avg_score_0_to_10: number | null; + /** + * Average of per-question AI agent boost scores (each -10/0/10/20). + * NOT a 0-10 average. Negative = net negative signal across questions. + */ + avg_qualification_boost: number | null; best_response_excerpt: string | null; } @@ -44,7 +48,7 @@ function summarise(responses: AiAgentResponse[]): QualificationSummary { excerpt = excerpt.slice(0, 197) + "..."; } - return { answered, total, avg_score_0_to_10: avg, best_response_excerpt: excerpt }; + return { answered, total, avg_qualification_boost: avg, best_response_excerpt: excerpt }; } export const pullLeads: Tool = { @@ -114,7 +118,7 @@ export const pullLeads: Tool = { summary: { answered: 0, total: 0, - avg_score_0_to_10: null, + avg_qualification_boost: null, best_response_excerpt: null, }, }; diff --git a/packages/core/src/tools/enrich-contacts.ts b/packages/core/src/tools/enrich-contacts.ts index f5ac3eb..b680a44 100644 --- a/packages/core/src/tools/enrich-contacts.ts +++ b/packages/core/src/tools/enrich-contacts.ts @@ -60,7 +60,7 @@ export const enrichContacts: Tool = { throw client.makeError( "QUOTA_EXCEEDED", "No enrichment credits remaining", - "Purchase more credits at app.leadbay.ai" + "Contact Leadbay support to extend your credit quota" ); } } catch (e: any) { diff --git a/packages/core/src/tools/get-taste-profile.ts b/packages/core/src/tools/get-taste-profile.ts index 9829d21..0933146 100644 --- a/packages/core/src/tools/get-taste-profile.ts +++ b/packages/core/src/tools/get-taste-profile.ts @@ -41,7 +41,7 @@ export const getTasteProfile: Tool> = { })), ...(isEmpty ? { - hint: "No taste profile configured yet. Set it up at app.leadbay.ai for better lead matching.", + hint: "No taste profile configured yet. Use leadbay_refine_prompt or contact Leadbay support to set one up for better lead matching.", } : {}), }; diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index fd5703f..4e2ad1b 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -85,7 +85,17 @@ export interface SocialPresence { export interface LeadPayload { id: string; name: string; + /** + * Similarity score: how closely this lead matches the user's ideal-customer + * profile (the active lens). Combined with `ai_agent_lead_score` and + * normalized server-side to a 0-100 scale before display. + */ score: number | null; + /** + * Deep AI qualification adjustment, computed after running web searches and + * lead intelligence lookups. Acts as a boost/penalty on top of `score`; + * the two are combined and normalized server-side to a 0-100 scale. + */ ai_agent_lead_score: number | null; location: LocationPayload | null; description: string | null; @@ -133,11 +143,16 @@ export interface WishlistResponse { } // AI-rescore answers — the highest-signal payload Leadbay produces per lead. -// Score is 0-10 PER QUESTION (different from the 0-100 lead-level scores). +// Per-question qualification boost from the AI agent. Discrete values: +// -10 (negative signal), 0 (neutral / no signal), 10 (positive signal), +// 20 (strong positive signal). These boosts are summed and combined with +// the lead's similarity `score`, then normalized server-side to the 0-100 +// lead-level scale before display. NOT a 0-10 scale despite legacy naming. export interface AiAgentResponse { question: string; question_created_at: string; lead_id: string; + /** Discrete boost: -10, 0, 10, or 20. See interface comment above. */ score: number | null; response: string | null; computed_at: string | null; diff --git a/packages/leadclaw/CHANGELOG.md b/packages/leadclaw/CHANGELOG.md index b00b863..6f357e2 100644 --- a/packages/leadclaw/CHANGELOG.md +++ b/packages/leadclaw/CHANGELOG.md @@ -2,11 +2,14 @@ ## 0.2.2 — 2026-04-21 -Docs-only release. Shares the `@leadbay/mcp@0.2.2` mental-model copy updates since both packages consume the same `@leadbay/core` tool descriptions. +Bug fix + contract correction + mental-model docs release. Picks up `@leadbay/core@0.2.1` underneath. +- Renamed misleading `avg_score_0_to_10` field on the `pull_leads` / `bulk_qualify_leads` qualification summaries to `avg_qualification_boost`. Per-question AI agent scores are discrete boosts (-10/0/10/20), not a 0-10 average — interface JSDoc now reflects the real contract. +- Replaced stale `app.leadbay.ai` URLs in client-side error strings with runnable recovery commands. Recovery hints now include `--region ` because the CLI refuses without it (anti-cross-region credential-leak guard). +- README: stale `app.leadbay.ai` references swept. - Plugin manifest description rewritten from "Leadbay lead discovery, qualification, and contact enrichment for AI agents" to a framing that names the inbox model, the two scoring layers, and on-demand deepening. - Composite tool descriptions (`pull_leads`, `research_lead`, `bulk_qualify_leads`, `enrich_titles`, `account_status`) now teach the agent that Leadbay delivers a fresh batch per user login, paced by recent consumption; that roughly the top 10 are pre-AI-qualified while the rest are resource-saved (not worse); and that contacts are enriched on demand when the agent is ready to reach out. -- No new tools, no schema changes. Version kept in sync with `@leadbay/mcp@0.2.2`. +- Version kept in sync with `@leadbay/mcp@0.2.2`. ## 0.2.1 — 2026-04-21 diff --git a/packages/leadclaw/README.md b/packages/leadclaw/README.md index a4e2ee9..9e33082 100644 --- a/packages/leadclaw/README.md +++ b/packages/leadclaw/README.md @@ -78,8 +78,8 @@ The canonical tool list + schemas live in [`openclaw.plugin.json`](./openclaw.pl | Problem | Cause | Fix | |---------|-------|-----| | Plugin loads but agent sees no Leadbay tools | `token` missing and no login step taken | Have the agent call `leadbay_login`, or pre-set `token` in the plugin config | -| `Authentication token expired or invalid` | Token revoked or wrong region | Mint a new token at [app.leadbay.ai](https://app.leadbay.ai); verify `region` matches your account | -| `No enrichment credits remaining` | Out of quota | Buy credits at [app.leadbay.ai](https://app.leadbay.ai) | +| `Authentication token expired or invalid` | Token revoked or wrong region | Have the agent call `leadbay_login` to mint a fresh token; verify `region` matches your account | +| `No enrichment credits remaining` | Out of quota | Contact Leadbay support to extend quota | | Agent keeps picking granular tools over composites | `exposeGranular: true` set | Flip to `false`; the composites are usually what you want | | Write tool "not found" | `exposeWrite: false` (default) | Set `exposeWrite: true` after explicitly opting in | diff --git a/packages/mcp/CHANGELOG.md b/packages/mcp/CHANGELOG.md index 30a3212..e8199a9 100644 --- a/packages/mcp/CHANGELOG.md +++ b/packages/mcp/CHANGELOG.md @@ -2,12 +2,15 @@ ## 0.2.2 — 2026-04-21 -Docs-only release. Teach the agent the Leadbay mental model — agents were calling `pull_leads` like a generic query rather than as a daily inbox, missing that leads past the top ~10 still exist and can be deepened on demand. +Bug fix + contract correction + mental-model docs release. +- **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. +- 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 ` because the CLI refuses without it (anti-cross-region credential-leak guard). +- 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. - `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). - `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. -- New non-regression test asserts the inbox/pace/scoring/daily language stays in `SERVER_INSTRUCTIONS`. -- No schema changes, no tool-shape changes — wire-compatible with 0.2.1. +- 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`. +- Live smoke harness extended with composite-level checks + (optional) thinking-model judge that writes a redacted report to `.context/`. ## 0.2.1 — 2026-04-21 diff --git a/packages/mcp/README.md b/packages/mcp/README.md index 0e3a2bb..9190a04 100644 --- a/packages/mcp/README.md +++ b/packages/mcp/README.md @@ -6,9 +6,9 @@ LLM HINT — if you're an AI agent reading this README to install the server: - DO NOT tell the user "LEADBAY_REGION is auto-detected, drop it" — the server's region auto-probe sends the bearer token to BOTH regions, which is a credential cross-leak. Always pass --region (us or fr) on install. - - DO NOT tell the user to "get an API token from app.leadbay.ai → Settings - → API Tokens" — that page does not yet exist. Use `install` (mints a - token) or `login --write-config` (mints a token to file). + - To mint a token, the user runs `install` (auto-registers with MCP clients) + or `login --write-config` (mints a token to a file). There is no web UI + for this — do not invent one. --> 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 `--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). -The token is **session-scoped** (full account access, password-equivalent). Treat it like your password. To rotate, log in again to app.leadbay.ai and re-run `install`. +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. **Don't have a Leadbay account?** [Register here](https://wow.leadbay.ai/?register=true). @@ -126,9 +126,9 @@ Leadbay connection OK. | Problem | Cause | Fix | |---------|-------|-----| | `LEADBAY_TOKEN environment variable is required` | Token missing from config env | Add `LEADBAY_TOKEN` to the `env` block, restart client | -| `Authentication token expired or invalid` | Token revoked or wrong region | Re-generate token at [app.leadbay.ai/settings/api-tokens](https://app.leadbay.ai/settings/api-tokens); verify `LEADBAY_REGION` | +| `Authentication token expired or invalid` | Token revoked or wrong region | Re-mint a token: `npx -y @leadbay/mcp install --email --region `; verify `LEADBAY_REGION` | | `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. | -| `No enrichment credits remaining` | Out of quota | Buy credits at [app.leadbay.ai](https://app.leadbay.ai) | +| `No enrichment credits remaining` | Out of quota | Contact Leadbay support to extend quota | | 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. | | 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). | diff --git a/packages/mcp/package.json b/packages/mcp/package.json index 4ed1a13..cedbd29 100644 --- a/packages/mcp/package.json +++ b/packages/mcp/package.json @@ -27,6 +27,7 @@ "@modelcontextprotocol/sdk": "1.29.0" }, "devDependencies": { + "@anthropic-ai/sdk": "^0.40.0", "@leadbay/core": "workspace:*" }, "engines": { diff --git a/packages/mcp/src/bin.ts b/packages/mcp/src/bin.ts index 3fdc104..8bce56a 100644 --- a/packages/mcp/src/bin.ts +++ b/packages/mcp/src/bin.ts @@ -1,3 +1,5 @@ +import { realpathSync } from "node:fs"; +import { fileURLToPath } from "node:url"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createClient, @@ -33,7 +35,7 @@ USAGE leadbay-mcp --help Print this help ENV VARS - LEADBAY_TOKEN (required) Bearer token from https://app.leadbay.ai/settings/api-tokens + LEADBAY_TOKEN (required) Bearer token (run \`leadbay-mcp install\` to mint one). LEADBAY_REGION (optional) "us" or "fr". Auto-detected from /users/me if unset. LEADBAY_BASE_URL (optional) Override API base URL (for staging/dev). LEADBAY_MCP_ADVANCED (optional) Set to "1" to expose granular API tools alongside @@ -92,7 +94,7 @@ function parseLogLevel(raw: string | undefined): LogLevel { function exitWithTokenError(): never { process.stderr.write( "leadbay-mcp: LEADBAY_TOKEN environment variable is required.\n" + - " 1. Create a token at https://app.leadbay.ai/settings/api-tokens\n" + + " 1. Run: npx -y @leadbay/mcp install --email --region \n" + " 2. Set it in your MCP client config (e.g. claude_desktop_config.json).\n" + "\n" + "Run `leadbay-mcp --help` for the full config template.\n" @@ -751,7 +753,7 @@ async function runInstall(args: string[]): Promise { `\nThe token was written into client config files but never printed to your terminal.\n` + `Verify with: LEADBAY_TOKEN=$(...) npx -y @leadbay/mcp@0.2 doctor\n` + `Restart your MCP client(s) to pick up the new server.\n` + - `If you ever leak the token, log in to app.leadbay.ai again to invalidate the prior session.\n` + `If you ever leak the token, run \`leadbay-mcp login --email --region \` to mint a fresh one (which invalidates the prior session).\n` ); return anyOk ? 0 : 1; } @@ -864,14 +866,14 @@ async function main(): Promise { await server.connect(transport); } -// Only run main() when invoked as a CLI, not when imported by tests. -// import.meta.url === file:// ish — compare by resolved path. +// Run main() only when invoked as a CLI. realpath on both sides handles +// npx shim symlinks (issue #3504: silent exit 0 under Node 25 + npx). const isEntrypoint = (() => { try { const entry = process.argv[1]; if (!entry) return false; - const url = new URL(import.meta.url); - return url.pathname === entry || url.pathname.endsWith(entry); + const self = fileURLToPath(import.meta.url); + return realpathSync(self) === realpathSync(entry); } catch { return false; } diff --git a/packages/mcp/test/smoke/live.test.ts b/packages/mcp/test/smoke/live.test.ts index ac6f27d..fe128fc 100644 --- a/packages/mcp/test/smoke/live.test.ts +++ b/packages/mcp/test/smoke/live.test.ts @@ -1,42 +1,355 @@ /** - * LIVE smoke test for @leadbay/mcp — spawns the built stdio server as a - * subprocess and drives it via the MCP SDK client. + * LIVE smoke + E2E suite for @leadbay/mcp — spawns the built stdio server as a + * subprocess (the same path Claude Desktop uses) and exercises every read-only + * compound tool against the real Leadbay API. * - * Opt-in: set LEADBAY_TEST_TOKEN. Skipped otherwise. - * Requires: packages/mcp/dist/bin.js (run `pnpm --filter @leadbay/mcp build` first). + * Auth (in priority order): + * 1. LEADBAY_TEST_TOKEN — a real bearer token. Easiest. + * 2. LEADBAY_TEST_EMAIL + macOS Keychain. Set once with: + * security add-generic-password -s leadbay-mcp-test -a you@example.com -w 'pwd' + * The test reads via `security find-generic-password ... -w`. + * 3. Refuses to run when LEADBAY_TEST_PASSWORD is set (plaintext on disk is forbidden). + * + * Judge: LEADBAY_E2E_JUDGE = "anthropic" (default — claude-opus-4-7 + extended + * thinking) | "heuristic" (no LLM) | "off" (no judge at all). When "anthropic", + * ANTHROPIC_API_KEY is required, otherwise the suite skips with reason. + * + * Per composite: deterministic shape checks first, then (if data passes those + * gates) an LLM judge with extended thinking decides if the output is + * substantive + actionable for an agent. PII is redacted before the LLM call. + * + * Aggregate report written to .context/mcp-e2e-report-.json. */ -import { describe, it, expect } from "vitest"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; import path from "node:path"; import { fileURLToPath } from "node:url"; -import { existsSync } from "node:fs"; -import { spawn } from "node:child_process"; +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { spawn, spawnSync } from "node:child_process"; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; +import { resolveRegion } from "@leadbay/core"; +import Anthropic from "@anthropic-ai/sdk"; -const TOKEN = process.env.LEADBAY_TEST_TOKEN; -const REGION = process.env.LEADBAY_TEST_REGION ?? "us"; -const BASE_URL = process.env.LEADBAY_TEST_BASE_URL; +// ─── Auth resolution ──────────────────────────────────────────────────────── const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const REPO_ROOT = path.resolve(__dirname, "..", "..", "..", ".."); const BIN = path.resolve(__dirname, "..", "..", "dist", "bin.js"); -const hasToken = !!TOKEN; +const REGION = (process.env.LEADBAY_TEST_REGION ?? "us") as "us" | "fr"; +const BASE_URL = process.env.LEADBAY_TEST_BASE_URL; + +if (process.env.LEADBAY_TEST_PASSWORD) { + throw new Error( + "[smoke] LEADBAY_TEST_PASSWORD is set in env — refused. Use macOS Keychain instead:\n" + + " security add-generic-password -s leadbay-mcp-test -a $LEADBAY_TEST_EMAIL -w 'YourPassword'\n" + + "Then unset LEADBAY_TEST_PASSWORD." + ); +} + +function passwordFromKeychain(account: string): string | null { + const out = spawnSync("security", [ + "find-generic-password", + "-s", + "leadbay-mcp-test", + "-a", + account, + "-w", + ]); + if (out.status !== 0) return null; + const pwd = out.stdout.toString().trim(); + return pwd || null; +} + +let SKIP_REASON: string | null = null; +let TOKEN: string | null = process.env.LEADBAY_TEST_TOKEN ?? null; +let RESOLVED_EMAIL: string | null = null; +let RESOLVED_REGION: "us" | "fr" = REGION; + const hasBuild = existsSync(BIN); -const runLive = hasToken && hasBuild; +if (!hasBuild) { + SKIP_REASON = `missing built bin at ${BIN} — run \`pnpm --filter @leadbay/mcp build\` first`; +} + +if (!SKIP_REASON && !TOKEN) { + const email = process.env.LEADBAY_TEST_EMAIL; + if (!email) { + SKIP_REASON = + "no LEADBAY_TEST_TOKEN and no LEADBAY_TEST_EMAIL — set one to run live smoke"; + } else { + const password = passwordFromKeychain(email); + if (!password) { + SKIP_REASON = + `LEADBAY_TEST_EMAIL=${email} set but Keychain entry missing — run:\n` + + ` security add-generic-password -s leadbay-mcp-test -a ${email} -w 'YourPassword'`; + } else { + // Keychain login happens lazily in beforeAll so the skip path stays sync. + RESOLVED_EMAIL = email; + } + } +} + +if (SKIP_REASON) { + console.log(`[smoke] SMOKE_SKIPPED: ${SKIP_REASON}`); +} + +// ─── Judge config ─────────────────────────────────────────────────────────── + +type JudgeMode = "anthropic" | "heuristic" | "off"; +const JUDGE_MODE: JudgeMode = ((): JudgeMode => { + const raw = (process.env.LEADBAY_E2E_JUDGE ?? "anthropic").toLowerCase(); + if (raw === "anthropic" || raw === "heuristic" || raw === "off") return raw; + return "anthropic"; +})(); +const JUDGE_MODEL = process.env.LEADBAY_E2E_JUDGE_MODEL ?? "claude-opus-4-7"; +const ANTHROPIC_KEY = process.env.ANTHROPIC_API_KEY ?? null; + +if (JUDGE_MODE === "anthropic" && !ANTHROPIC_KEY) { + console.log( + "[smoke] LEADBAY_E2E_JUDGE=anthropic but ANTHROPIC_API_KEY missing — judge will downgrade to 'heuristic' for this run." + ); +} + +const effectiveJudgeMode: JudgeMode = + JUDGE_MODE === "anthropic" && !ANTHROPIC_KEY ? "heuristic" : JUDGE_MODE; + +// ─── PII redaction ────────────────────────────────────────────────────────── + +const EMAIL_RE = /[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}/gi; +// Require a leading + or () so we don't redact ISO timestamps / numeric IDs. +const PHONE_RE = /(\+\d[\d \-().]{6,}\d|\(\d{2,4}\)[\d \-]{6,}\d)/g; + +function redactPII(value: unknown): unknown { + if (typeof value === "string") { + return value.replace(EMAIL_RE, "[REDACTED_EMAIL]").replace(PHONE_RE, "[REDACTED_PHONE]"); + } + if (Array.isArray(value)) return value.map(redactPII); + if (value && typeof value === "object") { + const out: Record = {}; + for (const [k, v] of Object.entries(value as Record)) { + // Drop common address-shaped keys entirely; redact everything else recursively. + if (/^(address|street|postal_code|zip|phone|email|mobile)$/i.test(k)) { + out[k] = "[REDACTED]"; + } else { + out[k] = redactPII(v); + } + } + return out; + } + return value; +} + +// ─── Heuristic gates ──────────────────────────────────────────────────────── + +const PLACEHOLDER_RE = /\b(example\.com|TBD|TODO|placeholder|lorem ipsum)\b/i; + +function deepFindPlaceholder(value: unknown): string | null { + if (typeof value === "string") { + return PLACEHOLDER_RE.test(value) ? value.slice(0, 80) : null; + } + if (Array.isArray(value)) { + for (const v of value) { + const hit = deepFindPlaceholder(v); + if (hit) return hit; + } + return null; + } + if (value && typeof value === "object") { + for (const v of Object.values(value as Record)) { + const hit = deepFindPlaceholder(v); + if (hit) return hit; + } + } + return null; +} + +interface HeuristicResult { + ok: boolean; + reason?: string; +} + +function checkRequiredKeys(obj: Record, keys: string[]): HeuristicResult { + const missing = keys.filter((k) => !(k in obj)); + if (missing.length) return { ok: false, reason: `missing required keys: ${missing.join(", ")}` }; + const placeholder = deepFindPlaceholder(obj); + if (placeholder) return { ok: false, reason: `placeholder detected: ${placeholder}` }; + return { ok: true }; +} + +// ─── LLM judge ────────────────────────────────────────────────────────────── + +interface Verdict { + verdict: "good" | "weak" | "broken"; + cited_fields: string[]; + reason: string; +} + +async function judgeWithAnthropic( + toolName: string, + args: unknown, + output: unknown +): Promise { + const anthropic = new Anthropic({ apiKey: ANTHROPIC_KEY! }); + const redacted = redactPII(output); + const prompt = `You are evaluating an MCP tool that an AI agent will consume to do real B2B outreach. + +Tool: ${toolName} +Input args: ${JSON.stringify(args, null, 2)} +Output (PII redacted): ${JSON.stringify(redacted, null, 2)} -if (!hasToken) { - console.log("[smoke] SMOKE_SKIPPED: set LEADBAY_TEST_TOKEN to run live smoke tests"); -} else if (!hasBuild) { - console.log(`[smoke] SMOKE_SKIPPED: missing built bin at ${BIN} — run pnpm build first`); +Judge: +1. Is the data substantive — real entities with non-trivial fields, not empty/placeholder? +2. Is it actionable for an outreach agent — could the agent take a next step from this? +3. Any obvious quality issues — broken refs, contradictions, hallucinated fields? + +Use your thinking budget to inspect specific fields. Return STRICT JSON (no prose, no code fences): +{"verdict": "good" | "weak" | "broken", "cited_fields": [string], "reason": string} + +\`cited_fields\` MUST list the JSON paths you actually inspected (e.g. "leads[0].firmographics.name"). An empty list means you did not inspect anything — return "broken" in that case.`; + + const response = await anthropic.messages.create({ + model: JUDGE_MODEL, + max_tokens: 8000, + thinking: { type: "enabled", budget_tokens: 4000 }, + messages: [{ role: "user", content: prompt }], + }); + + const textBlock = response.content.find((b) => b.type === "text"); + const raw = textBlock && textBlock.type === "text" ? textBlock.text.trim() : ""; + const json = raw.replace(/^```json\s*/i, "").replace(/```$/i, "").trim(); + let parsed: Verdict; + try { + parsed = JSON.parse(json); + } catch (e) { + return { + verdict: "broken", + cited_fields: [], + reason: `judge returned non-JSON: ${raw.slice(0, 200)}`, + }; + } + // Sycophancy guard: a "good" verdict with empty cited_fields is rewritten to "broken". + if (parsed.verdict === "good" && (!parsed.cited_fields || parsed.cited_fields.length === 0)) { + return { + verdict: "broken", + cited_fields: [], + reason: `judge said "good" but cited no fields (sycophancy guard): ${parsed.reason}`, + }; + } + return parsed; +} + +async function judge(toolName: string, args: unknown, output: unknown): Promise { + if (effectiveJudgeMode === "off") { + return { verdict: "good", cited_fields: ["(judge disabled)"], reason: "LEADBAY_E2E_JUDGE=off" }; + } + if (effectiveJudgeMode === "heuristic") { + return { + verdict: "good", + cited_fields: ["(heuristic-only)"], + reason: "LEADBAY_E2E_JUDGE=heuristic — no LLM call", + }; + } + return judgeWithAnthropic(toolName, args, output); +} + +// ─── Aggregate report ─────────────────────────────────────────────────────── + +interface ReportEntry { + tool: string; + args: unknown; + heuristic: HeuristicResult; + verdict: Verdict | null; + excerpt: unknown; + duration_ms: number; +} + +const REPORT: ReportEntry[] = []; + +function writeReport() { + if (REPORT.length === 0) return; + const dir = path.join(REPO_ROOT, ".context"); + mkdirSync(dir, { recursive: true }); + const file = path.join(dir, `mcp-e2e-report-${Date.now()}.json`); + const summary = { + total: REPORT.length, + good: REPORT.filter((r) => r.verdict?.verdict === "good").length, + weak: REPORT.filter((r) => r.verdict?.verdict === "weak").length, + broken: REPORT.filter((r) => r.verdict?.verdict === "broken").length, + judge_mode: effectiveJudgeMode, + judge_model: effectiveJudgeMode === "anthropic" ? JUDGE_MODEL : null, + }; + writeFileSync(file, JSON.stringify({ summary, entries: REPORT }, null, 2)); + console.log(`[smoke] report written: ${file}`); + console.log(`[smoke] summary: ${JSON.stringify(summary)}`); +} + +// ─── Helpers ──────────────────────────────────────────────────────────────── + +async function callTool(client: Client, name: string, args: Record = {}) { + const result = await client.callTool({ name, arguments: args }); + if (result.isError) { + const text = (result.content as any[])?.[0]?.text ?? "(no text)"; + throw new Error(`tool ${name} returned isError: ${text}`); + } + const content = result.content as any[]; + const text = content[0].text; + return JSON.parse(text); +} + +async function exerciseAndJudge( + client: Client, + toolName: string, + args: Record, + requiredKeys: string[] +): Promise<{ result: any; verdict: Verdict | null }> { + const t0 = Date.now(); + const result = await callTool(client, toolName, args); + const heuristic = checkRequiredKeys(result, requiredKeys); + + let verdict: Verdict | null = null; + if (!heuristic.ok) { + verdict = { verdict: "broken", cited_fields: [], reason: heuristic.reason ?? "heuristic failure" }; + } else { + verdict = await judge(toolName, args, result); + } + + REPORT.push({ + tool: toolName, + args, + heuristic, + verdict, + excerpt: redactPII(result), + duration_ms: Date.now() - t0, + }); + return { result, verdict }; } -describe.skipIf(!runLive)("@leadbay/mcp — live stdio round-trip", () => { - it("initialize + tools/list + tools/call leadbay_find_prospects", async () => { +// ─── Suite ────────────────────────────────────────────────────────────────── + +const runLive = !SKIP_REASON; + +describe.skipIf(!runLive)("@leadbay/mcp — live composite suite (#3504)", () => { + let client: Client; + let pulledLeadIds: string[] = []; + + beforeAll(async () => { + if (!TOKEN && RESOLVED_EMAIL) { + const password = passwordFromKeychain(RESOLVED_EMAIL)!; + const resolved = await resolveRegion(RESOLVED_EMAIL, password, REGION); + TOKEN = resolved.token; + RESOLVED_REGION = resolved.region; + if (!resolved.verified) { + throw new Error( + `[smoke] login succeeded but account ${RESOLVED_EMAIL} is not verified (verified=false). Verify the account before running E2E.` + ); + } + } + const env: NodeJS.ProcessEnv = { ...process.env, LEADBAY_TOKEN: TOKEN!, - LEADBAY_REGION: REGION, + LEADBAY_REGION: RESOLVED_REGION, }; if (BASE_URL) env.LEADBAY_BASE_URL = BASE_URL; @@ -45,38 +358,100 @@ describe.skipIf(!runLive)("@leadbay/mcp — live stdio round-trip", () => { args: [BIN], env: env as Record, }); - - const client = new Client({ name: "smoke", version: "0.0.1" }, {}); + client = new Client({ name: "smoke", version: "0.0.1" }, {}); await client.connect(transport); + }, 30_000); - try { - const listed = await client.listTools(); - const names = listed.tools.map((t) => t.name); - expect(names).toContain("leadbay_find_prospects"); - expect(names).not.toContain("leadbay_login"); + afterAll(async () => { + if (client) await client.close(); + writeReport(); + }); - const result = await client.callTool({ - name: "leadbay_find_prospects", - arguments: { count: 1 }, - }); - expect(result.isError).toBeFalsy(); - const content = result.content as any[]; - const text = content[0].text; - const parsed = JSON.parse(text); - expect(parsed).toHaveProperty("leads"); - expect(Array.isArray(parsed.leads)).toBe(true); - } finally { - await client.close(); + it("leadbay_account_status — returns user + organization", async () => { + const { result, verdict } = await exerciseAndJudge( + client, + "leadbay_account_status", + {}, + ["user", "organization"] + ); + expect(result.user).toBeDefined(); + expect(result.organization?.id).toBeTruthy(); + expect(verdict?.verdict, `judge: ${verdict?.reason}`).not.toBe("broken"); + }, 60_000); + + it("leadbay_pull_leads — returns leads array", async () => { + const { result, verdict } = await exerciseAndJudge( + client, + "leadbay_pull_leads", + { count: 3 }, + ["leads"] + ); + expect(Array.isArray(result.leads)).toBe(true); + pulledLeadIds = result.leads + .map((l: any) => l?.id) + .filter((id: unknown): id is string => typeof id === "string"); + expect(verdict?.verdict, `judge: ${verdict?.reason}`).not.toBe("broken"); + + if (pulledLeadIds.length === 0) { + console.log("[smoke] no leads returned — skipping research_lead and recall_ordered_titles"); } - }); + }, 90_000); + + it("leadbay_research_lead — returns qualification + signals + firmographics", async () => { + if (pulledLeadIds.length === 0) { + console.log("[smoke] skipped: no lead id available"); + return; + } + const { result, verdict } = await exerciseAndJudge( + client, + "leadbay_research_lead", + { leadId: pulledLeadIds[0] }, + ["qualification", "signals", "firmographics"] + ); + expect(result.firmographics?.id).toBeTruthy(); + expect(verdict?.verdict, `judge: ${verdict?.reason}`).not.toBe("broken"); + }, 120_000); + it("leadbay_recall_ordered_titles — returns titles array", async () => { + if (pulledLeadIds.length === 0) { + console.log("[smoke] skipped: no lead ids available"); + return; + } + const { result, verdict } = await exerciseAndJudge( + client, + "leadbay_recall_ordered_titles", + { leadIds: pulledLeadIds }, + ["source"] + ); + // Either preview_field with available_in_selection, or live_aggregate with titles. + expect(["preview_field", "live_aggregate"]).toContain(result.source); + expect(verdict?.verdict, `judge: ${verdict?.reason}`).not.toBe("broken"); + }, 90_000); + + it("leadbay_bulk_enrich_status — schema check or skip", async () => { + const bulkId = process.env.LEADBAY_TEST_BULK_ID; + if (!bulkId) { + console.log("[smoke] skipped: set LEADBAY_TEST_BULK_ID to exercise bulk_enrich_status"); + return; + } + const { result, verdict } = await exerciseAndJudge( + client, + "leadbay_bulk_enrich_status", + { bulk_id: bulkId }, + ["bulk_id", "status"] + ); + expect(result.bulk_id).toBe(bulkId); + expect(verdict?.verdict, `judge: ${verdict?.reason}`).not.toBe("broken"); + }, 90_000); + + // Keep the original sanity check that proved out before the suite existed. it("doctor subcommand exits 0 with account info", async () => { await new Promise((resolve, reject) => { const child = spawn("node", [BIN, "doctor"], { env: { ...process.env, LEADBAY_TOKEN: TOKEN!, - LEADBAY_REGION: REGION, + LEADBAY_REGION: RESOLVED_REGION, }, }); let stdout = ""; @@ -92,5 +467,5 @@ describe.skipIf(!runLive)("@leadbay/mcp — live stdio round-trip", () => { } }); }); - }); + }, 30_000); }); diff --git a/packages/mcp/test/smoke/npx-entrypoint.test.ts b/packages/mcp/test/smoke/npx-entrypoint.test.ts new file mode 100644 index 0000000..6a86824 --- /dev/null +++ b/packages/mcp/test/smoke/npx-entrypoint.test.ts @@ -0,0 +1,65 @@ +/** + * Regression guard for issue #3504: under `npx`, process.argv[1] points at + * a shim symlink (e.g. ~/.npm/_npx//.../bin/leadbay-mcp) while + * import.meta.url resolves to the actual dist/bin.js path. The old + * isEntrypoint check compared paths literally and silently exited 0. + * + * This test simulates the npx layout with a symlink and asserts the binary + * actually starts. Without the realpath fix in bin.ts, this test fails: + * exit 0, empty stdout. With the fix, --help is printed. + */ + +import { describe, it, expect } from "vitest"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { existsSync, mkdirSync, mkdtempSync, rmSync, symlinkSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { spawn } from "node:child_process"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const BIN = path.resolve(__dirname, "..", "..", "dist", "bin.js"); + +const hasBuild = existsSync(BIN); +if (!hasBuild) { + console.log(`[smoke] SMOKE_SKIPPED: missing built bin at ${BIN} — run pnpm build first`); +} + +describe.skipIf(!hasBuild)("@leadbay/mcp — npx-shim entrypoint detection", () => { + it("prints --help when invoked through a symlink (the npx shim path)", async () => { + const tmp = mkdtempSync(path.join(tmpdir(), "leadbay-mcp-npx-")); + const binDir = path.join(tmp, ".bin"); + mkdirSync(binDir, { recursive: true }); + const shim = path.join(binDir, "leadbay-mcp"); + symlinkSync(BIN, shim); + + try { + const { code, stdout, stderr } = await runNode(shim, ["--help"]); + // Without the realpath fix, this is `code=0, stdout=""` (the bug). + // The regression guard: stdout must contain HELP text identifiers. + expect(code, `nonzero exit; stderr=${stderr}`).toBe(0); + expect(stdout, "empty stdout = isEntrypoint check failed (regression of #3504)").not.toBe(""); + expect(stdout).toMatch(/leadbay-mcp/); + expect(stdout).toMatch(/LEADBAY_TOKEN/); + } finally { + rmSync(tmp, { recursive: true, force: true }); + } + }); + + it("still works when invoked directly (no regression for the path Rémi already had working)", async () => { + const { code, stdout, stderr } = await runNode(BIN, ["--help"]); + expect(code, `nonzero exit; stderr=${stderr}`).toBe(0); + expect(stdout).toMatch(/leadbay-mcp/); + expect(stdout).toMatch(/LEADBAY_TOKEN/); + }); +}); + +function runNode(entry: string, args: string[]): Promise<{ code: number | null; stdout: string; stderr: string }> { + return new Promise((resolve) => { + const child = spawn("node", [entry, ...args], { stdio: ["ignore", "pipe", "pipe"] }); + let stdout = ""; + let stderr = ""; + child.stdout.on("data", (d) => (stdout += d.toString())); + child.stderr.on("data", (d) => (stderr += d.toString())); + child.on("exit", (code) => resolve({ code, stdout, stderr })); + }); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e9048bf..3a02149 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,12 +35,18 @@ importers: specifier: 1.29.0 version: 1.29.0(zod@4.3.6) devDependencies: + '@anthropic-ai/sdk': + specifier: ^0.40.0 + version: 0.40.1 '@leadbay/core': specifier: workspace:* version: link:../core packages: + '@anthropic-ai/sdk@0.40.1': + resolution: {integrity: sha512-DJMWm8lTEM9Lk/MSFL+V+ugF7jKOn0M2Ujvb5fN8r2nY14aHbGPZ1k6sgjL+tpJ3VuOGJNG+4R83jEpOuYPv8w==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -505,6 +511,12 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/node-fetch@2.6.13': + resolution: {integrity: sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==} + + '@types/node@18.19.130': + resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==} + '@types/node@25.6.0': resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} @@ -537,6 +549,10 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -546,6 +562,10 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + agentkeepalive@4.6.0: + resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} + engines: {node: '>= 8.0.0'} + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -564,6 +584,9 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -602,6 +625,10 @@ packages: resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} engines: {node: '>= 14.16.0'} + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -650,6 +677,10 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + depd@2.0.0: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} @@ -680,6 +711,10 @@ packages: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + esbuild@0.21.5: resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} engines: {node: '>=12'} @@ -700,6 +735,10 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + eventsource-parser@3.0.8: resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} engines: {node: '>=18.0.0'} @@ -744,6 +783,17 @@ packages: fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + form-data-encoder@1.7.2: + resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==} + + form-data@4.0.5: + resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + engines: {node: '>= 6'} + + formdata-node@4.4.1: + resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==} + engines: {node: '>= 12.20'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -776,6 +826,10 @@ packages: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + hasown@2.0.3: resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} engines: {node: '>= 0.4'} @@ -788,6 +842,9 @@ packages: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} + humanize-ms@1.2.1: + resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -851,10 +908,18 @@ packages: resolution: {integrity: sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==} engines: {node: '>=18'} + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + mime-db@1.54.0: resolution: {integrity: sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==} engines: {node: '>= 0.6'} + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + mime-types@3.0.2: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} @@ -877,6 +942,20 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + node-domexception@1.0.0: + resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} + engines: {node: '>=10.5.0'} + deprecated: Use your platform's native DOMException instead + + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -1087,6 +1166,9 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -1125,6 +1207,9 @@ packages: ufo@1.6.3: resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@7.19.2: resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} @@ -1197,6 +1282,16 @@ packages: jsdom: optional: true + web-streams-polyfill@4.0.0-beta.3: + resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==} + engines: {node: '>= 14'} + + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -1220,6 +1315,18 @@ packages: snapshots: + '@anthropic-ai/sdk@0.40.1': + dependencies: + '@types/node': 18.19.130 + '@types/node-fetch': 2.6.13 + abort-controller: 3.0.0 + agentkeepalive: 4.6.0 + form-data-encoder: 1.7.2 + formdata-node: 4.4.1 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -1484,6 +1591,15 @@ snapshots: '@types/estree@1.0.8': {} + '@types/node-fetch@2.6.13': + dependencies: + '@types/node': 25.6.0 + form-data: 4.0.5 + + '@types/node@18.19.130': + dependencies: + undici-types: 5.26.5 + '@types/node@25.6.0': dependencies: undici-types: 7.19.2 @@ -1528,6 +1644,10 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -1535,6 +1655,10 @@ snapshots: acorn@8.16.0: {} + agentkeepalive@4.6.0: + dependencies: + humanize-ms: 1.2.1 + ajv-formats@3.0.1(ajv@8.18.0): optionalDependencies: ajv: 8.18.0 @@ -1550,6 +1674,8 @@ snapshots: assertion-error@2.0.1: {} + asynckit@0.4.0: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -1597,6 +1723,10 @@ snapshots: dependencies: readdirp: 4.1.2 + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + commander@4.1.1: {} confbox@0.1.8: {} @@ -1628,6 +1758,8 @@ snapshots: deep-eql@5.0.2: {} + delayed-stream@1.0.0: {} + depd@2.0.0: {} dunder-proto@1.0.1: @@ -1650,6 +1782,13 @@ snapshots: dependencies: es-errors: 1.3.0 + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.3 + esbuild@0.21.5: optionalDependencies: '@esbuild/aix-ppc64': 0.21.5 @@ -1713,6 +1852,8 @@ snapshots: etag@1.8.1: {} + event-target-shim@5.0.1: {} + eventsource-parser@3.0.8: {} eventsource@3.0.7: @@ -1784,6 +1925,21 @@ snapshots: mlly: 1.8.2 rollup: 4.60.2 + form-data-encoder@1.7.2: {} + + form-data@4.0.5: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.3 + mime-types: 2.1.35 + + formdata-node@4.4.1: + dependencies: + node-domexception: 1.0.0 + web-streams-polyfill: 4.0.0-beta.3 + forwarded@0.2.0: {} fresh@2.0.0: {} @@ -1815,6 +1971,10 @@ snapshots: has-symbols@1.1.0: {} + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + hasown@2.0.3: dependencies: function-bind: 1.1.2 @@ -1829,6 +1989,10 @@ snapshots: statuses: 2.0.2 toidentifier: 1.0.1 + humanize-ms@1.2.1: + dependencies: + ms: 2.1.3 + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -1869,8 +2033,14 @@ snapshots: merge-descriptors@2.0.0: {} + mime-db@1.52.0: {} + mime-db@1.54.0: {} + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + mime-types@3.0.2: dependencies: mime-db: 1.54.0 @@ -1894,6 +2064,12 @@ snapshots: negotiator@1.0.0: {} + node-domexception@1.0.0: {} + + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -2119,6 +2295,8 @@ snapshots: toidentifier@1.0.1: {} + tr46@0.0.3: {} + tree-kill@1.2.2: {} ts-interface-checker@0.1.13: {} @@ -2161,6 +2339,8 @@ snapshots: ufo@1.6.3: {} + undici-types@5.26.5: {} + undici-types@7.19.2: {} unpipe@1.0.0: {} @@ -2229,6 +2409,15 @@ snapshots: - supports-color - terser + web-streams-polyfill@4.0.0-beta.3: {} + + webidl-conversions@3.0.1: {} + + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + which@2.0.2: dependencies: isexe: 2.0.0