@coldiq/mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.d.ts +8 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +47 -0
- package/dist/client.js.map +1 -0
- package/dist/executor.d.ts +21 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +130 -0
- package/dist/executor.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/index.js.map +1 -0
- package/dist/registry.d.ts +49 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +3104 -0
- package/dist/registry.js.map +1 -0
- package/dist/tools/enrich-company.d.ts +22 -0
- package/dist/tools/enrich-company.d.ts.map +1 -0
- package/dist/tools/enrich-company.js +21 -0
- package/dist/tools/enrich-company.js.map +1 -0
- package/dist/tools/enrich-email.d.ts +24 -0
- package/dist/tools/enrich-email.d.ts.map +1 -0
- package/dist/tools/enrich-email.js +19 -0
- package/dist/tools/enrich-email.js.map +1 -0
- package/dist/tools/enrich-emails.d.ts +31 -0
- package/dist/tools/enrich-emails.d.ts.map +1 -0
- package/dist/tools/enrich-emails.js +146 -0
- package/dist/tools/enrich-emails.js.map +1 -0
- package/dist/tools/enrich-person.d.ts +26 -0
- package/dist/tools/enrich-person.d.ts.map +1 -0
- package/dist/tools/enrich-person.js +23 -0
- package/dist/tools/enrich-person.js.map +1 -0
- package/dist/tools/fetch-page-content.d.ts +22 -0
- package/dist/tools/fetch-page-content.d.ts.map +1 -0
- package/dist/tools/fetch-page-content.js +32 -0
- package/dist/tools/fetch-page-content.js.map +1 -0
- package/dist/tools/find-email.d.ts +24 -0
- package/dist/tools/find-email.d.ts.map +1 -0
- package/dist/tools/find-email.js +19 -0
- package/dist/tools/find-email.js.map +1 -0
- package/dist/tools/find-emails.d.ts +31 -0
- package/dist/tools/find-emails.d.ts.map +1 -0
- package/dist/tools/find-emails.js +146 -0
- package/dist/tools/find-emails.js.map +1 -0
- package/dist/tools/find-influencers.d.ts +29 -0
- package/dist/tools/find-influencers.d.ts.map +1 -0
- package/dist/tools/find-influencers.js +30 -0
- package/dist/tools/find-influencers.js.map +1 -0
- package/dist/tools/find-people.d.ts +26 -0
- package/dist/tools/find-people.d.ts.map +1 -0
- package/dist/tools/find-people.js +61 -0
- package/dist/tools/find-people.js.map +1 -0
- package/dist/tools/find-phone.d.ts +24 -0
- package/dist/tools/find-phone.d.ts.map +1 -0
- package/dist/tools/find-phone.js +48 -0
- package/dist/tools/find-phone.js.map +1 -0
- package/dist/tools/find-signals.d.ts +26 -0
- package/dist/tools/find-signals.d.ts.map +1 -0
- package/dist/tools/find-signals.js +82 -0
- package/dist/tools/find-signals.js.map +1 -0
- package/dist/tools/search-ads.d.ts +33 -0
- package/dist/tools/search-ads.d.ts.map +1 -0
- package/dist/tools/search-ads.js +33 -0
- package/dist/tools/search-ads.js.map +1 -0
- package/dist/tools/search-companies.d.ts +42 -0
- package/dist/tools/search-companies.d.ts.map +1 -0
- package/dist/tools/search-companies.js +37 -0
- package/dist/tools/search-companies.js.map +1 -0
- package/dist/tools/search-jobs.d.ts +51 -0
- package/dist/tools/search-jobs.d.ts.map +1 -0
- package/dist/tools/search-jobs.js +64 -0
- package/dist/tools/search-jobs.js.map +1 -0
- package/dist/tools/search-places.d.ts +47 -0
- package/dist/tools/search-places.d.ts.map +1 -0
- package/dist/tools/search-places.js +42 -0
- package/dist/tools/search-places.js.map +1 -0
- package/dist/tools/search-reddit.d.ts +27 -0
- package/dist/tools/search-reddit.d.ts.map +1 -0
- package/dist/tools/search-reddit.js +30 -0
- package/dist/tools/search-reddit.js.map +1 -0
- package/dist/tools/search-seo.d.ts +37 -0
- package/dist/tools/search-seo.d.ts.map +1 -0
- package/dist/tools/search-seo.js +49 -0
- package/dist/tools/search-seo.js.map +1 -0
- package/dist/tools/search-web.d.ts +23 -0
- package/dist/tools/search-web.d.ts.map +1 -0
- package/dist/tools/search-web.js +20 -0
- package/dist/tools/search-web.js.map +1 -0
- package/dist/tools/verify-email.d.ts +20 -0
- package/dist/tools/verify-email.d.ts.map +1 -0
- package/dist/tools/verify-email.js +15 -0
- package/dist/tools/verify-email.js.map +1 -0
- package/package.json +28 -0
- package/src/client.ts +60 -0
- package/src/executor.ts +182 -0
- package/src/index.ts +155 -0
- package/src/registry.ts +3159 -0
- package/src/tools/enrich-company.ts +25 -0
- package/src/tools/enrich-person.ts +27 -0
- package/src/tools/fetch-page-content.ts +36 -0
- package/src/tools/find-email.ts +23 -0
- package/src/tools/find-emails.ts +190 -0
- package/src/tools/find-influencers.ts +34 -0
- package/src/tools/find-people.ts +69 -0
- package/src/tools/find-phone.ts +53 -0
- package/src/tools/find-signals.ts +93 -0
- package/src/tools/search-ads.ts +44 -0
- package/src/tools/search-companies.ts +41 -0
- package/src/tools/search-jobs.ts +73 -0
- package/src/tools/search-places.ts +52 -0
- package/src/tools/search-reddit.ts +34 -0
- package/src/tools/search-seo.ts +59 -0
- package/src/tools/search-web.ts +24 -0
- package/src/tools/verify-email.ts +19 -0
- package/test-ads-live.ts +77 -0
- package/test-company-live.ts +91 -0
- package/test-email-live.ts +171 -0
- package/test-influencers-live.ts +66 -0
- package/test-jobs-live.ts +69 -0
- package/test-linkupapi-live.ts +137 -0
- package/test-phone-live.ts +41 -0
- package/test-places-live.ts +89 -0
- package/test-reddit-live.ts +66 -0
- package/test-search-live.ts +79 -0
- package/test-seo-live.ts +68 -0
- package/test-web-live.ts +67 -0
- package/tests/client.test.ts +90 -0
- package/tests/executor.test.ts +83 -0
- package/tests/gtm/01-icp-to-emails.test.ts +43 -0
- package/tests/gtm/02-icp-bulk-emails.test.ts +38 -0
- package/tests/gtm/03-icp-to-phones.test.ts +39 -0
- package/tests/gtm/04-funding-signal-outreach.test.ts +42 -0
- package/tests/gtm/05-hiring-signal-decisionmakers.test.ts +41 -0
- package/tests/gtm/06-intent-signal-outreach.test.ts +44 -0
- package/tests/gtm/07-places-to-content.test.ts +50 -0
- package/tests/gtm/08-domain-to-account.test.ts +44 -0
- package/tests/gtm/09-linkedin-to-everything.test.ts +41 -0
- package/tests/gtm/10-jobs-vs-signals-routing.test.ts +38 -0
- package/tests/gtm/11-find-vs-enrich-routing.test.ts +39 -0
- package/tests/gtm/12-bogus-domain-graceful.test.ts +42 -0
- package/tests/gtm/13-private-linkedin-graceful.test.ts +44 -0
- package/tests/gtm/14-empty-handoff.test.ts +43 -0
- package/tests/gtm/15-seo-reddit-research.test.ts +38 -0
- package/tests/gtm/README.md +59 -0
- package/tests/gtm/harness.ts +217 -0
- package/tests/gtm/tools-bridge.ts +232 -0
- package/tests/gtm-scenarios.md +32 -0
- package/tests/live/smoke-report.ts +255 -0
- package/tests/live/smoke.test.ts +134 -0
- package/tests/registry-enrich-person.test.ts +447 -0
- package/tests/registry-fetch-page-content.test.ts +90 -0
- package/tests/registry-find-people.test.ts +467 -0
- package/tests/registry-find-signals.test.ts +470 -0
- package/tests/registry-linkupapi.test.ts +331 -0
- package/tests/registry-search-companies.test.ts +188 -0
- package/tests/registry-search-jobs.test.ts +116 -0
- package/tests/registry.test.ts +2210 -0
- package/tests/tools/enrich-company.test.ts +92 -0
- package/tests/tools/enrich-email.test.ts +94 -0
- package/tests/tools/enrich-emails.test.ts +271 -0
- package/tests/tools/enrich-person.test.ts +140 -0
- package/tests/tools/fetch-page-content.test.ts +108 -0
- package/tests/tools/find-influencers.test.ts +91 -0
- package/tests/tools/find-people.test.ts +344 -0
- package/tests/tools/find-phone.test.ts +100 -0
- package/tests/tools/find-signals.test.ts +110 -0
- package/tests/tools/search-ads.test.ts +182 -0
- package/tests/tools/search-companies.test.ts +58 -0
- package/tests/tools/search-jobs.test.ts +210 -0
- package/tests/tools/search-places.test.ts +114 -0
- package/tests/tools/search-reddit.test.ts +125 -0
- package/tests/tools/search-seo.test.ts +183 -0
- package/tests/tools/search-web.test.ts +79 -0
- package/tests/tools/verify-email.test.ts +68 -0
- package/tsconfig.json +17 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,125 @@
|
|
|
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
|
+
reddit.async!.timeoutMs = 500
|
|
37
|
+
reddit.async!.pollIntervalMs = 20
|
|
38
|
+
|
|
39
|
+
try {
|
|
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
|
+
reddit.async!.timeoutMs = 500
|
|
76
|
+
reddit.async!.pollIntervalMs = 20
|
|
77
|
+
|
|
78
|
+
try {
|
|
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
|
+
reddit.async!.timeoutMs = 100
|
|
112
|
+
reddit.async!.pollIntervalMs = 20
|
|
113
|
+
|
|
114
|
+
try {
|
|
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
|
+
})
|
|
@@ -0,0 +1,183 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,79 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
2
|
+
import { initClient } from '../../src/client.js'
|
|
3
|
+
import { verifyEmailHandler } from '../../src/tools/verify-email.js'
|
|
4
|
+
|
|
5
|
+
describe('verify_email 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 verification result from FindyMail', async () => {
|
|
17
|
+
globalThis.fetch = vi.fn(async () =>
|
|
18
|
+
new Response(JSON.stringify({ status: 'valid', email: 'michel@coldiq.com' }), { status: 200 })
|
|
19
|
+
) as typeof fetch
|
|
20
|
+
|
|
21
|
+
const result = await verifyEmailHandler({ email: 'michel@coldiq.com' })
|
|
22
|
+
|
|
23
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
24
|
+
expect(parsed._meta.provider).toBe('findymail')
|
|
25
|
+
expect(parsed.data.status).toBe('valid')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('falls back to IcyPeas on FindyMail failure', async () => {
|
|
29
|
+
let callCount = 0
|
|
30
|
+
globalThis.fetch = vi.fn(async () => {
|
|
31
|
+
callCount++
|
|
32
|
+
if (callCount === 1) {
|
|
33
|
+
return new Response(JSON.stringify({ error: 'Provider error' }), { status: 502 })
|
|
34
|
+
}
|
|
35
|
+
return new Response(JSON.stringify({ result: 'valid' }), { status: 200 })
|
|
36
|
+
}) as typeof fetch
|
|
37
|
+
|
|
38
|
+
const result = await verifyEmailHandler({ email: 'michel@coldiq.com' })
|
|
39
|
+
|
|
40
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
41
|
+
expect(parsed._meta.provider).toBe('icypeas')
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('falls through to Instantly when FindyMail and IcyPeas both fail, polls until terminal status', async () => {
|
|
45
|
+
let callCount = 0
|
|
46
|
+
globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
|
|
47
|
+
const urlStr = typeof url === 'string' ? url : url instanceof URL ? url.href : url.url
|
|
48
|
+
callCount++
|
|
49
|
+
// Findymail and IcyPeas fail
|
|
50
|
+
if (urlStr.includes('findymail') || urlStr.includes('icypeas')) {
|
|
51
|
+
return new Response(JSON.stringify({ error: 'Provider error' }), { status: 502 })
|
|
52
|
+
}
|
|
53
|
+
// Instantly POST returns pending first
|
|
54
|
+
if (urlStr.includes('email-verification') && !urlStr.includes('%40')) {
|
|
55
|
+
return new Response(JSON.stringify({ email: 'michel@coldiq.com', status: 'pending' }), { status: 200 })
|
|
56
|
+
}
|
|
57
|
+
// Instantly GET poll returns terminal status
|
|
58
|
+
return new Response(JSON.stringify({ email: 'michel@coldiq.com', status: 'valid' }), { status: 200 })
|
|
59
|
+
}) as typeof fetch
|
|
60
|
+
|
|
61
|
+
const result = await verifyEmailHandler({ email: 'michel@coldiq.com' })
|
|
62
|
+
|
|
63
|
+
const parsed = JSON.parse(result.content[0].text)
|
|
64
|
+
expect(result.isError).toBeFalsy()
|
|
65
|
+
expect(parsed._meta.provider).toBe('instantly')
|
|
66
|
+
expect(parsed.data.status).toBe('valid')
|
|
67
|
+
})
|
|
68
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"rootDir": "src",
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"],
|
|
16
|
+
"exclude": ["node_modules", "dist", "tests"]
|
|
17
|
+
}
|