@coldiq/mcp 0.3.12 → 0.3.13

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 (115) hide show
  1. package/dist/tools/enrich-company.d.ts +3 -9
  2. package/dist/tools/enrich-company.d.ts.map +1 -1
  3. package/dist/tools/enrich-company.js +3 -16
  4. package/dist/tools/enrich-company.js.map +1 -1
  5. package/dist/tools/enrich-person.d.ts +3 -9
  6. package/dist/tools/enrich-person.d.ts.map +1 -1
  7. package/dist/tools/enrich-person.js +3 -12
  8. package/dist/tools/enrich-person.js.map +1 -1
  9. package/dist/tools/fetch-page-content.d.ts +3 -9
  10. package/dist/tools/fetch-page-content.d.ts.map +1 -1
  11. package/dist/tools/fetch-page-content.js +3 -12
  12. package/dist/tools/fetch-page-content.js.map +1 -1
  13. package/dist/tools/find-email.d.ts +3 -9
  14. package/dist/tools/find-email.d.ts.map +1 -1
  15. package/dist/tools/find-email.js +3 -40
  16. package/dist/tools/find-email.js.map +1 -1
  17. package/dist/tools/find-emails.d.ts +3 -4
  18. package/dist/tools/find-emails.d.ts.map +1 -1
  19. package/dist/tools/find-emails.js +8 -423
  20. package/dist/tools/find-emails.js.map +1 -1
  21. package/dist/tools/find-influencers.d.ts +3 -9
  22. package/dist/tools/find-influencers.d.ts.map +1 -1
  23. package/dist/tools/find-influencers.js +3 -12
  24. package/dist/tools/find-influencers.js.map +1 -1
  25. package/dist/tools/find-people.d.ts +3 -8
  26. package/dist/tools/find-people.d.ts.map +1 -1
  27. package/dist/tools/find-people.js +3 -98
  28. package/dist/tools/find-people.js.map +1 -1
  29. package/dist/tools/find-phone.d.ts +3 -9
  30. package/dist/tools/find-phone.d.ts.map +1 -1
  31. package/dist/tools/find-phone.js +4 -41
  32. package/dist/tools/find-phone.js.map +1 -1
  33. package/dist/tools/find-signals.d.ts +3 -9
  34. package/dist/tools/find-signals.d.ts.map +1 -1
  35. package/dist/tools/find-signals.js +3 -83
  36. package/dist/tools/find-signals.js.map +1 -1
  37. package/dist/tools/get-place-reviews.d.ts +3 -9
  38. package/dist/tools/get-place-reviews.d.ts.map +1 -1
  39. package/dist/tools/get-place-reviews.js +3 -12
  40. package/dist/tools/get-place-reviews.js.map +1 -1
  41. package/dist/tools/search-ads.d.ts +3 -9
  42. package/dist/tools/search-ads.d.ts.map +1 -1
  43. package/dist/tools/search-ads.js +3 -12
  44. package/dist/tools/search-ads.js.map +1 -1
  45. package/dist/tools/search-companies.d.ts +3 -9
  46. package/dist/tools/search-companies.d.ts.map +1 -1
  47. package/dist/tools/search-companies.js +3 -24
  48. package/dist/tools/search-companies.js.map +1 -1
  49. package/dist/tools/search-jobs.d.ts +3 -9
  50. package/dist/tools/search-jobs.d.ts.map +1 -1
  51. package/dist/tools/search-jobs.js +3 -27
  52. package/dist/tools/search-jobs.js.map +1 -1
  53. package/dist/tools/search-places.d.ts +3 -15
  54. package/dist/tools/search-places.d.ts.map +1 -1
  55. package/dist/tools/search-places.js +5 -76
  56. package/dist/tools/search-places.js.map +1 -1
  57. package/dist/tools/search-reddit.d.ts +3 -9
  58. package/dist/tools/search-reddit.d.ts.map +1 -1
  59. package/dist/tools/search-reddit.js +3 -12
  60. package/dist/tools/search-reddit.js.map +1 -1
  61. package/dist/tools/search-seo.d.ts +3 -9
  62. package/dist/tools/search-seo.d.ts.map +1 -1
  63. package/dist/tools/search-seo.js +3 -12
  64. package/dist/tools/search-seo.js.map +1 -1
  65. package/dist/tools/search-web.d.ts +3 -9
  66. package/dist/tools/search-web.d.ts.map +1 -1
  67. package/dist/tools/search-web.js +3 -12
  68. package/dist/tools/search-web.js.map +1 -1
  69. package/dist/tools/verify-email.d.ts +3 -10
  70. package/dist/tools/verify-email.d.ts.map +1 -1
  71. package/dist/tools/verify-email.js +3 -68
  72. package/dist/tools/verify-email.js.map +1 -1
  73. package/dist/verb-client.d.ts +35 -0
  74. package/dist/verb-client.d.ts.map +1 -0
  75. package/dist/verb-client.js +57 -0
  76. package/dist/verb-client.js.map +1 -0
  77. package/package.json +1 -1
  78. package/src/tools/enrich-company.ts +3 -16
  79. package/src/tools/enrich-person.ts +3 -12
  80. package/src/tools/fetch-page-content.ts +3 -12
  81. package/src/tools/find-email.ts +3 -42
  82. package/src/tools/find-emails.ts +8 -506
  83. package/src/tools/find-influencers.ts +3 -12
  84. package/src/tools/find-people.ts +3 -107
  85. package/src/tools/find-phone.ts +4 -42
  86. package/src/tools/find-signals.ts +3 -88
  87. package/src/tools/get-place-reviews.ts +3 -12
  88. package/src/tools/search-ads.ts +3 -12
  89. package/src/tools/search-companies.ts +3 -23
  90. package/src/tools/search-jobs.ts +3 -30
  91. package/src/tools/search-places.ts +5 -72
  92. package/src/tools/search-reddit.ts +3 -12
  93. package/src/tools/search-seo.ts +3 -12
  94. package/src/tools/search-web.ts +3 -12
  95. package/src/tools/verify-email.ts +3 -76
  96. package/src/verb-client.ts +93 -0
  97. package/tests/verb-client.test.ts +152 -0
  98. package/tests/tools/enrich-company.test.ts +0 -92
  99. package/tests/tools/enrich-person.test.ts +0 -140
  100. package/tests/tools/fetch-page-content.test.ts +0 -108
  101. package/tests/tools/find-email.test.ts +0 -137
  102. package/tests/tools/find-emails.test.ts +0 -818
  103. package/tests/tools/find-influencers.test.ts +0 -91
  104. package/tests/tools/find-people.test.ts +0 -621
  105. package/tests/tools/find-phone.test.ts +0 -100
  106. package/tests/tools/find-signals.test.ts +0 -175
  107. package/tests/tools/get-place-reviews.test.ts +0 -73
  108. package/tests/tools/search-ads.test.ts +0 -182
  109. package/tests/tools/search-companies.test.ts +0 -192
  110. package/tests/tools/search-jobs.test.ts +0 -210
  111. package/tests/tools/search-places.test.ts +0 -210
  112. package/tests/tools/search-reddit.test.ts +0 -194
  113. package/tests/tools/search-seo.test.ts +0 -183
  114. package/tests/tools/search-web.test.ts +0 -79
  115. package/tests/tools/verify-email.test.ts +0 -142
@@ -0,0 +1,152 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+
3
+ // Mock the HTTP client so callVerb's envelope-building can be asserted without
4
+ // touching the network. The factory returns a vi.fn() we capture below.
5
+ const callApiMock = vi.fn()
6
+ vi.mock('../src/client.js', () => ({
7
+ callApi: (...args: unknown[]) => callApiMock(...args),
8
+ }))
9
+
10
+ import { callVerb } from '../src/verb-client.js'
11
+
12
+ beforeEach(() => {
13
+ callApiMock.mockReset()
14
+ callApiMock.mockResolvedValue({ ok: true, status: 200, data: { data: { ok: true }, _meta: {} }, headers: {} })
15
+ })
16
+
17
+ describe('callVerb', () => {
18
+ it('maps single input into envelope.input and forwards the verb path', async () => {
19
+ await callVerb('/email/find', { first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' })
20
+
21
+ expect(callApiMock).toHaveBeenCalledTimes(1)
22
+ const [method, path, envelope] = callApiMock.mock.calls[0]
23
+ expect(method).toBe('POST')
24
+ expect(path).toBe('/email/find')
25
+ expect(envelope).toEqual({
26
+ input: { first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' },
27
+ })
28
+ })
29
+
30
+ it('maps use_providers (non-empty array) to envelope.provider and keeps it out of input', async () => {
31
+ await callVerb('/email/find', {
32
+ first_name: 'Michel',
33
+ domain: 'coldiq.com',
34
+ use_providers: ['prospeo', 'fullenrich'],
35
+ })
36
+
37
+ const [, , envelope] = callApiMock.mock.calls[0]
38
+ expect(envelope.provider).toEqual(['prospeo', 'fullenrich'])
39
+ expect(envelope.input).toEqual({ first_name: 'Michel', domain: 'coldiq.com' })
40
+ expect(envelope.input).not.toHaveProperty('use_providers')
41
+ })
42
+
43
+ it('omits provider when use_providers is empty or absent', async () => {
44
+ await callVerb('/email/find', { domain: 'coldiq.com', use_providers: [] })
45
+ const [, , envelope] = callApiMock.mock.calls[0]
46
+ expect(envelope).not.toHaveProperty('provider')
47
+ })
48
+
49
+ it('passes fields and reveal through to the envelope', async () => {
50
+ await callVerb('/people/search', {
51
+ job_titles: ['CEO'],
52
+ fields: 'verbose',
53
+ reveal: true,
54
+ })
55
+
56
+ const [, , envelope] = callApiMock.mock.calls[0]
57
+ expect(envelope.fields).toBe('verbose')
58
+ expect(envelope.reveal).toBe(true)
59
+ expect(envelope.input).toEqual({ job_titles: ['CEO'] })
60
+ })
61
+
62
+ it('only sets fields/reveal when defined', async () => {
63
+ await callVerb('/people/search', { job_titles: ['CEO'] })
64
+ const [, , envelope] = callApiMock.mock.calls[0]
65
+ expect(envelope).not.toHaveProperty('fields')
66
+ expect(envelope).not.toHaveProperty('reveal')
67
+ })
68
+
69
+ it('bulk mode maps the named list field to inputs[]', async () => {
70
+ const people = [
71
+ { id: 'a', first_name: 'Michel', last_name: 'Lieben', domain: 'coldiq.com' },
72
+ { id: 'b', first_name: 'Ada', last_name: 'Lovelace', domain: 'microsoft.com' },
73
+ ]
74
+ await callVerb('/email/find', { people, use_providers: ['prospeo'] }, { bulkField: 'people' })
75
+
76
+ const [, , envelope] = callApiMock.mock.calls[0]
77
+ expect(envelope.inputs).toEqual(people)
78
+ expect(envelope).not.toHaveProperty('input')
79
+ expect(envelope.provider).toEqual(['prospeo'])
80
+ })
81
+
82
+ it('bulk mode merges sibling rest fields alongside inputs', async () => {
83
+ await callVerb(
84
+ '/email/find',
85
+ { people: [{ id: 'a' }], country: 'US' },
86
+ { bulkField: 'people' },
87
+ )
88
+ const [, , envelope] = callApiMock.mock.calls[0]
89
+ expect(envelope.inputs).toEqual([{ id: 'a' }])
90
+ expect(envelope.country).toBe('US')
91
+ })
92
+
93
+ it('forwards res.data and does not set isError on a 2xx response', async () => {
94
+ callApiMock.mockResolvedValue({ ok: true, status: 200, data: { data: { email: 'x@y.com' } }, headers: {} })
95
+ const out = await callVerb('/email/find', { domain: 'coldiq.com' })
96
+
97
+ expect(out.content[0].type).toBe('text')
98
+ expect(JSON.parse(out.content[0].text)).toEqual({ data: { email: 'x@y.com' } })
99
+ expect(out.isError).toBe(false)
100
+ })
101
+
102
+ it('sets isError when the response is not ok', async () => {
103
+ callApiMock.mockResolvedValue({
104
+ ok: false,
105
+ status: 404,
106
+ data: { error: 'No usable result', providers_tried: ['prospeo'] },
107
+ headers: {},
108
+ })
109
+ const out = await callVerb('/email/find', { domain: 'coldiq.com' })
110
+
111
+ expect(out.isError).toBe(true)
112
+ expect(JSON.parse(out.content[0].text)).toEqual({ error: 'No usable result', providers_tried: ['prospeo'] })
113
+ })
114
+
115
+ // --- schema-drift mappings (find_phone / search_places / find_emails) ---
116
+
117
+ it('remap renames input keys before building the envelope (company_domain → domain)', async () => {
118
+ await callVerb('/phone/find', { first_name: 'Michel', company_domain: 'coldiq.com', company_name: 'ColdIQ' }, { remap: { company_domain: 'domain' } })
119
+ const [, , envelope] = callApiMock.mock.calls[0]
120
+ expect(envelope.input).toEqual({ first_name: 'Michel', domain: 'coldiq.com', company_name: 'ColdIQ' })
121
+ expect(envelope.input).not.toHaveProperty('company_domain')
122
+ })
123
+
124
+ it('providerFromField routes a tool-specific provider field into the provider pin', async () => {
125
+ await callVerb('/places/search', { query: 'coffee', provider: 'openmart' }, { providerFromField: 'provider' })
126
+ const [, , envelope] = callApiMock.mock.calls[0]
127
+ expect(envelope.provider).toEqual(['openmart'])
128
+ expect(envelope.input).toEqual({ query: 'coffee' })
129
+ expect(envelope.input).not.toHaveProperty('provider')
130
+ })
131
+
132
+ it('providerFromField defers to an explicit use_providers when both are present', async () => {
133
+ await callVerb('/places/search', { query: 'coffee', provider: 'openmart', use_providers: ['google_maps'] }, { providerFromField: 'provider' })
134
+ const [, , envelope] = callApiMock.mock.calls[0]
135
+ expect(envelope.provider).toEqual(['google_maps'])
136
+ expect(envelope.input).toEqual({ query: 'coffee' })
137
+ })
138
+
139
+ it('reattachIdField re-attaches each input id onto bulk results by index', async () => {
140
+ callApiMock.mockResolvedValue({
141
+ ok: true,
142
+ status: 200,
143
+ data: { results: [{ data: { email: 'm@coldiq.com' } }, { error: 'not found' }] },
144
+ headers: {},
145
+ })
146
+ const people = [{ id: 'michel', first_name: 'Michel', domain: 'coldiq.com' }, { id: 'ada', first_name: 'Ada', domain: 'microsoft.com' }]
147
+ const out = await callVerb('/email/find', { people }, { bulkField: 'people', reattachIdField: 'id' })
148
+ const results = JSON.parse(out.content[0].text).results
149
+ expect(results[0]).toEqual({ id: 'michel', data: { email: 'm@coldiq.com' } })
150
+ expect(results[1]).toEqual({ id: 'ada', error: 'not found' })
151
+ })
152
+ })
@@ -1,92 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { initClient } from '../../src/client.js'
3
- import { enrichCompanyHandler } from '../../src/tools/enrich-company.js'
4
-
5
- describe('enrich_company handler', () => {
6
- const originalFetch = globalThis.fetch
7
-
8
- beforeEach(() => {
9
- initClient('http://test-api.local', 'test-key')
10
- })
11
-
12
- afterEach(() => {
13
- globalThis.fetch = originalFetch
14
- })
15
-
16
- it('returns error when no identifier provided', async () => {
17
- const result = await enrichCompanyHandler({})
18
- expect(result.isError).toBe(true)
19
- const parsed = JSON.parse(result.content[0].text)
20
- expect(parsed.error).toMatch(/domain.*linkedin_url.*name/i)
21
- })
22
-
23
- it('returns enriched company from CompanyEnrich (GET)', async () => {
24
- let capturedUrl = ''
25
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
26
- capturedUrl = url.toString()
27
- return new Response(JSON.stringify({ name: 'Stripe', domain: 'stripe.com', employees: 8000 }), { status: 200 })
28
- }) as typeof fetch
29
-
30
- const result = await enrichCompanyHandler({ domain: 'stripe.com' })
31
-
32
- expect(capturedUrl).toContain('domain=stripe.com')
33
- expect(capturedUrl).toContain('/companyenrich/companies/enrich')
34
-
35
- const parsed = JSON.parse(result.content[0].text)
36
- expect(parsed._meta.provider).toBe('companyenrich')
37
- expect(parsed.data.name).toBe('Stripe')
38
- })
39
-
40
- it('falls back to Apollo on CompanyEnrich failure', async () => {
41
- let callCount = 0
42
- globalThis.fetch = vi.fn(async () => {
43
- callCount++
44
- if (callCount === 1) {
45
- return new Response(JSON.stringify({ error: 'Provider error' }), { status: 502 })
46
- }
47
- return new Response(JSON.stringify({ organization: { name: 'Stripe' } }), { status: 200 })
48
- }) as typeof fetch
49
-
50
- const result = await enrichCompanyHandler({ domain: 'stripe.com' })
51
-
52
- const parsed = JSON.parse(result.content[0].text)
53
- expect(parsed._meta.provider).toBe('apollo')
54
- })
55
-
56
- it('skips CompanyEnrich GET and Apollo when only linkedin_url is provided', async () => {
57
- const calledEndpoints: string[] = []
58
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
59
- const endpoint = url.toString()
60
- calledEndpoints.push(endpoint)
61
- // PDL (3rd provider) succeeds
62
- if (endpoint.includes('/pdl/company/enrich')) {
63
- return new Response(JSON.stringify({ name: 'Stripe', website: 'stripe.com' }), { status: 200 })
64
- }
65
- return new Response(JSON.stringify({ error: 'not found' }), { status: 404 })
66
- }) as typeof fetch
67
-
68
- const result = await enrichCompanyHandler({ linkedin_url: 'https://www.linkedin.com/company/stripe' })
69
-
70
- expect(calledEndpoints.every((u) => !u.includes('/companyenrich/companies/enrich?'))).toBe(true)
71
- expect(calledEndpoints.every((u) => !u.includes('/apollo/'))).toBe(true)
72
- const parsed = JSON.parse(result.content[0].text)
73
- expect(parsed._meta.provider).toBe('pdl')
74
- })
75
-
76
- it('skips all domain-only providers when only name is provided', async () => {
77
- const calledEndpoints: string[] = []
78
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
79
- const endpoint = url.toString()
80
- calledEndpoints.push(endpoint)
81
- if (endpoint.includes('/pdl/company/enrich')) {
82
- return new Response(JSON.stringify({ name: 'Stripe', website: 'stripe.com' }), { status: 200 })
83
- }
84
- return new Response(JSON.stringify({ error: 'not found' }), { status: 404 })
85
- }) as typeof fetch
86
-
87
- await enrichCompanyHandler({ name: 'Stripe' })
88
-
89
- expect(calledEndpoints.every((u) => !u.includes('/companyenrich/companies/enrich?'))).toBe(true)
90
- expect(calledEndpoints.every((u) => !u.includes('/apollo/'))).toBe(true)
91
- })
92
- })
@@ -1,140 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { initClient } from '../../src/client.js'
3
- import { enrichPersonHandler } from '../../src/tools/enrich-person.js'
4
-
5
- describe('enrich_person handler', () => {
6
- const originalFetch = globalThis.fetch
7
-
8
- beforeEach(() => {
9
- initClient('http://test-api.local', 'test-key')
10
- })
11
-
12
- afterEach(() => {
13
- globalThis.fetch = originalFetch
14
- })
15
-
16
- it('email input — linkupapi-email-reverse wins on first try', async () => {
17
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
18
- const u = url.toString()
19
- if (u.includes('/linkupapi/data/profil/enrich')) {
20
- // profile-enrich not applicable for email-only, should not be called
21
- throw new Error('profile-enrich should not fire for email-only input')
22
- }
23
- if (u.includes('/linkupapi/data/mail/reverse')) {
24
- return new Response(JSON.stringify({ status: 'success', data: { linkedin_url: 'https://linkedin.com/in/michel-lieben', name: 'Michel Lieben' } }), { status: 200 })
25
- }
26
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
27
- }) as typeof fetch
28
-
29
- const result = await enrichPersonHandler({ email: 'michel@coldiq.com' })
30
-
31
- expect(result.isError).toBeFalsy()
32
- const parsed = JSON.parse(result.content[0].text)
33
- expect(parsed._meta.provider).toBe('linkupapi-email-reverse')
34
- // linkupapi wraps: { status, data: { ... } }
35
- expect(parsed.data.data.linkedin_url).toContain('linkedin.com')
36
- })
37
-
38
- it('email input — falls back to pdl-person-enrich when linkupapi-email-reverse returns empty', async () => {
39
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
40
- const u = url.toString()
41
- if (u.includes('/linkupapi/data/mail/reverse')) {
42
- // hasResult fails: status not 'success'
43
- return new Response(JSON.stringify({ status: 'not_found', data: null }), { status: 200 })
44
- }
45
- if (u.includes('/pdl/person/enrich')) {
46
- return new Response(JSON.stringify({ status: 200, data: { full_name: 'Michel Lieben', linkedin_url: 'https://linkedin.com/in/michel-lieben' } }), { status: 200 })
47
- }
48
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
49
- }) as typeof fetch
50
-
51
- const result = await enrichPersonHandler({ email: 'michel@coldiq.com' })
52
-
53
- expect(result.isError).toBeFalsy()
54
- const parsed = JSON.parse(result.content[0].text)
55
- expect(parsed._meta.provider).toBe('pdl-person-enrich')
56
- // PDL wraps: { status: 200, data: { ... } }
57
- expect(parsed.data.data.full_name).toBe('Michel Lieben')
58
- })
59
-
60
- it('name+company input — linkupapi-profile-enrich wins (highest priority for this input type)', async () => {
61
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
62
- const u = url.toString()
63
- if (u.includes('/linkupapi/data/profil/enrich')) {
64
- return new Response(JSON.stringify({ status: 'success', data: { linkedin_url: 'https://linkedin.com/in/michel-lieben', headline: 'CEO at ColdIQ' } }), { status: 200 })
65
- }
66
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
67
- }) as typeof fetch
68
-
69
- const result = await enrichPersonHandler({ first_name: 'Michel', last_name: 'Lieben', company_name: 'ColdIQ' })
70
-
71
- expect(result.isError).toBeFalsy()
72
- const parsed = JSON.parse(result.content[0].text)
73
- expect(parsed._meta.provider).toBe('linkupapi-profile-enrich')
74
- // linkupapi wraps: { status, data: { ... } }
75
- expect(parsed.data.data.headline).toBe('CEO at ColdIQ')
76
- })
77
-
78
- it('linkedin_url input — findymail-business-profile wins after non-applicable providers are skipped', async () => {
79
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
80
- const u = url.toString()
81
- // linkupapi-profile-enrich not applicable (no first_name)
82
- // linkupapi-email-reverse not applicable (no email)
83
- // pdl-person-enrich fires but returns no match
84
- if (u.includes('/pdl/person/enrich')) {
85
- return new Response(JSON.stringify({ status: 404, data: null }), { status: 200 })
86
- }
87
- // apollo-people-match fires but returns no person
88
- if (u.includes('/apollo/people/match')) {
89
- return new Response(JSON.stringify({ person: null }), { status: 200 })
90
- }
91
- // blitzapi-reverse-email not applicable (no email)
92
- // findymail-business-profile: applicable (linkedin_url present)
93
- if (u.includes('/findymail/search/business-profile')) {
94
- return new Response(JSON.stringify({ name: 'Michel Lieben', email: 'michel@coldiq.com', linkedin_url: 'https://linkedin.com/in/michel-lieben' }), { status: 200 })
95
- }
96
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
97
- }) as typeof fetch
98
-
99
- const result = await enrichPersonHandler({ linkedin_url: 'https://www.linkedin.com/in/michel-lieben' })
100
-
101
- expect(result.isError).toBeFalsy()
102
- const parsed = JSON.parse(result.content[0].text)
103
- expect(parsed._meta.provider).toBe('findymail-business-profile')
104
- expect(parsed.data.name).toBe('Michel Lieben')
105
- })
106
-
107
- it('phone input — ai-ark-reverse-lookup wins (gated on phone present)', async () => {
108
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
109
- const u = url.toString()
110
- if (u.includes('/pdl/person/enrich')) {
111
- return new Response(JSON.stringify({ status: 404, data: null }), { status: 200 })
112
- }
113
- if (u.includes('/apollo/people/match')) {
114
- return new Response(JSON.stringify({ person: null }), { status: 200 })
115
- }
116
- if (u.includes('/ai-ark/people/reverse-lookup')) {
117
- return new Response(JSON.stringify({ name: 'Michel Lieben', email: 'michel@coldiq.com' }), { status: 200 })
118
- }
119
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
120
- }) as typeof fetch
121
-
122
- const result = await enrichPersonHandler({ phone: '+32123456789' })
123
-
124
- expect(result.isError).toBeFalsy()
125
- const parsed = JSON.parse(result.content[0].text)
126
- expect(parsed._meta.provider).toBe('ai-ark-reverse-lookup')
127
- })
128
-
129
- it('all providers fail — returns isError with error object', async () => {
130
- globalThis.fetch = vi.fn(async () => {
131
- return new Response(JSON.stringify({ error: 'Provider unavailable' }), { status: 503 })
132
- }) as typeof fetch
133
-
134
- const result = await enrichPersonHandler({ email: 'michel@coldiq.com' })
135
-
136
- expect(result.isError).toBe(true)
137
- const parsed = JSON.parse(result.content[0].text)
138
- expect(parsed.error).toBeTruthy()
139
- })
140
- })
@@ -1,108 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { initClient } from '../../src/client.js'
3
- import { fetchPageContentHandler } from '../../src/tools/fetch-page-content.js'
4
-
5
- describe('fetch_page_content handler', () => {
6
- const originalFetch = globalThis.fetch
7
-
8
- beforeEach(() => {
9
- initClient('http://test-api.local', 'test-key')
10
- })
11
-
12
- afterEach(() => {
13
- globalThis.fetch = originalFetch
14
- })
15
-
16
- it('single URL — exa-contents returns results', async () => {
17
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
18
- const u = url.toString()
19
- if (u.includes('/exa/contents')) {
20
- return new Response(JSON.stringify({
21
- results: [{ url: 'https://coldiq.com', text: 'ColdIQ page text' }],
22
- }), { status: 200 })
23
- }
24
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
25
- }) as typeof fetch
26
-
27
- const result = await fetchPageContentHandler({ urls: ['https://coldiq.com'] })
28
-
29
- expect(result.isError).toBeFalsy()
30
- const parsed = JSON.parse(result.content[0].text)
31
- expect(parsed._meta.provider).toBe('exa-contents')
32
- expect(parsed.data.results).toHaveLength(1)
33
- expect(parsed.data.results[0].text).toBe('ColdIQ page text')
34
- })
35
-
36
- it('batch URLs — all returned in one call', async () => {
37
- globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
38
- const u = url.toString()
39
- if (u.includes('/exa/contents')) {
40
- const body = JSON.parse((opts?.body as string) ?? '{}')
41
- return new Response(JSON.stringify({
42
- results: body.urls.map((pageUrl: string) => ({ url: pageUrl, text: `text for ${pageUrl}` })),
43
- }), { status: 200 })
44
- }
45
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
46
- }) as typeof fetch
47
-
48
- const urls = ['https://coldiq.com', 'https://hubspot.com', 'https://salesforce.com']
49
- const result = await fetchPageContentHandler({ urls })
50
-
51
- expect(result.isError).toBeFalsy()
52
- const parsed = JSON.parse(result.content[0].text)
53
- expect(parsed._meta.provider).toBe('exa-contents')
54
- expect(parsed.data.results).toHaveLength(3)
55
- })
56
-
57
- it('include_summary=true — summary flag forwarded to exa', async () => {
58
- let capturedBody: Record<string, unknown> | null = null
59
-
60
- globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
61
- const u = url.toString()
62
- if (u.includes('/exa/contents')) {
63
- capturedBody = JSON.parse((opts?.body as string) ?? '{}')
64
- return new Response(JSON.stringify({
65
- results: [{ url: 'https://coldiq.com', text: 'text', summary: 'summary text' }],
66
- }), { status: 200 })
67
- }
68
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
69
- }) as typeof fetch
70
-
71
- await fetchPageContentHandler({ urls: ['https://coldiq.com'], include_summary: true })
72
-
73
- expect(capturedBody).not.toBeNull()
74
- expect(capturedBody!.summary).toEqual({})
75
- })
76
-
77
- it('include_summary=false (default) — no summary field in request', async () => {
78
- let capturedBody: Record<string, unknown> | null = null
79
-
80
- globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
81
- const u = url.toString()
82
- if (u.includes('/exa/contents')) {
83
- capturedBody = JSON.parse((opts?.body as string) ?? '{}')
84
- return new Response(JSON.stringify({
85
- results: [{ url: 'https://coldiq.com', text: 'text' }],
86
- }), { status: 200 })
87
- }
88
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
89
- }) as typeof fetch
90
-
91
- await fetchPageContentHandler({ urls: ['https://coldiq.com'] })
92
-
93
- expect(capturedBody).not.toBeNull()
94
- expect(capturedBody!.summary).toBeUndefined()
95
- })
96
-
97
- it('provider fails — returns isError', async () => {
98
- globalThis.fetch = vi.fn(async () => {
99
- return new Response(JSON.stringify({ error: 'upstream error' }), { status: 502 })
100
- }) as typeof fetch
101
-
102
- const result = await fetchPageContentHandler({ urls: ['https://coldiq.com'] })
103
-
104
- expect(result.isError).toBe(true)
105
- const parsed = JSON.parse(result.content[0].text)
106
- expect(parsed.error).toBeTruthy()
107
- })
108
- })
@@ -1,137 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { initClient } from '../../src/client.js'
3
- import { findEmailHandler } from '../../src/tools/find-email.js'
4
-
5
- describe('find_email handler (waterfall)', () => {
6
- const originalFetch = globalThis.fetch
7
-
8
- beforeEach(() => {
9
- initClient('http://test-api.local', 'test-key')
10
- })
11
-
12
- afterEach(() => {
13
- globalThis.fetch = originalFetch
14
- })
15
-
16
- it('returns email from first provider that finds one', async () => {
17
- globalThis.fetch = vi.fn(async () =>
18
- new Response(JSON.stringify({ email: 'michel@coldiq.com' }), { status: 200 })
19
- ) as typeof fetch
20
-
21
- const result = await findEmailHandler({
22
- first_name: 'Michel',
23
- last_name: 'Lieben',
24
- domain: 'coldiq.com',
25
- })
26
-
27
- const parsed = JSON.parse(result.content[0].text)
28
- expect(parsed._meta.provider).toBe('findymail')
29
- expect(parsed.data.email).toBe('michel@coldiq.com')
30
- })
31
-
32
- it('falls through to IcyPeas when FindyMail has no result', async () => {
33
- let callCount = 0
34
- globalThis.fetch = vi.fn(async () => {
35
- callCount++
36
- if (callCount === 1) {
37
- // FindyMail — no email found
38
- return new Response(JSON.stringify({ email: null }), { status: 200 })
39
- }
40
- // IcyPeas — found
41
- return new Response(JSON.stringify({ email: 'michel@coldiq.com' }), { status: 200 })
42
- }) as typeof fetch
43
-
44
- const result = await findEmailHandler({
45
- first_name: 'Michel',
46
- last_name: 'Lieben',
47
- domain: 'coldiq.com',
48
- })
49
-
50
- const parsed = JSON.parse(result.content[0].text)
51
- expect(parsed._meta.provider).toBe('icypeas')
52
- })
53
-
54
- it('falls through to Prospeo when FindyMail and IcyPeas both fail', async () => {
55
- let callCount = 0
56
- globalThis.fetch = vi.fn(async () => {
57
- callCount++
58
- if (callCount <= 2) {
59
- return new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
60
- }
61
- // Prospeo success response — email is nested under person.email.email
62
- return new Response(
63
- JSON.stringify({ error: false, person: { email: { email: 'michel@coldiq.com', status: 'VERIFIED' } } }),
64
- { status: 200 },
65
- )
66
- }) as typeof fetch
67
-
68
- const result = await findEmailHandler({
69
- first_name: 'Michel',
70
- last_name: 'Lieben',
71
- domain: 'coldiq.com',
72
- })
73
-
74
- const parsed = JSON.parse(result.content[0].text)
75
- expect(parsed._meta.provider).toBe('prospeo')
76
- })
77
-
78
- it('returns error when all providers fail', async () => {
79
- globalThis.fetch = vi.fn(async () =>
80
- new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
81
- ) as typeof fetch
82
-
83
- const result = await findEmailHandler({
84
- first_name: 'Unknown',
85
- last_name: 'Person',
86
- domain: 'unknown.xyz',
87
- })
88
-
89
- expect(result.isError).toBe(true)
90
- const parsed = JSON.parse(result.content[0].text)
91
- // No linkedin_url → blitzapi and limadata-work-email-linkedin are skipped via isApplicable
92
- expect(parsed.providers_tried.length).toBe(6) // findymail, icypeas, limadata-work-email, prospeo, fullenrich, linkupapi
93
- })
94
-
95
- describe('use_providers', () => {
96
- it('Scenario A — pinned provider succeeds', async () => {
97
- globalThis.fetch = vi.fn(async () =>
98
- new Response(JSON.stringify({ email: 'michel@coldiq.com' }), { status: 200 })
99
- ) as typeof fetch
100
-
101
- const result = await findEmailHandler({
102
- first_name: 'Michel',
103
- domain: 'coldiq.com',
104
- use_providers: ['findymail'],
105
- })
106
-
107
- expect(result.isError).toBeUndefined()
108
- const parsed = JSON.parse(result.content[0].text)
109
- expect(parsed._meta.provider).toBe('findymail')
110
- })
111
-
112
- it('Scenario C — gated provider without required input returns helpful error', async () => {
113
- const result = await findEmailHandler({
114
- first_name: 'Michel',
115
- domain: 'coldiq.com',
116
- use_providers: ['blitzapi'],
117
- })
118
-
119
- expect(result.isError).toBe(true)
120
- const parsed = JSON.parse(result.content[0].text)
121
- expect(parsed.error).toContain("'blitzapi' requires")
122
- expect(parsed.error).toContain('ColdIQ will automatically pick the best tool')
123
- })
124
-
125
- it('Scenario E — unrecognized provider returns isError', async () => {
126
- const result = await findEmailHandler({
127
- first_name: 'Michel',
128
- domain: 'coldiq.com',
129
- use_providers: ['apollo'],
130
- })
131
-
132
- expect(result.isError).toBe(true)
133
- const parsed = JSON.parse(result.content[0].text)
134
- expect(parsed.error).toContain("'apollo' is not a recognized provider")
135
- })
136
- })
137
- })