@coldiq/mcp 0.3.11 → 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 (135) hide show
  1. package/dist/client.d.ts.map +1 -1
  2. package/dist/client.js +4 -0
  3. package/dist/client.js.map +1 -1
  4. package/dist/index.js +2 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/session-usage.d.ts +59 -0
  7. package/dist/session-usage.d.ts.map +1 -0
  8. package/dist/session-usage.js +96 -0
  9. package/dist/session-usage.js.map +1 -0
  10. package/dist/tools/enrich-company.d.ts +3 -9
  11. package/dist/tools/enrich-company.d.ts.map +1 -1
  12. package/dist/tools/enrich-company.js +3 -16
  13. package/dist/tools/enrich-company.js.map +1 -1
  14. package/dist/tools/enrich-person.d.ts +3 -9
  15. package/dist/tools/enrich-person.d.ts.map +1 -1
  16. package/dist/tools/enrich-person.js +3 -12
  17. package/dist/tools/enrich-person.js.map +1 -1
  18. package/dist/tools/fetch-page-content.d.ts +3 -9
  19. package/dist/tools/fetch-page-content.d.ts.map +1 -1
  20. package/dist/tools/fetch-page-content.js +3 -12
  21. package/dist/tools/fetch-page-content.js.map +1 -1
  22. package/dist/tools/find-email.d.ts +3 -9
  23. package/dist/tools/find-email.d.ts.map +1 -1
  24. package/dist/tools/find-email.js +3 -40
  25. package/dist/tools/find-email.js.map +1 -1
  26. package/dist/tools/find-emails.d.ts +3 -4
  27. package/dist/tools/find-emails.d.ts.map +1 -1
  28. package/dist/tools/find-emails.js +8 -423
  29. package/dist/tools/find-emails.js.map +1 -1
  30. package/dist/tools/find-influencers.d.ts +3 -9
  31. package/dist/tools/find-influencers.d.ts.map +1 -1
  32. package/dist/tools/find-influencers.js +3 -12
  33. package/dist/tools/find-influencers.js.map +1 -1
  34. package/dist/tools/find-people.d.ts +3 -8
  35. package/dist/tools/find-people.d.ts.map +1 -1
  36. package/dist/tools/find-people.js +3 -98
  37. package/dist/tools/find-people.js.map +1 -1
  38. package/dist/tools/find-phone.d.ts +3 -9
  39. package/dist/tools/find-phone.d.ts.map +1 -1
  40. package/dist/tools/find-phone.js +4 -41
  41. package/dist/tools/find-phone.js.map +1 -1
  42. package/dist/tools/find-signals.d.ts +3 -9
  43. package/dist/tools/find-signals.d.ts.map +1 -1
  44. package/dist/tools/find-signals.js +3 -83
  45. package/dist/tools/find-signals.js.map +1 -1
  46. package/dist/tools/get-place-reviews.d.ts +3 -9
  47. package/dist/tools/get-place-reviews.d.ts.map +1 -1
  48. package/dist/tools/get-place-reviews.js +3 -12
  49. package/dist/tools/get-place-reviews.js.map +1 -1
  50. package/dist/tools/get-session-usage.d.ts +10 -0
  51. package/dist/tools/get-session-usage.d.ts.map +1 -0
  52. package/dist/tools/get-session-usage.js +14 -0
  53. package/dist/tools/get-session-usage.js.map +1 -0
  54. package/dist/tools/search-ads.d.ts +3 -9
  55. package/dist/tools/search-ads.d.ts.map +1 -1
  56. package/dist/tools/search-ads.js +3 -12
  57. package/dist/tools/search-ads.js.map +1 -1
  58. package/dist/tools/search-companies.d.ts +3 -9
  59. package/dist/tools/search-companies.d.ts.map +1 -1
  60. package/dist/tools/search-companies.js +3 -24
  61. package/dist/tools/search-companies.js.map +1 -1
  62. package/dist/tools/search-jobs.d.ts +3 -9
  63. package/dist/tools/search-jobs.d.ts.map +1 -1
  64. package/dist/tools/search-jobs.js +3 -27
  65. package/dist/tools/search-jobs.js.map +1 -1
  66. package/dist/tools/search-places.d.ts +3 -15
  67. package/dist/tools/search-places.d.ts.map +1 -1
  68. package/dist/tools/search-places.js +5 -76
  69. package/dist/tools/search-places.js.map +1 -1
  70. package/dist/tools/search-reddit.d.ts +3 -9
  71. package/dist/tools/search-reddit.d.ts.map +1 -1
  72. package/dist/tools/search-reddit.js +3 -12
  73. package/dist/tools/search-reddit.js.map +1 -1
  74. package/dist/tools/search-seo.d.ts +3 -9
  75. package/dist/tools/search-seo.d.ts.map +1 -1
  76. package/dist/tools/search-seo.js +3 -12
  77. package/dist/tools/search-seo.js.map +1 -1
  78. package/dist/tools/search-web.d.ts +3 -9
  79. package/dist/tools/search-web.d.ts.map +1 -1
  80. package/dist/tools/search-web.js +3 -12
  81. package/dist/tools/search-web.js.map +1 -1
  82. package/dist/tools/verify-email.d.ts +3 -10
  83. package/dist/tools/verify-email.d.ts.map +1 -1
  84. package/dist/tools/verify-email.js +3 -68
  85. package/dist/tools/verify-email.js.map +1 -1
  86. package/dist/verb-client.d.ts +35 -0
  87. package/dist/verb-client.d.ts.map +1 -0
  88. package/dist/verb-client.js +57 -0
  89. package/dist/verb-client.js.map +1 -0
  90. package/package.json +1 -1
  91. package/src/client.ts +4 -0
  92. package/src/index.ts +8 -0
  93. package/src/session-usage.ts +130 -0
  94. package/src/tools/enrich-company.ts +3 -16
  95. package/src/tools/enrich-person.ts +3 -12
  96. package/src/tools/fetch-page-content.ts +3 -12
  97. package/src/tools/find-email.ts +3 -42
  98. package/src/tools/find-emails.ts +8 -506
  99. package/src/tools/find-influencers.ts +3 -12
  100. package/src/tools/find-people.ts +3 -107
  101. package/src/tools/find-phone.ts +4 -42
  102. package/src/tools/find-signals.ts +3 -88
  103. package/src/tools/get-place-reviews.ts +3 -12
  104. package/src/tools/get-session-usage.ts +18 -0
  105. package/src/tools/search-ads.ts +3 -12
  106. package/src/tools/search-companies.ts +3 -23
  107. package/src/tools/search-jobs.ts +3 -30
  108. package/src/tools/search-places.ts +5 -72
  109. package/src/tools/search-reddit.ts +3 -12
  110. package/src/tools/search-seo.ts +3 -12
  111. package/src/tools/search-web.ts +3 -12
  112. package/src/tools/verify-email.ts +3 -76
  113. package/src/verb-client.ts +93 -0
  114. package/tests/executor.test.ts +69 -0
  115. package/tests/session-usage.test.ts +130 -0
  116. package/tests/tools/get-session-usage.test.ts +44 -0
  117. package/tests/verb-client.test.ts +152 -0
  118. package/tests/tools/enrich-company.test.ts +0 -92
  119. package/tests/tools/enrich-person.test.ts +0 -140
  120. package/tests/tools/fetch-page-content.test.ts +0 -108
  121. package/tests/tools/find-email.test.ts +0 -137
  122. package/tests/tools/find-emails.test.ts +0 -818
  123. package/tests/tools/find-influencers.test.ts +0 -91
  124. package/tests/tools/find-people.test.ts +0 -621
  125. package/tests/tools/find-phone.test.ts +0 -100
  126. package/tests/tools/find-signals.test.ts +0 -175
  127. package/tests/tools/get-place-reviews.test.ts +0 -73
  128. package/tests/tools/search-ads.test.ts +0 -182
  129. package/tests/tools/search-companies.test.ts +0 -192
  130. package/tests/tools/search-jobs.test.ts +0 -210
  131. package/tests/tools/search-places.test.ts +0 -210
  132. package/tests/tools/search-reddit.test.ts +0 -194
  133. package/tests/tools/search-seo.test.ts +0 -183
  134. package/tests/tools/search-web.test.ts +0 -79
  135. package/tests/tools/verify-email.test.ts +0 -142
@@ -1,194 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { initClient } from '../../src/client.js'
3
- import { searchRedditHandler } from '../../src/tools/search-reddit.js'
4
-
5
- describe('search_reddit 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('query search — reddit provider creates async job and polls to completion', async () => {
17
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
18
- const u = url.toString()
19
- if (u.includes('/reddit/scrape') && !u.includes('/scrape/')) {
20
- return new Response(JSON.stringify({ jobId: 'r-1' }), { status: 200 })
21
- }
22
- if (u.includes('/reddit/scrape/r-1')) {
23
- return new Response(JSON.stringify({
24
- jobId: 'r-1',
25
- status: 'done',
26
- items: [{ title: 'Best CRM for startups', subreddit: 'r/sales', score: 342 }],
27
- }), { status: 200 })
28
- }
29
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
30
- }) as typeof fetch
31
-
32
- const { getProviders } = await import('../../src/registry.js')
33
- const providers = getProviders('search_reddit')
34
- const reddit = providers.find((p) => p.id === 'reddit')!
35
- const orig = { t: reddit.async!.timeoutMs, i: reddit.async!.pollIntervalMs }
36
-
37
- try {
38
- reddit.async!.timeoutMs = 500
39
- reddit.async!.pollIntervalMs = 20
40
- const result = await searchRedditHandler({ start_urls: ['https://www.reddit.com/r/sales/'], query: 'best CRM for startups', limit: 5 })
41
-
42
- expect(result.isError).toBeFalsy()
43
- const parsed = JSON.parse(result.content[0].text)
44
- expect(parsed._meta.provider).toBe('reddit')
45
- expect(parsed.data.items).toHaveLength(1)
46
- } finally {
47
- reddit.async!.timeoutMs = orig.t
48
- reddit.async!.pollIntervalMs = orig.i
49
- }
50
- })
51
-
52
- it('start_urls scrape — query is omitted from request body', async () => {
53
- let capturedBody: Record<string, unknown> | null = null
54
-
55
- globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
56
- const u = url.toString()
57
- if (u.includes('/reddit/scrape') && !u.includes('/scrape/')) {
58
- capturedBody = JSON.parse((opts?.body as string) ?? '{}')
59
- return new Response(JSON.stringify({ jobId: 'r-2' }), { status: 200 })
60
- }
61
- if (u.includes('/reddit/scrape/r-2')) {
62
- return new Response(JSON.stringify({
63
- jobId: 'r-2',
64
- status: 'done',
65
- items: [{ title: 'Reddit post', subreddit: 'r/entrepreneur' }],
66
- }), { status: 200 })
67
- }
68
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
69
- }) as typeof fetch
70
-
71
- const { getProviders } = await import('../../src/registry.js')
72
- const providers = getProviders('search_reddit')
73
- const reddit = providers.find((p) => p.id === 'reddit')!
74
- const orig = { t: reddit.async!.timeoutMs, i: reddit.async!.pollIntervalMs }
75
-
76
- try {
77
- reddit.async!.timeoutMs = 500
78
- reddit.async!.pollIntervalMs = 20
79
- await searchRedditHandler({
80
- start_urls: ['https://www.reddit.com/r/entrepreneur/'],
81
- limit: 5,
82
- })
83
-
84
- expect(capturedBody).not.toBeNull()
85
- // searchQueries is undefined when no query field provided
86
- expect(capturedBody!.searchQueries).toBeUndefined()
87
- expect(capturedBody!.startUrls).toEqual([{ url: 'https://www.reddit.com/r/entrepreneur/' }])
88
- } finally {
89
- reddit.async!.timeoutMs = orig.t
90
- reddit.async!.pollIntervalMs = orig.i
91
- }
92
- })
93
-
94
- it('async timeout — returns isError', async () => {
95
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
96
- const u = url.toString()
97
- if (u.includes('/reddit/scrape') && !u.includes('/scrape/')) {
98
- return new Response(JSON.stringify({ jobId: 'r-3' }), { status: 200 })
99
- }
100
- // Always running — never completes
101
- if (u.includes('/reddit/scrape/r-3')) {
102
- return new Response(JSON.stringify({ jobId: 'r-3', status: 'running' }), { status: 200 })
103
- }
104
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
105
- }) as typeof fetch
106
-
107
- const { getProviders } = await import('../../src/registry.js')
108
- const providers = getProviders('search_reddit')
109
- const reddit = providers.find((p) => p.id === 'reddit')!
110
- const orig = { t: reddit.async!.timeoutMs, i: reddit.async!.pollIntervalMs }
111
-
112
- try {
113
- reddit.async!.timeoutMs = 100
114
- reddit.async!.pollIntervalMs = 20
115
- const result = await searchRedditHandler({ start_urls: ['https://www.reddit.com/r/sales/'], query: 'cold email', limit: 5 })
116
-
117
- expect(result.isError).toBe(true)
118
- const parsed = JSON.parse(result.content[0].text)
119
- expect(parsed.error).toBeTruthy()
120
- } finally {
121
- reddit.async!.timeoutMs = orig.t
122
- reddit.async!.pollIntervalMs = orig.i
123
- }
124
- })
125
-
126
- it('rewrites a bare subreddit URL + query into an in-subreddit search URL', async () => {
127
- let capturedBody: Record<string, unknown> | null = null
128
- globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
129
- const u = url.toString()
130
- if (u.includes('/reddit/scrape') && !u.includes('/scrape/')) {
131
- capturedBody = JSON.parse((opts?.body as string) ?? '{}')
132
- return new Response(JSON.stringify({ jobId: 'r-4' }), { status: 200 })
133
- }
134
- if (u.includes('/reddit/scrape/r-4')) {
135
- return new Response(JSON.stringify({ jobId: 'r-4', status: 'done', items: [{ title: 'lemlist review' }] }), { status: 200 })
136
- }
137
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
138
- }) as typeof fetch
139
-
140
- const { getProviders } = await import('../../src/registry.js')
141
- const reddit = getProviders('search_reddit').find((p) => p.id === 'reddit')!
142
- const orig = { t: reddit.async!.timeoutMs, i: reddit.async!.pollIntervalMs }
143
- try {
144
- reddit.async!.timeoutMs = 500
145
- reddit.async!.pollIntervalMs = 20
146
- await searchRedditHandler({
147
- start_urls: ['https://www.reddit.com/r/coldemail/'],
148
- query: 'lemlist',
149
- time: 'month',
150
- limit: 5,
151
- })
152
- const urls = (capturedBody!.startUrls as Array<{ url: string }>).map((s) => s.url)
153
- expect(urls[0]).toContain('/r/coldemail/search/')
154
- expect(urls[0]).toContain('q=lemlist')
155
- expect(urls[0]).toContain('restrict_sr=1')
156
- expect(urls[0]).toContain('t=month')
157
- // Query now lives in the URL — top-level searchQueries is dropped to avoid a global search.
158
- expect(capturedBody!.searchQueries).toBeUndefined()
159
- } finally {
160
- reddit.async!.timeoutMs = orig.t
161
- reddit.async!.pollIntervalMs = orig.i
162
- }
163
- })
164
-
165
- it('leaves an existing search URL untouched and keeps it as the start url', async () => {
166
- let capturedBody: Record<string, unknown> | null = null
167
- const searchUrl = 'https://www.reddit.com/search/?q=lemlist&sort=new'
168
- globalThis.fetch = vi.fn(async (url: string | URL | Request, opts?: RequestInit) => {
169
- const u = url.toString()
170
- if (u.includes('/reddit/scrape') && !u.includes('/scrape/')) {
171
- capturedBody = JSON.parse((opts?.body as string) ?? '{}')
172
- return new Response(JSON.stringify({ jobId: 'r-5' }), { status: 200 })
173
- }
174
- if (u.includes('/reddit/scrape/r-5')) {
175
- return new Response(JSON.stringify({ jobId: 'r-5', status: 'done', items: [{ title: 'x' }] }), { status: 200 })
176
- }
177
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
178
- }) as typeof fetch
179
-
180
- const { getProviders } = await import('../../src/registry.js')
181
- const reddit = getProviders('search_reddit').find((p) => p.id === 'reddit')!
182
- const orig = { t: reddit.async!.timeoutMs, i: reddit.async!.pollIntervalMs }
183
- try {
184
- reddit.async!.timeoutMs = 500
185
- reddit.async!.pollIntervalMs = 20
186
- await searchRedditHandler({ start_urls: [searchUrl], query: 'lemlist', limit: 5 })
187
- const urls = (capturedBody!.startUrls as Array<{ url: string }>).map((s) => s.url)
188
- expect(urls[0]).toBe(searchUrl)
189
- } finally {
190
- reddit.async!.timeoutMs = orig.t
191
- reddit.async!.pollIntervalMs = orig.i
192
- }
193
- })
194
- })
@@ -1,183 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { initClient } from '../../src/client.js'
3
- import { searchSeoHandler } from '../../src/tools/search-seo.js'
4
-
5
- describe('search_seo 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('category=keywords — kw_search_volume fires first', async () => {
17
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
18
- const u = url.toString()
19
- if (u.includes('/dataforseo/keywords/google-ads/search-volume')) {
20
- return new Response(JSON.stringify({ tasks: [{ result: [{ keyword: 'cold email', search_volume: 5400 }] }] }), { status: 200 })
21
- }
22
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
23
- }) as typeof fetch
24
-
25
- const result = await searchSeoHandler({ category: 'keywords', keywords: ['cold email'] })
26
-
27
- expect(result.isError).toBeFalsy()
28
- const parsed = JSON.parse(result.content[0].text)
29
- expect(parsed._meta.provider).toBe('kw_search_volume')
30
- })
31
-
32
- it('category=serp (google engine default) — serp_google fires', async () => {
33
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
34
- const u = url.toString()
35
- if (u.includes('/dataforseo/serp/google/organic')) {
36
- return new Response(JSON.stringify({ tasks: [{ result: [{ items: [{ title: 'ColdIQ', url: 'https://coldiq.com' }] }] }] }), { status: 200 })
37
- }
38
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
39
- }) as typeof fetch
40
-
41
- const result = await searchSeoHandler({ category: 'serp', keyword: 'cold email tool' })
42
-
43
- expect(result.isError).toBeFalsy()
44
- const parsed = JSON.parse(result.content[0].text)
45
- expect(parsed._meta.provider).toBe('serp_google')
46
- })
47
-
48
- it('category=serp engine=bing — serp_bing fires, serp_google skipped', async () => {
49
- let googleCalled = false
50
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
51
- const u = url.toString()
52
- if (u.includes('/dataforseo/serp/google/organic')) { googleCalled = true }
53
- if (u.includes('/dataforseo/serp/bing/organic')) {
54
- return new Response(JSON.stringify({ tasks: [{ result: [{ items: [{ title: 'Bing result' }] }] }] }), { status: 200 })
55
- }
56
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
57
- }) as typeof fetch
58
-
59
- const result = await searchSeoHandler({ category: 'serp', keyword: 'cold email', engine: 'bing' })
60
-
61
- expect(result.isError).toBeFalsy()
62
- const parsed = JSON.parse(result.content[0].text)
63
- expect(parsed._meta.provider).toBe('serp_bing')
64
- expect(googleCalled).toBe(false)
65
- })
66
-
67
- it('category=serp engine=youtube — serp_youtube fires', async () => {
68
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
69
- const u = url.toString()
70
- if (u.includes('/dataforseo/serp/youtube/organic')) {
71
- return new Response(JSON.stringify({ tasks: [{ result: [{ items: [{ title: 'YouTube result' }] }] }] }), { status: 200 })
72
- }
73
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
74
- }) as typeof fetch
75
-
76
- const result = await searchSeoHandler({ category: 'serp', keyword: 'cold email', engine: 'youtube' })
77
-
78
- expect(result.isError).toBeFalsy()
79
- const parsed = JSON.parse(result.content[0].text)
80
- expect(parsed._meta.provider).toBe('serp_youtube')
81
- })
82
-
83
- it('category=backlinks — bl_summary fires first', async () => {
84
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
85
- const u = url.toString()
86
- if (u.includes('/dataforseo/backlinks/summary')) {
87
- return new Response(JSON.stringify({ tasks: [{ result: [{ total_count: 1234, rank: 85 }] }] }), { status: 200 })
88
- }
89
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
90
- }) as typeof fetch
91
-
92
- const result = await searchSeoHandler({ category: 'backlinks', target: 'coldiq.com' })
93
-
94
- expect(result.isError).toBeFalsy()
95
- const parsed = JSON.parse(result.content[0].text)
96
- expect(parsed._meta.provider).toBe('bl_summary')
97
- })
98
-
99
- it('category=domain action=technologies — domain_tech fires', async () => {
100
- let whoisCalled = false
101
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
102
- const u = url.toString()
103
- if (u.includes('/dataforseo/domain-analytics/whois')) { whoisCalled = true }
104
- if (u.includes('/dataforseo/domain-analytics/technologies')) {
105
- return new Response(JSON.stringify({ tasks: [{ result: [{ technologies: { cms: ['WordPress'] } }] }] }), { status: 200 })
106
- }
107
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
108
- }) as typeof fetch
109
-
110
- const result = await searchSeoHandler({ category: 'domain', target: 'coldiq.com', action: 'technologies' })
111
-
112
- expect(result.isError).toBeFalsy()
113
- const parsed = JSON.parse(result.content[0].text)
114
- expect(parsed._meta.provider).toBe('domain_tech')
115
- expect(whoisCalled).toBe(false)
116
- })
117
-
118
- it('category=domain action=whois — domain_whois fires, domain_tech skipped', async () => {
119
- let techCalled = false
120
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
121
- const u = url.toString()
122
- if (u.includes('/dataforseo/domain-analytics/technologies')) { techCalled = true }
123
- if (u.includes('/dataforseo/domain-analytics/whois')) {
124
- return new Response(JSON.stringify({ tasks: [{ result: [{ items: [{ domain: 'coldiq.com', expiration_datetime: '2026-01-01' }] }] }] }), { status: 200 })
125
- }
126
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
127
- }) as typeof fetch
128
-
129
- const result = await searchSeoHandler({ category: 'domain', target: 'coldiq.com', action: 'whois' })
130
-
131
- expect(result.isError).toBeFalsy()
132
- const parsed = JSON.parse(result.content[0].text)
133
- expect(parsed._meta.provider).toBe('domain_whois')
134
- expect(techCalled).toBe(false)
135
- })
136
-
137
- it('category=labs with target and no lab_action — labs_rank_overview fires (default)', async () => {
138
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
139
- const u = url.toString()
140
- if (u.includes('/dataforseo/labs/domain-rank-overview')) {
141
- return new Response(JSON.stringify({ tasks: [{ result: [{ metrics: { organic: { pos_1: 3 } }, rank: 90 }] }] }), { status: 200 })
142
- }
143
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
144
- }) as typeof fetch
145
-
146
- const result = await searchSeoHandler({ category: 'labs', target: 'coldiq.com' })
147
-
148
- expect(result.isError).toBeFalsy()
149
- const parsed = JSON.parse(result.content[0].text)
150
- expect(parsed._meta.provider).toBe('labs_rank_overview')
151
- })
152
-
153
- it('category=page page_action=content — page_content fires, page_lighthouse skipped', async () => {
154
- let lighthouseCalled = false
155
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
156
- const u = url.toString()
157
- if (u.includes('/dataforseo/on-page/lighthouse')) { lighthouseCalled = true }
158
- if (u.includes('/dataforseo/on-page/content-parsing')) {
159
- return new Response(JSON.stringify({ tasks: [{ result: [{ content: 'ColdIQ homepage text', plain_text: 'ColdIQ' }] }] }), { status: 200 })
160
- }
161
- return new Response(JSON.stringify({ error: 'unexpected' }), { status: 500 })
162
- }) as typeof fetch
163
-
164
- const result = await searchSeoHandler({ category: 'page', url: 'https://coldiq.com', page_action: 'content' })
165
-
166
- expect(result.isError).toBeFalsy()
167
- const parsed = JSON.parse(result.content[0].text)
168
- expect(parsed._meta.provider).toBe('page_content')
169
- expect(lighthouseCalled).toBe(false)
170
- })
171
-
172
- it('all providers fail — returns isError', async () => {
173
- globalThis.fetch = vi.fn(async () => {
174
- return new Response(JSON.stringify({ error: 'down' }), { status: 503 })
175
- }) as typeof fetch
176
-
177
- const result = await searchSeoHandler({ category: 'serp', keyword: 'cold email' })
178
-
179
- expect(result.isError).toBe(true)
180
- const parsed = JSON.parse(result.content[0].text)
181
- expect(parsed.error).toBeTruthy()
182
- })
183
- })
@@ -1,79 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { initClient } from '../../src/client.js'
3
- import { searchWebHandler } from '../../src/tools/search-web.js'
4
-
5
- describe('search_web 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 Serper results for general search', async () => {
17
- globalThis.fetch = vi.fn(async () =>
18
- new Response(JSON.stringify({ organic: [{ title: 'ColdIQ', link: 'https://coldiq.com' }] }), { status: 200 })
19
- ) as typeof fetch
20
-
21
- const result = await searchWebHandler({ query: 'ColdIQ', search_type: 'general' })
22
-
23
- const parsed = JSON.parse(result.content[0].text)
24
- expect(parsed._meta.provider).toBe('serper')
25
- expect(parsed.data.organic).toHaveLength(1)
26
- })
27
-
28
- it('prefers Exa for neural search type', async () => {
29
- let capturedUrl = ''
30
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
31
- capturedUrl = url.toString()
32
- return new Response(JSON.stringify({ results: [{ title: 'ColdIQ', url: 'https://coldiq.com' }] }), { status: 200 })
33
- }) as typeof fetch
34
-
35
- const result = await searchWebHandler({ query: 'B2B sales intelligence', search_type: 'neural' })
36
-
37
- expect(capturedUrl).toContain('/exa/search')
38
- const parsed = JSON.parse(result.content[0].text)
39
- expect(parsed._meta.provider).toBe('exa')
40
- })
41
-
42
- it('falls back to LimaData when Serper fails', async () => {
43
- let callCount = 0
44
- globalThis.fetch = vi.fn(async () => {
45
- callCount++
46
- if (callCount === 1) {
47
- return new Response(JSON.stringify({ error: 'Rate limited' }), { status: 429 })
48
- }
49
- return new Response(JSON.stringify({ organic: [{ title: 'ColdIQ', link: 'https://coldiq.com' }] }), { status: 200 })
50
- }) as typeof fetch
51
-
52
- const result = await searchWebHandler({ query: 'ColdIQ', search_type: 'general' })
53
-
54
- const parsed = JSON.parse(result.content[0].text)
55
- expect(parsed._meta.provider).toBe('limadata')
56
- })
57
-
58
- it('falls back to Exa when Serper and LimaData both fail', async () => {
59
- let callCount = 0
60
- let capturedUrls: string[] = []
61
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
62
- capturedUrls.push(url.toString())
63
- callCount++
64
- if (callCount === 1) {
65
- return new Response(JSON.stringify({ error: 'Rate limited' }), { status: 429 })
66
- }
67
- if (callCount === 2) {
68
- return new Response(JSON.stringify({ error: 'Service unavailable' }), { status: 500 })
69
- }
70
- return new Response(JSON.stringify({ results: [{ title: 'ColdIQ', url: 'https://coldiq.com' }] }), { status: 200 })
71
- }) as typeof fetch
72
-
73
- const result = await searchWebHandler({ query: 'ColdIQ', search_type: 'general' })
74
-
75
- expect(capturedUrls[2]).toContain('/exa/search')
76
- const parsed = JSON.parse(result.content[0].text)
77
- expect(parsed._meta.provider).toBe('exa')
78
- })
79
- })
@@ -1,142 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
- import { initClient } from '../../src/client.js'
3
- import { verifyEmailHandler, addNormalizedStatus } from '../../src/tools/verify-email.js'
4
-
5
- describe('addNormalizedStatus', () => {
6
- it('findymail verified=true → status valid', () => {
7
- const out = addNormalizedStatus({ email: 'michel@coldiq.com', verified: true, provider: 'Google' }, 'findymail') as Record<string, unknown>
8
- expect(out.status).toBe('valid')
9
- // Original fields preserved for back-compat.
10
- expect(out.verified).toBe(true)
11
- expect(out.provider).toBe('Google')
12
- })
13
-
14
- it('findymail verified=false → status invalid', () => {
15
- const out = addNormalizedStatus({ email: 'bad@example.com', verified: false, provider: 'Other' }, 'findymail') as Record<string, unknown>
16
- expect(out.status).toBe('invalid')
17
- })
18
-
19
- it('icypeas item.status=catch_all → catch_all', () => {
20
- const out = addNormalizedStatus({ success: true, item: { status: 'catch_all', email: 'x@y.com' } }, 'icypeas') as Record<string, unknown>
21
- expect(out.status).toBe('catch_all')
22
- })
23
-
24
- it('icypeas missing item → unknown', () => {
25
- const out = addNormalizedStatus({ success: true }, 'icypeas') as Record<string, unknown>
26
- expect(out.status).toBe('unknown')
27
- })
28
-
29
- it('upstream status="catchall" is normalized to catch_all', () => {
30
- const out = addNormalizedStatus({ status: 'catchall', email: 'x@y.com' }, 'instantly') as Record<string, unknown>
31
- expect(out.status).toBe('catch_all')
32
- })
33
-
34
- it('upstream status="risky" stays risky', () => {
35
- const out = addNormalizedStatus({ status: 'risky' }, 'instantly') as Record<string, unknown>
36
- expect(out.status).toBe('risky')
37
- })
38
-
39
- it('upstream status="disposable" stays disposable', () => {
40
- const out = addNormalizedStatus({ status: 'disposable' }, 'instantly') as Record<string, unknown>
41
- expect(out.status).toBe('disposable')
42
- })
43
-
44
- it('upstream status="VALID" is lowercased to valid', () => {
45
- const out = addNormalizedStatus({ status: 'VALID' }, 'instantly') as Record<string, unknown>
46
- expect(out.status).toBe('valid')
47
- })
48
-
49
- it('unknown provider id with no signal fields → unknown', () => {
50
- const out = addNormalizedStatus({ foo: 'bar' }, 'mystery-provider') as Record<string, unknown>
51
- expect(out.status).toBe('unknown')
52
- })
53
-
54
- it('null / non-object data is returned unchanged', () => {
55
- expect(addNormalizedStatus(null, 'findymail')).toBe(null)
56
- expect(addNormalizedStatus('a string', 'findymail')).toBe('a string')
57
- })
58
-
59
- it('linkupapi-validate verified=true → valid', () => {
60
- const out = addNormalizedStatus({ verified: true }, 'linkupapi-validate') as Record<string, unknown>
61
- expect(out.status).toBe('valid')
62
- })
63
- })
64
-
65
- describe('verify_email handler', () => {
66
- const originalFetch = globalThis.fetch
67
-
68
- beforeEach(() => {
69
- initClient('http://test-api.local', 'test-key')
70
- })
71
-
72
- afterEach(() => {
73
- globalThis.fetch = originalFetch
74
- })
75
-
76
- it('normalizes findymail real upstream shape (verified boolean) to status=valid', async () => {
77
- globalThis.fetch = vi.fn(async () =>
78
- new Response(JSON.stringify({ email: 'michel@coldiq.com', verified: true, provider: 'Google' }), { status: 200 })
79
- ) as typeof fetch
80
-
81
- const result = await verifyEmailHandler({ email: 'michel@coldiq.com' })
82
-
83
- const parsed = JSON.parse(result.content[0].text)
84
- expect(parsed._meta.provider).toBe('findymail')
85
- expect(parsed.data.status).toBe('valid')
86
- // Raw fields still surfaced for callers that already depend on them.
87
- expect(parsed.data.verified).toBe(true)
88
- expect(parsed.data.provider).toBe('Google')
89
- })
90
-
91
- it('normalizes findymail verified=false to status=invalid', async () => {
92
- globalThis.fetch = vi.fn(async () =>
93
- new Response(JSON.stringify({ email: 'bad@example.com', verified: false, provider: 'Other' }), { status: 200 })
94
- ) as typeof fetch
95
-
96
- const result = await verifyEmailHandler({ email: 'bad@example.com' })
97
-
98
- const parsed = JSON.parse(result.content[0].text)
99
- expect(parsed.data.status).toBe('invalid')
100
- })
101
-
102
- it('falls back to IcyPeas on FindyMail failure', async () => {
103
- let callCount = 0
104
- globalThis.fetch = vi.fn(async () => {
105
- callCount++
106
- if (callCount === 1) {
107
- return new Response(JSON.stringify({ error: 'Provider error' }), { status: 502 })
108
- }
109
- return new Response(JSON.stringify({ result: 'valid' }), { status: 200 })
110
- }) as typeof fetch
111
-
112
- const result = await verifyEmailHandler({ email: 'michel@coldiq.com' })
113
-
114
- const parsed = JSON.parse(result.content[0].text)
115
- expect(parsed._meta.provider).toBe('icypeas')
116
- })
117
-
118
- it('falls through to Instantly when FindyMail and IcyPeas both fail, polls until terminal status', async () => {
119
- let callCount = 0
120
- globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
121
- const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.href : url.url
122
- callCount++
123
- // Findymail and IcyPeas fail
124
- if (urlStr.includes('findymail') || urlStr.includes('icypeas')) {
125
- return new Response(JSON.stringify({ error: 'Provider error' }), { status: 502 })
126
- }
127
- // Instantly POST returns pending first
128
- if (urlStr.includes('email-verification') && !urlStr.includes('%40')) {
129
- return new Response(JSON.stringify({ email: 'michel@coldiq.com', status: 'pending' }), { status: 200 })
130
- }
131
- // Instantly GET poll returns terminal status
132
- return new Response(JSON.stringify({ email: 'michel@coldiq.com', status: 'valid' }), { status: 200 })
133
- }) as typeof fetch
134
-
135
- const result = await verifyEmailHandler({ email: 'michel@coldiq.com' })
136
-
137
- const parsed = JSON.parse(result.content[0].text)
138
- expect(result.isError).toBeFalsy()
139
- expect(parsed._meta.provider).toBe('instantly')
140
- expect(parsed.data.status).toBe('valid')
141
- })
142
- })