@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.
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +4 -0
- package/dist/client.js.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/session-usage.d.ts +59 -0
- package/dist/session-usage.d.ts.map +1 -0
- package/dist/session-usage.js +96 -0
- package/dist/session-usage.js.map +1 -0
- package/dist/tools/enrich-company.d.ts +3 -9
- package/dist/tools/enrich-company.d.ts.map +1 -1
- package/dist/tools/enrich-company.js +3 -16
- package/dist/tools/enrich-company.js.map +1 -1
- package/dist/tools/enrich-person.d.ts +3 -9
- package/dist/tools/enrich-person.d.ts.map +1 -1
- package/dist/tools/enrich-person.js +3 -12
- package/dist/tools/enrich-person.js.map +1 -1
- package/dist/tools/fetch-page-content.d.ts +3 -9
- package/dist/tools/fetch-page-content.d.ts.map +1 -1
- package/dist/tools/fetch-page-content.js +3 -12
- package/dist/tools/fetch-page-content.js.map +1 -1
- package/dist/tools/find-email.d.ts +3 -9
- package/dist/tools/find-email.d.ts.map +1 -1
- package/dist/tools/find-email.js +3 -40
- package/dist/tools/find-email.js.map +1 -1
- package/dist/tools/find-emails.d.ts +3 -4
- package/dist/tools/find-emails.d.ts.map +1 -1
- package/dist/tools/find-emails.js +8 -423
- package/dist/tools/find-emails.js.map +1 -1
- package/dist/tools/find-influencers.d.ts +3 -9
- package/dist/tools/find-influencers.d.ts.map +1 -1
- package/dist/tools/find-influencers.js +3 -12
- package/dist/tools/find-influencers.js.map +1 -1
- package/dist/tools/find-people.d.ts +3 -8
- package/dist/tools/find-people.d.ts.map +1 -1
- package/dist/tools/find-people.js +3 -98
- package/dist/tools/find-people.js.map +1 -1
- package/dist/tools/find-phone.d.ts +3 -9
- package/dist/tools/find-phone.d.ts.map +1 -1
- package/dist/tools/find-phone.js +4 -41
- package/dist/tools/find-phone.js.map +1 -1
- package/dist/tools/find-signals.d.ts +3 -9
- package/dist/tools/find-signals.d.ts.map +1 -1
- package/dist/tools/find-signals.js +3 -83
- package/dist/tools/find-signals.js.map +1 -1
- package/dist/tools/get-place-reviews.d.ts +3 -9
- package/dist/tools/get-place-reviews.d.ts.map +1 -1
- package/dist/tools/get-place-reviews.js +3 -12
- package/dist/tools/get-place-reviews.js.map +1 -1
- package/dist/tools/get-session-usage.d.ts +10 -0
- package/dist/tools/get-session-usage.d.ts.map +1 -0
- package/dist/tools/get-session-usage.js +14 -0
- package/dist/tools/get-session-usage.js.map +1 -0
- package/dist/tools/search-ads.d.ts +3 -9
- package/dist/tools/search-ads.d.ts.map +1 -1
- package/dist/tools/search-ads.js +3 -12
- package/dist/tools/search-ads.js.map +1 -1
- package/dist/tools/search-companies.d.ts +3 -9
- package/dist/tools/search-companies.d.ts.map +1 -1
- package/dist/tools/search-companies.js +3 -24
- package/dist/tools/search-companies.js.map +1 -1
- package/dist/tools/search-jobs.d.ts +3 -9
- package/dist/tools/search-jobs.d.ts.map +1 -1
- package/dist/tools/search-jobs.js +3 -27
- package/dist/tools/search-jobs.js.map +1 -1
- package/dist/tools/search-places.d.ts +3 -15
- package/dist/tools/search-places.d.ts.map +1 -1
- package/dist/tools/search-places.js +5 -76
- package/dist/tools/search-places.js.map +1 -1
- package/dist/tools/search-reddit.d.ts +3 -9
- package/dist/tools/search-reddit.d.ts.map +1 -1
- package/dist/tools/search-reddit.js +3 -12
- package/dist/tools/search-reddit.js.map +1 -1
- package/dist/tools/search-seo.d.ts +3 -9
- package/dist/tools/search-seo.d.ts.map +1 -1
- package/dist/tools/search-seo.js +3 -12
- package/dist/tools/search-seo.js.map +1 -1
- package/dist/tools/search-web.d.ts +3 -9
- package/dist/tools/search-web.d.ts.map +1 -1
- package/dist/tools/search-web.js +3 -12
- package/dist/tools/search-web.js.map +1 -1
- package/dist/tools/verify-email.d.ts +3 -10
- package/dist/tools/verify-email.d.ts.map +1 -1
- package/dist/tools/verify-email.js +3 -68
- package/dist/tools/verify-email.js.map +1 -1
- package/dist/verb-client.d.ts +35 -0
- package/dist/verb-client.d.ts.map +1 -0
- package/dist/verb-client.js +57 -0
- package/dist/verb-client.js.map +1 -0
- package/package.json +1 -1
- package/src/client.ts +4 -0
- package/src/index.ts +8 -0
- package/src/session-usage.ts +130 -0
- package/src/tools/enrich-company.ts +3 -16
- package/src/tools/enrich-person.ts +3 -12
- package/src/tools/fetch-page-content.ts +3 -12
- package/src/tools/find-email.ts +3 -42
- package/src/tools/find-emails.ts +8 -506
- package/src/tools/find-influencers.ts +3 -12
- package/src/tools/find-people.ts +3 -107
- package/src/tools/find-phone.ts +4 -42
- package/src/tools/find-signals.ts +3 -88
- package/src/tools/get-place-reviews.ts +3 -12
- package/src/tools/get-session-usage.ts +18 -0
- package/src/tools/search-ads.ts +3 -12
- package/src/tools/search-companies.ts +3 -23
- package/src/tools/search-jobs.ts +3 -30
- package/src/tools/search-places.ts +5 -72
- package/src/tools/search-reddit.ts +3 -12
- package/src/tools/search-seo.ts +3 -12
- package/src/tools/search-web.ts +3 -12
- package/src/tools/verify-email.ts +3 -76
- package/src/verb-client.ts +93 -0
- package/tests/executor.test.ts +69 -0
- package/tests/session-usage.test.ts +130 -0
- package/tests/tools/get-session-usage.test.ts +44 -0
- package/tests/verb-client.test.ts +152 -0
- package/tests/tools/enrich-company.test.ts +0 -92
- package/tests/tools/enrich-person.test.ts +0 -140
- package/tests/tools/fetch-page-content.test.ts +0 -108
- package/tests/tools/find-email.test.ts +0 -137
- package/tests/tools/find-emails.test.ts +0 -818
- package/tests/tools/find-influencers.test.ts +0 -91
- package/tests/tools/find-people.test.ts +0 -621
- package/tests/tools/find-phone.test.ts +0 -100
- package/tests/tools/find-signals.test.ts +0 -175
- package/tests/tools/get-place-reviews.test.ts +0 -73
- package/tests/tools/search-ads.test.ts +0 -182
- package/tests/tools/search-companies.test.ts +0 -192
- package/tests/tools/search-jobs.test.ts +0 -210
- package/tests/tools/search-places.test.ts +0 -210
- package/tests/tools/search-reddit.test.ts +0 -194
- package/tests/tools/search-seo.test.ts +0 -183
- package/tests/tools/search-web.test.ts +0 -79
- 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
|
-
})
|