@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,270 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createServer } from "node:http";
4
+ import { callQuickEnrichTool, createClient } 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/lookups/country-codes") {
22
+ res.end(JSON.stringify(["US", "GB"]));
23
+ return;
24
+ }
25
+ if (url.pathname === "/api/lookups/industries") {
26
+ res.end(JSON.stringify(["Computer Software"]));
27
+ return;
28
+ }
29
+ if (url.pathname === "/api/lookups/employee-ranges") {
30
+ res.end(JSON.stringify(["51-200"]));
31
+ return;
32
+ }
33
+ if (url.pathname === "/api/lookups/revenue-ranges") {
34
+ res.end(JSON.stringify(["10M-50M"]));
35
+ return;
36
+ }
37
+ if (url.pathname === "/api/lookups/company-services") {
38
+ res.end(JSON.stringify([{ service: "DevOps", count: 10 }, { service: "AWS", count: 7 }]));
39
+ return;
40
+ }
41
+
42
+ if (url.pathname === "/api/employees/email-search") {
43
+ const email = url.searchParams.get("email");
44
+ res.end(JSON.stringify({
45
+ success: true,
46
+ data: {
47
+ first_name: email?.startsWith("b") ? "Bee" : "Ada",
48
+ last_name: "Tester",
49
+ email,
50
+ company_url: "example.com",
51
+ },
52
+ meta: { credits_used: 1, remaining_credits: 800 },
53
+ }));
54
+ return;
55
+ }
56
+
57
+ if (url.pathname === "/api/employees/dataset-search") {
58
+ const companyUrl = url.searchParams.get("company_url");
59
+ res.end(JSON.stringify({
60
+ success: true,
61
+ data: [{
62
+ first_name: "Dana",
63
+ last_name: "Domain",
64
+ title: url.searchParams.get("title") || "Founder",
65
+ email: `founder@${companyUrl}`,
66
+ company_url: companyUrl,
67
+ }],
68
+ meta: { page: 1, per_page: 1, total: 1, last_page: 1, credits_used: 1, remaining_credits: 799 },
69
+ }));
70
+ return;
71
+ }
72
+
73
+ if (url.pathname === "/api/employees/contact-finder") {
74
+ const page = Number(body?.page || 1);
75
+ res.end(JSON.stringify({
76
+ success: true,
77
+ data: [{
78
+ first_name: page === 1 ? "Page" : "Second",
79
+ last_name: "Contact",
80
+ title: page === 1 ? "CEO" : "CFO",
81
+ employee_linkedin: `https://linkedin.com/in/contact-${page}`,
82
+ company_url: "resume-contact.com",
83
+ has_email: true,
84
+ has_phone: false,
85
+ }],
86
+ meta: { page, per_page: body?.per_page || 10, total: 2, last_page: 2, credits_used: 0, remaining_credits: 800 },
87
+ }));
88
+ return;
89
+ }
90
+
91
+ if (url.pathname === "/api/companies/company-finder") {
92
+ res.end(JSON.stringify({
93
+ success: true,
94
+ data: [],
95
+ meta: { page: body?.page || 1, per_page: body?.per_page || 10, total: 0, last_page: 1, credits_used: 0, remaining_credits: 800 },
96
+ }));
97
+ return;
98
+ }
99
+
100
+ res.statusCode = 404;
101
+ res.end(JSON.stringify({ error: "not found" }));
102
+ });
103
+
104
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
105
+ const { port } = server.address();
106
+
107
+ try {
108
+ process.env.QUICKENRICH_API_KEY = "ops-test-key";
109
+ process.env.QUICKENRICH_API_BASE_URL = `http://127.0.0.1:${port}`;
110
+ const client = createClient();
111
+
112
+ const reverse = await callQuickEnrichTool("quickenrich_batch_reverse_email", {
113
+ emails: ["a@example.com", "b@example.com", "c@example.com"],
114
+ resume_state: { completed: ["a@example.com"] },
115
+ max_credits: 1,
116
+ response: { mode: "full" },
117
+ }, client);
118
+
119
+ const reverseRequests = requests.filter((entry) => entry.path === "/api/employees/email-search");
120
+ assert(reverse.requestCount === 1, `Expected one new reverse-email request, got ${reverse.requestCount}.`);
121
+ assert(reverseRequests[0].query.email === "b@example.com", `Expected resume to skip a@example.com, got ${reverseRequests[0].query.email}.`);
122
+ assert(reverse.resumeState.completed.includes("a@example.com"), "Resume state should retain prior completed email.");
123
+ assert(reverse.resumeState.completed.includes("b@example.com"), "Resume state should include newly completed email.");
124
+ assert(reverse.resumeState.pending.includes("c@example.com"), "Resume state should report pending email after credit stop.");
125
+
126
+ const reverseRequestsBeforeInvalid = requests.filter((entry) => entry.path === "/api/employees/email-search").length;
127
+ const invalidReverse = await callQuickEnrichTool("quickenrich_batch_reverse_email", {
128
+ emails: ["bad@example.invalid", "prior-failed@example.com"],
129
+ resume_state: { failed: ["prior-failed@example.com"] },
130
+ response: { mode: "full" },
131
+ }, client);
132
+ const reverseRequestsAfterInvalid = requests.filter((entry) => entry.path === "/api/employees/email-search").length;
133
+ assert(invalidReverse.ok === false, "Invalid reverse-email inputs should fail closed.");
134
+ assert(invalidReverse.requestCount === 0, `Invalid/prior-failed reverse-email batch should make 0 requests, got ${invalidReverse.requestCount}.`);
135
+ assert(reverseRequestsAfterInvalid === reverseRequestsBeforeInvalid, "Invalid/prior-failed reverse-email batch should not call vendor API.");
136
+ assert(invalidReverse.errors.some((entry) => entry.code === "invalid_email"), "Invalid reverse-email batch should report invalid_email.");
137
+ assert(invalidReverse.resumeState.failed.includes("bad@example.invalid"), "Invalid email should be marked failed in resume state.");
138
+ assert(invalidReverse.resumeState.failed.includes("prior-failed@example.com"), "Prior failed email should remain failed in resume state.");
139
+ assert(!invalidReverse.resumeState.pending.includes("prior-failed@example.com"), "Prior failed email should not remain pending.");
140
+
141
+ const domainResume = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
142
+ mode: "domain_search",
143
+ companies: [
144
+ { company_url: "resume-skip.com", title: "Founder" },
145
+ { company_url: "resume-go.com", title: "Founder" },
146
+ { company_url: "resume-pending.com", title: "Founder" },
147
+ ],
148
+ resume_state: { completed: ["resume-skip.com|founder"] },
149
+ max_credits: 1,
150
+ per_page: 1,
151
+ pages_per_company: 1,
152
+ response: { mode: "full" },
153
+ }, client);
154
+ const domainRequests = requests.filter((entry) => entry.path === "/api/employees/dataset-search");
155
+ assert(domainResume.requestCount === 1, `Expected one domain request before credit stop, got ${domainResume.requestCount}.`);
156
+ assert(domainRequests[0].query.company_url === "resume-go.com", `Expected resume to skip resume-skip.com, got ${domainRequests[0].query.company_url}.`);
157
+ assert(domainResume.resumeState.completed.includes("resume-skip.com|founder"), "Domain resume should retain prior completed key.");
158
+ assert(domainResume.resumeState.completed.includes("resume-go.com|founder"), "Domain resume should include newly completed key.");
159
+ assert(domainResume.resumeState.pending.includes("resume-pending.com|founder"), "Domain resume should leave credit-stopped company pending.");
160
+
161
+ const directContactRequestsBefore = requests.filter((entry) => entry.path === "/api/employees/contact-finder").length;
162
+ const invalidDirectContacts = await callQuickEnrichTool("quickenrich_contact_finder", {
163
+ filters: { country_code: ["ZZ"] },
164
+ validate_filters: true,
165
+ response: { mode: "full" },
166
+ }, client);
167
+ const directContactRequestsAfter = requests.filter((entry) => entry.path === "/api/employees/contact-finder").length;
168
+ assert(invalidDirectContacts.ok === false, "Standalone contact finder should fail invalid filters.");
169
+ assert(directContactRequestsAfter === directContactRequestsBefore, "Invalid standalone contact finder should not call vendor API.");
170
+ assert(invalidDirectContacts.validation?.errors?.some((entry) => entry.field === "country_code"), "Standalone contact finder should report invalid country_code.");
171
+
172
+ const contactResumeFirst = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
173
+ mode: "contact_finder",
174
+ filters: { country_code: ["US"] },
175
+ pages_per_company: 2,
176
+ per_page: 1,
177
+ max_results: 1,
178
+ response: { mode: "full" },
179
+ }, client);
180
+ assert(contactResumeFirst.requestCount === 1, `Expected first contact resume run to request one page, got ${contactResumeFirst.requestCount}.`);
181
+ assert(contactResumeFirst.resumeState.pendingCount === 1, "First contact resume run should leave page 2 pending.");
182
+ assert(contactResumeFirst.resumeState.nextPage === 2, `Expected contact resume nextPage=2, got ${contactResumeFirst.resumeState.nextPage}.`);
183
+
184
+ const contactRequestsBeforeResume = requests.filter((entry) => entry.path === "/api/employees/contact-finder").length;
185
+ const contactResumeSecond = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
186
+ mode: "contact_finder",
187
+ filters: { country_code: ["US"] },
188
+ pages_per_company: 2,
189
+ per_page: 1,
190
+ max_results: 2,
191
+ resume_state: contactResumeFirst.resumeState,
192
+ response: { mode: "full" },
193
+ }, client);
194
+ const resumedContactRequests = requests.filter((entry) => entry.path === "/api/employees/contact-finder").slice(contactRequestsBeforeResume);
195
+ assert(contactResumeSecond.requestCount === 1, `Expected resumed contact finder to request one page, got ${contactResumeSecond.requestCount}.`);
196
+ assert(resumedContactRequests[0].body.page === 2, `Expected resumed contact finder to start at page 2, got ${resumedContactRequests[0].body.page}.`);
197
+ assert(contactResumeSecond.resumeState.pendingCount === 0, "Resumed contact finder should have no pending pages.");
198
+
199
+ const contactRequestsBeforeStaleResume = requests.filter((entry) => entry.path === "/api/employees/contact-finder").length;
200
+ const contactStaleResume = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
201
+ mode: "contact_finder",
202
+ filters: { country_code: ["US"], city: ["Austin"] },
203
+ pages_per_company: 2,
204
+ per_page: 1,
205
+ max_results: 1,
206
+ resume_state: contactResumeFirst.resumeState,
207
+ response: { mode: "full" },
208
+ }, client);
209
+ const staleResumeRequests = requests.filter((entry) => entry.path === "/api/employees/contact-finder").slice(contactRequestsBeforeStaleResume);
210
+ assert(contactStaleResume.requestCount === 1, `Expected stale resume run to request one page, got ${contactStaleResume.requestCount}.`);
211
+ assert(staleResumeRequests[0].body.page === 1, `Fingerprint-mismatched resume should restart at page 1, got ${staleResumeRequests[0].body.page}.`);
212
+ assert(contactStaleResume.resumeState.fingerprint !== contactResumeFirst.resumeState.fingerprint, "Changed filters should produce a new finder fingerprint.");
213
+
214
+ const validContacts = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
215
+ mode: "contact_finder",
216
+ filters: {
217
+ country_code: ["US"],
218
+ industry_linkedin: ["Computer Software"],
219
+ number_of_employees: ["51-200"],
220
+ revenue: ["10M-50M"],
221
+ },
222
+ validate_filters: true,
223
+ response: { mode: "full" },
224
+ }, client);
225
+ assert(validContacts.ok === true, "Valid contact finder filters should pass validation.");
226
+ assert(validContacts.validation?.ok === true, "Validation metadata should report ok=true.");
227
+
228
+ const directCompanyRequestsBefore = requests.filter((entry) => entry.path === "/api/companies/company-finder").length;
229
+ const invalidDirectCompanies = await callQuickEnrichTool("quickenrich_company_finder", {
230
+ filters: {
231
+ services: ["MadeUpService"],
232
+ country_code: ["ZZ"],
233
+ },
234
+ validate_filters: true,
235
+ response: { mode: "full" },
236
+ }, client);
237
+ const directCompanyRequestsAfter = requests.filter((entry) => entry.path === "/api/companies/company-finder").length;
238
+ assert(invalidDirectCompanies.ok === false, "Standalone company finder should fail invalid filters.");
239
+ assert(directCompanyRequestsAfter === directCompanyRequestsBefore, "Invalid standalone company finder should not call vendor API.");
240
+ assert(invalidDirectCompanies.validation?.errors?.some((entry) => entry.field === "services"), "Standalone company finder should report invalid services.");
241
+
242
+ const invalidCompanies = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
243
+ mode: "company_finder",
244
+ filters: {
245
+ services: ["MadeUpService"],
246
+ country_code: ["ZZ"],
247
+ },
248
+ validate_filters: true,
249
+ response: { mode: "full" },
250
+ }, client);
251
+ assert(invalidCompanies.ok === false, "Invalid company finder filters should fail closed before API call.");
252
+ assert(invalidCompanies.validation?.ok === false, "Validation metadata should report ok=false.");
253
+ assert(invalidCompanies.validation.errors.some((entry) => entry.field === "services"), "Validation should flag invalid services.");
254
+ assert(invalidCompanies.validation.errors.some((entry) => entry.field === "country_code"), "Validation should flag invalid country_code.");
255
+
256
+ const companyRequests = requests.filter((entry) => entry.path === "/api/companies/company-finder");
257
+ assert(companyRequests.length === 0, "Invalid validated company finder should not call vendor API.");
258
+
259
+ console.log(JSON.stringify({
260
+ ok: true,
261
+ reverseRequests: reverse.requestCount,
262
+ invalidRequests: invalidReverse.requestCount,
263
+ domainRequests: domainResume.requestCount,
264
+ contactResumeNextPage: contactResumeFirst.resumeState.nextPage,
265
+ pending: reverse.resumeState.pending,
266
+ validationErrors: invalidCompanies.validation.errors.length,
267
+ }, null, 2));
268
+ } finally {
269
+ server.close();
270
+ }
@@ -0,0 +1,93 @@
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
+ const requests = [];
18
+
19
+ const server = createServer(async (req, res) => {
20
+ active += 1;
21
+ maxActive = Math.max(maxActive, active);
22
+ try {
23
+ const chunks = [];
24
+ for await (const chunk of req) chunks.push(chunk);
25
+ const bodyText = Buffer.concat(chunks).toString("utf8");
26
+ const body = bodyText ? JSON.parse(bodyText) : {};
27
+ const url = new URL(req.url, "http://127.0.0.1");
28
+ requests.push({ path: url.pathname, body });
29
+ await sleep(60);
30
+ res.setHeader("content-type", "application/json");
31
+ if (url.pathname === "/api/employees/contact-finder") {
32
+ const page = Number(body.page || 1);
33
+ res.end(JSON.stringify({
34
+ success: true,
35
+ data: [{
36
+ first_name: `Page${page}`,
37
+ last_name: "Contact",
38
+ title: page === 1 ? "CEO" : "Founder",
39
+ employee_linkedin: `https://linkedin.com/in/page-${page}`,
40
+ company_url: "floor.example",
41
+ company_name: "Floor Inc",
42
+ has_email: true,
43
+ }],
44
+ meta: { page, per_page: 1, total: 4, last_page: 4, credits_used: 0, remaining_credits: 9999 },
45
+ }));
46
+ return;
47
+ }
48
+ res.statusCode = 404;
49
+ res.end(JSON.stringify({ error: "not found" }));
50
+ } finally {
51
+ active -= 1;
52
+ }
53
+ });
54
+
55
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
56
+ const { port } = server.address();
57
+
58
+ try {
59
+ process.env.QUICKENRICH_API_KEY = "paged-floor-key";
60
+ process.env.QUICKENRICH_API_BASE_URL = `http://127.0.0.1:${port}`;
61
+ const client = createClient();
62
+
63
+ const startedAt = performance.now();
64
+ const result = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
65
+ mode: "contact_finder",
66
+ filters: { company_name: ["Floor Inc"] },
67
+ pages_per_company: 4,
68
+ per_page: 1,
69
+ max_results: 4,
70
+ concurrency: 4,
71
+ requests_per_minute: 120,
72
+ burst_size: 4,
73
+ response: { mode: "full" },
74
+ }, client);
75
+ const durationMs = performance.now() - startedAt;
76
+
77
+ const contactRequests = requests.filter((entry) => entry.path === "/api/employees/contact-finder");
78
+ assert(result.ok === true, "Paged finder floor batch should succeed.");
79
+ assert(result.requestCount === 4, `Expected 4 finder requests, got ${result.requestCount}.`);
80
+ assert(result.leadCount === 4, `Expected 4 finder records, got ${result.leadCount}.`);
81
+ assert(contactRequests.map((entry) => entry.body.page).join(",") === "1,2,3,4", "Finder should request pages 1 through 4.");
82
+ assert(maxActive >= 2, `Finder auto-pagination should fan out after page 1, maxActive=${maxActive}.`);
83
+ assert(durationMs < 220, `Finder auto-pagination should stay near the 2-hop floor, got ${durationMs.toFixed(1)}ms.`);
84
+
85
+ console.log(JSON.stringify({
86
+ ok: true,
87
+ durationMs: Math.round(durationMs),
88
+ requestCount: result.requestCount,
89
+ maxActive,
90
+ }, null, 2));
91
+ } finally {
92
+ server.close();
93
+ }
@@ -0,0 +1,111 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { createServer } from "node:http";
4
+ import { callQuickEnrichTool, createClient } from "./lib/core.mjs";
5
+
6
+ function assert(condition, message) {
7
+ if (!condition) throw new Error(message);
8
+ }
9
+
10
+ let active = 0;
11
+ let maxActive = 0;
12
+ const requests = [];
13
+
14
+ const server = createServer(async (req, res) => {
15
+ active += 1;
16
+ maxActive = Math.max(maxActive, active);
17
+ try {
18
+ await new Promise((resolve) => setTimeout(resolve, 25));
19
+ const url = new URL(req.url, "http://127.0.0.1");
20
+ requests.push({ path: url.pathname, query: Object.fromEntries(url.searchParams.entries()) });
21
+ res.setHeader("content-type", "application/json");
22
+ if (url.pathname === "/api/employees/dataset-search") {
23
+ res.end(JSON.stringify({
24
+ success: true,
25
+ data: [
26
+ {
27
+ first_name: "Ada",
28
+ last_name: "Lovelace",
29
+ title: "Founder",
30
+ email: "N/A",
31
+ employee_linkedin: `https://linkedin.com/in/${url.searchParams.get("company_url")}-ada`,
32
+ has_email: false,
33
+ has_phone: false,
34
+ company_url: url.searchParams.get("company_url"),
35
+ company_name: "Example",
36
+ },
37
+ ],
38
+ meta: { page: Number(url.searchParams.get("page") || 1), per_page: 20, total: 1, last_page: 1, credits_used: 1, remaining_credits: 9990 },
39
+ }));
40
+ return;
41
+ }
42
+ res.statusCode = 404;
43
+ res.end(JSON.stringify({ error: "not found" }));
44
+ } finally {
45
+ active -= 1;
46
+ }
47
+ });
48
+
49
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
50
+ const { port } = server.address();
51
+
52
+ try {
53
+ process.env.QUICKENRICH_API_KEY = "perf-test-key";
54
+ process.env.QUICKENRICH_API_BASE_URL = `http://127.0.0.1:${port}`;
55
+ const client = createClient();
56
+ const result = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
57
+ companies: [
58
+ { company_url: "example.com", title: "Founder" },
59
+ { company_url: "example.com", title: "Founder" },
60
+ { company_url: "sample.io", title: "Founder" },
61
+ { company_url: "sample.io", title: "Founder" },
62
+ ],
63
+ pages_per_company: 1,
64
+ concurrency: 2,
65
+ response: { mode: "full" },
66
+ }, client);
67
+
68
+ const domainRequests = requests.filter((entry) => entry.path === "/api/employees/dataset-search");
69
+ assert(result.ok === true, "Batch should succeed.");
70
+ assert(domainRequests.length === 2, `Expected coalesced domain requests for 2 unique company/title/page keys, got ${domainRequests.length}.`);
71
+ assert(result.requestCount === 2, `Expected requestCount=2 after coalescing, got ${result.requestCount}.`);
72
+ assert(result.creditsUsed === 2, `Expected creditsUsed=2 after coalescing, got ${result.creditsUsed}.`);
73
+ assert(result.remainingCredits === 9990, `Expected remainingCredits=9990, got ${result.remainingCredits}.`);
74
+ assert(result.leadCount === 2, `Expected 2 deduped leads, got ${result.leadCount}.`);
75
+ assert(result.leads.every((lead) => lead.email !== "N/A"), "Placeholder email values must be normalized away.");
76
+ assert(maxActive <= 2, `Concurrency cap failed: maxActive=${maxActive}.`);
77
+
78
+ maxActive = 0;
79
+ const limitedStart = requests.length;
80
+ const limited = await callQuickEnrichTool("quickenrich_batch_pull_leads", {
81
+ companies: [
82
+ { company_url: "credit-one.example", title: "Founder" },
83
+ { company_url: "credit-two.example", title: "Founder" },
84
+ { company_url: "credit-three.example", title: "Founder" },
85
+ { company_url: "credit-four.example", title: "Founder" },
86
+ ],
87
+ pages_per_company: 1,
88
+ concurrency: 4,
89
+ max_credits: 2,
90
+ response: { mode: "full" },
91
+ }, client);
92
+ const limitedRequests = requests.slice(limitedStart).filter((entry) => entry.path === "/api/employees/dataset-search");
93
+ assert(limited.ok === true, "Credit-limited batch should succeed with pending work, not fail.");
94
+ assert(limited.requestCount === 2, `Expected max_credits=2 to start two paid requests, got ${limited.requestCount}.`);
95
+ assert(limitedRequests.length === 2, `Expected two vendor requests under max_credits=2, got ${limitedRequests.length}.`);
96
+ assert(limited.creditsUsed === 2, `Expected creditsUsed=2 under max_credits=2, got ${limited.creditsUsed}.`);
97
+ assert(limited.creditBudget?.reserved === 0, `Credit reservations should settle to zero, got ${limited.creditBudget?.reserved}.`);
98
+ assert(limited.creditBudget?.remaining === 0, `Expected no remaining credit budget, got ${limited.creditBudget?.remaining}.`);
99
+ assert(limited.resumeState?.pendingCount === 2, `Expected two pending companies after credit stop, got ${limited.resumeState?.pendingCount}.`);
100
+ assert(maxActive >= 2, `Credit-limited paid lane should retain safe parallelism, maxActive=${maxActive}.`);
101
+
102
+ console.log(JSON.stringify({
103
+ ok: true,
104
+ requests: domainRequests.length,
105
+ maxActive,
106
+ limitedRequests: limitedRequests.length,
107
+ limitedPending: limited.resumeState.pendingCount,
108
+ }, null, 2));
109
+ } finally {
110
+ server.close();
111
+ }
@@ -0,0 +1,109 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
4
+ import { resolve, join } from "node:path";
5
+
6
+ const MANAGED_LINES = [
7
+ "- Quick Enrich MCP: call `quickenrich_agent_graph` first, then prefer `quickenrich_pipeline` or batch tools with `input_file`, `output_file`, `resume_file`, `projection`, and `max_credits`. <!-- quickenrich-mcp:line1 -->",
8
+ "- Quick Enrich MCP auth: run `quickenrich-token set` once and `quickenrich-token doctor` before live runs; never paste API keys into prompts. <!-- quickenrich-mcp:line2 -->",
9
+ ];
10
+
11
+ function usage() {
12
+ return `Usage:
13
+ quickenrich-agent-docs install [--cwd <path>] [--json] [--dry-run]
14
+ quickenrich-agent-docs uninstall [--cwd <path>] [--json] [--dry-run]
15
+ `;
16
+ }
17
+
18
+ function parseArgs(argv) {
19
+ const args = { _: [] };
20
+ for (let index = 0; index < argv.length; index += 1) {
21
+ const arg = argv[index];
22
+ if (arg === "--json") args.json = true;
23
+ else if (arg === "--dry-run") args.dryRun = true;
24
+ else if (arg === "--cwd") {
25
+ args.cwd = argv[index + 1];
26
+ index += 1;
27
+ } else args._.push(arg);
28
+ }
29
+ return args;
30
+ }
31
+
32
+ function targetFiles(cwd) {
33
+ return [
34
+ firstExisting(cwd, ["CLAUDE.md", "Claude.md", "claude.md"]) || "CLAUDE.md",
35
+ firstExisting(cwd, ["AGENTS.md", "agents.md", "Agents.md"]) || "AGENTS.md",
36
+ ].map((name) => join(cwd, name));
37
+ }
38
+
39
+ function firstExisting(cwd, names) {
40
+ return names.find((name) => existsSync(join(cwd, name)));
41
+ }
42
+
43
+ function stripManagedLines(text) {
44
+ return String(text || "")
45
+ .split(/\r?\n/)
46
+ .filter((line) => !line.includes("<!-- quickenrich-mcp:line"))
47
+ .join("\n")
48
+ .replace(/\n+$/, "");
49
+ }
50
+
51
+ function installFile(path, dryRun = false) {
52
+ const before = existsSync(path) ? readFileSync(path, "utf8") : "";
53
+ const stripped = stripManagedLines(before);
54
+ const next = `${stripped}${stripped ? "\n\n" : ""}${MANAGED_LINES.join("\n")}\n`;
55
+ if (!dryRun && next !== before) writeFileSync(path, next, { mode: 0o644 });
56
+ return {
57
+ path,
58
+ existed: Boolean(before),
59
+ changed: next !== before,
60
+ managedLines: MANAGED_LINES.length,
61
+ };
62
+ }
63
+
64
+ function uninstallFile(path, dryRun = false) {
65
+ if (!existsSync(path)) {
66
+ return { path, existed: false, changed: false, managedLines: 0 };
67
+ }
68
+ const before = readFileSync(path, "utf8");
69
+ const stripped = stripManagedLines(before);
70
+ const next = stripped ? `${stripped}\n` : "";
71
+ if (!dryRun && next !== before) writeFileSync(path, next, { mode: 0o644 });
72
+ return {
73
+ path,
74
+ existed: true,
75
+ changed: next !== before,
76
+ managedLines: 0,
77
+ };
78
+ }
79
+
80
+ function print(payload, json = false) {
81
+ if (json) {
82
+ console.log(JSON.stringify(payload, null, 2));
83
+ return;
84
+ }
85
+ console.log(`${payload.action}: ${payload.files.length} files`);
86
+ for (const file of payload.files) {
87
+ console.log(`${file.changed ? "updated" : "unchanged"} ${file.path}`);
88
+ }
89
+ }
90
+
91
+ const args = parseArgs(process.argv.slice(2));
92
+ const action = args._[0] || "install";
93
+ const cwd = resolve(args.cwd || process.cwd());
94
+
95
+ try {
96
+ if (action === "install") {
97
+ print({ ok: true, action, cwd, dryRun: Boolean(args.dryRun), files: targetFiles(cwd).map((path) => installFile(path, args.dryRun)) }, Boolean(args.json));
98
+ } else if (action === "uninstall" || action === "remove") {
99
+ print({ ok: true, action: "uninstall", cwd, dryRun: Boolean(args.dryRun), files: targetFiles(cwd).map((path) => uninstallFile(path, args.dryRun)) }, Boolean(args.json));
100
+ } else if (action === "help" || action === "--help" || action === "-h") {
101
+ process.stdout.write(usage());
102
+ } else {
103
+ process.stderr.write(usage());
104
+ process.exitCode = 2;
105
+ }
106
+ } catch (error) {
107
+ console.error(error.message || String(error));
108
+ process.exitCode = 1;
109
+ }