import { getClient, extractIdFromUrn, formatPaginatedResponse, formatPaginatedMarkdown, formatEntityMarkdown } from '../client.js';
import { formatError } from '../utils/errors.js';
import { LookupCompanyInput, GetCompanyInput, GetCompanyEmployeesInput, GetCompanyConnectionsInput } from '../schemas/inputs.js';
// ============================================================================
// Response Types
// ============================================================================
interface CompanyWebsite {
url?: string;
domain?: string;
}
interface CompanyLocation {
city?: string;
state?: string;
country?: string;
}
interface CompanyContact {
emails?: string[];
primary_email?: string;
exec_emails?: string[];
}
interface CompanySocial {
url?: string;
follower_count?: number;
}
interface CompanyFunding {
funding_total?: number;
num_funding_rounds?: number;
funding_stage?: string;
last_funding_at?: string;
investors?: Array<{
entity_urn?: string;
name?: string;
}>;
}
interface Company {
id: number;
entity_urn: string;
name: string;
legal_name?: string;
description?: string;
website?: CompanyWebsite;
logo_url?: string;
headcount?: number;
founding_date?: { date?: string };
location?: CompanyLocation;
contact?: CompanyContact;
socials?: Record<string, CompanySocial>;
funding?: CompanyFunding;
customer_type?: string;
company_type?: string;
stage?: string;
web_traffic?: number;
}
interface EmployeesResponse {
count: number;
page_info?: {
next?: string;
has_next?: boolean;
};
results: string[];
}
interface UserConnection {
user_urn: string;
customer_urn?: string;
target_person_urn: string;
target_person_email_address?: string;
target_company_urn: string;
connection_sources: string[];
}
// ============================================================================
// Formatters
// ============================================================================
function formatCompanyMarkdown(company: Company): string {
const lines: string[] = [];
lines.push(`## ${company.name} (ID: ${company.id})`);
lines.push('');
if (company.description) {
lines.push(`*${company.description.slice(0, 300)}${company.description.length > 300 ? '...' : ''}*`);
lines.push('');
}
// Basic info
lines.push('### Basic Info');
if (company.website?.domain) {
lines.push(`- **Website:** ${company.website.domain}`);
}
if (company.location) {
const loc = [company.location.city, company.location.state, company.location.country].filter(Boolean).join(', ');
if (loc) lines.push(`- **Location:** ${loc}`);
}
if (company.headcount) {
lines.push(`- **Headcount:** ${company.headcount}`);
}
if (company.founding_date?.date) {
lines.push(`- **Founded:** ${company.founding_date.date.slice(0, 10)}`);
}
if (company.company_type) {
lines.push(`- **Type:** ${company.company_type}`);
}
if (company.stage) {
lines.push(`- **Stage:** ${company.stage}`);
}
// Funding
if (company.funding) {
lines.push('');
lines.push('### Funding');
if (company.funding.funding_total) {
lines.push(`- **Total Raised:** $${(company.funding.funding_total / 1000000).toFixed(1)}M`);
}
if (company.funding.funding_stage) {
lines.push(`- **Stage:** ${company.funding.funding_stage}`);
}
if (company.funding.num_funding_rounds) {
lines.push(`- **Rounds:** ${company.funding.num_funding_rounds}`);
}
if (company.funding.investors && company.funding.investors.length > 0) {
const investorNames = company.funding.investors.slice(0, 5).map(i => i.name).filter(Boolean).join(', ');
if (investorNames) {
lines.push(`- **Investors:** ${investorNames}`);
}
}
}
// Contact
if (company.contact) {
lines.push('');
lines.push('### Contact');
if (company.contact.primary_email) {
lines.push(`- **Primary Email:** ${company.contact.primary_email}`);
}
if (company.contact.emails && company.contact.emails.length > 0) {
lines.push(`- **Emails:** ${company.contact.emails.slice(0, 3).join(', ')}`);
}
}
// Social
if (company.socials) {
const socialEntries = Object.entries(company.socials);
if (socialEntries.length > 0) {
lines.push('');
lines.push('### Social');
for (const [platform, data] of socialEntries) {
if (data.url) {
const followers = data.follower_count ? ` (${data.follower_count.toLocaleString()} followers)` : '';
lines.push(`- **${platform}:** ${data.url}${followers}`);
}
}
}
}
return lines.join('\n');
}
function formatEmployeeMarkdown(urn: string, index: number): string {
const id = extractIdFromUrn(urn);
return `${index + 1}. **${urn}** (ID: ${id})`;
}
function formatConnectionMarkdown(conn: UserConnection, index: number): string {
const lines: string[] = [];
const personId = extractIdFromUrn(conn.target_person_urn);
lines.push(`${index + 1}. **Connection to person ${personId}**`);
lines.push(` - Person URN: ${conn.target_person_urn}`);
if (conn.target_person_email_address) {
lines.push(` - Email: ${conn.target_person_email_address}`);
}
lines.push(` - Connected via: ${conn.connection_sources.join(', ')}`);
return lines.join('\n');
}
// ============================================================================
// Executors
// ============================================================================
/**
* Execute lookup company tool
*
* Two-step process:
* 1. POST /companies to enrich/lookup (returns massive ~853KB response but includes ID)
* 2. POST /companies/batchGet with field filtering to get slim response (~3KB)
*
* @see POST /companies (enrichment)
* @see POST /companies/batchGet (filtered fetch)
*/
export async function executeLookupCompany(input: LookupCompanyInput): Promise<string> {
try {
const client = getClient();
// Build params - only one identifier should be provided
const params: Record<string, string | undefined> = {};
if (input.website_domain) {
params.website_domain = input.website_domain;
} else if (input.website_url) {
params.website_url = input.website_url;
} else if (input.linkedin_url) {
params.linkedin_url = input.linkedin_url;
} else if (input.crunchbase_url) {
params.crunchbase_url = input.crunchbase_url;
} else if (input.pitchbook_url) {
params.pitchbook_url = input.pitchbook_url;
} else if (input.twitter_url) {
params.twitter_url = input.twitter_url;
} else if (input.instagram_url) {
params.instagram_url = input.instagram_url;
} else if (input.facebook_url) {
params.facebook_url = input.facebook_url;
} else if (input.angellist_url) {
params.angellist_url = input.angellist_url;
} else {
return 'Error: At least one identifier is required (website_domain, website_url, linkedin_url, crunchbase_url, pitchbook_url, twitter_url, instagram_url, facebook_url, or angellist_url)';
}
// Step 1: Enrich/lookup to get the company ID (returns massive ~853KB response)
const enrichResponse = await client.post<Company>('/companies', params);
if (!enrichResponse.id) {
return 'Error: Company lookup failed - no ID returned';
}
// Step 2: Fetch with field filtering to get slim response (~3KB)
const requestBody: { ids: string[]; include_fields?: string[] } = {
ids: [String(enrichResponse.id)]
};
// Use basic field set for reasonable response size
requestBody.include_fields = [
'name', 'description', 'website', 'headcount',
'location', 'funding', 'stage', 'founding_date',
'company_type', 'customer_type', 'contact', 'socials',
'id', 'entity_urn' // Ensure these are included
];
// batchGet returns direct array [...], NOT { results: [...] }
const companies = await client.postJson<Company[]>('/companies/batchGet', requestBody);
const company = companies[0];
if (!company) {
return `Error: Company with ID ${enrichResponse.id} not found in batchGet`;
}
if (input.response_format === 'markdown') {
return formatEntityMarkdown(company.name, [
{ content: formatCompanyMarkdown(company) }
]);
}
return JSON.stringify(company, null, 2);
} catch (error) {
return formatError(error);
}
}
/**
* Execute get company tool
*
* Uses POST /companies/batchGet for proper field filtering.
* The GET /companies/{id} endpoint ignores include_fields parameter.
*
* @see POST /companies/batchGet
*/
export async function executeGetCompany(input: GetCompanyInput): Promise<string> {
try {
const client = getClient();
const companyId = extractIdFromUrn(input.company_id);
// Use POST batchGet for proper field filtering
// GET /companies/{id} ignores include_fields (always returns ~368KB)
const requestBody: { ids: string[]; include_fields?: string[] } = {
ids: [companyId]
};
// Default to basic fields if none specified (reduces ~368KB to ~3KB)
if (input.include_fields && input.include_fields.length > 0) {
requestBody.include_fields = input.include_fields;
} else {
// Basic field set for reasonable response size
requestBody.include_fields = [
'name', 'description', 'website', 'headcount',
'location', 'funding', 'stage', 'founding_date',
'company_type', 'customer_type', 'contact', 'socials'
];
}
// batchGet returns direct array [...], NOT { results: [...] }
const companies = await client.postJson<Company[]>('/companies/batchGet', requestBody);
const company = companies[0];
if (!company) {
return `Error: Company with ID ${companyId} not found`;
}
if (input.response_format === 'markdown') {
return formatEntityMarkdown(company.name, [
{ content: formatCompanyMarkdown(company) }
]);
}
return JSON.stringify(company, null, 2);
} catch (error) {
return formatError(error);
}
}
/**
* Execute get company employees tool
*
* @see GET /companies/{id}/employees
*/
export async function executeGetCompanyEmployees(input: GetCompanyEmployeesInput): Promise<string> {
try {
const client = getClient();
const companyId = extractIdFromUrn(input.company_id);
const params: Record<string, string | number | undefined> = {};
if (input.size !== undefined) {
params.size = input.size;
}
if (input.page !== undefined) {
params.page = input.page;
}
if (input.employee_group_type) {
params.employee_group_type = input.employee_group_type;
}
if (input.employee_status) {
params.employee_status = input.employee_status;
}
const response = await client.get<EmployeesResponse>(`/companies/${companyId}/employees`, params);
// Enrich results with extracted IDs
const enrichedResults = response.results.map(urn => ({
urn,
id: extractIdFromUrn(urn)
}));
if (input.response_format === 'markdown') {
const lines: string[] = [];
lines.push(`# Employees of Company ${companyId}`);
lines.push('');
lines.push(`**Total employees:** ${response.count}`);
if (input.employee_group_type) {
lines.push(`**Filter:** ${input.employee_group_type}`);
}
if (input.employee_status) {
lines.push(`**Status:** ${input.employee_status}`);
}
lines.push('');
if (enrichedResults.length === 0) {
lines.push('No employees found matching the criteria.');
} else {
for (let i = 0; i < response.results.length; i++) {
lines.push(formatEmployeeMarkdown(response.results[i], i));
}
}
lines.push('');
lines.push('---');
lines.push('*Use harmonic_get_person with the ID to get full person details.*');
return lines.join('\n');
}
return JSON.stringify({
data: enrichedResults,
count: enrichedResults.length,
totalEmployees: response.count,
companyId,
summary: `Found ${enrichedResults.length} of ${response.count} employees for company ${companyId}`
}, null, 2);
} catch (error) {
return formatError(error);
}
}
/**
* Execute get company connections tool
*
* @see GET /companies/{id}/userConnections
*/
export async function executeGetCompanyConnections(input: GetCompanyConnectionsInput): Promise<string> {
try {
const client = getClient();
const companyId = extractIdFromUrn(input.company_id);
const params: Record<string, string | number | undefined> = {};
if (input.first !== undefined) {
params.first = input.first;
}
if (input.after) {
params.after = input.after;
}
const connections = await client.get<UserConnection[]>(`/companies/${companyId}/userConnections`, params);
if (input.response_format === 'markdown') {
const lines: string[] = [];
lines.push(`# Team Connections to Company ${companyId}`);
lines.push('');
lines.push(`**Found:** ${connections.length} connections`);
lines.push('');
if (connections.length === 0) {
lines.push('No team connections found for this company.');
lines.push('');
lines.push('This means no one on your team has documented connections to people at this company via email, LinkedIn, or calendar.');
} else {
for (let i = 0; i < connections.length; i++) {
lines.push(formatConnectionMarkdown(connections[i], i));
lines.push('');
}
}
return lines.join('\n');
}
return JSON.stringify({
data: connections,
count: connections.length,
companyId,
summary: `Found ${connections.length} team connections to company ${companyId}`
}, null, 2);
} catch (error) {
return formatError(error);
}
}