chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:20:55 +08:00
commit d25d482dc2
13754 changed files with 4996608 additions and 0 deletions
@@ -0,0 +1,78 @@
import {
extractProspeoError,
type ProspeoAccountInformationParams,
type ProspeoAccountInformationResponse,
} from '@/tools/prospeo/types'
import type { ToolConfig } from '@/tools/types'
export const accountInformationTool: ToolConfig<
ProspeoAccountInformationParams,
ProspeoAccountInformationResponse
> = {
id: 'prospeo_account_information',
name: 'Prospeo Account Information',
description:
'Retrieve the current plan, remaining credits, and renewal date of your Prospeo account.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Prospeo API key',
},
},
request: {
url: 'https://api.prospeo.io/account-information',
method: 'GET',
headers: (params) => ({
'X-KEY': params.apiKey,
}),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
throw new Error(await extractProspeoError(response))
}
const data = await response.json()
const r = data.response ?? {}
return {
success: true,
output: {
current_plan: r.current_plan ?? null,
current_team_members: r.current_team_members ?? null,
remaining_credits: r.remaining_credits ?? null,
used_credits: r.used_credits ?? null,
next_quota_renewal_days: r.next_quota_renewal_days ?? null,
next_quota_renewal_date: r.next_quota_renewal_date ?? null,
},
}
},
outputs: {
current_plan: { type: 'string', description: 'Current Prospeo plan name', optional: true },
current_team_members: {
type: 'number',
description: 'Number of team members in your team',
optional: true,
},
remaining_credits: {
type: 'number',
description: 'Number of credits remaining',
optional: true,
},
used_credits: { type: 'number', description: 'Number of credits already used', optional: true },
next_quota_renewal_days: {
type: 'number',
description: 'Days until the next quota renewal',
optional: true,
},
next_quota_renewal_date: {
type: 'string',
description: 'Date and time of the next quota renewal',
optional: true,
},
},
}
@@ -0,0 +1,96 @@
import { prospeoHosting } from '@/tools/prospeo/hosting'
import {
extractProspeoError,
type ProspeoBulkEnrichCompanyParams,
type ProspeoBulkEnrichCompanyResponse,
} from '@/tools/prospeo/types'
import { parseDataArray } from '@/tools/prospeo/utils'
import type { ToolConfig } from '@/tools/types'
export const bulkEnrichCompanyTool: ToolConfig<
ProspeoBulkEnrichCompanyParams,
ProspeoBulkEnrichCompanyResponse
> = {
id: 'prospeo_bulk_enrich_company',
name: 'Prospeo Bulk Enrich Company',
description: 'Enrich up to 50 company records at once.',
version: '1.0.0',
hosting: prospeoHosting<ProspeoBulkEnrichCompanyParams>((_params, output) => {
// Prospeo reports the exact credits spent for the batch in total_cost.
if (typeof output.total_cost !== 'number') {
throw new Error('Prospeo bulk enrich company response missing total_cost')
}
return output.total_cost
}),
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Prospeo API key',
},
data: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Array of up to 50 company records to enrich. Each must include an "identifier" plus one of: company_website, company_linkedin_url, company_name, or company_id.',
},
},
request: {
url: 'https://api.prospeo.io/bulk-enrich-company',
method: 'POST',
headers: (params) => ({
'X-KEY': params.apiKey,
'Content-Type': 'application/json',
}),
body: (params) => ({ data: parseDataArray(params.data) }),
},
transformResponse: async (response: Response) => {
if (!response.ok) {
throw new Error(await extractProspeoError(response))
}
const data = await response.json()
return {
success: true,
output: {
total_cost: data.total_cost ?? 0,
matched: data.matched ?? [],
not_matched: data.not_matched ?? [],
invalid_datapoints: data.invalid_datapoints ?? [],
},
}
},
outputs: {
total_cost: { type: 'number', description: 'Total credits spent by the request' },
matched: {
type: 'array',
description: 'Matched company records (identifier, company)',
items: {
type: 'object',
properties: {
identifier: {
type: 'string',
description: 'The identifier you submitted for this record',
},
company: { type: 'json', description: 'The matched company object', optional: true },
},
},
},
not_matched: {
type: 'array',
description: 'Identifiers of records we could not match',
items: { type: 'string' },
},
invalid_datapoints: {
type: 'array',
description: 'Identifiers of records that did not meet the minimum matching requirements',
items: { type: 'string' },
},
},
}
@@ -0,0 +1,127 @@
import { prospeoHosting } from '@/tools/prospeo/hosting'
import {
extractProspeoError,
type ProspeoBulkEnrichPersonParams,
type ProspeoBulkEnrichPersonResponse,
} from '@/tools/prospeo/types'
import { parseDataArray } from '@/tools/prospeo/utils'
import type { ToolConfig } from '@/tools/types'
export const bulkEnrichPersonTool: ToolConfig<
ProspeoBulkEnrichPersonParams,
ProspeoBulkEnrichPersonResponse
> = {
id: 'prospeo_bulk_enrich_person',
name: 'Prospeo Bulk Enrich Person',
description: 'Enrich up to 50 person records at once.',
version: '1.0.0',
hosting: prospeoHosting<ProspeoBulkEnrichPersonParams>((_params, output) => {
// Prospeo reports the exact credits spent for the batch in total_cost.
if (typeof output.total_cost !== 'number') {
throw new Error('Prospeo bulk enrich person response missing total_cost')
}
return output.total_cost
}),
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Prospeo API key',
},
data: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Array of up to 50 person records to enrich. Each must include an "identifier" plus one of: linkedin_url, email, person_id, or (first_name + last_name + company_*), or (full_name + company_*).',
},
only_verified_email: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Only return records with a verified email',
},
enrich_mobile: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Reveal mobile numbers (10 credits per match; email included)',
},
only_verified_mobile: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Only return records that have a mobile (implies enrich_mobile)',
},
},
request: {
url: 'https://api.prospeo.io/bulk-enrich-person',
method: 'POST',
headers: (params) => ({
'X-KEY': params.apiKey,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = { data: parseDataArray(params.data) }
if (params.only_verified_email !== undefined)
body.only_verified_email = params.only_verified_email
if (params.enrich_mobile !== undefined) body.enrich_mobile = params.enrich_mobile
if (params.only_verified_mobile !== undefined)
body.only_verified_mobile = params.only_verified_mobile
return body
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
throw new Error(await extractProspeoError(response))
}
const data = await response.json()
return {
success: true,
output: {
total_cost: data.total_cost ?? 0,
matched: data.matched ?? [],
not_matched: data.not_matched ?? [],
invalid_datapoints: data.invalid_datapoints ?? [],
},
}
},
outputs: {
total_cost: { type: 'number', description: 'Total credits spent by the request' },
matched: {
type: 'array',
description: 'Matched records (identifier, person, company)',
items: {
type: 'object',
properties: {
identifier: {
type: 'string',
description: 'The identifier you submitted for this record',
},
person: { type: 'json', description: 'The matched person object', optional: true },
company: {
type: 'json',
description: 'The current company of the matched person',
optional: true,
},
},
},
},
not_matched: {
type: 'array',
description: 'Identifiers of records we could not match given the filters',
items: { type: 'string' },
},
invalid_datapoints: {
type: 'array',
description: 'Identifiers of records that did not meet the minimum matching requirements',
items: { type: 'string' },
},
},
}
+100
View File
@@ -0,0 +1,100 @@
import { prospeoHosting } from '@/tools/prospeo/hosting'
import {
extractProspeoError,
type ProspeoEnrichCompanyParams,
type ProspeoEnrichCompanyResponse,
} from '@/tools/prospeo/types'
import type { ToolConfig } from '@/tools/types'
export const enrichCompanyTool: ToolConfig<
ProspeoEnrichCompanyParams,
ProspeoEnrichCompanyResponse
> = {
id: 'prospeo_enrich_company',
name: 'Prospeo Enrich Company',
description: 'Enrich a company with complete B2B data.',
version: '1.0.0',
hosting: prospeoHosting<ProspeoEnrichCompanyParams>((_params, output) => {
// 1 credit per company match; no charge on a no-match or repeat enrichment.
if (output.free_enrichment === true) return 0
return output.company ? 1 : 0
}),
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Prospeo API key',
},
company_website: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Company website (e.g., "intercom.com")',
},
company_linkedin_url: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: "Company's public LinkedIn URL",
},
company_name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Company name (use combined with website for best accuracy)',
},
company_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Prospeo company_id from a previously enriched company',
},
},
request: {
url: 'https://api.prospeo.io/enrich-company',
method: 'POST',
headers: (params) => ({
'X-KEY': params.apiKey,
'Content-Type': 'application/json',
}),
body: (params) => {
const data: Record<string, unknown> = {}
if (params.company_website) data.company_website = params.company_website
if (params.company_linkedin_url) data.company_linkedin_url = params.company_linkedin_url
if (params.company_name) data.company_name = params.company_name
if (params.company_id) data.company_id = params.company_id
return { data }
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
throw new Error(await extractProspeoError(response))
}
const data = await response.json()
return {
success: true,
output: {
free_enrichment: data.free_enrichment ?? false,
company: data.company ?? null,
},
}
},
outputs: {
free_enrichment: {
type: 'boolean',
description: 'True if this enrichment was free (already enriched in the past)',
},
company: {
type: 'json',
description:
'The matched company object including name, website, domain, industry, employee_count, location, social URLs, funding, and technology',
optional: true,
},
},
}
+169
View File
@@ -0,0 +1,169 @@
import { prospeoHosting } from '@/tools/prospeo/hosting'
import {
extractProspeoError,
type ProspeoEnrichPersonParams,
type ProspeoEnrichPersonResponse,
} from '@/tools/prospeo/types'
import type { ToolConfig } from '@/tools/types'
export const enrichPersonTool: ToolConfig<ProspeoEnrichPersonParams, ProspeoEnrichPersonResponse> =
{
id: 'prospeo_enrich_person',
name: 'Prospeo Enrich Person',
description: 'Enrich a person with complete B2B profile data, email address and mobile.',
version: '1.0.0',
hosting: prospeoHosting<ProspeoEnrichPersonParams>((_params, output) => {
// No charge on a no-match or a repeat enrichment.
if (output.free_enrichment === true) return 0
const person = output.person as Record<string, unknown> | null
if (!person) return 0
// 10 credits when a mobile is revealed, otherwise 1 for the person match.
const mobile = person.mobile as { revealed?: boolean } | undefined
return mobile?.revealed ? 10 : 1
}),
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Prospeo API key',
},
first_name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'First name of the person',
},
last_name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Last name of the person',
},
full_name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Full name of the person (alternative to first_name + last_name)',
},
linkedin_url: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: "Person's public LinkedIn URL",
},
email: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Work email of the person',
},
company_name: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Company name',
},
company_website: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Company website',
},
company_linkedin_url: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: "Company's public LinkedIn URL",
},
person_id: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description: 'Prospeo person_id from a previous Search Person response',
},
only_verified_email: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Only return records with a verified email',
},
enrich_mobile: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Reveal mobile number (10 credits per match; email included)',
},
only_verified_mobile: {
type: 'boolean',
required: false,
visibility: 'user-or-llm',
description: 'Only return records that have a mobile (implies enrich_mobile)',
},
},
request: {
url: 'https://api.prospeo.io/enrich-person',
method: 'POST',
headers: (params) => ({
'X-KEY': params.apiKey,
'Content-Type': 'application/json',
}),
body: (params) => {
const data: Record<string, unknown> = {}
if (params.first_name) data.first_name = params.first_name
if (params.last_name) data.last_name = params.last_name
if (params.full_name) data.full_name = params.full_name
if (params.linkedin_url) data.linkedin_url = params.linkedin_url
if (params.email) data.email = params.email
if (params.company_name) data.company_name = params.company_name
if (params.company_website) data.company_website = params.company_website
if (params.company_linkedin_url) data.company_linkedin_url = params.company_linkedin_url
if (params.person_id) data.person_id = params.person_id
const body: Record<string, unknown> = { data }
if (params.only_verified_email !== undefined)
body.only_verified_email = params.only_verified_email
if (params.enrich_mobile !== undefined) body.enrich_mobile = params.enrich_mobile
if (params.only_verified_mobile !== undefined)
body.only_verified_mobile = params.only_verified_mobile
return body
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
throw new Error(await extractProspeoError(response))
}
const data = await response.json()
return {
success: true,
output: {
free_enrichment: data.free_enrichment ?? false,
person: data.person ?? null,
company: data.company ?? null,
},
}
},
outputs: {
free_enrichment: {
type: 'boolean',
description: 'True if this enrichment was free (already enriched in the past)',
},
person: {
type: 'json',
description:
'The matched person object including person_id, name, linkedin_url, current_job_title, job_history, mobile, email, location, and skills',
optional: true,
},
company: {
type: 'json',
description:
'The current company of the matched person including name, website, domain, industry, employee_count, location, social URLs, funding, and technology',
optional: true,
},
},
}
+43
View File
@@ -0,0 +1,43 @@
import type { ToolHostingConfig } from '@/tools/types'
/**
* Env var prefix for Prospeo hosted keys. Provide keys as
* `PROSPEO_API_KEY_COUNT` plus `PROSPEO_API_KEY_1..N`.
*/
export const PROSPEO_API_KEY_PREFIX = 'PROSPEO_API_KEY'
/**
* Dollar cost of a single Prospeo credit.
*
* Prospeo charges per match: 1 credit per person/company match, 10 credits when
* a mobile is revealed, and never on a no-match or a repeat enrichment. Based on
* the $39/month Starter plan (1,000 credits ≈ $0.039/credit) — https://prospeo.io/pricing.
*/
export const PROSPEO_CREDIT_USD = 0.039
/**
* Build a Prospeo `hosting` config. `getCredits` returns the number of Prospeo
* credits the call consumed, derived from the tool's output (prefer the
* API-reported `total_cost` for bulk endpoints; otherwise compute from the
* `free`/`free_enrichment` flag and the match).
*/
export function prospeoHosting<P>(
getCredits: (params: P, output: Record<string, unknown>) => number
): ToolHostingConfig<P> {
return {
envKeyPrefix: PROSPEO_API_KEY_PREFIX,
apiKeyParam: 'apiKey',
byokProviderId: 'prospeo',
pricing: {
type: 'custom',
getCost: (params, output) => {
const credits = getCredits(params, output)
return { cost: credits * PROSPEO_CREDIT_USD, metadata: { credits } }
},
},
rateLimit: {
mode: 'per_request',
requestsPerMinute: 60,
},
}
}
+17
View File
@@ -0,0 +1,17 @@
import { accountInformationTool } from '@/tools/prospeo/account_information'
import { bulkEnrichCompanyTool } from '@/tools/prospeo/bulk_enrich_company'
import { bulkEnrichPersonTool } from '@/tools/prospeo/bulk_enrich_person'
import { enrichCompanyTool } from '@/tools/prospeo/enrich_company'
import { enrichPersonTool } from '@/tools/prospeo/enrich_person'
import { searchCompanyTool } from '@/tools/prospeo/search_company'
import { searchPersonTool } from '@/tools/prospeo/search_person'
import { searchSuggestionsTool } from '@/tools/prospeo/search_suggestions'
export const prospeoAccountInformationTool = accountInformationTool
export const prospeoEnrichPersonTool = enrichPersonTool
export const prospeoEnrichCompanyTool = enrichCompanyTool
export const prospeoBulkEnrichPersonTool = bulkEnrichPersonTool
export const prospeoBulkEnrichCompanyTool = bulkEnrichCompanyTool
export const prospeoSearchPersonTool = searchPersonTool
export const prospeoSearchCompanyTool = searchCompanyTool
export const prospeoSearchSuggestionsTool = searchSuggestionsTool
+95
View File
@@ -0,0 +1,95 @@
import { prospeoHosting } from '@/tools/prospeo/hosting'
import {
extractProspeoError,
PROSPEO_PAGINATION_OUTPUT,
type ProspeoSearchCompanyParams,
type ProspeoSearchCompanyResponse,
} from '@/tools/prospeo/types'
import { parseFiltersObject } from '@/tools/prospeo/utils'
import type { ToolConfig } from '@/tools/types'
export const searchCompanyTool: ToolConfig<
ProspeoSearchCompanyParams,
ProspeoSearchCompanyResponse
> = {
id: 'prospeo_search_company',
name: 'Prospeo Search Company',
description: 'Search for companies using 20+ filters to build account lists.',
version: '1.0.0',
hosting: prospeoHosting<ProspeoSearchCompanyParams>((_params, output) => {
// 1 credit per page that returns at least one result; free on 30-day dedup.
if (output.free === true) return 0
const results = output.results
return Array.isArray(results) && results.length > 0 ? 1 : 0
}),
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Prospeo API key',
},
filters: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Filter configuration object. See https://prospeo.io/api-docs/filters-documentation for all supported filters (e.g., company_industry, company_headcount_range, company_funding).',
},
page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page number (defaults to 1). Up to 1000 pages of 25 results.',
},
},
request: {
url: 'https://api.prospeo.io/search-company',
method: 'POST',
headers: (params) => ({
'X-KEY': params.apiKey,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = { filters: parseFiltersObject(params.filters) }
if (params.page !== undefined) body.page = params.page
return body
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
throw new Error(await extractProspeoError(response))
}
const data = await response.json()
return {
success: true,
output: {
free: data.free ?? false,
results: data.results ?? [],
pagination: data.pagination ?? null,
},
}
},
outputs: {
free: {
type: 'boolean',
description: 'True if the request was free due to 30-day result-set deduplication',
},
results: {
type: 'array',
description: 'Up to 25 matching companies',
items: {
type: 'object',
properties: {
company: { type: 'json', description: 'Matched company object' },
},
},
},
pagination: PROSPEO_PAGINATION_OUTPUT,
},
}
+97
View File
@@ -0,0 +1,97 @@
import { prospeoHosting } from '@/tools/prospeo/hosting'
import {
extractProspeoError,
PROSPEO_PAGINATION_OUTPUT,
type ProspeoSearchPersonParams,
type ProspeoSearchPersonResponse,
} from '@/tools/prospeo/types'
import { parseFiltersObject } from '@/tools/prospeo/utils'
import type { ToolConfig } from '@/tools/types'
export const searchPersonTool: ToolConfig<ProspeoSearchPersonParams, ProspeoSearchPersonResponse> =
{
id: 'prospeo_search_person',
name: 'Prospeo Search Person',
description: 'Search for leads using 20+ filters to build targeted contact lists.',
version: '1.0.0',
hosting: prospeoHosting<ProspeoSearchPersonParams>((_params, output) => {
// 1 credit per page that returns at least one result; free on 30-day dedup.
if (output.free === true) return 0
const results = output.results
return Array.isArray(results) && results.length > 0 ? 1 : 0
}),
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Prospeo API key',
},
filters: {
type: 'json',
required: true,
visibility: 'user-or-llm',
description:
'Filter configuration object. See https://prospeo.io/api-docs/filters-documentation for all supported filters (e.g., person_seniority, company_industry, person_location).',
},
page: {
type: 'number',
required: false,
visibility: 'user-or-llm',
description: 'Page number (defaults to 1). Up to 1000 pages of 25 results.',
},
},
request: {
url: 'https://api.prospeo.io/search-person',
method: 'POST',
headers: (params) => ({
'X-KEY': params.apiKey,
'Content-Type': 'application/json',
}),
body: (params) => {
const body: Record<string, unknown> = { filters: parseFiltersObject(params.filters) }
if (params.page !== undefined) body.page = params.page
return body
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
throw new Error(await extractProspeoError(response))
}
const data = await response.json()
return {
success: true,
output: {
free: data.free ?? false,
results: data.results ?? [],
pagination: data.pagination ?? null,
},
}
},
outputs: {
free: {
type: 'boolean',
description: 'True if the request was free due to 30-day result-set deduplication',
},
results: {
type: 'array',
description: 'Up to 25 search results (person + company, no email/mobile)',
items: {
type: 'object',
properties: {
person: {
type: 'json',
description: 'Matched person (no email/mobile in search response)',
},
company: { type: 'json', description: 'Current company of the person', optional: true },
},
},
},
pagination: PROSPEO_PAGINATION_OUTPUT,
},
}
@@ -0,0 +1,105 @@
import {
extractProspeoError,
type ProspeoSearchSuggestionsParams,
type ProspeoSearchSuggestionsResponse,
} from '@/tools/prospeo/types'
import type { ToolConfig } from '@/tools/types'
export const searchSuggestionsTool: ToolConfig<
ProspeoSearchSuggestionsParams,
ProspeoSearchSuggestionsResponse
> = {
id: 'prospeo_search_suggestions',
name: 'Prospeo Search Suggestions',
description:
'Free endpoint to retrieve valid location or job title values for use in Search filters. Provide exactly one of location_search or job_title_search.',
version: '1.0.0',
params: {
apiKey: {
type: 'string',
required: true,
visibility: 'user-only',
description: 'Prospeo API key',
},
location_search: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Search query for location suggestions (minimum 2 characters). Mutually exclusive with job_title_search.',
},
job_title_search: {
type: 'string',
required: false,
visibility: 'user-or-llm',
description:
'Search query for job title suggestions (minimum 2 characters). Mutually exclusive with location_search.',
},
},
request: {
url: 'https://api.prospeo.io/search-suggestions',
method: 'POST',
headers: (params) => ({
'X-KEY': params.apiKey,
'Content-Type': 'application/json',
}),
body: (params) => {
if (!params.location_search && !params.job_title_search) {
throw new Error(
'Provide exactly one of location_search or job_title_search (minimum 2 characters).'
)
}
if (params.location_search && params.job_title_search) {
throw new Error(
'location_search and job_title_search are mutually exclusive — provide only one.'
)
}
const body: Record<string, unknown> = {}
if (params.location_search) body.location_search = params.location_search
if (params.job_title_search) body.job_title_search = params.job_title_search
return body
},
},
transformResponse: async (response: Response) => {
if (!response.ok) {
throw new Error(await extractProspeoError(response))
}
const data = await response.json()
return {
success: true,
output: {
location_suggestions: data.location_suggestions ?? [],
job_title_suggestions: data.job_title_suggestions ?? [],
},
}
},
outputs: {
location_suggestions: {
type: 'array',
description:
'Location suggestions when using location_search (empty when searching job titles)',
optional: true,
items: {
type: 'object',
properties: {
name: { type: 'string', description: 'Formatted location name to use in filters' },
type: {
type: 'string',
description: 'Location type (COUNTRY, STATE, CITY, or ZONE)',
},
},
},
},
job_title_suggestions: {
type: 'array',
description:
'Up to 25 job title suggestions ordered by popularity when using job_title_search (empty when searching locations)',
optional: true,
items: { type: 'string' },
},
},
}
+192
View File
@@ -0,0 +1,192 @@
import type { OutputProperty, ToolResponse } from '@/tools/types'
export interface ProspeoBaseParams {
apiKey: string
}
export interface ProspeoPersonData {
first_name?: string
last_name?: string
full_name?: string
linkedin_url?: string
email?: string
company_name?: string
company_website?: string
company_linkedin_url?: string
person_id?: string
}
export interface ProspeoCompanyData {
company_name?: string
company_website?: string
company_linkedin_url?: string
company_id?: string
}
export interface ProspeoPaginationOutput {
current_page: number
per_page: number
total_page: number
total_count: number
}
export const PROSPEO_PAGINATION_OUTPUT: OutputProperty = {
type: 'object',
description: 'Pagination details',
optional: true,
properties: {
current_page: { type: 'number', description: 'Current page number' },
per_page: { type: 'number', description: 'Results per page' },
total_page: { type: 'number', description: 'Total number of pages' },
total_count: { type: 'number', description: 'Total number of matching records' },
},
}
/** Account Information */
export interface ProspeoAccountInformationParams extends ProspeoBaseParams {}
export interface ProspeoAccountInformationResponse extends ToolResponse {
output: {
current_plan: string | null
current_team_members: number | null
remaining_credits: number | null
used_credits: number | null
next_quota_renewal_days: number | null
next_quota_renewal_date: string | null
}
}
/** Enrich Person */
export interface ProspeoEnrichPersonParams extends ProspeoBaseParams, ProspeoPersonData {
only_verified_email?: boolean
enrich_mobile?: boolean
only_verified_mobile?: boolean
}
export interface ProspeoEnrichPersonResponse extends ToolResponse {
output: {
free_enrichment: boolean
person: Record<string, unknown> | null
company: Record<string, unknown> | null
}
}
/** Enrich Company */
export interface ProspeoEnrichCompanyParams extends ProspeoBaseParams, ProspeoCompanyData {}
export interface ProspeoEnrichCompanyResponse extends ToolResponse {
output: {
free_enrichment: boolean
company: Record<string, unknown> | null
}
}
/** Bulk Enrich Person */
export interface ProspeoBulkEnrichPersonParams extends ProspeoBaseParams {
data: Array<ProspeoPersonData & { identifier: string }>
only_verified_email?: boolean
enrich_mobile?: boolean
only_verified_mobile?: boolean
}
export interface ProspeoBulkEnrichPersonResponse extends ToolResponse {
output: {
total_cost: number
matched: Array<{
identifier: string
person: Record<string, unknown> | null
company: Record<string, unknown> | null
}>
not_matched: string[]
invalid_datapoints: string[]
}
}
/** Bulk Enrich Company */
export interface ProspeoBulkEnrichCompanyParams extends ProspeoBaseParams {
data: Array<ProspeoCompanyData & { identifier: string }>
}
export interface ProspeoBulkEnrichCompanyResponse extends ToolResponse {
output: {
total_cost: number
matched: Array<{
identifier: string
company: Record<string, unknown> | null
}>
not_matched: string[]
invalid_datapoints: string[]
}
}
/** Search Person */
export interface ProspeoSearchPersonParams extends ProspeoBaseParams {
filters: Record<string, unknown> | string
page?: number
}
export interface ProspeoSearchPersonResponse extends ToolResponse {
output: {
free: boolean
results: Array<{
person: Record<string, unknown> | null
company: Record<string, unknown> | null
}>
pagination: ProspeoPaginationOutput | null
}
}
/** Search Company */
export interface ProspeoSearchCompanyParams extends ProspeoBaseParams {
filters: Record<string, unknown> | string
page?: number
}
export interface ProspeoSearchCompanyResponse extends ToolResponse {
output: {
free: boolean
results: Array<{
company: Record<string, unknown> | null
}>
pagination: ProspeoPaginationOutput | null
}
}
/** Search Suggestions */
export interface ProspeoSearchSuggestionsParams extends ProspeoBaseParams {
location_search?: string
job_title_search?: string
}
export interface ProspeoSearchSuggestionsResponse extends ToolResponse {
output: {
location_suggestions: Array<{ name: string; type: string }>
job_title_suggestions: string[]
}
}
export type ProspeoResponse =
| ProspeoAccountInformationResponse
| ProspeoEnrichPersonResponse
| ProspeoEnrichCompanyResponse
| ProspeoBulkEnrichPersonResponse
| ProspeoBulkEnrichCompanyResponse
| ProspeoSearchPersonResponse
| ProspeoSearchCompanyResponse
| ProspeoSearchSuggestionsResponse
/**
* Build a Prospeo API error message from a non-OK response payload.
*/
export async function extractProspeoError(response: Response): Promise<string> {
try {
const data = (await response.json()) as {
error_code?: string
filter_error?: string
message?: string
}
const parts = [data.error_code, data.filter_error, data.message].filter(Boolean)
if (parts.length > 0) return parts.join(': ')
} catch {}
return `Prospeo API error: ${response.status}`
}
+27
View File
@@ -0,0 +1,27 @@
export function parseDataArray(value: unknown): unknown[] {
if (Array.isArray(value)) return value
if (typeof value === 'string' && value.trim().length > 0) {
try {
const parsed = JSON.parse(value)
return Array.isArray(parsed) ? parsed : []
} catch {
return []
}
}
return []
}
export function parseFiltersObject(value: unknown): Record<string, unknown> {
if (value && typeof value === 'object' && !Array.isArray(value)) {
return value as Record<string, unknown>
}
if (typeof value === 'string' && value.trim().length > 0) {
try {
const parsed = JSON.parse(value)
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as Record<string, unknown>
}
} catch {}
}
return {}
}