@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,201 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { appendFileSync } from "node:fs";
4
+ import {
5
+ SERVER_VERSION,
6
+ TOOL_DEFINITIONS,
7
+ callQuickEnrichTool,
8
+ createClient,
9
+ redact,
10
+ selectTools,
11
+ summarizeValue,
12
+ toolPayloadText,
13
+ } from "./lib/core.mjs";
14
+
15
+ const SERVER_NAME = process.env.QUICKENRICH_MCP_SERVER_NAME || "quick-enrich";
16
+ const MAX_MESSAGE_BYTES = Number(process.env.QUICKENRICH_MCP_MAX_MESSAGE_BYTES || 8 * 1024 * 1024);
17
+ const DEBUG_LOG = process.env.QUICKENRICH_MCP_DEBUG_LOG;
18
+ const VERBOSE_ERRORS = /^(1|true|yes|debug|verbose)$/i.test(String(process.env.QUICKENRICH_MCP_VERBOSE_ERRORS || ""));
19
+ const PRETTY_TOOL_OUTPUT = /^(1|true|yes|pretty)$/i.test(String(process.env.QUICKENRICH_MCP_PRETTY_JSON || ""));
20
+
21
+ const client = createClient();
22
+
23
+ function debugLog(event) {
24
+ if (!DEBUG_LOG) return;
25
+ try {
26
+ appendFileSync(DEBUG_LOG, `${JSON.stringify({ t: new Date().toISOString(), ...event })}\n`, { mode: 0o600 });
27
+ } catch {
28
+ // Debug logging must never break MCP transport.
29
+ }
30
+ }
31
+
32
+ function writeMessage(message) {
33
+ let json = JSON.stringify(message);
34
+ if (Buffer.byteLength(json, "utf8") > MAX_MESSAGE_BYTES) {
35
+ json = JSON.stringify({
36
+ jsonrpc: "2.0",
37
+ id: message?.id ?? null,
38
+ error: {
39
+ code: -32000,
40
+ message: `MCP response exceeded ${MAX_MESSAGE_BYTES} bytes. Use response:{mode:"compact"} or response:{mode:"meta"}.`,
41
+ },
42
+ });
43
+ }
44
+ const bytes = Buffer.byteLength(json, "utf8");
45
+ debugLog({ direction: "out", id: message?.id, bytes, error: message?.error?.message, framing: responseFraming });
46
+ if (responseFraming === "json-lines") {
47
+ process.stdout.write(`${json}\n`);
48
+ return;
49
+ }
50
+ process.stdout.write(`Content-Length: ${bytes}\r\n\r\n${json}`);
51
+ }
52
+
53
+ function success(id, result) {
54
+ writeMessage({ jsonrpc: "2.0", id, result });
55
+ }
56
+
57
+ function failure(id, error) {
58
+ debugLog({
59
+ direction: "error",
60
+ id,
61
+ name: error?.name,
62
+ message: error?.message || String(error),
63
+ stack: VERBOSE_ERRORS ? error?.stack : undefined,
64
+ resultShape: error?.result ? summarizeValue(redact(error.result)) : undefined,
65
+ });
66
+ writeMessage({
67
+ jsonrpc: "2.0",
68
+ id,
69
+ error: {
70
+ code: -32000,
71
+ message: error?.message || String(error),
72
+ data: {
73
+ result: error?.result ? redact(error.result) : undefined,
74
+ debug: VERBOSE_ERRORS ? redact({ name: error?.name, stack: error?.stack }) : undefined,
75
+ },
76
+ },
77
+ });
78
+ }
79
+
80
+ function textResult(payload) {
81
+ return {
82
+ content: [
83
+ {
84
+ type: "text",
85
+ text: toolPayloadText(payload, PRETTY_TOOL_OUTPUT),
86
+ },
87
+ ],
88
+ };
89
+ }
90
+
91
+ async function handle(message) {
92
+ if (!message || typeof message !== "object") return;
93
+ debugLog({
94
+ direction: "in",
95
+ id: message.id,
96
+ method: message.method,
97
+ paramKeys: message.params && typeof message.params === "object" ? Object.keys(message.params) : undefined,
98
+ });
99
+ if (message.method === "initialize") {
100
+ return success(message.id, {
101
+ protocolVersion: message.params?.protocolVersion || "2024-11-05",
102
+ capabilities: { tools: {}, resources: {}, prompts: {} },
103
+ serverInfo: { name: SERVER_NAME, version: SERVER_VERSION },
104
+ });
105
+ }
106
+ if (message.method === "notifications/initialized") return;
107
+ if (message.method === "ping") return success(message.id, {});
108
+ if (message.method === "resources/list") return success(message.id, { resources: [] });
109
+ if (message.method === "resources/templates/list") return success(message.id, { resourceTemplates: [] });
110
+ if (message.method === "prompts/list") return success(message.id, { prompts: [] });
111
+ if (message.method === "tools/list") return success(message.id, { tools: selectTools(TOOL_DEFINITIONS) });
112
+ if (message.method === "tools/call") {
113
+ try {
114
+ const result = await callQuickEnrichTool(message.params?.name, message.params?.arguments || {}, client);
115
+ return success(message.id, textResult(result));
116
+ } catch (error) {
117
+ return failure(message.id, error);
118
+ }
119
+ }
120
+ if (message.id !== undefined) return failure(message.id, new Error(`Unsupported MCP method: ${message.method}`));
121
+ }
122
+
123
+ let buffer = Buffer.alloc(0);
124
+ let responseFraming = "content-length";
125
+
126
+ function firstNonWhitespaceBytePosition() {
127
+ let index = 0;
128
+ while (index < buffer.length) {
129
+ const value = buffer[index];
130
+ if (value === 32 || value === 9 || value === 10 || value === 13) {
131
+ index += 1;
132
+ continue;
133
+ }
134
+ return index;
135
+ }
136
+ return -1;
137
+ }
138
+
139
+ function parseJsonLineFrame() {
140
+ const newlineIndex = buffer.indexOf("\n");
141
+ if (newlineIndex === -1) return false;
142
+
143
+ const lineEnd = newlineIndex > 0 && buffer[newlineIndex - 1] === 13 ? newlineIndex - 1 : newlineIndex;
144
+ const line = buffer.slice(0, lineEnd).toString("utf8");
145
+ buffer = buffer.slice(newlineIndex + 1);
146
+ if (!line) return true;
147
+ responseFraming = "json-lines";
148
+ try {
149
+ void handle(JSON.parse(line));
150
+ } catch (error) {
151
+ failure(null, error);
152
+ }
153
+ return true;
154
+ }
155
+
156
+ function parseContentLengthFrame() {
157
+ const headerEnd = buffer.indexOf("\r\n\r\n");
158
+ if (headerEnd === -1) return false;
159
+ const header = buffer.slice(0, headerEnd).toString("utf8");
160
+ const match = header.match(/Content-Length:\s*(\d+)/i);
161
+ if (!match) {
162
+ buffer = buffer.slice(headerEnd + 4);
163
+ return true;
164
+ }
165
+ responseFraming = "content-length";
166
+ const length = Number(match[1]);
167
+ if (length > MAX_MESSAGE_BYTES) {
168
+ buffer = Buffer.alloc(0);
169
+ failure(null, new Error(`MCP message declared ${length} bytes, above ${MAX_MESSAGE_BYTES}.`));
170
+ return false;
171
+ }
172
+ const bodyStart = headerEnd + 4;
173
+ const bodyEnd = bodyStart + length;
174
+ if (buffer.length < bodyEnd) return false;
175
+ const body = buffer.slice(bodyStart, bodyEnd).toString("utf8");
176
+ buffer = buffer.slice(bodyEnd);
177
+ try {
178
+ void handle(JSON.parse(body));
179
+ } catch (error) {
180
+ failure(null, error);
181
+ }
182
+ return true;
183
+ }
184
+
185
+ process.stdin.on("data", (chunk) => {
186
+ buffer = Buffer.concat([buffer, chunk]);
187
+ if (buffer.length > MAX_MESSAGE_BYTES) {
188
+ buffer = Buffer.alloc(0);
189
+ failure(null, new Error(`MCP message exceeded ${MAX_MESSAGE_BYTES} bytes.`));
190
+ return;
191
+ }
192
+ while (true) {
193
+ const prefixIndex = firstNonWhitespaceBytePosition();
194
+ if (prefixIndex === -1) return;
195
+ const first = buffer[prefixIndex];
196
+ const parsed = first === 0x7b || first === 0x5b ? parseJsonLineFrame() : parseContentLengthFrame();
197
+ if (!parsed) return;
198
+ }
199
+ });
200
+
201
+ process.stdin.resume();
@@ -0,0 +1,175 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { statSync } from "node:fs";
4
+ import { stdin as input, stdout as output } from "node:process";
5
+ import { createInterface } from "node:readline/promises";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import {
9
+ DEFAULT_DATA_DIR,
10
+ TOOL_DEFINITIONS,
11
+ agentGraph,
12
+ authStatus,
13
+ defaultConfigPath,
14
+ removeToken,
15
+ selectTools,
16
+ writeToken,
17
+ } from "./lib/core.mjs";
18
+
19
+ const __dirname = dirname(fileURLToPath(import.meta.url));
20
+ const serverPath = join(__dirname, "quickenrich-mcp.mjs");
21
+
22
+ function usage() {
23
+ return `Usage:
24
+ quickenrich-token status [--json] [--config <path>]
25
+ quickenrich-token set [token] [--config <path>]
26
+ quickenrich-token remove [--config <path>]
27
+ quickenrich-token test [--json] [--config <path>]
28
+ quickenrich-token doctor [--json] [--config <path>]
29
+ quickenrich-token install-config [--json]
30
+ `;
31
+ }
32
+
33
+ function parseArgs(argv) {
34
+ const args = { _: [] };
35
+ for (let i = 0; i < argv.length; i += 1) {
36
+ const arg = argv[i];
37
+ if (arg === "--json") args.json = true;
38
+ else if (arg === "--config") {
39
+ args.config = argv[i + 1];
40
+ i += 1;
41
+ } else args._.push(arg);
42
+ }
43
+ return args;
44
+ }
45
+
46
+ async function readTokenFromStdin() {
47
+ if (!input.isTTY) {
48
+ const chunks = [];
49
+ for await (const chunk of input) chunks.push(chunk);
50
+ return Buffer.concat(chunks).toString("utf8").trim();
51
+ }
52
+ const rl = createInterface({ input, output });
53
+ try {
54
+ return (await rl.question("Quick Enrich API key: ")).trim();
55
+ } finally {
56
+ rl.close();
57
+ }
58
+ }
59
+
60
+ function printStatus(json = false) {
61
+ const status = authStatus();
62
+ if (json) {
63
+ console.log(JSON.stringify(status, null, 2));
64
+ return;
65
+ }
66
+ console.log(`Quick Enrich API key: ${status.configured ? "configured" : "not configured"}`);
67
+ console.log(`Source: ${status.source}`);
68
+ console.log(`Config: ${status.configPath}`);
69
+ console.log(`API base: ${status.apiBaseUrl}`);
70
+ }
71
+
72
+ function doctorPayload() {
73
+ const status = authStatus();
74
+ const defaultTools = selectTools(TOOL_DEFINITIONS);
75
+ const fullTools = selectTools(TOOL_DEFINITIONS, "full");
76
+ const graph = agentGraph();
77
+ return {
78
+ ok: true,
79
+ auth: {
80
+ configured: status.configured,
81
+ source: status.source,
82
+ apiBaseUrl: status.apiBaseUrl,
83
+ configPath: status.configPath,
84
+ },
85
+ tools: {
86
+ defaultCount: defaultTools.length,
87
+ defaultBytes: Buffer.byteLength(JSON.stringify({ tools: defaultTools }), "utf8"),
88
+ fullCount: fullTools.length,
89
+ defaultBudgetBytes: graph.evalBudgets.toolsListBytes,
90
+ },
91
+ artifacts: {
92
+ dataDir: DEFAULT_DATA_DIR,
93
+ cacheDir: graph.cacheLedger.paths.cacheDir,
94
+ ledgerPath: graph.cacheLedger.paths.ledgerPath,
95
+ sharedThrottleDir: process.env.QUICKENRICH_MCP_SHARED_THROTTLE_DIR || join(DEFAULT_DATA_DIR, "throttle"),
96
+ },
97
+ server: {
98
+ command: process.execPath,
99
+ args: [serverPath],
100
+ },
101
+ };
102
+ }
103
+
104
+ function printDoctor(json = false) {
105
+ const payload = doctorPayload();
106
+ if (json) {
107
+ console.log(JSON.stringify(payload, null, 2));
108
+ return;
109
+ }
110
+ console.log(`Quick Enrich MCP doctor: ok`);
111
+ console.log(`Auth: ${payload.auth.configured ? "configured" : "not configured"} (${payload.auth.source})`);
112
+ console.log(`Default tools: ${payload.tools.defaultCount}, ${payload.tools.defaultBytes} bytes`);
113
+ console.log(`Cache: ${payload.artifacts.cacheDir}`);
114
+ }
115
+
116
+ function installConfigPayload() {
117
+ return {
118
+ mcpServers: {
119
+ "quick-enrich": {
120
+ command: process.execPath,
121
+ args: [serverPath],
122
+ env: {
123
+ QUICKENRICH_MCP_TOOL_MODE: "agent",
124
+ },
125
+ },
126
+ },
127
+ };
128
+ }
129
+
130
+ const args = parseArgs(process.argv.slice(2));
131
+ const command = args._[0] || "status";
132
+ if (args.config) process.env.QUICKENRICH_MCP_CONFIG = args.config;
133
+
134
+ try {
135
+ if (command === "status") {
136
+ printStatus(Boolean(args.json));
137
+ } else if (command === "set") {
138
+ const token = args._[1] || await readTokenFromStdin();
139
+ const path = writeToken(token, args.config || defaultConfigPath());
140
+ const mode = statSync(path).mode & 0o777;
141
+ if (mode !== 0o600) throw new Error(`Config permissions are ${mode.toString(8)}, expected 600.`);
142
+ console.log(`Saved Quick Enrich API key to ${path}`);
143
+ } else if (command === "remove" || command === "rm") {
144
+ const path = removeToken(args.config || defaultConfigPath());
145
+ console.log(`Removed Quick Enrich API key from ${path}`);
146
+ } else if (command === "test") {
147
+ const status = authStatus();
148
+ const payload = {
149
+ ok: true,
150
+ configured: status.configured,
151
+ source: status.source,
152
+ apiBaseUrl: status.apiBaseUrl,
153
+ note: "Non-spend local auth/config test only. Use an MCP lookup call to verify network access.",
154
+ };
155
+ if (args.json) console.log(JSON.stringify(payload, null, 2));
156
+ else {
157
+ console.log(`Quick Enrich config test: ${payload.configured ? "configured" : "not configured"}`);
158
+ console.log(payload.note);
159
+ }
160
+ } else if (command === "doctor") {
161
+ printDoctor(Boolean(args.json));
162
+ } else if (command === "install-config") {
163
+ const payload = installConfigPayload();
164
+ if (args.json) console.log(JSON.stringify(payload, null, 2));
165
+ else console.log(JSON.stringify(payload, null, 2));
166
+ } else if (command === "help" || command === "--help" || command === "-h") {
167
+ process.stdout.write(usage());
168
+ } else {
169
+ process.stderr.write(usage());
170
+ process.exitCode = 2;
171
+ }
172
+ } catch (error) {
173
+ console.error(error.message || String(error));
174
+ process.exitCode = 1;
175
+ }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createServer } from "node:http";
4
+ import { performance } from "node:perf_hooks";
5
+ import { callQuickEnrichTool, createClient } from "./lib/core.mjs";
6
+
7
+ function assert(condition, message) {
8
+ if (!condition) throw new Error(message);
9
+ }
10
+
11
+ const startedAt = performance.now();
12
+ const starts = [];
13
+
14
+ const server = createServer(async (req, res) => {
15
+ const url = new URL(req.url, "http://127.0.0.1");
16
+ if (url.pathname === "/api/employees/dataset-search") {
17
+ starts.push(performance.now() - startedAt);
18
+ res.setHeader("content-type", "application/json");
19
+ res.end(JSON.stringify({
20
+ success: true,
21
+ data: [],
22
+ meta: { page: 1, per_page: 5, total: 0, last_page: 1, credits_used: 0, remaining_credits: 9999 },
23
+ }));
24
+ return;
25
+ }
26
+ res.statusCode = 404;
27
+ res.end(JSON.stringify({ error: "not found" }));
28
+ });
29
+
30
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
31
+ const { port } = server.address();
32
+
33
+ try {
34
+ process.env.QUICKENRICH_API_KEY = "rate-limit-test-key";
35
+ process.env.QUICKENRICH_API_BASE_URL = `http://127.0.0.1:${port}`;
36
+ const result = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
37
+ companies: [
38
+ { company_url: "one.example", title: "Founder" },
39
+ { company_url: "two.example", title: "Founder" },
40
+ { company_url: "three.example", title: "Founder" },
41
+ ],
42
+ pages_per_company: 1,
43
+ per_page: 5,
44
+ concurrency: 3,
45
+ requests_per_minute: 300,
46
+ burst_size: 1,
47
+ response: { mode: "full" },
48
+ }, createClient());
49
+
50
+ assert(result.ok === true, "Batch should succeed.");
51
+ assert(result.requestCount === 3, `Expected 3 requests, got ${result.requestCount}.`);
52
+ assert(result.throttle?.requestsPerMinute === 300, `Expected 300 rpm throttle, got ${result.throttle?.requestsPerMinute}.`);
53
+ assert(result.throttle?.burstSize === 1, `Expected burstSize=1, got ${result.throttle?.burstSize}.`);
54
+ assert(starts.length === 3, `Expected 3 request starts, got ${starts.length}.`);
55
+
56
+ const firstGap = starts[1] - starts[0];
57
+ const secondGap = starts[2] - starts[1];
58
+ assert(firstGap >= 170, `Expected first paced gap >=170ms, got ${firstGap.toFixed(1)}ms.`);
59
+ assert(secondGap >= 170, `Expected second paced gap >=170ms, got ${secondGap.toFixed(1)}ms.`);
60
+
61
+ console.log(JSON.stringify({
62
+ ok: true,
63
+ starts: starts.map((value) => Math.round(value)),
64
+ firstGap: Math.round(firstGap),
65
+ secondGap: Math.round(secondGap),
66
+ throttle: result.throttle,
67
+ }, null, 2));
68
+ } finally {
69
+ server.close();
70
+ }
@@ -0,0 +1,82 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createServer } from "node:http";
4
+ import { performance } from "node:perf_hooks";
5
+ import { callQuickEnrichTool, createClient } from "./lib/core.mjs";
6
+
7
+ function assert(condition, message) {
8
+ if (!condition) throw new Error(message);
9
+ }
10
+
11
+ function sleep(ms) {
12
+ return new Promise((resolve) => setTimeout(resolve, ms));
13
+ }
14
+
15
+ let active = 0;
16
+ let maxActive = 0;
17
+ let requestCount = 0;
18
+
19
+ const server = createServer(async (req, res) => {
20
+ const url = new URL(req.url, "http://127.0.0.1");
21
+ if (url.pathname !== "/api/employees/email-search") {
22
+ res.statusCode = 404;
23
+ res.end(JSON.stringify({ error: "not found" }));
24
+ return;
25
+ }
26
+ active += 1;
27
+ requestCount += 1;
28
+ maxActive = Math.max(maxActive, active);
29
+ try {
30
+ await sleep(45);
31
+ const email = url.searchParams.get("email");
32
+ res.setHeader("content-type", "application/json");
33
+ res.end(JSON.stringify({
34
+ success: true,
35
+ data: {
36
+ first_name: "Fast",
37
+ last_name: "Reverse",
38
+ email,
39
+ company_url: "floor.dev",
40
+ },
41
+ meta: { credits_used: 1, remaining_credits: 9999 },
42
+ }));
43
+ } finally {
44
+ active -= 1;
45
+ }
46
+ });
47
+
48
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
49
+ const { port } = server.address();
50
+
51
+ try {
52
+ process.env.QUICKENRICH_API_KEY = "reverse-concurrency-floor-key";
53
+ process.env.QUICKENRICH_API_BASE_URL = `http://127.0.0.1:${port}`;
54
+ const client = createClient();
55
+ const startedAt = performance.now();
56
+ const result = await callQuickEnrichTool("quickenrich_batch_reverse_email", {
57
+ emails: Array.from({ length: 64 }, (_, index) => `fast-${index}@floor.dev`),
58
+ concurrency: 64,
59
+ requests_per_minute: 100_000,
60
+ burst_size: 128,
61
+ export_format: "jsonl",
62
+ response: { mode: "full", maxOutputChars: 1_000_000 },
63
+ }, client);
64
+ const durationMs = performance.now() - startedAt;
65
+
66
+ assert(result.ok === true, "Reverse concurrency floor batch should succeed.");
67
+ assert(result.requestCount === 64, `Expected 64 reverse requests, got ${result.requestCount}.`);
68
+ assert(result.foundCount === 64, `Expected 64 found rows, got ${result.foundCount}.`);
69
+ assert(requestCount === 64, `Expected mock server to receive 64 requests, got ${requestCount}.`);
70
+ assert(maxActive >= 60, `Reverse batch should run as one high-concurrency wave, maxActive=${maxActive}.`);
71
+ assert(durationMs < 95, `Reverse high-concurrency wave should stay near one request-latency hop, got ${durationMs.toFixed(1)}ms.`);
72
+
73
+ console.log(JSON.stringify({
74
+ ok: true,
75
+ durationMs: Math.round(durationMs),
76
+ requestCount: result.requestCount,
77
+ maxActive,
78
+ exportBytes: Buffer.byteLength(result.export?.text || ""),
79
+ }, null, 2));
80
+ } finally {
81
+ server.close();
82
+ }
@@ -0,0 +1,42 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { compactToolPayload, toolPayloadText } from "./lib/core.mjs";
4
+
5
+ function assert(condition, message) {
6
+ if (!condition) throw new Error(message);
7
+ }
8
+
9
+ const payload = {
10
+ ok: true,
11
+ rows: Array.from({ length: 750 }, (_, index) => ({
12
+ index,
13
+ first_name: `First${index}`,
14
+ last_name: `Last${index}`,
15
+ email: `person-${index}@example.com`,
16
+ company_url: `company-${index}.example`,
17
+ title: "Founder",
18
+ })),
19
+ };
20
+
21
+ const compacted = compactToolPayload(payload, { mode: "full", maxOutputChars: 1_000_000 });
22
+ const first = toolPayloadText(compacted);
23
+ const second = toolPayloadText(compacted);
24
+
25
+ assert(first === second, "Tool payload serialization should be stable.");
26
+ assert(first.includes("\"rows\""), "Serialized payload should include full rows.");
27
+ assert(!Object.getOwnPropertySymbols(compacted).some((symbol) => symbol.description?.includes("secret")), "Serialization cache must not expose secret-like symbols.");
28
+
29
+ const redacted = compactToolPayload({
30
+ ok: true,
31
+ Authorization: "Bearer secret-token",
32
+ nested: { api_key: "secret-key" },
33
+ }, { mode: "full" });
34
+ const redactedText = toolPayloadText(redacted);
35
+ assert(!redactedText.includes("secret-token"), "Cached serialization must preserve bearer redaction.");
36
+ assert(!redactedText.includes("secret-key"), "Cached serialization must preserve api_key redaction.");
37
+
38
+ console.log(JSON.stringify({
39
+ ok: true,
40
+ bytes: Buffer.byteLength(first),
41
+ rows: compacted.rows.length,
42
+ }, null, 2));
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createServer } from "node:http";
4
+ import { performance } from "node:perf_hooks";
5
+ import { callQuickEnrichTool, createClient } from "./lib/core.mjs";
6
+
7
+ function assert(condition, message) {
8
+ if (!condition) throw new Error(message);
9
+ }
10
+
11
+ const startedAt = performance.now();
12
+ const starts = [];
13
+
14
+ const server = createServer(async (req, res) => {
15
+ const url = new URL(req.url, "http://127.0.0.1");
16
+ if (url.pathname === "/api/employees/email-search") {
17
+ starts.push({ email: url.searchParams.get("email"), t: performance.now() - startedAt });
18
+ res.setHeader("content-type", "application/json");
19
+ res.end(JSON.stringify({
20
+ success: true,
21
+ data: [],
22
+ meta: { credits_used: 0, remaining_credits: 9999 },
23
+ }));
24
+ return;
25
+ }
26
+ if (url.pathname === "/api/employees/dataset-search") {
27
+ starts.push({ domain: url.searchParams.get("company_url"), t: performance.now() - startedAt });
28
+ res.setHeader("content-type", "application/json");
29
+ res.end(JSON.stringify({
30
+ success: true,
31
+ data: [],
32
+ meta: { page: 1, per_page: 5, total: 0, last_page: 1, credits_used: 0, remaining_credits: 9999 },
33
+ }));
34
+ return;
35
+ }
36
+ res.statusCode = 404;
37
+ res.end(JSON.stringify({ error: "not found" }));
38
+ });
39
+
40
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
41
+ const { port } = server.address();
42
+
43
+ try {
44
+ process.env.QUICKENRICH_API_KEY = "token-bucket-test-key";
45
+ process.env.QUICKENRICH_API_BASE_URL = `http://127.0.0.1:${port}`;
46
+ const client = createClient();
47
+
48
+ const burstResult = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
49
+ companies: [
50
+ { company_url: "burst-one.example", title: "Founder" },
51
+ { company_url: "burst-two.example", title: "Founder" },
52
+ { company_url: "burst-three.example", title: "Founder" },
53
+ { company_url: "burst-four.example", title: "Founder" },
54
+ ],
55
+ pages_per_company: 1,
56
+ per_page: 5,
57
+ concurrency: 4,
58
+ requests_per_minute: 300,
59
+ response: { mode: "full" },
60
+ }, client);
61
+
62
+ const burstStarts = starts.filter((entry) => entry.domain?.startsWith("burst-"));
63
+ const burstSpan = Math.max(...burstStarts.map((entry) => entry.t)) - Math.min(...burstStarts.map((entry) => entry.t));
64
+ assert(burstResult.ok === true, "Burst batch should succeed.");
65
+ assert(burstResult.requestCount === 4, `Expected 4 burst requests, got ${burstResult.requestCount}.`);
66
+ assert(burstResult.throttle?.burstSize >= 4, `Default domain-search burst should allow at least 4 starts, got ${burstResult.throttle?.burstSize}.`);
67
+ assert(burstSpan < 100, `Default compliant burst should start 4 requests within 100ms, got ${burstSpan.toFixed(1)}ms.`);
68
+
69
+ await callQuickEnrichTool("quickenrich_batch_pull_leads", {
70
+ companies: [{ company_url: "strict-one.example", title: "Founder" }],
71
+ pages_per_company: 1,
72
+ per_page: 5,
73
+ concurrency: 1,
74
+ requests_per_minute: 300,
75
+ burst_size: 1,
76
+ response: { mode: "full" },
77
+ }, client);
78
+
79
+ await callQuickEnrichTool("quickenrich_batch_pull_leads", {
80
+ companies: [{ company_url: "strict-two.example", title: "Founder" }],
81
+ pages_per_company: 1,
82
+ per_page: 5,
83
+ concurrency: 1,
84
+ requests_per_minute: 300,
85
+ burst_size: 1,
86
+ response: { mode: "full" },
87
+ }, client);
88
+
89
+ const strictStarts = starts.filter((entry) => entry.domain?.startsWith("strict-"));
90
+ assert(strictStarts.length === 2, `Expected 2 strict starts, got ${strictStarts.length}.`);
91
+ const strictGap = strictStarts[1].t - strictStarts[0].t;
92
+ assert(strictGap >= 170, `Strict burst_size=1 should be shared across calls and wait >=170ms, got ${strictGap.toFixed(1)}ms.`);
93
+
94
+ const reverseResult = await callQuickEnrichTool("quickenrich_batch_reverse_email", {
95
+ emails: [
96
+ "burst-email-one@gmail.com",
97
+ "burst-email-two@gmail.com",
98
+ "burst-email-three@gmail.com",
99
+ ],
100
+ concurrency: 3,
101
+ requests_per_minute: 1000,
102
+ response: { mode: "full" },
103
+ }, client);
104
+
105
+ const reverseStarts = starts.filter((entry) => entry.email?.startsWith("burst-email-"));
106
+ const reverseSpan = Math.max(...reverseStarts.map((entry) => entry.t)) - Math.min(...reverseStarts.map((entry) => entry.t));
107
+ assert(reverseResult.ok === true, "Reverse-email burst batch should succeed.");
108
+ assert(reverseResult.throttle?.burstSize >= 3, `Default reverse-email burst should allow at least 3 starts, got ${reverseResult.throttle?.burstSize}.`);
109
+ assert(reverseSpan < 100, `Default reverse-email burst should start 3 requests within 100ms, got ${reverseSpan.toFixed(1)}ms.`);
110
+
111
+ console.log(JSON.stringify({
112
+ ok: true,
113
+ burstSpan: Math.round(burstSpan),
114
+ strictGap: Math.round(strictGap),
115
+ reverseSpan: Math.round(reverseSpan),
116
+ burstThrottle: burstResult.throttle,
117
+ reverseThrottle: reverseResult.throttle,
118
+ }, null, 2));
119
+ } finally {
120
+ server.close();
121
+ }