@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,112 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createServer } from "node:http";
4
+ import {
5
+ callQuickEnrichTool,
6
+ createClient,
7
+ } from "./lib/core.mjs";
8
+
9
+ function assert(condition, message) {
10
+ if (!condition) throw new Error(message);
11
+ }
12
+
13
+ const requests = [];
14
+
15
+ const server = createServer(async (req, res) => {
16
+ const chunks = [];
17
+ for await (const chunk of req) chunks.push(chunk);
18
+ const bodyText = Buffer.concat(chunks).toString("utf8");
19
+ const url = new URL(req.url, "http://127.0.0.1");
20
+ requests.push({
21
+ method: req.method,
22
+ path: url.pathname,
23
+ query: Object.fromEntries(url.searchParams.entries()),
24
+ auth: req.headers.authorization,
25
+ body: bodyText ? JSON.parse(bodyText) : null,
26
+ });
27
+
28
+ res.setHeader("content-type", "application/json");
29
+ res.setHeader("x-ratelimit-limit", "300");
30
+ res.setHeader("x-ratelimit-remaining", "299");
31
+
32
+ if (url.pathname === "/api/employees/dataset-search") {
33
+ res.end(JSON.stringify({
34
+ success: true,
35
+ data: [
36
+ {
37
+ first_name: "Ada",
38
+ last_name: "Lovelace",
39
+ title: "Founder",
40
+ employee_linkedin: "https://linkedin.com/in/ada",
41
+ has_email: true,
42
+ has_phone: false,
43
+ company_url: url.searchParams.get("company_url"),
44
+ company_name: "Example",
45
+ country_code: "US",
46
+ },
47
+ ],
48
+ meta: { page: Number(url.searchParams.get("page") || 1), per_page: 20, total: 1, last_page: 1 },
49
+ }));
50
+ return;
51
+ }
52
+
53
+ if (url.pathname === "/api/employees/search") {
54
+ res.end(JSON.stringify({
55
+ success: true,
56
+ data: {
57
+ first_name: "Ada",
58
+ last_name: "Lovelace",
59
+ title: "Founder",
60
+ email: "ada@example.com",
61
+ employee_phone: "(415) 555-0100",
62
+ employee_linkedin: "https://linkedin.com/in/ada",
63
+ company_url: url.searchParams.get("company_url"),
64
+ },
65
+ }));
66
+ return;
67
+ }
68
+
69
+ if (url.pathname === "/api/lookups/country-codes") {
70
+ res.end(JSON.stringify(["US", "GB"]));
71
+ return;
72
+ }
73
+
74
+ res.statusCode = 404;
75
+ res.end(JSON.stringify({ error: "not found" }));
76
+ });
77
+
78
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
79
+ const { port } = server.address();
80
+
81
+ try {
82
+ process.env.QUICKENRICH_API_KEY = "test-secret-key";
83
+ process.env.QUICKENRICH_API_BASE_URL = `http://127.0.0.1:${port}`;
84
+ const client = createClient();
85
+
86
+ const batch = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
87
+ companies: [{ company_url: "example.com", title: "Founder" }],
88
+ has_email: true,
89
+ enrich_contacts: true,
90
+ max_enrichments: 1,
91
+ response: { mode: "full" },
92
+ }, client);
93
+
94
+ assert(batch.ok === true, "Batch should succeed.");
95
+ assert(batch.leadCount === 1, "Batch should return one lead.");
96
+ assert(batch.leads[0].email === "ada@example.com", "Batch enrichment should merge email.");
97
+ assert(batch.leads[0].phone === "+14155550100", "Batch enrichment should normalize phone from employee search.");
98
+
99
+ const lookup = await callQuickEnrichTool("quickenrich_lookup_values", { lookup: "country_codes" }, client);
100
+ assert(Array.isArray(lookup.data), "Lookup should return public lookup data.");
101
+
102
+ assert(requests.some((entry) => entry.path === "/api/employees/dataset-search"), "Domain search was not called.");
103
+ assert(requests.find((entry) => entry.path === "/api/employees/dataset-search").query.has_email === "true", "Domain search should forward has_email=true.");
104
+ assert(requests.some((entry) => entry.path === "/api/employees/search"), "Employee search was not called.");
105
+ assert(requests.filter((entry) => entry.path !== "/api/lookups/country-codes").every((entry) => entry.auth === "Bearer test-secret-key"), "Authenticated calls must use bearer auth.");
106
+ assert(requests.find((entry) => entry.path === "/api/lookups/country-codes").auth === undefined, "Lookup calls should not require auth.");
107
+ assert(!JSON.stringify(batch).includes("test-secret-key"), "Tool output leaked API key.");
108
+
109
+ console.log(JSON.stringify({ ok: true }, null, 2));
110
+ } finally {
111
+ server.close();
112
+ }
@@ -0,0 +1,273 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createServer } from "node:http";
4
+ import { TOOL_DEFINITIONS, callQuickEnrichTool, createClient, selectTools } from "./lib/core.mjs";
5
+
6
+ function assert(condition, message) {
7
+ if (!condition) throw new Error(message);
8
+ }
9
+
10
+ const requests = [];
11
+
12
+ const server = createServer(async (req, res) => {
13
+ const chunks = [];
14
+ for await (const chunk of req) chunks.push(chunk);
15
+ const bodyText = Buffer.concat(chunks).toString("utf8");
16
+ const body = bodyText ? JSON.parse(bodyText) : null;
17
+ const url = new URL(req.url, "http://127.0.0.1");
18
+ requests.push({ method: req.method, path: url.pathname, query: Object.fromEntries(url.searchParams.entries()), body });
19
+ res.setHeader("content-type", "application/json");
20
+
21
+ if (url.pathname === "/api/employees/email-search") {
22
+ const email = url.searchParams.get("email");
23
+ res.end(JSON.stringify({
24
+ success: true,
25
+ data: email?.startsWith("missing") ? [] : {
26
+ first_name: "Ada",
27
+ last_name: "Lovelace",
28
+ email,
29
+ title: "Founder",
30
+ company_url: "example.com",
31
+ },
32
+ meta: { credits_used: email?.startsWith("missing") ? 0 : 1, remaining_credits: 900 },
33
+ }));
34
+ return;
35
+ }
36
+
37
+ if (url.pathname === "/api/employees/search") {
38
+ res.end(JSON.stringify({
39
+ success: true,
40
+ data: {
41
+ first_name: url.searchParams.get("first_name") || "Ada",
42
+ last_name: url.searchParams.get("last_name") || "Lovelace",
43
+ title: "Founder",
44
+ email: `${String(url.searchParams.get("first_name") || "ada").toLowerCase()}@example.com`,
45
+ employee_linkedin: url.searchParams.get("linkedin_url") || "https://linkedin.com/in/ada",
46
+ company_url: url.searchParams.get("company_url") || "example.com",
47
+ },
48
+ meta: { credits_used: 1, remaining_credits: 899 },
49
+ }));
50
+ return;
51
+ }
52
+
53
+ if (url.pathname === "/api/employees/phone-search") {
54
+ res.end(JSON.stringify({
55
+ success: true,
56
+ data: {
57
+ first_name: "Ada",
58
+ last_name: "Lovelace",
59
+ employee_phone: "(415) 555-0100",
60
+ company_url: url.searchParams.get("company_url") || "example.com",
61
+ },
62
+ meta: { credits_used: 1, remaining_credits: 898 },
63
+ }));
64
+ return;
65
+ }
66
+
67
+ if (url.pathname === "/api/employees/dataset-search") {
68
+ const companyUrl = url.searchParams.get("company_url");
69
+ res.end(JSON.stringify({
70
+ success: true,
71
+ data: [{
72
+ first_name: "Csv",
73
+ last_name: "Lead",
74
+ title: url.searchParams.get("title") || "Founder",
75
+ email: `founder@${companyUrl}`,
76
+ employee_linkedin: `https://linkedin.com/in/${companyUrl}-founder`,
77
+ company_url: companyUrl,
78
+ company_name: companyUrl,
79
+ has_email: true,
80
+ }],
81
+ meta: { page: 1, per_page: 20, total: 1, last_page: 1, credits_used: 1, remaining_credits: 897 },
82
+ }));
83
+ return;
84
+ }
85
+
86
+ if (url.pathname === "/api/employees/contact-finder") {
87
+ const page = Number(body?.page || 1);
88
+ res.end(JSON.stringify({
89
+ success: true,
90
+ data: page === 1 ? [
91
+ {
92
+ first_name: "Jane",
93
+ last_name: "Doe",
94
+ title: "CEO",
95
+ employee_linkedin: "https://linkedin.com/in/jane",
96
+ has_email: true,
97
+ has_phone: true,
98
+ company_url: "acme.com",
99
+ company_name: "Acme",
100
+ country_code: "US",
101
+ },
102
+ ] : [
103
+ {
104
+ first_name: "John",
105
+ last_name: "Smith",
106
+ title: "CFO",
107
+ employee_linkedin: "https://linkedin.com/in/john",
108
+ has_email: true,
109
+ has_phone: false,
110
+ company_url: "acme.com",
111
+ company_name: "Acme",
112
+ country_code: "US",
113
+ },
114
+ ],
115
+ meta: { page, per_page: body?.per_page || 1, total: 2, last_page: 2, credits_used: 0, remaining_credits: 898 },
116
+ }));
117
+ return;
118
+ }
119
+
120
+ if (url.pathname === "/api/companies/company-finder") {
121
+ const page = Number(body?.page || 1);
122
+ res.end(JSON.stringify({
123
+ success: true,
124
+ data: [
125
+ {
126
+ company_name: page === 1 ? "Acme Cloud" : "Beta Cloud",
127
+ url: page === 1 ? "acme.com" : "beta.com",
128
+ email_domain: page === 1 ? "acme.com" : "beta.com",
129
+ final_email_domain: page === 1 ? "acme.com" : "beta.com",
130
+ phone: "+1-555-0100",
131
+ linkedin_url: page === 1 ? "https://linkedin.com/company/acme" : "https://linkedin.com/company/beta",
132
+ city: "Austin",
133
+ region_code: "TX",
134
+ country_code: "US",
135
+ industry: "Computer Software",
136
+ employee_count: "51-200",
137
+ revenue: "10M-50M",
138
+ services: ["DevOps", "AWS"],
139
+ service_count: 2,
140
+ home_page_text_snippet: "Cloud migration help",
141
+ bio_li_snippet: "Cloud solutions provider",
142
+ },
143
+ ],
144
+ meta: { page, per_page: body?.per_page || 1, total: 2, last_page: 2, credits_used: 1, remaining_credits: 897 },
145
+ }));
146
+ return;
147
+ }
148
+
149
+ res.statusCode = 404;
150
+ res.end(JSON.stringify({ error: "not found" }));
151
+ });
152
+
153
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
154
+ const { port } = server.address();
155
+
156
+ try {
157
+ process.env.QUICKENRICH_API_KEY = "native-test-key";
158
+ process.env.QUICKENRICH_API_BASE_URL = `http://127.0.0.1:${port}`;
159
+ const client = createClient();
160
+ const toolNames = TOOL_DEFINITIONS.map((tool) => tool.name);
161
+ const agentToolNames = selectTools(TOOL_DEFINITIONS, "agent").map((tool) => tool.name);
162
+ assert(toolNames.includes("quickenrich_batch_reverse_email"), "Missing native batch reverse-email tool.");
163
+ assert(toolNames.includes("quickenrich_batch_employee_search"), "Missing native batch employee-search tool.");
164
+ assert(agentToolNames.includes("quickenrich_batch_reverse_email"), "Agent surface should expose batch reverse-email.");
165
+ assert(agentToolNames.includes("quickenrich_lookup_values"), "Agent surface should expose lookup discovery.");
166
+
167
+ const reverse = await callQuickEnrichTool("quickenrich_batch_reverse_email", {
168
+ input_csv: "email\nada@example.com\nmissing@example.com\nada@example.com\n",
169
+ max_credits: 1,
170
+ concurrency: 4,
171
+ export_format: "jsonl",
172
+ response: { mode: "full" },
173
+ }, client);
174
+ assert(reverse.requestCount === 1, `Credit guard should stop duplicate/remainder after 1 paid reverse lookup, got ${reverse.requestCount}.`);
175
+ assert(reverse.creditsUsed === 1, `Expected 1 reverse-email credit, got ${reverse.creditsUsed}.`);
176
+ assert(reverse.creditBudget.stopped === true, "Reverse-email credit budget should report stopped=true.");
177
+ assert(reverse.export?.format === "jsonl", "Reverse-email batch should return JSONL export.");
178
+ assert(reverse.export.text.split("\n").filter(Boolean).length === 1, "Reverse-email JSONL export should contain one found row.");
179
+
180
+ const exact = await callQuickEnrichTool("quickenrich_batch_employee_search", {
181
+ input_jsonl: '{"company_url":"example.com","first_name":"Ada","last_name":"Lovelace"}\n{"company_url":"example.com","first_name":"Grace","last_name":"Hopper"}\n',
182
+ include_phone: true,
183
+ max_credits: 4,
184
+ requests_per_minute: 1000,
185
+ burst_size: 2,
186
+ export_format: "csv",
187
+ response: { mode: "full" },
188
+ }, client);
189
+ assert(exact.requestCount === 4, `Expected 2 email + 2 phone requests, got ${exact.requestCount}.`);
190
+ assert(exact.creditsUsed === 4, `Expected 4 exact batch credits, got ${exact.creditsUsed}.`);
191
+ assert(exact.export?.format === "csv", "Exact batch should return CSV export.");
192
+ assert(exact.export.text.includes("email"), "Exact CSV export should include email header.");
193
+
194
+ const domainCsv = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
195
+ mode: "domain_search",
196
+ input_csv: "company_url,title\ncsv-one.example,Founder\ncsv-one.example,Founder\ncsv-two.example,CEO\n",
197
+ concurrency: 4,
198
+ export_format: "jsonl",
199
+ response: { mode: "full" },
200
+ }, client);
201
+ const csvDomainRequests = requests.filter((entry) => entry.path === "/api/employees/dataset-search");
202
+ assert(domainCsv.requestCount === 2, `CSV domain batch should coalesce duplicate companies to 2 requests, got ${domainCsv.requestCount}.`);
203
+ assert(domainCsv.inputCompanyCount === 3, `CSV domain batch should report 3 input rows, got ${domainCsv.inputCompanyCount}.`);
204
+ assert(domainCsv.companyCount === 2, `CSV domain batch should report 2 unique companies, got ${domainCsv.companyCount}.`);
205
+ assert(domainCsv.coalescedCompanyCount === 1, `CSV domain batch should coalesce 1 duplicate, got ${domainCsv.coalescedCompanyCount}.`);
206
+ assert(csvDomainRequests.some((entry) => entry.query.company_url === "csv-two.example" && entry.query.title === "CEO"), "CSV domain batch should preserve per-row titles.");
207
+ assert(domainCsv.export?.format === "jsonl", "CSV domain batch should return JSONL export.");
208
+
209
+ const contacts = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
210
+ mode: "contact_finder",
211
+ filters: {
212
+ company_name: { include: ["Acme"] },
213
+ country_code: ["US"],
214
+ title: { include: ["CEO"], exclude: ["Intern"] },
215
+ },
216
+ has_email: true,
217
+ has_phone: true,
218
+ max_results: 2,
219
+ per_page: 1,
220
+ pages_per_company: 3,
221
+ requests_per_minute: 120,
222
+ burst_size: 1,
223
+ export_format: "jsonl",
224
+ response: { mode: "full" },
225
+ }, client);
226
+ assert(contacts.requestCount === 2, `Contact finder should auto-page to 2 requests, got ${contacts.requestCount}.`);
227
+ assert(contacts.leadCount === 2, `Contact finder should return 2 leads, got ${contacts.leadCount}.`);
228
+ assert(contacts.creditsUsed === 0, "Contact finder should remain free in mock.");
229
+ assert(contacts.export?.format === "jsonl", "Contact finder should return JSONL export.");
230
+
231
+ const contactBody = requests.find((entry) => entry.path === "/api/employees/contact-finder")?.body;
232
+ assert(contactBody.company_name.include[0] === "Acme", "Typed contact builder should include company_name.");
233
+ assert(contactBody.country_code.include[0] === "US", "Typed contact builder should normalize array filters.");
234
+ assert(contactBody.has_email === true && contactBody.has_phone === true, "Typed contact builder should forward data flags.");
235
+
236
+ const companies = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
237
+ mode: "company_finder",
238
+ filters: {
239
+ home_page_text: ["cloud migration"],
240
+ services: { include: ["DevOps"] },
241
+ industry: ["Computer Software"],
242
+ country_code: ["US"],
243
+ },
244
+ max_results: 2,
245
+ max_credits: 1,
246
+ per_page: 5,
247
+ pages_per_company: 3,
248
+ export_format: "csv",
249
+ response: { mode: "full" },
250
+ }, client);
251
+ assert(companies.requestCount === 1, `Company credit guard should stop after 1 page, got ${companies.requestCount}.`);
252
+ assert(companies.companyCount === 1, `Expected 1 company due credit guard, got ${companies.companyCount}.`);
253
+ assert(companies.companies[0].domain === "acme.com", "Company normalization should expose domain.");
254
+ assert(Array.isArray(companies.companies[0].services), "Company normalization should preserve services.");
255
+ assert(companies.export?.format === "csv", "Company finder should return CSV export.");
256
+ assert(companies.creditBudget.stopped === true, "Company finder credit budget should stop after max_credits.");
257
+
258
+ const companyBody = requests.find((entry) => entry.path === "/api/companies/company-finder")?.body;
259
+ assert(companyBody.per_page === 1, `Credit-limited company finder should shrink per_page to remaining budget, got ${companyBody.per_page}.`);
260
+ assert(companyBody.home_page_text.include[0] === "cloud migration", "Typed company builder should include home_page_text.");
261
+ assert(companyBody.services.include[0] === "DevOps", "Typed company builder should include services.");
262
+
263
+ console.log(JSON.stringify({
264
+ ok: true,
265
+ reverseRequests: reverse.requestCount,
266
+ exactRequests: exact.requestCount,
267
+ domainRequests: domainCsv.requestCount,
268
+ contactRequests: contacts.requestCount,
269
+ companyRequests: companies.requestCount,
270
+ }, null, 2));
271
+ } finally {
272
+ server.close();
273
+ }
@@ -0,0 +1,43 @@
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
+ function assert(condition, message) {
16
+ if (!condition) throw new Error(message);
17
+ }
18
+
19
+ const client = createClient();
20
+ const suffix = `${Date.now()}-${Math.random().toString(16).slice(2)}`;
21
+ const result = await callQuickEnrichTool("quickenrich_batch_reverse_email", {
22
+ emails: [
23
+ `qe-native-${suffix}@gmail.com`,
24
+ `qe-native-${suffix}@gmail.com`,
25
+ `missing-native-${suffix}@gmail.com`,
26
+ ],
27
+ response: { mode: "full" },
28
+ }, client);
29
+
30
+ assert(result.ok === true, "Native batch reverse-email live smoke should return ok=true.");
31
+ assert(result.requestCount <= 2, `Expected deduped live batch to make <=2 requests, got ${result.requestCount}.`);
32
+ assert(Number(result.creditsUsed || 0) === 0, `Native live batch should use 0 credits, got ${result.creditsUsed}.`);
33
+ assert(result.resumeState?.pendingCount === 0, "Native live batch should have no pending items after no-credit misses.");
34
+
35
+ console.log(JSON.stringify({
36
+ ok: true,
37
+ skipped: false,
38
+ tool: "quickenrich_batch_reverse_email",
39
+ requestCount: result.requestCount,
40
+ creditsUsed: result.creditsUsed,
41
+ resumeState: result.resumeState,
42
+ throttle: result.throttle,
43
+ }, null, 2));
@@ -0,0 +1,80 @@
1
+ #!/usr/bin/env node
2
+
3
+ import {
4
+ TOOL_DEFINITIONS,
5
+ agentGraph,
6
+ authStatus,
7
+ compactToolPayload,
8
+ planNaturalLanguage,
9
+ redact,
10
+ } from "./lib/core.mjs";
11
+
12
+ function assert(condition, message) {
13
+ if (!condition) throw new Error(message);
14
+ }
15
+
16
+ const names = TOOL_DEFINITIONS.map((tool) => tool.name);
17
+ assert(new Set(names).size === names.length, "Tool names must be unique.");
18
+ for (const required of [
19
+ "quickenrich_auth_status",
20
+ "quickenrich_agent_graph",
21
+ "quickenrich_natural_language",
22
+ "quickenrich_pipeline",
23
+ "quickenrich_batch_pull_leads",
24
+ "quickenrich_employee_search",
25
+ "quickenrich_phone_search",
26
+ "quickenrich_reverse_email",
27
+ "quickenrich_batch_reverse_email",
28
+ "quickenrich_batch_employee_search",
29
+ "quickenrich_domain_search",
30
+ "quickenrich_contact_finder",
31
+ "quickenrich_company_finder",
32
+ "quickenrich_lookup_values",
33
+ ]) {
34
+ assert(names.includes(required), `Missing tool: ${required}`);
35
+ }
36
+
37
+ const graph = agentGraph();
38
+ assert(graph.rateLimitsPerMinute.domain_search === 300, "Domain search rate limit should be documented.");
39
+ assert(graph.creditNotes.contact_finder.includes("discovery"), "Contact finder credit note should explain discovery-only behavior.");
40
+ assert(graph.endpointCoverage.length === 7, `Expected all Quick Enrich API surfaces in endpointCoverage, got ${graph.endpointCoverage.length}.`);
41
+ assert(graph.agentRouting.some((entry) => entry.tool === "quickenrich_batch_pull_leads"), "Agent graph should prefer native batch lead pulls.");
42
+ assert(graph.lookupHints.tool === "quickenrich_lookup_values", "Agent graph should route lookup discovery to lookup_values.");
43
+ assert(graph.budgetMath.exact_email_phone.includes("phone"), "Agent graph should document email+phone budget math.");
44
+
45
+ const status = authStatus();
46
+ assert(!JSON.stringify(status).match(/sk-|test-token-123/), "Auth status must not include token-looking values.");
47
+
48
+ const plan = planNaturalLanguage("Batch pull founder leads for example.com and sample.io");
49
+ assert(plan.tool === "quickenrich_batch_pull_leads", "Natural language planner should choose batch lead pull.");
50
+ assert(plan.arguments.companies.length === 2, "Planner should extract two domains.");
51
+
52
+ const emailBatchPlan = planNaturalLanguage("Reverse lookup these emails: ada@example.com grace@example.com");
53
+ assert(emailBatchPlan.tool === "quickenrich_batch_reverse_email", "Planner should batch multiple reverse-email lookups.");
54
+ assert(emailBatchPlan.arguments.emails.length === 2, "Planner should extract both email addresses.");
55
+
56
+ const companyFinderPlan = planNaturalLanguage("Use company finder for cloud companies");
57
+ assert(companyFinderPlan.tool === "quickenrich_batch_pull_leads", "Planner should route company finder intent through batch_pull_leads.");
58
+ assert(companyFinderPlan.arguments.mode === "company_finder", "Planner should set company_finder mode.");
59
+
60
+ const contactFinderPlan = planNaturalLanguage("Use contact finder for CEOs at software companies");
61
+ assert(contactFinderPlan.tool === "quickenrich_batch_pull_leads", "Planner should route contact finder intent through batch_pull_leads.");
62
+ assert(contactFinderPlan.arguments.mode === "contact_finder", "Planner should set contact_finder mode.");
63
+
64
+ const exactJsonlPlan = planNaturalLanguage('Batch exact employee search from JSONL: {"company_url":"example.com","first_name":"Ada","last_name":"Lovelace"}');
65
+ assert(exactJsonlPlan.tool === "quickenrich_batch_employee_search", "Planner should route exact-person JSONL to batch employee search.");
66
+ assert(exactJsonlPlan.requires.includes("input_jsonl"), "Exact-person JSONL route should require input_jsonl payload.");
67
+
68
+ const reverseCsvPlan = planNaturalLanguage("Reverse lookup a CSV of emails");
69
+ assert(reverseCsvPlan.tool === "quickenrich_batch_reverse_email", "Planner should route reverse-email CSV to batch reverse-email.");
70
+ assert(reverseCsvPlan.requires.includes("input_csv"), "Reverse-email CSV route should require input_csv payload.");
71
+
72
+ const redacted = JSON.stringify(redact({ Authorization: "Bearer secret-token", nested: { api_key: "abc" } }));
73
+ assert(!redacted.includes("secret-token"), "Bearer token was not redacted.");
74
+ assert(!redacted.includes("abc"), "api_key was not redacted.");
75
+
76
+ const compacted = compactToolPayload({ ok: true, rows: Array.from({ length: 50 }, (_, i) => ({ i })) }, { maxItems: 3 });
77
+ assert(compacted.rows.length === 4, "Compact payload should limit arrays and append omitted count.");
78
+ assert(compacted._meta.truncated === true, "Compact payload should report truncation.");
79
+
80
+ console.log(JSON.stringify({ ok: true }, null, 2));