@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.
Files changed (176) hide show
  1. package/dist/client.d.ts +8 -0
  2. package/dist/client.d.ts.map +1 -0
  3. package/dist/client.js +47 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/executor.d.ts +21 -0
  6. package/dist/executor.d.ts.map +1 -0
  7. package/dist/executor.js +130 -0
  8. package/dist/executor.js.map +1 -0
  9. package/dist/index.d.ts +3 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +49 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/registry.d.ts +49 -0
  14. package/dist/registry.d.ts.map +1 -0
  15. package/dist/registry.js +3104 -0
  16. package/dist/registry.js.map +1 -0
  17. package/dist/tools/enrich-company.d.ts +22 -0
  18. package/dist/tools/enrich-company.d.ts.map +1 -0
  19. package/dist/tools/enrich-company.js +21 -0
  20. package/dist/tools/enrich-company.js.map +1 -0
  21. package/dist/tools/enrich-email.d.ts +24 -0
  22. package/dist/tools/enrich-email.d.ts.map +1 -0
  23. package/dist/tools/enrich-email.js +19 -0
  24. package/dist/tools/enrich-email.js.map +1 -0
  25. package/dist/tools/enrich-emails.d.ts +31 -0
  26. package/dist/tools/enrich-emails.d.ts.map +1 -0
  27. package/dist/tools/enrich-emails.js +146 -0
  28. package/dist/tools/enrich-emails.js.map +1 -0
  29. package/dist/tools/enrich-person.d.ts +26 -0
  30. package/dist/tools/enrich-person.d.ts.map +1 -0
  31. package/dist/tools/enrich-person.js +23 -0
  32. package/dist/tools/enrich-person.js.map +1 -0
  33. package/dist/tools/fetch-page-content.d.ts +22 -0
  34. package/dist/tools/fetch-page-content.d.ts.map +1 -0
  35. package/dist/tools/fetch-page-content.js +32 -0
  36. package/dist/tools/fetch-page-content.js.map +1 -0
  37. package/dist/tools/find-email.d.ts +24 -0
  38. package/dist/tools/find-email.d.ts.map +1 -0
  39. package/dist/tools/find-email.js +19 -0
  40. package/dist/tools/find-email.js.map +1 -0
  41. package/dist/tools/find-emails.d.ts +31 -0
  42. package/dist/tools/find-emails.d.ts.map +1 -0
  43. package/dist/tools/find-emails.js +146 -0
  44. package/dist/tools/find-emails.js.map +1 -0
  45. package/dist/tools/find-influencers.d.ts +29 -0
  46. package/dist/tools/find-influencers.d.ts.map +1 -0
  47. package/dist/tools/find-influencers.js +30 -0
  48. package/dist/tools/find-influencers.js.map +1 -0
  49. package/dist/tools/find-people.d.ts +26 -0
  50. package/dist/tools/find-people.d.ts.map +1 -0
  51. package/dist/tools/find-people.js +61 -0
  52. package/dist/tools/find-people.js.map +1 -0
  53. package/dist/tools/find-phone.d.ts +24 -0
  54. package/dist/tools/find-phone.d.ts.map +1 -0
  55. package/dist/tools/find-phone.js +48 -0
  56. package/dist/tools/find-phone.js.map +1 -0
  57. package/dist/tools/find-signals.d.ts +26 -0
  58. package/dist/tools/find-signals.d.ts.map +1 -0
  59. package/dist/tools/find-signals.js +82 -0
  60. package/dist/tools/find-signals.js.map +1 -0
  61. package/dist/tools/search-ads.d.ts +33 -0
  62. package/dist/tools/search-ads.d.ts.map +1 -0
  63. package/dist/tools/search-ads.js +33 -0
  64. package/dist/tools/search-ads.js.map +1 -0
  65. package/dist/tools/search-companies.d.ts +42 -0
  66. package/dist/tools/search-companies.d.ts.map +1 -0
  67. package/dist/tools/search-companies.js +37 -0
  68. package/dist/tools/search-companies.js.map +1 -0
  69. package/dist/tools/search-jobs.d.ts +51 -0
  70. package/dist/tools/search-jobs.d.ts.map +1 -0
  71. package/dist/tools/search-jobs.js +64 -0
  72. package/dist/tools/search-jobs.js.map +1 -0
  73. package/dist/tools/search-places.d.ts +47 -0
  74. package/dist/tools/search-places.d.ts.map +1 -0
  75. package/dist/tools/search-places.js +42 -0
  76. package/dist/tools/search-places.js.map +1 -0
  77. package/dist/tools/search-reddit.d.ts +27 -0
  78. package/dist/tools/search-reddit.d.ts.map +1 -0
  79. package/dist/tools/search-reddit.js +30 -0
  80. package/dist/tools/search-reddit.js.map +1 -0
  81. package/dist/tools/search-seo.d.ts +37 -0
  82. package/dist/tools/search-seo.d.ts.map +1 -0
  83. package/dist/tools/search-seo.js +49 -0
  84. package/dist/tools/search-seo.js.map +1 -0
  85. package/dist/tools/search-web.d.ts +23 -0
  86. package/dist/tools/search-web.d.ts.map +1 -0
  87. package/dist/tools/search-web.js +20 -0
  88. package/dist/tools/search-web.js.map +1 -0
  89. package/dist/tools/verify-email.d.ts +20 -0
  90. package/dist/tools/verify-email.d.ts.map +1 -0
  91. package/dist/tools/verify-email.js +15 -0
  92. package/dist/tools/verify-email.js.map +1 -0
  93. package/package.json +28 -0
  94. package/src/client.ts +60 -0
  95. package/src/executor.ts +182 -0
  96. package/src/index.ts +155 -0
  97. package/src/registry.ts +3159 -0
  98. package/src/tools/enrich-company.ts +25 -0
  99. package/src/tools/enrich-person.ts +27 -0
  100. package/src/tools/fetch-page-content.ts +36 -0
  101. package/src/tools/find-email.ts +23 -0
  102. package/src/tools/find-emails.ts +190 -0
  103. package/src/tools/find-influencers.ts +34 -0
  104. package/src/tools/find-people.ts +69 -0
  105. package/src/tools/find-phone.ts +53 -0
  106. package/src/tools/find-signals.ts +93 -0
  107. package/src/tools/search-ads.ts +44 -0
  108. package/src/tools/search-companies.ts +41 -0
  109. package/src/tools/search-jobs.ts +73 -0
  110. package/src/tools/search-places.ts +52 -0
  111. package/src/tools/search-reddit.ts +34 -0
  112. package/src/tools/search-seo.ts +59 -0
  113. package/src/tools/search-web.ts +24 -0
  114. package/src/tools/verify-email.ts +19 -0
  115. package/test-ads-live.ts +77 -0
  116. package/test-company-live.ts +91 -0
  117. package/test-email-live.ts +171 -0
  118. package/test-influencers-live.ts +66 -0
  119. package/test-jobs-live.ts +69 -0
  120. package/test-linkupapi-live.ts +137 -0
  121. package/test-phone-live.ts +41 -0
  122. package/test-places-live.ts +89 -0
  123. package/test-reddit-live.ts +66 -0
  124. package/test-search-live.ts +79 -0
  125. package/test-seo-live.ts +68 -0
  126. package/test-web-live.ts +67 -0
  127. package/tests/client.test.ts +90 -0
  128. package/tests/executor.test.ts +83 -0
  129. package/tests/gtm/01-icp-to-emails.test.ts +43 -0
  130. package/tests/gtm/02-icp-bulk-emails.test.ts +38 -0
  131. package/tests/gtm/03-icp-to-phones.test.ts +39 -0
  132. package/tests/gtm/04-funding-signal-outreach.test.ts +42 -0
  133. package/tests/gtm/05-hiring-signal-decisionmakers.test.ts +41 -0
  134. package/tests/gtm/06-intent-signal-outreach.test.ts +44 -0
  135. package/tests/gtm/07-places-to-content.test.ts +50 -0
  136. package/tests/gtm/08-domain-to-account.test.ts +44 -0
  137. package/tests/gtm/09-linkedin-to-everything.test.ts +41 -0
  138. package/tests/gtm/10-jobs-vs-signals-routing.test.ts +38 -0
  139. package/tests/gtm/11-find-vs-enrich-routing.test.ts +39 -0
  140. package/tests/gtm/12-bogus-domain-graceful.test.ts +42 -0
  141. package/tests/gtm/13-private-linkedin-graceful.test.ts +44 -0
  142. package/tests/gtm/14-empty-handoff.test.ts +43 -0
  143. package/tests/gtm/15-seo-reddit-research.test.ts +38 -0
  144. package/tests/gtm/README.md +59 -0
  145. package/tests/gtm/harness.ts +217 -0
  146. package/tests/gtm/tools-bridge.ts +232 -0
  147. package/tests/gtm-scenarios.md +32 -0
  148. package/tests/live/smoke-report.ts +255 -0
  149. package/tests/live/smoke.test.ts +134 -0
  150. package/tests/registry-enrich-person.test.ts +447 -0
  151. package/tests/registry-fetch-page-content.test.ts +90 -0
  152. package/tests/registry-find-people.test.ts +467 -0
  153. package/tests/registry-find-signals.test.ts +470 -0
  154. package/tests/registry-linkupapi.test.ts +331 -0
  155. package/tests/registry-search-companies.test.ts +188 -0
  156. package/tests/registry-search-jobs.test.ts +116 -0
  157. package/tests/registry.test.ts +2210 -0
  158. package/tests/tools/enrich-company.test.ts +92 -0
  159. package/tests/tools/enrich-email.test.ts +94 -0
  160. package/tests/tools/enrich-emails.test.ts +271 -0
  161. package/tests/tools/enrich-person.test.ts +140 -0
  162. package/tests/tools/fetch-page-content.test.ts +108 -0
  163. package/tests/tools/find-influencers.test.ts +91 -0
  164. package/tests/tools/find-people.test.ts +344 -0
  165. package/tests/tools/find-phone.test.ts +100 -0
  166. package/tests/tools/find-signals.test.ts +110 -0
  167. package/tests/tools/search-ads.test.ts +182 -0
  168. package/tests/tools/search-companies.test.ts +58 -0
  169. package/tests/tools/search-jobs.test.ts +210 -0
  170. package/tests/tools/search-places.test.ts +114 -0
  171. package/tests/tools/search-reddit.test.ts +125 -0
  172. package/tests/tools/search-seo.test.ts +183 -0
  173. package/tests/tools/search-web.test.ts +79 -0
  174. package/tests/tools/verify-email.test.ts +68 -0
  175. package/tsconfig.json +17 -0
  176. package/vitest.config.ts +7 -0
@@ -0,0 +1,90 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { initClient, callApi } from '../src/client.js'
3
+
4
+ describe('client', () => {
5
+ const originalFetch = globalThis.fetch
6
+
7
+ beforeEach(() => {
8
+ initClient('http://test-api.local', 'test-key-123')
9
+ })
10
+
11
+ afterEach(() => {
12
+ globalThis.fetch = originalFetch
13
+ })
14
+
15
+ it('throws if COLDIQ_API_KEY is empty', () => {
16
+ expect(() => initClient('http://localhost', '')).toThrow('COLDIQ_API_KEY is required')
17
+ })
18
+
19
+ it('prefixes paths with /v1', async () => {
20
+ let capturedUrl = ''
21
+ globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
22
+ capturedUrl = url.toString()
23
+ return new Response(JSON.stringify({ ok: true }), { status: 200 })
24
+ }) as typeof fetch
25
+
26
+ await callApi('POST', '/companyenrich/companies/search', { pageSize: 10 })
27
+
28
+ expect(capturedUrl).toBe('http://test-api.local/v1/companyenrich/companies/search')
29
+ })
30
+
31
+ it('sets Authorization header with Bearer token', async () => {
32
+ let capturedHeaders: Record<string, string> = {}
33
+ globalThis.fetch = vi.fn(async (_url: string | URL | Request, init?: RequestInit) => {
34
+ capturedHeaders = Object.fromEntries(Object.entries(init?.headers ?? {}))
35
+ return new Response(JSON.stringify({}), { status: 200 })
36
+ }) as typeof fetch
37
+
38
+ await callApi('POST', '/test', { foo: 'bar' })
39
+
40
+ expect(capturedHeaders['Authorization']).toBe('Bearer test-key-123')
41
+ expect(capturedHeaders['Content-Type']).toBe('application/json')
42
+ })
43
+
44
+ it('appends query params for GET requests', async () => {
45
+ let capturedUrl = ''
46
+ globalThis.fetch = vi.fn(async (url: string | URL | Request) => {
47
+ capturedUrl = url.toString()
48
+ return new Response(JSON.stringify({}), { status: 200 })
49
+ }) as typeof fetch
50
+
51
+ await callApi('GET', '/companies/enrich', undefined, { domain: 'stripe.com' })
52
+
53
+ expect(capturedUrl).toContain('domain=stripe.com')
54
+ })
55
+
56
+ it('returns ok: false on network error', async () => {
57
+ globalThis.fetch = vi.fn(async () => {
58
+ throw new Error('Network timeout')
59
+ }) as typeof fetch
60
+
61
+ const result = await callApi('POST', '/test', {})
62
+
63
+ expect(result.ok).toBe(false)
64
+ expect(result.status).toBe(0)
65
+ expect((result.data as Record<string, unknown>).error).toBe('Network timeout')
66
+ })
67
+
68
+ it('returns parsed JSON on success', async () => {
69
+ globalThis.fetch = vi.fn(async () =>
70
+ new Response(JSON.stringify({ companies: [{ name: 'ColdIQ' }] }), { status: 200 })
71
+ ) as typeof fetch
72
+
73
+ const result = await callApi('POST', '/test', {})
74
+
75
+ expect(result.ok).toBe(true)
76
+ expect(result.status).toBe(200)
77
+ expect((result.data as Record<string, unknown>).companies).toEqual([{ name: 'ColdIQ' }])
78
+ })
79
+
80
+ it('returns ok: false for 4xx/5xx responses', async () => {
81
+ globalThis.fetch = vi.fn(async () =>
82
+ new Response(JSON.stringify({ error: 'Not found' }), { status: 404 })
83
+ ) as typeof fetch
84
+
85
+ const result = await callApi('GET', '/test')
86
+
87
+ expect(result.ok).toBe(false)
88
+ expect(result.status).toBe(404)
89
+ })
90
+ })
@@ -0,0 +1,83 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
2
+ import { initClient } from '../src/client.js'
3
+ import { executeWithFallback } from '../src/executor.js'
4
+
5
+ describe('executor postFilter integration', () => {
6
+ const originalFetch = globalThis.fetch
7
+
8
+ beforeEach(() => {
9
+ initClient('http://test-api.local', 'test-key-123')
10
+ })
11
+
12
+ afterEach(() => {
13
+ globalThis.fetch = originalFetch
14
+ })
15
+
16
+ it('invokes Apollo postFilter — strips accounts and drops wrong-country orgs → falls through to error', async () => {
17
+ // Apollo is at priority 7. Mock all higher-priority providers to return empty so Apollo is reached.
18
+ // Apollo returns orgs with wrong country + accounts key.
19
+ // postFilter should: strip accounts, drop German org, keep no French orgs → hasResult false → error.
20
+ let callCount = 0
21
+ globalThis.fetch = vi.fn(async () => {
22
+ callCount++
23
+ if (callCount <= 5) {
24
+ // companyenrich, fullenrich, pdl, blitzapi, signalbase — all return empty
25
+ return new Response(JSON.stringify({}), { status: 200 })
26
+ }
27
+ if (callCount === 6) {
28
+ // theirstack — gated (no keywords in input), but if reached, return empty
29
+ return new Response(JSON.stringify({}), { status: 200 })
30
+ }
31
+ // Apollo (call 7 or beyond): returns organizations with wrong country + CRM accounts
32
+ return new Response(
33
+ JSON.stringify({
34
+ organizations: [{ name: 'GermanCo', country: 'Germany', estimated_num_employees: 100 }],
35
+ accounts: [{ id: 1, name: 'CRM leak' }],
36
+ }),
37
+ { status: 200 },
38
+ )
39
+ }) as typeof fetch
40
+
41
+ // No keywords → TheirStack skipped; no min_founded_year → Apollo eligible
42
+ const result = await executeWithFallback('search_companies', {
43
+ countries: ['FR'],
44
+ min_employees: 50,
45
+ max_employees: 200,
46
+ limit: 5,
47
+ })
48
+
49
+ // Apollo's postFilter should drop GermanCo (wrong country) → hasResult false → falls through
50
+ expect('error' in result).toBe(true)
51
+ expect(callCount).toBeGreaterThan(6)
52
+ })
53
+
54
+ it('passes raw data through unchanged when postFilter is not set (enrich_company)', async () => {
55
+ // enrich_company providers (companyenrich, apollo, pdl) have no postFilter.
56
+ // Verify a successful response is returned as-is.
57
+ globalThis.fetch = vi.fn(async () =>
58
+ new Response(JSON.stringify({ name: 'ColdIQ', domain: 'coldiq.com' }), { status: 200 })
59
+ ) as typeof fetch
60
+
61
+ const result = await executeWithFallback('enrich_company', { domain: 'coldiq.com' })
62
+
63
+ expect('data' in result).toBe(true)
64
+ if ('data' in result) {
65
+ expect((result.data as Record<string, unknown>).domain).toBe('coldiq.com')
66
+ }
67
+ })
68
+
69
+ it('passes raw data through when postFilter is set but no strict filters requested', async () => {
70
+ // search_companies with no year/employee/country filter → postFilter is a no-op.
71
+ globalThis.fetch = vi.fn(async () =>
72
+ new Response(JSON.stringify({ data: [{ name: 'ColdIQ' }] }), { status: 200 })
73
+ ) as typeof fetch
74
+
75
+ const result = await executeWithFallback('search_companies', { keywords: ['SaaS'], limit: 5 })
76
+
77
+ expect('data' in result).toBe(true)
78
+ if ('data' in result) {
79
+ const orgs = (result.data as Record<string, unknown>).data as unknown[]
80
+ expect(orgs.length).toBe(1)
81
+ }
82
+ })
83
+ })
@@ -0,0 +1,43 @@
1
+ /**
2
+ * GTM-01: Full classical ICP → email sequence (CSV-style)
3
+ *
4
+ * Prompt: GTM engineer builds a single-row lead list from scratch.
5
+ * Expected flow: search_companies → find_people → find_email → verify_email
6
+ * Forbidden: search_web (must use structured tools, not fallback to web)
7
+ * Est. credits: ~4
8
+ */
9
+
10
+ import { describe, it, beforeAll } from 'vitest'
11
+ import { initClient } from '../../src/client.js'
12
+ import { runAgent, expectTool, expectNoTool, expectOrder, writeArtifact } from './harness.js'
13
+
14
+ const RUN_LIVE =
15
+ process.env.LIVE_TESTS === '1' && !!process.env.ANTHROPIC_API_KEY && !!process.env.COLDIQ_API_KEY
16
+
17
+ describe.runIf(RUN_LIVE)('gtm-01: ICP → verified emails (CSV-style)', () => {
18
+ beforeAll(() => {
19
+ initClient(process.env.COLDIQ_API_URL, process.env.COLDIQ_API_KEY)
20
+ })
21
+
22
+ it('routes through search → people → email → verify in order', async () => {
23
+ const result = await runAgent({
24
+ prompt:
25
+ "I'm building a lead CSV. Do this step by step: " +
26
+ '1) Find 1 B2B SaaS company in France with under 200 employees. ' +
27
+ '2) Find the CEO at that company (limit 1). ' +
28
+ '3) Get their professional email address. ' +
29
+ '4) Verify the email is deliverable. ' +
30
+ 'Give me the final result as: Company | CEO name | Email | Deliverable (yes/no).',
31
+ maxToolCalls: 10,
32
+ })
33
+
34
+ expectTool(result.transcript, 'search_companies')
35
+ expectTool(result.transcript, 'find_people')
36
+ expectTool(result.transcript, 'find_email')
37
+ expectTool(result.transcript, 'verify_email')
38
+ expectOrder(result.transcript, ['search_companies', 'find_people', 'find_email', 'verify_email'])
39
+ expectNoTool(result.transcript, 'search_web')
40
+
41
+ writeArtifact('01-icp-to-emails', result)
42
+ }, 8 * 60_000)
43
+ })
@@ -0,0 +1,38 @@
1
+ /**
2
+ * GTM-02: Multi-company bulk email lookup
3
+ *
4
+ * Prompt: GTM engineer has 3 known target companies, wants emails in one batch.
5
+ * Expected flow: find_people → find_emails (batch, NOT per-person find_email loop)
6
+ * Forbidden: find_email (singular per person), search_companies (companies already known)
7
+ * Est. credits: ~4
8
+ */
9
+
10
+ import { describe, it, beforeAll } from 'vitest'
11
+ import { initClient } from '../../src/client.js'
12
+ import { runAgent, expectTool, expectNoTool, writeArtifact } from './harness.js'
13
+
14
+ const RUN_LIVE =
15
+ process.env.LIVE_TESTS === '1' && !!process.env.ANTHROPIC_API_KEY && !!process.env.COLDIQ_API_KEY
16
+
17
+ describe.runIf(RUN_LIVE)('gtm-02: multi-company bulk email lookup', () => {
18
+ beforeAll(() => {
19
+ initClient(process.env.COLDIQ_API_URL, process.env.COLDIQ_API_KEY)
20
+ })
21
+
22
+ it('uses find_emails (batch) instead of individual find_email calls', async () => {
23
+ const result = await runAgent({
24
+ prompt:
25
+ 'I have 3 target companies: Stripe (stripe.com), Notion (notion.so), and Vercel (vercel.com). ' +
26
+ 'Find 1 Head of Sales, VP Sales, or CRO at each company (limit 1 per company). ' +
27
+ 'Then use the bulk email finder to get all their professional emails in one batch — do NOT look them up one by one. ' +
28
+ 'Return a table: Name | Company | Email.',
29
+ maxToolCalls: 8,
30
+ })
31
+
32
+ expectTool(result.transcript, 'find_people')
33
+ expectTool(result.transcript, 'find_emails')
34
+ expectNoTool(result.transcript, 'search_companies')
35
+
36
+ writeArtifact('02-icp-bulk-emails', result)
37
+ }, 8 * 60_000)
38
+ })
@@ -0,0 +1,39 @@
1
+ /**
2
+ * GTM-03: Person → phone number (cold call enrichment)
3
+ *
4
+ * Prompt: GTM engineer needs a phone number for a known target person.
5
+ * Expected flow: find_people → find_phone
6
+ * Forbidden: enrich_person (they don't have a LinkedIn URL, not a full enrichment), search_web
7
+ * Est. credits: ~3
8
+ */
9
+
10
+ import { describe, it, beforeAll } from 'vitest'
11
+ import { initClient } from '../../src/client.js'
12
+ import { runAgent, expectTool, expectNoTool, expectOrder, writeArtifact } from './harness.js'
13
+
14
+ const RUN_LIVE =
15
+ process.env.LIVE_TESTS === '1' && !!process.env.ANTHROPIC_API_KEY && !!process.env.COLDIQ_API_KEY
16
+
17
+ describe.runIf(RUN_LIVE)('gtm-03: find person then phone number', () => {
18
+ beforeAll(() => {
19
+ initClient(process.env.COLDIQ_API_URL, process.env.COLDIQ_API_KEY)
20
+ })
21
+
22
+ it('finds CEO at ColdIQ then retrieves their phone number', async () => {
23
+ const result = await runAgent({
24
+ prompt:
25
+ "I need to cold call the CEO of ColdIQ (domain: coldiq.com). " +
26
+ 'First find the CEO there (limit 1 result), then get their phone number using their LinkedIn profile URL from the search result. ' +
27
+ "If no phone is found, say so — that's fine.",
28
+ maxToolCalls: 6,
29
+ })
30
+
31
+ expectTool(result.transcript, 'find_people')
32
+ expectTool(result.transcript, 'find_phone')
33
+ expectOrder(result.transcript, ['find_people', 'find_phone'])
34
+ expectNoTool(result.transcript, 'enrich_person')
35
+ expectNoTool(result.transcript, 'search_web')
36
+
37
+ writeArtifact('03-icp-to-phones', result)
38
+ }, 5 * 60_000)
39
+ })
@@ -0,0 +1,42 @@
1
+ /**
2
+ * GTM-04: Funding signal → decision-maker → email
3
+ *
4
+ * Prompt: GTM engineer targets companies that just raised money.
5
+ * Expected flow: find_signals (funding) → find_people → find_email
6
+ * Forbidden: search_companies (signals, not firmographic search)
7
+ * Est. credits: ~4
8
+ */
9
+
10
+ import { describe, it, beforeAll } from 'vitest'
11
+ import { initClient } from '../../src/client.js'
12
+ import { runAgent, expectTool, expectNoTool, expectOrder, expectParam, writeArtifact } from './harness.js'
13
+
14
+ const RUN_LIVE =
15
+ process.env.LIVE_TESTS === '1' && !!process.env.ANTHROPIC_API_KEY && !!process.env.COLDIQ_API_KEY
16
+
17
+ describe.runIf(RUN_LIVE)('gtm-04: funding signal → outreach contact', () => {
18
+ beforeAll(() => {
19
+ initClient(process.env.COLDIQ_API_URL, process.env.COLDIQ_API_KEY)
20
+ })
21
+
22
+ it('routes through signals (funding) → people → email', async () => {
23
+ const result = await runAgent({
24
+ prompt:
25
+ 'I target companies right after they raise money. ' +
26
+ 'Find 1 company that recently received Series A funding in France. ' +
27
+ 'Then find their CEO or Founder (limit 1). ' +
28
+ 'Then get the CEO/Founder email. ' +
29
+ 'Return: Company | CEO name | Email.',
30
+ maxToolCalls: 8,
31
+ })
32
+
33
+ expectTool(result.transcript, 'find_signals')
34
+ expectTool(result.transcript, 'find_people')
35
+ expectTool(result.transcript, 'find_email')
36
+ expectOrder(result.transcript, ['find_signals', 'find_people', 'find_email'])
37
+ expectNoTool(result.transcript, 'search_companies')
38
+ expectParam(result.transcript, 'find_signals', 'signal_type', (v) => String(v) === 'funding')
39
+
40
+ writeArtifact('04-funding-signal-outreach', result)
41
+ }, 8 * 60_000)
42
+ })
@@ -0,0 +1,41 @@
1
+ /**
2
+ * GTM-05: Job posting signal → decision-maker lookup
3
+ *
4
+ * Prompt: "Companies hiring SDRs in NYC" should use live job postings (search_jobs),
5
+ * NOT the company-level hiring signal (find_signals). This is the key boundary pair.
6
+ * Then find VP Sales at the hiring company.
7
+ * Expected flow: search_jobs → find_people
8
+ * Forbidden: find_signals (wrong tool for individual job postings)
9
+ * Est. credits: ~4
10
+ */
11
+
12
+ import { describe, it, beforeAll } from 'vitest'
13
+ import { initClient } from '../../src/client.js'
14
+ import { runAgent, expectTool, expectNoTool, expectOrder, writeArtifact } from './harness.js'
15
+
16
+ const RUN_LIVE =
17
+ process.env.LIVE_TESTS === '1' && !!process.env.ANTHROPIC_API_KEY && !!process.env.COLDIQ_API_KEY
18
+
19
+ describe.runIf(RUN_LIVE)('gtm-05: job postings → decision-maker', () => {
20
+ beforeAll(() => {
21
+ initClient(process.env.COLDIQ_API_URL, process.env.COLDIQ_API_KEY)
22
+ })
23
+
24
+ it('uses search_jobs (not find_signals) to find hiring companies, then finds VP Sales', async () => {
25
+ const result = await runAgent({
26
+ prompt:
27
+ 'Find companies in New York that are actively posting SDR or Business Development Rep job openings right now. ' +
28
+ 'Use the job postings tool to find actual live listings (use the minimum allowed limit). ' +
29
+ 'Pick the first company from the results, then find the VP of Sales or Head of Sales at that company (limit 1 person). ' +
30
+ 'Return: Company | Job title of decision-maker | LinkedIn URL.',
31
+ maxToolCalls: 6,
32
+ })
33
+
34
+ expectTool(result.transcript, 'search_jobs')
35
+ expectTool(result.transcript, 'find_people')
36
+ expectOrder(result.transcript, ['search_jobs', 'find_people'])
37
+ expectNoTool(result.transcript, 'find_signals')
38
+
39
+ writeArtifact('05-hiring-signal-decisionmakers', result)
40
+ }, 8 * 60_000)
41
+ })
@@ -0,0 +1,44 @@
1
+ /**
2
+ * GTM-06: Buying intent signal → outreach contact
3
+ *
4
+ * Prompt: GTM engineer validates a known target company's buying signals before outreach.
5
+ * find_signals(intent) returns what topics a company is actively evaluating (based on job signals).
6
+ * Expected flow: find_signals (intent) → find_people → find_email
7
+ * Forbidden: search_companies (we already know the target company)
8
+ * Est. credits: ~4
9
+ */
10
+
11
+ import { describe, it, beforeAll } from 'vitest'
12
+ import { initClient } from '../../src/client.js'
13
+ import { runAgent, expectTool, expectNoTool, expectOrder, expectParam, writeArtifact } from './harness.js'
14
+
15
+ const RUN_LIVE =
16
+ process.env.LIVE_TESTS === '1' && !!process.env.ANTHROPIC_API_KEY && !!process.env.COLDIQ_API_KEY
17
+
18
+ describe.runIf(RUN_LIVE)('gtm-06: buying intent signal → outreach', () => {
19
+ beforeAll(() => {
20
+ initClient(process.env.COLDIQ_API_URL, process.env.COLDIQ_API_KEY)
21
+ })
22
+
23
+ it('routes through find_signals(intent) → find_people → find_email', async () => {
24
+ const result = await runAgent({
25
+ prompt:
26
+ 'I want to personalize my outreach to ColdIQ before contacting them. ' +
27
+ 'First, check their buying intent signals to understand what technologies they are actively evaluating (limit 1 signal). ' +
28
+ 'Use the intent signal tool — not a company search. ' +
29
+ 'Then find their CEO or VP Sales (limit 1 person). ' +
30
+ 'Then get that person\'s professional email. ' +
31
+ 'Return: Company | Top buying intent topic | Contact name | Email.',
32
+ maxToolCalls: 8,
33
+ })
34
+
35
+ expectTool(result.transcript, 'find_signals')
36
+ expectTool(result.transcript, 'find_people')
37
+ expectTool(result.transcript, 'find_email')
38
+ expectOrder(result.transcript, ['find_signals', 'find_people', 'find_email'])
39
+ expectNoTool(result.transcript, 'search_companies')
40
+ expectParam(result.transcript, 'find_signals', 'signal_type', (v) => String(v) === 'intent')
41
+
42
+ writeArtifact('06-intent-signal-outreach', result)
43
+ }, 8 * 60_000)
44
+ })
@@ -0,0 +1,50 @@
1
+ /**
2
+ * GTM-07: Local place search → website content extraction
3
+ *
4
+ * Prompt: Local business research — find a place, then read its website.
5
+ * Expected flow: search_places → fetch_page_content
6
+ * Forbidden: search_web (we need structured place data first, not a web search)
7
+ * Est. credits: ~3
8
+ */
9
+
10
+ import { describe, it, beforeAll } from 'vitest'
11
+ import { initClient } from '../../src/client.js'
12
+ import { runAgent, expectTool, expectNoTool, expectOrder, writeArtifact } from './harness.js'
13
+
14
+ const RUN_LIVE =
15
+ process.env.LIVE_TESTS === '1' && !!process.env.ANTHROPIC_API_KEY && !!process.env.COLDIQ_API_KEY
16
+
17
+ describe.runIf(RUN_LIVE)('gtm-07: place search → website content', () => {
18
+ beforeAll(() => {
19
+ initClient(process.env.COLDIQ_API_URL, process.env.COLDIQ_API_KEY)
20
+ })
21
+
22
+ it('finds a coffee shop near Times Square then fetches its website content', async () => {
23
+ const result = await runAgent({
24
+ prompt:
25
+ 'I am doing local business research. ' +
26
+ 'Find 1 coffee shop near Times Square in New York (limit 1 result). ' +
27
+ 'If the place has a website URL in the result, fetch the content of that website. ' +
28
+ "If there's no website URL in the result, just report what information you found about the place. " +
29
+ 'Summarize what the place offers.',
30
+ maxToolCalls: 6,
31
+ })
32
+
33
+ expectTool(result.transcript, 'search_places')
34
+ expectNoTool(result.transcript, 'search_web')
35
+
36
+ const transcript = result.transcript
37
+ const hasPageFetch = transcript.some((c) => c.tool === 'fetch_page_content')
38
+ const searchPlacesResult = transcript.find((c) => c.tool === 'search_places')
39
+ // If search_places returned a website URL, the agent should have fetched it
40
+ if (hasPageFetch) {
41
+ expectOrder(transcript, ['search_places', 'fetch_page_content'])
42
+ } else {
43
+ // No website URL in result — acceptable, agent should report the place data
44
+ const hasData = searchPlacesResult && !searchPlacesResult.isError
45
+ if (!hasData) throw new Error('search_places returned an error and no fetch_page_content was attempted')
46
+ }
47
+
48
+ writeArtifact('07-places-to-content', result)
49
+ }, 8 * 60_000)
50
+ })
@@ -0,0 +1,44 @@
1
+ /**
2
+ * GTM-08: Domain → full account intelligence
3
+ *
4
+ * Prompt: GTM engineer targets a specific known company, wants a full picture.
5
+ * Expected flow: enrich_company → find_people → find_email
6
+ * Forbidden: search_companies (we already know the domain, firmographic search is wasteful)
7
+ * Est. credits: ~4
8
+ */
9
+
10
+ import { describe, it, beforeAll } from 'vitest'
11
+ import { initClient } from '../../src/client.js'
12
+ import { runAgent, expectTool, expectNoTool, expectOrder, expectParam, writeArtifact } from './harness.js'
13
+
14
+ const RUN_LIVE =
15
+ process.env.LIVE_TESTS === '1' && !!process.env.ANTHROPIC_API_KEY && !!process.env.COLDIQ_API_KEY
16
+
17
+ describe.runIf(RUN_LIVE)('gtm-08: domain → account + contact + email', () => {
18
+ beforeAll(() => {
19
+ initClient(process.env.COLDIQ_API_URL, process.env.COLDIQ_API_KEY)
20
+ })
21
+
22
+ it('enriches company then finds contact then gets email', async () => {
23
+ const result = await runAgent({
24
+ prompt:
25
+ "I want to target ColdIQ for outbound. Their domain is coldiq.com. " +
26
+ '1) Enrich the company to understand what they do, their size, and industry. ' +
27
+ '2) Find 1 key decision-maker there — CEO, Founder, or VP Sales. ' +
28
+ '3) Get their professional email. ' +
29
+ 'Return: a brief company summary + the contact name + email.',
30
+ maxToolCalls: 8,
31
+ })
32
+
33
+ expectTool(result.transcript, 'enrich_company')
34
+ expectTool(result.transcript, 'find_people')
35
+ expectTool(result.transcript, 'find_email')
36
+ expectOrder(result.transcript, ['enrich_company', 'find_people', 'find_email'])
37
+ expectNoTool(result.transcript, 'search_companies')
38
+ expectParam(result.transcript, 'enrich_company', 'domain', (v) =>
39
+ String(v).toLowerCase().includes('coldiq'),
40
+ )
41
+
42
+ writeArtifact('08-domain-to-account', result)
43
+ }, 8 * 60_000)
44
+ })
@@ -0,0 +1,41 @@
1
+ /**
2
+ * GTM-09: LinkedIn URL → person enrichment + company enrichment
3
+ *
4
+ * Prompt: GTM engineer has a LinkedIn URL, wants full profile and company context.
5
+ * Expected flow: enrich_person → enrich_company (using domain from person enrichment)
6
+ * Forbidden: find_people (we have the URL already, no need to search)
7
+ * Est. credits: ~3
8
+ */
9
+
10
+ import { describe, it, beforeAll } from 'vitest'
11
+ import { initClient } from '../../src/client.js'
12
+ import { runAgent, expectTool, expectNoTool, expectOrder, expectParam, writeArtifact } from './harness.js'
13
+
14
+ const RUN_LIVE =
15
+ process.env.LIVE_TESTS === '1' && !!process.env.ANTHROPIC_API_KEY && !!process.env.COLDIQ_API_KEY
16
+
17
+ describe.runIf(RUN_LIVE)('gtm-09: LinkedIn URL → person + company enrichment', () => {
18
+ beforeAll(() => {
19
+ initClient(process.env.COLDIQ_API_URL, process.env.COLDIQ_API_KEY)
20
+ })
21
+
22
+ it('enriches person then enriches their company', async () => {
23
+ const result = await runAgent({
24
+ prompt:
25
+ 'Enrich this LinkedIn profile: https://www.linkedin.com/in/michel-lieben. ' +
26
+ 'After getting the person data, also enrich their company using the domain from the person result. ' +
27
+ 'Give me: full name, job title, company name, company size, and company industry.',
28
+ maxToolCalls: 6,
29
+ })
30
+
31
+ expectTool(result.transcript, 'enrich_person')
32
+ expectTool(result.transcript, 'enrich_company')
33
+ expectOrder(result.transcript, ['enrich_person', 'enrich_company'])
34
+ expectNoTool(result.transcript, 'find_people')
35
+ expectParam(result.transcript, 'enrich_person', 'linkedin_url', (v) =>
36
+ String(v).toLowerCase().includes('michel-lieben'),
37
+ )
38
+
39
+ writeArtifact('09-linkedin-to-everything', result)
40
+ }, 6 * 60_000)
41
+ })
@@ -0,0 +1,38 @@
1
+ /**
2
+ * GTM-10: Boundary pair — search_jobs vs find_signals(hiring)
3
+ *
4
+ * "Companies hiring sales reps right now" should route to search_jobs (individual live postings),
5
+ * NOT find_signals (company-level aggregated hiring surge signal).
6
+ *
7
+ * This is a pure routing test — we care about WHICH tool is chosen, not the result.
8
+ * Expected: search_jobs
9
+ * Forbidden: find_signals
10
+ * Est. credits: ~2
11
+ */
12
+
13
+ import { describe, it, beforeAll } from 'vitest'
14
+ import { initClient } from '../../src/client.js'
15
+ import { runAgent, expectTool, expectNoTool, writeArtifact } from './harness.js'
16
+
17
+ const RUN_LIVE =
18
+ process.env.LIVE_TESTS === '1' && !!process.env.ANTHROPIC_API_KEY && !!process.env.COLDIQ_API_KEY
19
+
20
+ describe.runIf(RUN_LIVE)('gtm-10: routing — search_jobs vs find_signals(hiring)', () => {
21
+ beforeAll(() => {
22
+ initClient(process.env.COLDIQ_API_URL, process.env.COLDIQ_API_KEY)
23
+ })
24
+
25
+ it('routes to search_jobs, not find_signals, for live job posting queries', async () => {
26
+ const result = await runAgent({
27
+ prompt:
28
+ 'What companies in New York are posting job openings for Account Executive or Sales Rep right now? ' +
29
+ 'I want to see the actual job listings — company name, job title, and posting date. Use the minimum allowed limit.',
30
+ maxToolCalls: 4,
31
+ })
32
+
33
+ expectTool(result.transcript, 'search_jobs')
34
+ expectNoTool(result.transcript, 'find_signals')
35
+
36
+ writeArtifact('10-jobs-vs-signals-routing', result)
37
+ }, 5 * 60_000)
38
+ })
@@ -0,0 +1,39 @@
1
+ /**
2
+ * GTM-11: Boundary pair — find_email vs enrich_person
3
+ *
4
+ * "What is X's email at company Y?" should route to find_email (name+domain → email),
5
+ * NOT enrich_person (which enriches a full profile from a LinkedIn URL or known email).
6
+ *
7
+ * Expected: find_email
8
+ * Forbidden: enrich_person
9
+ * Est. credits: ~1
10
+ */
11
+
12
+ import { describe, it, beforeAll } from 'vitest'
13
+ import { initClient } from '../../src/client.js'
14
+ import { runAgent, expectTool, expectNoTool, expectParam, writeArtifact } from './harness.js'
15
+
16
+ const RUN_LIVE =
17
+ process.env.LIVE_TESTS === '1' && !!process.env.ANTHROPIC_API_KEY && !!process.env.COLDIQ_API_KEY
18
+
19
+ describe.runIf(RUN_LIVE)('gtm-11: routing — find_email vs enrich_person', () => {
20
+ beforeAll(() => {
21
+ initClient(process.env.COLDIQ_API_URL, process.env.COLDIQ_API_KEY)
22
+ })
23
+
24
+ it('routes to find_email (not enrich_person) for name + domain → email queries', async () => {
25
+ const result = await runAgent({
26
+ prompt:
27
+ "What is Michel Lieben's professional email address? He is the CEO at ColdIQ — their domain is coldiq.com.",
28
+ maxToolCalls: 4,
29
+ })
30
+
31
+ expectTool(result.transcript, 'find_email')
32
+ expectNoTool(result.transcript, 'enrich_person')
33
+ expectParam(result.transcript, 'find_email', 'domain', (v) =>
34
+ String(v).toLowerCase().includes('coldiq'),
35
+ )
36
+
37
+ writeArtifact('11-find-vs-enrich-routing', result)
38
+ }, 3 * 60_000)
39
+ })