@mackody/quickenrich-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/.codex-plugin/plugin.json +39 -0
  2. package/.env.example +17 -0
  3. package/.mcp.json +17 -0
  4. package/README.md +283 -0
  5. package/agents/AGENTS.md +6 -0
  6. package/agents/CLAUDE.md +6 -0
  7. package/package.json +40 -0
  8. package/scripts/agent-docs-smoke.mjs +72 -0
  9. package/scripts/agent-envelope-workflow-smoke.mjs +92 -0
  10. package/scripts/agent-surface-smoke.mjs +102 -0
  11. package/scripts/benchmark-contract-smoke.mjs +46 -0
  12. package/scripts/envelope-budget-smoke.mjs +37 -0
  13. package/scripts/killer-contract-smoke.mjs +206 -0
  14. package/scripts/lib/benchmark.mjs +92 -0
  15. package/scripts/lib/core.mjs +3303 -0
  16. package/scripts/live-benchmark.mjs +518 -0
  17. package/scripts/live-smoke.mjs +61 -0
  18. package/scripts/local-floor-benchmark.mjs +366 -0
  19. package/scripts/mcp-config-smoke.mjs +110 -0
  20. package/scripts/mcp-live-smoke.mjs +115 -0
  21. package/scripts/mock-api-smoke.mjs +112 -0
  22. package/scripts/native-capabilities-smoke.mjs +273 -0
  23. package/scripts/native-live-smoke.mjs +43 -0
  24. package/scripts/offline-contract-smoke.mjs +80 -0
  25. package/scripts/ops-contract-smoke.mjs +270 -0
  26. package/scripts/paged-finder-floor-smoke.mjs +93 -0
  27. package/scripts/perf-contract-smoke.mjs +111 -0
  28. package/scripts/quickenrich-agent-docs.mjs +109 -0
  29. package/scripts/quickenrich-mcp.mjs +201 -0
  30. package/scripts/quickenrich-token.mjs +175 -0
  31. package/scripts/rate-limit-contract-smoke.mjs +70 -0
  32. package/scripts/reverse-concurrency-floor-smoke.mjs +82 -0
  33. package/scripts/serialization-floor-smoke.mjs +42 -0
  34. package/scripts/token-bucket-contract-smoke.mjs +121 -0
  35. package/scripts/token-cli-smoke.mjs +59 -0
  36. package/skills/quickenrich/SKILL.md +20 -0
@@ -0,0 +1,518 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { spawn } from "node:child_process";
4
+ import { readFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { performance } from "node:perf_hooks";
7
+ import { fileURLToPath } from "node:url";
8
+ import {
9
+ apiBaseUrl,
10
+ apiKey,
11
+ authStatus,
12
+ callQuickEnrichTool,
13
+ createClient,
14
+ redact,
15
+ } from "./lib/core.mjs";
16
+ import {
17
+ makeDuplicateDomainBenchmark,
18
+ summarizeBenchmarkComparison,
19
+ } from "./lib/benchmark.mjs";
20
+
21
+ const __dirname = dirname(fileURLToPath(import.meta.url));
22
+ const repoRoot = join(__dirname, "..");
23
+ const DEFAULT_CREDIT_BENCH_DOMAINS = [
24
+ "danosseasoning.com",
25
+ "sagegoddess.com",
26
+ ];
27
+
28
+ const creditMode = truthy(process.env.QUICKENRICH_BENCH_SPEND_CREDITS) || String(process.env.QUICKENRICH_BENCH_MODE || "").toLowerCase() === "credit";
29
+ const configuredCreditDomains = parseDomains(process.env.QUICKENRICH_BENCH_DOMAINS);
30
+ const domainInputs = creditMode ? configuredCreditDomains.length
31
+ ? configuredCreditDomains
32
+ : DEFAULT_CREDIT_BENCH_DOMAINS
33
+ : [];
34
+
35
+ const settings = {
36
+ creditMode,
37
+ noCreditGuard: !creditMode,
38
+ requireCredits: creditMode && !falsey(process.env.QUICKENRICH_BENCH_REQUIRE_CREDITS),
39
+ domainInputs,
40
+ iterations: envInteger("QUICKENRICH_BENCH_ITERATIONS", 1, 5, creditMode ? 1 : 2),
41
+ uniqueCount: domainInputs.length || envInteger("QUICKENRICH_BENCH_UNIQUE", 1, 24, 6),
42
+ duplicateFactor: envInteger("QUICKENRICH_BENCH_DUPLICATE_FACTOR", 1, 8, 2),
43
+ concurrency: envInteger("QUICKENRICH_BENCH_CONCURRENCY", 1, 32, 8),
44
+ perPage: envInteger("QUICKENRICH_BENCH_PER_PAGE", 1, 20, 5),
45
+ title: process.env.QUICKENRICH_BENCH_TITLE || "Founder",
46
+ timeoutMs: envInteger("QUICKENRICH_BENCH_TIMEOUT_MS", 1000, 120_000, 30_000),
47
+ metaLane: !creditMode && truthy(process.env.QUICKENRICH_BENCH_META_LANE),
48
+ };
49
+
50
+ const status = authStatus();
51
+ if (!status.configured) {
52
+ console.log(JSON.stringify({
53
+ ok: true,
54
+ skipped: true,
55
+ reason: "Quick Enrich API key is not configured. Set QUICKENRICH_API_KEY or run node scripts/quickenrich-token.mjs set.",
56
+ }, null, 2));
57
+ process.exit(0);
58
+ }
59
+
60
+ const mcp = startMcpClient();
61
+ const rows = [];
62
+
63
+ try {
64
+ await initializeMcp(mcp);
65
+ for (let iteration = 1; iteration <= settings.iterations; iteration += 1) {
66
+ rows.push(await measureScenario("raw_sequential", iteration, () => (
67
+ runRawSequential(makeFixture(iteration, "raw-seq").companies)
68
+ )));
69
+ rows.push(await measureScenario("raw_concurrent_deduped", iteration, () => (
70
+ runRawConcurrent(makeFixture(iteration, "raw-concurrent").uniqueCompanies)
71
+ )));
72
+ rows.push(await measureScenario("direct_core_batch", iteration, () => (
73
+ runDirectCoreBatch(makeFixture(iteration, "core-batch").companies)
74
+ )));
75
+ rows.push(await measureScenario("mcp_batch_stdio", iteration, () => (
76
+ runMcpBatch(mcp, makeFixture(iteration, "mcp-batch").companies)
77
+ )));
78
+ if (settings.metaLane) {
79
+ rows.push(await measureScenario("mcp_batch_stdio_meta", iteration, () => (
80
+ runMcpBatch(mcp, makeFixture(iteration, "mcp-meta").companies, { response: { mode: "meta" } })
81
+ )));
82
+ }
83
+ }
84
+
85
+ const averages = aggregateRows(rows);
86
+ const mcpComparison = summarizeBenchmarkComparison({
87
+ rows: averages,
88
+ baselineLabel: "raw_sequential",
89
+ candidateLabel: "mcp_batch_stdio",
90
+ optimizedLabel: "raw_concurrent_deduped",
91
+ });
92
+ const directCoreComparison = summarizeBenchmarkComparison({
93
+ rows: averages,
94
+ baselineLabel: "raw_sequential",
95
+ candidateLabel: "direct_core_batch",
96
+ optimizedLabel: "raw_concurrent_deduped",
97
+ });
98
+ const mcpMetaComparison = averages.some((row) => row.label === "mcp_batch_stdio_meta")
99
+ ? summarizeBenchmarkComparison({
100
+ rows: averages,
101
+ baselineLabel: "raw_sequential",
102
+ candidateLabel: "mcp_batch_stdio_meta",
103
+ optimizedLabel: "raw_concurrent_deduped",
104
+ })
105
+ : undefined;
106
+ const totalCreditsSpent = roundNumber(rows.reduce((sum, row) => sum + Number(row.creditsUsed || 0), 0));
107
+ if (settings.requireCredits && totalCreditsSpent <= 0) {
108
+ throw new Error("Credit benchmark did not spend any credits. Override domains with QUICKENRICH_BENCH_DOMAINS or set QUICKENRICH_BENCH_REQUIRE_CREDITS=0.");
109
+ }
110
+
111
+ console.log(JSON.stringify({
112
+ ok: true,
113
+ skipped: false,
114
+ apiBaseUrl: apiBaseUrl(),
115
+ benchmarkShape: {
116
+ iterations: settings.iterations,
117
+ inputCompanyCount: settings.uniqueCount * settings.duplicateFactor,
118
+ uniqueCompanyCount: settings.uniqueCount,
119
+ duplicateFactor: settings.duplicateFactor,
120
+ coalescedCompanyCount: settings.uniqueCount * (settings.duplicateFactor - 1),
121
+ title: settings.title,
122
+ perPage: settings.perPage,
123
+ concurrency: settings.concurrency,
124
+ metaLane: settings.metaLane || undefined,
125
+ mode: settings.creditMode ? "credit" : "no_credit",
126
+ domains: settings.creditMode ? settings.domainInputs : undefined,
127
+ noCreditGuard: settings.noCreditGuard,
128
+ requireCredits: settings.requireCredits || undefined,
129
+ totalCreditsSpent,
130
+ },
131
+ averagedRows: averages,
132
+ comparisons: {
133
+ mcpVsNaiveRawSequential: mcpComparison,
134
+ mcpMetaVsNaiveRawSequential: mcpMetaComparison,
135
+ directCoreVsNaiveRawSequential: directCoreComparison,
136
+ mcpCreditUsageVsNaiveRawSequential: creditComparison(averages, "raw_sequential", "mcp_batch_stdio"),
137
+ mcpMetaCreditUsageVsNaiveRawSequential: creditComparison(averages, "raw_sequential", "mcp_batch_stdio_meta"),
138
+ directCoreCreditUsageVsNaiveRawSequential: creditComparison(averages, "raw_sequential", "direct_core_batch"),
139
+ },
140
+ rows,
141
+ }, null, 2));
142
+ } catch (error) {
143
+ console.error(JSON.stringify({
144
+ ok: false,
145
+ error: error.message || String(error),
146
+ data: error.result ? redact(error.result) : undefined,
147
+ rows,
148
+ }, null, 2));
149
+ process.exitCode = 1;
150
+ } finally {
151
+ await mcp.close();
152
+ }
153
+
154
+ function makeFixture(iteration, lane) {
155
+ if (settings.domainInputs.length) {
156
+ const uniqueCompanies = settings.domainInputs.map((domain) => ({
157
+ company_url: domain,
158
+ title: settings.title,
159
+ }));
160
+ const companies = uniqueCompanies.flatMap((company) => (
161
+ Array.from({ length: settings.duplicateFactor }, () => ({ ...company }))
162
+ ));
163
+ return {
164
+ title: settings.title,
165
+ uniqueCount: uniqueCompanies.length,
166
+ duplicateFactor: settings.duplicateFactor,
167
+ inputCount: companies.length,
168
+ coalescedCount: companies.length - uniqueCompanies.length,
169
+ companies,
170
+ uniqueCompanies,
171
+ };
172
+ }
173
+ return makeDuplicateDomainBenchmark({
174
+ uniqueCount: settings.uniqueCount,
175
+ duplicateFactor: settings.duplicateFactor,
176
+ prefix: `qe-mcp-live-${Date.now()}-${iteration}-${lane}`,
177
+ title: settings.title,
178
+ });
179
+ }
180
+
181
+ async function measureScenario(label, iteration, run) {
182
+ const startedAt = performance.now();
183
+ const result = await run();
184
+ return {
185
+ label,
186
+ iteration,
187
+ durationMs: roundNumber(performance.now() - startedAt),
188
+ ...result,
189
+ };
190
+ }
191
+
192
+ async function runRawSequential(companies) {
193
+ const results = [];
194
+ for (const company of companies) {
195
+ results.push(await rawDomainSearch(company));
196
+ }
197
+ return combineCallResults(results, { requestCount: companies.length });
198
+ }
199
+
200
+ async function runRawConcurrent(companies) {
201
+ const results = await withConcurrency(companies, settings.concurrency, rawDomainSearch);
202
+ return combineCallResults(results, { requestCount: companies.length });
203
+ }
204
+
205
+ async function runDirectCoreBatch(companies) {
206
+ const result = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
207
+ mode: "domain_search",
208
+ companies,
209
+ pages_per_company: 1,
210
+ per_company_limit: settings.perPage,
211
+ per_page: settings.perPage,
212
+ concurrency: settings.concurrency,
213
+ enrich_contacts: false,
214
+ response: { mode: "compact", maxOutputChars: 12_000 },
215
+ }, createClient());
216
+ assertBatchGuard(result, "direct_core_batch");
217
+ return {
218
+ ok: result.ok,
219
+ requestCount: result.requestCount,
220
+ creditsUsed: numeric(result.creditsUsed, 0),
221
+ remainingCredits: result.remainingCredits,
222
+ leadCount: numeric(result.leadCount, 0),
223
+ companyCount: result.companyCount,
224
+ coalescedCompanyCount: result.coalescedCompanyCount,
225
+ };
226
+ }
227
+
228
+ async function runMcpBatch(mcp, companies, options = {}) {
229
+ const responseOptions = options.response || { mode: "compact", maxOutputChars: 12_000 };
230
+ const response = await mcp.request("tools/call", {
231
+ name: "quickenrich_batch_pull_leads",
232
+ arguments: {
233
+ mode: "domain_search",
234
+ companies,
235
+ pages_per_company: 1,
236
+ per_company_limit: settings.perPage,
237
+ per_page: settings.perPage,
238
+ concurrency: settings.concurrency,
239
+ enrich_contacts: false,
240
+ response: responseOptions,
241
+ },
242
+ }, settings.timeoutMs + 10_000);
243
+ if (response.error) throw new Error(`MCP batch call failed: ${JSON.stringify(response.error)}`);
244
+ const payload = JSON.parse(response.result?.content?.[0]?.text || "{}");
245
+ assertBatchGuard(payload, "mcp_batch_stdio");
246
+ return {
247
+ ok: payloadScalar(payload, "ok"),
248
+ requestCount: payloadScalar(payload, "requestCount"),
249
+ creditsUsed: numeric(payloadScalar(payload, "creditsUsed"), 0),
250
+ remainingCredits: payloadScalar(payload, "remainingCredits"),
251
+ leadCount: numeric(payloadScalar(payload, "leadCount"), 0),
252
+ companyCount: payloadScalar(payload, "companyCount"),
253
+ coalescedCompanyCount: payloadScalar(payload, "coalescedCompanyCount"),
254
+ };
255
+ }
256
+
257
+ async function rawDomainSearch(company) {
258
+ const url = new URL("/api/employees/dataset-search", `${apiBaseUrl()}/`);
259
+ url.searchParams.set("company_url", company.company_url);
260
+ url.searchParams.set("title", company.title || settings.title);
261
+ url.searchParams.set("page", "1");
262
+ url.searchParams.set("per_page", String(settings.perPage));
263
+
264
+ const controller = new AbortController();
265
+ const timeout = setTimeout(() => controller.abort(), settings.timeoutMs);
266
+ try {
267
+ const response = await fetch(url, {
268
+ method: "GET",
269
+ headers: {
270
+ Accept: "application/json",
271
+ Authorization: `Bearer ${apiKey()}`,
272
+ "User-Agent": "quickenrich-mcp-live-benchmark/0.1.0",
273
+ },
274
+ signal: controller.signal,
275
+ });
276
+ const text = await response.text();
277
+ const payload = text ? JSON.parse(text) : {};
278
+ if (!response.ok) {
279
+ const error = new Error(`Raw Quick Enrich domain search failed with HTTP ${response.status}`);
280
+ error.result = { status: response.status, data: payload };
281
+ throw error;
282
+ }
283
+ const records = payloadRecords(payload);
284
+ const creditsUsed = numeric(payload?.meta?.credits_used, 0);
285
+ if (settings.noCreditGuard && creditsUsed > 0) {
286
+ throw new Error(`No-match raw benchmark unexpectedly used ${creditsUsed} credits for ${company.company_url}.`);
287
+ }
288
+ if (settings.noCreditGuard && records.length > 0) {
289
+ throw new Error(`No-match raw benchmark unexpectedly returned ${records.length} records for ${company.company_url}.`);
290
+ }
291
+ return {
292
+ requestCount: 1,
293
+ creditsUsed,
294
+ remainingCredits: numeric(payload?.meta?.remaining_credits),
295
+ leadCount: records.length,
296
+ };
297
+ } finally {
298
+ clearTimeout(timeout);
299
+ }
300
+ }
301
+
302
+ async function initializeMcp(mcp) {
303
+ const init = await mcp.request("initialize", { protocolVersion: "2024-11-05" });
304
+ if (init.error) throw new Error(`MCP initialize failed: ${JSON.stringify(init.error)}`);
305
+ const list = await mcp.request("tools/list");
306
+ if (list.error) throw new Error(`MCP tools/list failed: ${JSON.stringify(list.error)}`);
307
+ const tools = list.result?.tools?.map((tool) => tool.name) || [];
308
+ if (!tools.includes("quickenrich_batch_pull_leads")) {
309
+ throw new Error("MCP server did not expose quickenrich_batch_pull_leads.");
310
+ }
311
+ }
312
+
313
+ function startMcpClient() {
314
+ const mcpConfig = JSON.parse(readFileSync(join(repoRoot, ".mcp.json"), "utf8"));
315
+ const server = mcpConfig.mcpServers?.["quick-enrich"];
316
+ if (!server?.command) throw new Error("Missing quick-enrich MCP server command.");
317
+
318
+ const proc = spawn(server.command, server.args || [], {
319
+ cwd: repoRoot,
320
+ env: {
321
+ ...process.env,
322
+ ...(server.env || {}),
323
+ CLAUDE_PLUGIN_ROOT: repoRoot,
324
+ },
325
+ stdio: ["pipe", "pipe", "pipe"],
326
+ });
327
+ let buffer = Buffer.alloc(0);
328
+ let nextId = 1;
329
+ let stderr = "";
330
+ const pending = new Map();
331
+
332
+ proc.stdout.on("data", (chunk) => {
333
+ buffer = Buffer.concat([buffer, chunk]);
334
+ parseMcpFrames();
335
+ });
336
+ proc.stderr.on("data", (chunk) => {
337
+ stderr += chunk.toString("utf8");
338
+ stderr = stderr.slice(-8000);
339
+ });
340
+ proc.on("exit", (code, signal) => {
341
+ const error = new Error(`MCP process exited before benchmark completed: code=${code} signal=${signal} stderr=${stderr.trim()}`);
342
+ for (const { reject, timeout } of pending.values()) {
343
+ clearTimeout(timeout);
344
+ reject(error);
345
+ }
346
+ pending.clear();
347
+ });
348
+
349
+ function request(method, params = {}, timeoutMs = settings.timeoutMs) {
350
+ const id = nextId;
351
+ nextId += 1;
352
+ const message = JSON.stringify({ jsonrpc: "2.0", id, method, params });
353
+ return new Promise((resolve, reject) => {
354
+ const timeout = setTimeout(() => {
355
+ pending.delete(id);
356
+ reject(new Error(`Timed out waiting for MCP response id=${id} method=${method}`));
357
+ }, timeoutMs);
358
+ pending.set(id, { resolve, reject, timeout });
359
+ proc.stdin.write(`Content-Length: ${Buffer.byteLength(message)}\r\n\r\n${message}`);
360
+ });
361
+ }
362
+
363
+ function parseMcpFrames() {
364
+ while (true) {
365
+ const headerEnd = buffer.indexOf("\r\n\r\n");
366
+ if (headerEnd === -1) return;
367
+ const header = buffer.slice(0, headerEnd).toString("utf8");
368
+ const match = header.match(/Content-Length:\s*(\d+)/i);
369
+ if (!match) throw new Error(`Invalid MCP header: ${header}`);
370
+ const length = Number(match[1]);
371
+ const bodyStart = headerEnd + 4;
372
+ const bodyEnd = bodyStart + length;
373
+ if (buffer.length < bodyEnd) return;
374
+ const body = buffer.slice(bodyStart, bodyEnd).toString("utf8");
375
+ buffer = buffer.slice(bodyEnd);
376
+ const response = JSON.parse(body);
377
+ const waiter = pending.get(response.id);
378
+ if (!waiter) continue;
379
+ pending.delete(response.id);
380
+ clearTimeout(waiter.timeout);
381
+ waiter.resolve(response);
382
+ }
383
+ }
384
+
385
+ async function close() {
386
+ if (proc.exitCode !== null || proc.killed) return;
387
+ proc.kill();
388
+ await new Promise((resolve) => proc.once("exit", resolve));
389
+ }
390
+
391
+ return { request, close };
392
+ }
393
+
394
+ function aggregateRows(rows) {
395
+ const labels = [...new Set(rows.map((row) => row.label))];
396
+ return labels.map((label) => {
397
+ const group = rows.filter((row) => row.label === label);
398
+ const durations = group.map((row) => Number(row.durationMs)).filter(Number.isFinite);
399
+ return {
400
+ label,
401
+ iterations: group.length,
402
+ durationMs: average(durations),
403
+ minDurationMs: roundNumber(Math.min(...durations)),
404
+ maxDurationMs: roundNumber(Math.max(...durations)),
405
+ requestCount: average(group.map((row) => row.requestCount)),
406
+ totalRequests: roundNumber(group.reduce((sum, row) => sum + Number(row.requestCount || 0), 0)),
407
+ creditsUsed: roundNumber(group.reduce((sum, row) => sum + Number(row.creditsUsed || 0), 0)),
408
+ leadCount: roundNumber(group.reduce((sum, row) => sum + Number(row.leadCount || 0), 0)),
409
+ coalescedCompanyCount: average(group.map((row) => row.coalescedCompanyCount).filter((value) => value !== undefined)),
410
+ };
411
+ });
412
+ }
413
+
414
+ function combineCallResults(results, defaults = {}) {
415
+ return {
416
+ requestCount: defaults.requestCount ?? results.reduce((sum, result) => sum + Number(result.requestCount || 0), 0),
417
+ creditsUsed: results.reduce((sum, result) => sum + Number(result.creditsUsed || 0), 0),
418
+ remainingCredits: minDefined(results.map((result) => result.remainingCredits)),
419
+ leadCount: results.reduce((sum, result) => sum + Number(result.leadCount || 0), 0),
420
+ };
421
+ }
422
+
423
+ function assertBatchGuard(payload, label) {
424
+ if (payloadScalar(payload, "ok") !== true) throw new Error(`${label} returned ok=false: ${JSON.stringify(redact(payload))}`);
425
+ const creditsUsed = numeric(payloadScalar(payload, "creditsUsed"), 0);
426
+ if (settings.noCreditGuard && creditsUsed > 0) throw new Error(`${label} unexpectedly used ${creditsUsed} credits.`);
427
+ const leadCount = numeric(payloadScalar(payload, "leadCount"), 0);
428
+ if (settings.noCreditGuard && leadCount > 0) throw new Error(`${label} unexpectedly returned ${leadCount} benchmark records.`);
429
+ }
430
+
431
+ function payloadScalar(payload, key) {
432
+ if (!payload || typeof payload !== "object") return undefined;
433
+ if (Object.hasOwn(payload, key)) return payload[key];
434
+ return payload.scalars?.[key];
435
+ }
436
+
437
+ async function withConcurrency(items, concurrency, worker) {
438
+ const output = new Array(items.length);
439
+ let next = 0;
440
+ const workers = Array.from({ length: Math.min(concurrency, items.length || 1) }, async () => {
441
+ while (next < items.length) {
442
+ const index = next;
443
+ next += 1;
444
+ output[index] = await worker(items[index], index);
445
+ }
446
+ });
447
+ await Promise.all(workers);
448
+ return output;
449
+ }
450
+
451
+ function payloadRecords(payload) {
452
+ if (Array.isArray(payload?.data)) return payload.data;
453
+ if (payload?.data && typeof payload.data === "object") return [payload.data];
454
+ if (Array.isArray(payload)) return payload;
455
+ return [];
456
+ }
457
+
458
+ function parseDomains(value) {
459
+ return String(value || "")
460
+ .split(/[\s,]+/)
461
+ .map((entry) => entry.trim().toLowerCase())
462
+ .filter(Boolean)
463
+ .map((entry) => entry.replace(/^https?:\/\//, "").replace(/^www\./, "").replace(/\/.*$/, ""))
464
+ .filter((entry, index, all) => all.indexOf(entry) === index);
465
+ }
466
+
467
+ function creditComparison(rows, baselineLabel, candidateLabel) {
468
+ const baseline = rows.find((row) => row.label === baselineLabel);
469
+ const candidate = rows.find((row) => row.label === candidateLabel);
470
+ if (!baseline || !candidate) return undefined;
471
+ const baselineCredits = numeric(baseline.creditsUsed, 0);
472
+ const candidateCredits = numeric(candidate.creditsUsed, 0);
473
+ const saved = baselineCredits - candidateCredits;
474
+ return {
475
+ baselineLabel,
476
+ candidateLabel,
477
+ baselineCredits,
478
+ candidateCredits,
479
+ creditsSaved: roundNumber(saved),
480
+ creditReductionPct: baselineCredits > 0 ? roundNumber((saved / baselineCredits) * 100) : undefined,
481
+ };
482
+ }
483
+
484
+ function envInteger(name, min, max, fallback) {
485
+ const value = Number.parseInt(process.env[name] || "", 10);
486
+ if (!Number.isFinite(value)) return fallback;
487
+ return Math.min(max, Math.max(min, value));
488
+ }
489
+
490
+ function truthy(value) {
491
+ return /^(1|true|yes|on|credit|spend)$/i.test(String(value || ""));
492
+ }
493
+
494
+ function falsey(value) {
495
+ return /^(0|false|no|off)$/i.test(String(value || ""));
496
+ }
497
+
498
+ function numeric(value, fallback = undefined) {
499
+ const number = Number(value);
500
+ return Number.isFinite(number) ? number : fallback;
501
+ }
502
+
503
+ function minDefined(values) {
504
+ const numbers = values.map((value) => Number(value)).filter(Number.isFinite);
505
+ return numbers.length ? Math.min(...numbers) : undefined;
506
+ }
507
+
508
+ function average(values) {
509
+ const numbers = values.map((value) => Number(value)).filter(Number.isFinite);
510
+ if (!numbers.length) return undefined;
511
+ return roundNumber(numbers.reduce((sum, value) => sum + value, 0) / numbers.length);
512
+ }
513
+
514
+ function roundNumber(value) {
515
+ const number = Number(value);
516
+ if (!Number.isFinite(number)) return undefined;
517
+ return Math.round(number * 100) / 100;
518
+ }
@@ -0,0 +1,61 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { authStatus, callQuickEnrichTool, createClient } from "./lib/core.mjs";
4
+
5
+ const status = authStatus();
6
+ if (!status.configured) {
7
+ console.log(JSON.stringify({
8
+ ok: true,
9
+ skipped: true,
10
+ reason: "Quick Enrich API key is not configured. Set QUICKENRICH_API_KEY or run node scripts/quickenrich-token.mjs set.",
11
+ }, null, 2));
12
+ process.exit(0);
13
+ }
14
+
15
+ const client = createClient();
16
+ let publicLookup;
17
+ try {
18
+ publicLookup = await callQuickEnrichTool("quickenrich_lookup_values", {
19
+ lookup: "country_codes",
20
+ response: { mode: "meta" },
21
+ }, client);
22
+ } catch (error) {
23
+ publicLookup = {
24
+ ok: false,
25
+ nonBlocking: true,
26
+ error: error.message || String(error),
27
+ data: error.result,
28
+ };
29
+ }
30
+
31
+ const authProbeEmail = `qe-mcp-smoke-${Date.now()}-${Math.random().toString(16).slice(2)}@gmail.com`;
32
+ const authProbe = await callQuickEnrichTool("quickenrich_reverse_email", {
33
+ email: authProbeEmail,
34
+ response: { mode: "compact", maxItems: 3, maxOutputChars: 4000 },
35
+ }, client);
36
+
37
+ if (!authProbe?.ok) {
38
+ console.error(JSON.stringify({ ok: false, publicLookup, authProbe }, null, 2));
39
+ process.exit(1);
40
+ }
41
+
42
+ const creditsUsed = Number(authProbe.meta?.credits_used || 0);
43
+ if (Number.isFinite(creditsUsed) && creditsUsed > 0) {
44
+ console.error(JSON.stringify({ ok: false, reason: "Auth probe unexpectedly used credits.", creditsUsed, authProbe }, null, 2));
45
+ process.exit(1);
46
+ }
47
+
48
+ console.log(JSON.stringify({
49
+ ok: true,
50
+ skipped: false,
51
+ checks: ["reverse_email:no_match_auth_probe"],
52
+ diagnostics: ["lookup_values:country_codes"],
53
+ publicLookup,
54
+ authProbe: {
55
+ ok: authProbe.ok,
56
+ found: authProbe.found,
57
+ meta: authProbe.meta,
58
+ rateLimit: authProbe.rateLimit,
59
+ rateLimitPerMinute: authProbe.rateLimitPerMinute,
60
+ },
61
+ }, null, 2));