@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
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin client for the deployed /v1 GTM verb endpoints.
|
|
3
|
+
*
|
|
4
|
+
* Each verb takes an envelope `{ input: {...filters}, provider?, fields?, reveal? }`
|
|
5
|
+
* (or `{ inputs: [...] }` for bulk) and returns `{ data, _meta }` (or `{ results }`
|
|
6
|
+
* for bulk, or `{ error, providers_tried }` on failure with a 4xx/5xx status).
|
|
7
|
+
*
|
|
8
|
+
* The MCP tool schemas expose `use_providers` (the ordered provider list) and the
|
|
9
|
+
* optional `fields` / `reveal` knobs alongside the verb's filter fields. This helper
|
|
10
|
+
* splits those out and maps everything else into the verb envelope, so each tool
|
|
11
|
+
* handler collapses to a single delegating call.
|
|
12
|
+
*
|
|
13
|
+
* A few MCP tool schemas predate the verb input schemas and need light mapping:
|
|
14
|
+
* - `remap`: rename input keys before building the envelope (e.g. find_phone's
|
|
15
|
+
* `company_domain` → the verb's `domain`).
|
|
16
|
+
* - `providerFromField`: a tool-specific provider-pin field (e.g. search_places'
|
|
17
|
+
* `provider: openmart|google_maps`) → the envelope's provider list.
|
|
18
|
+
* - `reattachIdField`: bulk verbs return `results` positionally with no echo of the
|
|
19
|
+
* per-item correlation id, so re-attach `input[bulkField][i][id]` onto `results[i]`
|
|
20
|
+
* so callers can still match results back.
|
|
21
|
+
*/
|
|
22
|
+
export interface CallVerbOptions {
|
|
23
|
+
bulkField?: string;
|
|
24
|
+
remap?: Record<string, string>;
|
|
25
|
+
providerFromField?: string;
|
|
26
|
+
reattachIdField?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function callVerb(verbPath: string, input: Record<string, unknown>, opts?: CallVerbOptions): Promise<{
|
|
29
|
+
content: [{
|
|
30
|
+
type: 'text';
|
|
31
|
+
text: string;
|
|
32
|
+
}];
|
|
33
|
+
isError?: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
//# sourceMappingURL=verb-client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verb-client.d.ts","sourceRoot":"","sources":["../src/verb-client.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC9B,iBAAiB,CAAC,EAAE,MAAM,CAAA;IAC1B,eAAe,CAAC,EAAE,MAAM,CAAA;CACzB;AAED,wBAAsB,QAAQ,CAC5B,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,IAAI,CAAC,EAAE,eAAe,GACrB,OAAO,CAAC;IAAE,OAAO,EAAE,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,CA0D3E"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { callApi } from './client.js';
|
|
2
|
+
export async function callVerb(verbPath, input, opts) {
|
|
3
|
+
// 1. Rename any schema-drift field names before splitting the envelope.
|
|
4
|
+
let work = input;
|
|
5
|
+
if (opts?.remap) {
|
|
6
|
+
work = {};
|
|
7
|
+
for (const [k, v] of Object.entries(input)) {
|
|
8
|
+
work[opts.remap[k] ?? k] = v;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const { use_providers, fields, reveal, ...rest } = work;
|
|
12
|
+
// 2. Resolve the provider pin: explicit use_providers wins; otherwise a
|
|
13
|
+
// tool-specific provider field (e.g. search_places `provider`).
|
|
14
|
+
let providerPin = Array.isArray(use_providers) && use_providers.length ? use_providers : undefined;
|
|
15
|
+
if (opts?.providerFromField && rest[opts.providerFromField] !== undefined) {
|
|
16
|
+
const v = rest[opts.providerFromField];
|
|
17
|
+
delete rest[opts.providerFromField];
|
|
18
|
+
if (!providerPin && typeof v === 'string' && v)
|
|
19
|
+
providerPin = [v];
|
|
20
|
+
}
|
|
21
|
+
// 3. Build the envelope (single vs bulk).
|
|
22
|
+
let envelope;
|
|
23
|
+
let list;
|
|
24
|
+
if (opts?.bulkField) {
|
|
25
|
+
const { [opts.bulkField]: l, ...siblings } = rest;
|
|
26
|
+
list = Array.isArray(l) ? l : [];
|
|
27
|
+
envelope = { ...siblings, inputs: list };
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
envelope = { input: rest };
|
|
31
|
+
}
|
|
32
|
+
if (providerPin)
|
|
33
|
+
envelope.provider = providerPin;
|
|
34
|
+
if (fields !== undefined)
|
|
35
|
+
envelope.fields = fields;
|
|
36
|
+
if (reveal !== undefined)
|
|
37
|
+
envelope.reveal = reveal;
|
|
38
|
+
const res = await callApi('POST', verbPath, envelope);
|
|
39
|
+
// 4. Re-attach the per-item correlation id to bulk results (positional).
|
|
40
|
+
let data = res.data;
|
|
41
|
+
if (opts?.reattachIdField && opts?.bulkField && list &&
|
|
42
|
+
data && typeof data === 'object' && Array.isArray(data.results)) {
|
|
43
|
+
const idField = opts.reattachIdField;
|
|
44
|
+
const results = data.results.map((r, i) => {
|
|
45
|
+
const id = list?.[i]?.[idField];
|
|
46
|
+
return id !== undefined && r && typeof r === 'object' && !Array.isArray(r)
|
|
47
|
+
? { [idField]: id, ...r }
|
|
48
|
+
: r;
|
|
49
|
+
});
|
|
50
|
+
data = { ...data, results };
|
|
51
|
+
}
|
|
52
|
+
return {
|
|
53
|
+
content: [{ type: 'text', text: JSON.stringify(data) }],
|
|
54
|
+
isError: !res.ok,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=verb-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"verb-client.js","sourceRoot":"","sources":["../src/verb-client.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
package/src/client.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Agent, setGlobalDispatcher } from 'undici'
|
|
2
|
+
import { recordChargeFromHeaders } from './session-usage.js'
|
|
2
3
|
|
|
3
4
|
export type ApiResponse = {
|
|
4
5
|
ok: boolean
|
|
@@ -75,6 +76,9 @@ export async function callApi(
|
|
|
75
76
|
res.headers.forEach((value, key) => {
|
|
76
77
|
headers[key.toLowerCase()] = value
|
|
77
78
|
})
|
|
79
|
+
// Tally this call's credit charge for the per-session usage report. No-op
|
|
80
|
+
// unless the response carries a charge header, so polls/errors are skipped.
|
|
81
|
+
recordChargeFromHeaders(path, headers)
|
|
78
82
|
return { ok: res.ok, status: res.status, data, headers }
|
|
79
83
|
} catch (err) {
|
|
80
84
|
const isTimeout =
|
package/src/index.ts
CHANGED
|
@@ -130,6 +130,13 @@ import {
|
|
|
130
130
|
getCreditBalanceHandler,
|
|
131
131
|
} from './tools/get-credit-balance.js'
|
|
132
132
|
|
|
133
|
+
import {
|
|
134
|
+
getSessionUsageName,
|
|
135
|
+
getSessionUsageDescription,
|
|
136
|
+
getSessionUsageSchema,
|
|
137
|
+
getSessionUsageHandler,
|
|
138
|
+
} from './tools/get-session-usage.js'
|
|
139
|
+
|
|
133
140
|
import {
|
|
134
141
|
extractPostEngagementName,
|
|
135
142
|
extractPostEngagementDescription,
|
|
@@ -192,6 +199,7 @@ server.tool(searchSeoName, searchSeoDescription, searchSeoSchema, searchSeoHandl
|
|
|
192
199
|
server.tool(findSignalsName, findSignalsDescription, findSignalsSchema, findSignalsHandler)
|
|
193
200
|
server.tool(fetchPageContentName, fetchPageContentDescription, fetchPageContentSchema, fetchPageContentHandler)
|
|
194
201
|
server.tool(getCreditBalanceName, getCreditBalanceDescription, getCreditBalanceSchema, getCreditBalanceHandler)
|
|
202
|
+
server.tool(getSessionUsageName, getSessionUsageDescription, getSessionUsageSchema, getSessionUsageHandler)
|
|
195
203
|
server.tool(listDataSourcesName, listDataSourcesDescription, listDataSourcesSchema, listDataSourcesHandler)
|
|
196
204
|
server.tool(extractPostEngagementName, extractPostEngagementDescription, extractPostEngagementSchema, extractPostEngagementHandler)
|
|
197
205
|
server.tool(getPlaceReviewsName, getPlaceReviewsDescription, getPlaceReviewsSchema, getPlaceReviewsHandler)
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-process ColdIQ credit accumulator for one MCP session ("campaign").
|
|
3
|
+
*
|
|
4
|
+
* The ColdIQ MCP server runs as a stdio subprocess spawned per client
|
|
5
|
+
* conversation, so a module-level singleton here is naturally scoped to a
|
|
6
|
+
* single campaign: it starts empty when the process boots and is discarded
|
|
7
|
+
* when the conversation's server exits. We tally the per-call charges the API
|
|
8
|
+
* already reports via the `X-ColdIQ-Credits-Charged` / `-Remaining` response
|
|
9
|
+
* headers (the same signal the executor surfaces in `_meta`), so reporting
|
|
10
|
+
* session spend never issues an extra billable call.
|
|
11
|
+
*
|
|
12
|
+
* Caveats this module deliberately accepts (and the tool surfaces to the user):
|
|
13
|
+
* - It only counts calls made during the current process lifetime; resuming or
|
|
14
|
+
* restarting the conversation starts a fresh tally.
|
|
15
|
+
* - `charged` reflects what the API billed at call time. For async jobs that is
|
|
16
|
+
* the submit-time reservation; later webhook settlement is not visible here.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const CHARGED_HEADER = 'x-coldiq-credits-charged'
|
|
20
|
+
const REMAINING_HEADER = 'x-coldiq-credits-remaining'
|
|
21
|
+
|
|
22
|
+
export interface EndpointUsage {
|
|
23
|
+
/** Number of billed calls to this endpoint this session. */
|
|
24
|
+
calls: number
|
|
25
|
+
/** Total net ColdIQ credits charged across those calls. */
|
|
26
|
+
credits: number
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SessionUsageSnapshot {
|
|
30
|
+
/** ISO 8601 timestamp of when this MCP session (process) started. */
|
|
31
|
+
startedAt: string
|
|
32
|
+
/** Milliseconds elapsed since the session started. */
|
|
33
|
+
durationMs: number
|
|
34
|
+
/** Count of calls that charged credits this session. */
|
|
35
|
+
billedCalls: number
|
|
36
|
+
/** Sum of net credits charged across all billed calls this session. */
|
|
37
|
+
totalCreditsCharged: number
|
|
38
|
+
/** Balance the API reported after the most recent billed call, if any. */
|
|
39
|
+
balanceAfterLastCharge?: number
|
|
40
|
+
/** Per-endpoint breakdown, highest spend first. */
|
|
41
|
+
byEndpoint: Array<{ endpoint: string; calls: number; credits: number }>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface MutableState {
|
|
45
|
+
startedAt: number
|
|
46
|
+
billedCalls: number
|
|
47
|
+
totalCreditsCharged: number
|
|
48
|
+
balanceAfterLastCharge?: number
|
|
49
|
+
byEndpoint: Map<string, EndpointUsage>
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const state: MutableState = {
|
|
53
|
+
startedAt: Date.now(),
|
|
54
|
+
billedCalls: 0,
|
|
55
|
+
totalCreditsCharged: 0,
|
|
56
|
+
byEndpoint: new Map(),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseHeaderNumber(raw: string | undefined): number | undefined {
|
|
60
|
+
if (raw === undefined) return undefined
|
|
61
|
+
const n = Number(raw)
|
|
62
|
+
return Number.isFinite(n) ? n : undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Credits can be fractional (settlement refunds). Round to 4 dp so summed
|
|
66
|
+
// floating-point noise never leaks into the reported totals.
|
|
67
|
+
function round(n: number): number {
|
|
68
|
+
return Math.round(n * 10000) / 10000
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Record a single billed call. `charged` is the net credits for this call;
|
|
73
|
+
* `remaining`, when present, is the authoritative balance afterwards.
|
|
74
|
+
* Non-finite or negative charges are ignored so a malformed header can't
|
|
75
|
+
* corrupt the running total.
|
|
76
|
+
*/
|
|
77
|
+
export function recordCharge(endpoint: string, charged: number, remaining?: number): void {
|
|
78
|
+
if (!Number.isFinite(charged) || charged < 0) return
|
|
79
|
+
state.billedCalls += 1
|
|
80
|
+
state.totalCreditsCharged += charged
|
|
81
|
+
if (remaining !== undefined && Number.isFinite(remaining)) {
|
|
82
|
+
state.balanceAfterLastCharge = remaining
|
|
83
|
+
}
|
|
84
|
+
const existing = state.byEndpoint.get(endpoint)
|
|
85
|
+
if (existing) {
|
|
86
|
+
existing.calls += 1
|
|
87
|
+
existing.credits += charged
|
|
88
|
+
} else {
|
|
89
|
+
state.byEndpoint.set(endpoint, { calls: 1, credits: charged })
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Record a charge from a response's (lowercased) header map. A no-op when the
|
|
95
|
+
* response carries no charge header — polls, errors, and unbilled routes are
|
|
96
|
+
* silently skipped, so this is safe to call after every request.
|
|
97
|
+
*/
|
|
98
|
+
export function recordChargeFromHeaders(endpoint: string, headers: Record<string, string>): void {
|
|
99
|
+
const charged = parseHeaderNumber(headers[CHARGED_HEADER])
|
|
100
|
+
if (charged === undefined) return
|
|
101
|
+
recordCharge(endpoint, charged, parseHeaderNumber(headers[REMAINING_HEADER]))
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Build a snapshot of session spend so far. `now` is injectable for tests. */
|
|
105
|
+
export function getSessionUsage(now: number = Date.now()): SessionUsageSnapshot {
|
|
106
|
+
const byEndpoint = Array.from(state.byEndpoint.entries())
|
|
107
|
+
.map(([endpoint, u]) => ({ endpoint, calls: u.calls, credits: round(u.credits) }))
|
|
108
|
+
.sort((a, b) => b.credits - a.credits)
|
|
109
|
+
|
|
110
|
+
const snap: SessionUsageSnapshot = {
|
|
111
|
+
startedAt: new Date(state.startedAt).toISOString(),
|
|
112
|
+
durationMs: now - state.startedAt,
|
|
113
|
+
billedCalls: state.billedCalls,
|
|
114
|
+
totalCreditsCharged: round(state.totalCreditsCharged),
|
|
115
|
+
byEndpoint,
|
|
116
|
+
}
|
|
117
|
+
if (state.balanceAfterLastCharge !== undefined) {
|
|
118
|
+
snap.balanceAfterLastCharge = state.balanceAfterLastCharge
|
|
119
|
+
}
|
|
120
|
+
return snap
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Test-only: reset the accumulator to a known clean state. */
|
|
124
|
+
export function resetSessionUsage(startedAt: number = Date.now()): void {
|
|
125
|
+
state.startedAt = startedAt
|
|
126
|
+
state.billedCalls = 0
|
|
127
|
+
state.totalCreditsCharged = 0
|
|
128
|
+
state.balanceAfterLastCharge = undefined
|
|
129
|
+
state.byEndpoint.clear()
|
|
130
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const enrichCompanyName = 'enrich_company'
|
|
6
6
|
|
|
@@ -15,18 +15,5 @@ export const enrichCompanySchema = {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export async function enrichCompanyHandler(input: Record<string, unknown>) {
|
|
18
|
-
|
|
19
|
-
if (!restInput.domain && !restInput.linkedin_url && !restInput.name) {
|
|
20
|
-
const err = { error: 'At least one of domain, linkedin_url, or name is required', providers_tried: [] }
|
|
21
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(err) }], isError: true }
|
|
22
|
-
}
|
|
23
|
-
const resolved = resolvePreferredProviders('enrich_company', restInput, rawUseProviders)
|
|
24
|
-
if (!resolved.ok) {
|
|
25
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
26
|
-
}
|
|
27
|
-
const result = await executeWithFallback('enrich_company', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
28
|
-
if (isExecutionError(result)) {
|
|
29
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
30
|
-
}
|
|
31
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
18
|
+
return callVerb('/company/enrich', input)
|
|
32
19
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const enrichPersonName = 'enrich_person'
|
|
6
6
|
|
|
@@ -22,14 +22,5 @@ export const enrichPersonSchema = {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export async function enrichPersonHandler(input: Record<string, unknown>) {
|
|
25
|
-
|
|
26
|
-
const resolved = resolvePreferredProviders('enrich_person', restInput, rawUseProviders)
|
|
27
|
-
if (!resolved.ok) {
|
|
28
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
29
|
-
}
|
|
30
|
-
const result = await executeWithFallback('enrich_person', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
31
|
-
if (isExecutionError(result)) {
|
|
32
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
33
|
-
}
|
|
34
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
25
|
+
return callVerb('/person/enrich', input)
|
|
35
26
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const fetchPageContentName = 'fetch_page_content'
|
|
6
6
|
|
|
@@ -30,14 +30,5 @@ export const fetchPageContentSchema = {
|
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
export async function fetchPageContentHandler(input: Record<string, unknown>) {
|
|
33
|
-
|
|
34
|
-
const resolved = resolvePreferredProviders('fetch_page_content', restInput, rawUseProviders)
|
|
35
|
-
if (!resolved.ok) {
|
|
36
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
37
|
-
}
|
|
38
|
-
const result = await executeWithFallback('fetch_page_content', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
39
|
-
if (isExecutionError(result)) {
|
|
40
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
41
|
-
}
|
|
42
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
33
|
+
return callVerb('/web/fetch', input)
|
|
43
34
|
}
|
package/src/tools/find-email.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod'
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { callVerb } from '../verb-client.js'
|
|
3
|
+
import { getProvidersForCapability } from '../utils/provider-resolver.js'
|
|
4
4
|
|
|
5
5
|
export const findEmailName = 'find_email'
|
|
6
6
|
|
|
@@ -19,45 +19,6 @@ export const findEmailSchema = {
|
|
|
19
19
|
use_providers: z.array(z.string()).optional().describe(`Optional ordered list of providers to use. Leave empty to let ColdIQ automatically pick the best tool for your inputs — recommended for most use cases. Available providers: ${getProvidersForCapability('find_email').join(', ')}. Provider names are matched fuzzily, so minor typos are tolerated.`),
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const inputSchema = z
|
|
23
|
-
.object({
|
|
24
|
-
first_name: z.string().optional(),
|
|
25
|
-
last_name: z.string().optional(),
|
|
26
|
-
domain: z.string().optional(),
|
|
27
|
-
company_name: z.string().optional(),
|
|
28
|
-
linkedin_url: z.string().optional(),
|
|
29
|
-
})
|
|
30
|
-
.superRefine((val, ctx) => {
|
|
31
|
-
const hasLinkedIn = typeof val.linkedin_url === 'string' && val.linkedin_url.length > 0
|
|
32
|
-
const hasName = typeof val.first_name === 'string' && val.first_name.length > 0
|
|
33
|
-
const hasDomain =
|
|
34
|
-
(typeof val.domain === 'string' && val.domain.length > 0) ||
|
|
35
|
-
(typeof val.company_name === 'string' && val.company_name.length > 0)
|
|
36
|
-
if (!hasLinkedIn && !(hasName && hasDomain)) {
|
|
37
|
-
ctx.addIssue({
|
|
38
|
-
code: 'custom',
|
|
39
|
-
message: 'Provide either linkedin_url or both first_name and domain/company_name',
|
|
40
|
-
})
|
|
41
|
-
}
|
|
42
|
-
})
|
|
43
|
-
|
|
44
22
|
export async function findEmailHandler(input: Record<string, unknown>) {
|
|
45
|
-
|
|
46
|
-
const resolved = resolvePreferredProviders('find_email', restInput, rawUseProviders)
|
|
47
|
-
if (!resolved.ok) {
|
|
48
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(resolved.error) }], isError: true }
|
|
49
|
-
}
|
|
50
|
-
const validation = inputSchema.safeParse(restInput)
|
|
51
|
-
if (!validation.success) {
|
|
52
|
-
const msg = validation.error.issues.map((i) => i.message).join('; ')
|
|
53
|
-
return {
|
|
54
|
-
content: [{ type: 'text' as const, text: JSON.stringify({ error: msg }) }],
|
|
55
|
-
isError: true,
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
const result = await executeWithFallback('find_email', restInput, { providers: resolved.providers, matchedFrom: resolved.matchedFrom })
|
|
59
|
-
if (isExecutionError(result)) {
|
|
60
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }], isError: true }
|
|
61
|
-
}
|
|
62
|
-
return { content: [{ type: 'text' as const, text: JSON.stringify(result) }] }
|
|
23
|
+
return callVerb('/email/find', input)
|
|
63
24
|
}
|