/**
* PostcardAI API Client
* Handles all HTTP communication with the PostcardAI API
*
* Credential loading priority:
* 1. POSTCARDAI_API_KEY environment variable
* 2. ~/.postcardai/config.json (from CLI login)
* 3. POSTCARDAI_CONFIG_PATH custom location
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as os from 'node:os';
const API_BASE_URL = process.env.POSTCARDAI_API_URL || 'https://api.postcard.ai/v1';
interface PostcardAIConfig {
apiKey: string;
keyId: string;
organizationId: string;
organizationName: string;
organizationSlug: string;
environment: 'live' | 'test';
}
/**
* Load API key from config file
*/
function loadApiKeyFromConfig(): string | null {
// Check custom config path first
const customPath = process.env.POSTCARDAI_CONFIG_PATH;
if (customPath) {
try {
if (fs.existsSync(customPath)) {
const content = fs.readFileSync(customPath, 'utf-8');
const config = JSON.parse(content) as PostcardAIConfig;
if (config.apiKey) {
return config.apiKey;
}
}
} catch {
// Ignore errors, fall through to default path
}
}
// Check default config path
const defaultPath = path.join(os.homedir(), '.postcardai', 'config.json');
try {
if (fs.existsSync(defaultPath)) {
const content = fs.readFileSync(defaultPath, 'utf-8');
const config = JSON.parse(content) as PostcardAIConfig;
if (config.apiKey) {
return config.apiKey;
}
}
} catch {
// Ignore errors
}
return null;
}
/**
* Get API key from environment or config file
*/
function getApiKey(): string {
// 1. Check environment variable first
const envKey = process.env.POSTCARDAI_API_KEY;
if (envKey) {
return envKey;
}
// 2. Try loading from config file
const configKey = loadApiKeyFromConfig();
if (configKey) {
return configKey;
}
// 3. No credentials found
throw new Error(
`PostcardAI API key not found.
To authenticate, either:
1. Run: npx @postcardai/cli login
2. Set POSTCARDAI_API_KEY environment variable
Get your API key from https://app.postcard.ai/settings/api`
);
}
class PostcardAIClient {
private apiKey: string;
private baseUrl: string;
constructor() {
this.apiKey = getApiKey();
this.baseUrl = API_BASE_URL;
}
async request<T>(
method: string,
path: string,
body?: unknown,
queryParams?: Record<string, unknown>
): Promise<T> {
let url = `${this.baseUrl}${path}`;
if (queryParams) {
const params = new URLSearchParams();
for (const [key, value] of Object.entries(queryParams)) {
if (value !== undefined) {
params.append(key, String(value));
}
}
const queryString = params.toString();
if (queryString) {
url += `?${queryString}`;
}
}
const headers: Record<string, string> = {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'User-Agent': 'PostcardAI-MCP/0.1.0',
};
const options: RequestInit = {
method,
headers,
};
if (body && (method === 'POST' || method === 'PATCH' || method === 'PUT')) {
options.body = JSON.stringify(body);
}
const response = await fetch(url, options);
const contentType = response.headers.get('content-type');
if (!contentType?.includes('application/json')) {
if (!response.ok) {
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
}
return {} as T;
}
const data = await response.json();
if (!response.ok) {
const errorMessage = data.error?.message || data.message || `API request failed: ${response.status}`;
throw new Error(errorMessage);
}
return data as T;
}
async get<T>(path: string, queryParams?: Record<string, unknown>): Promise<T> {
return this.request<T>('GET', path, undefined, queryParams);
}
async post<T>(path: string, body?: unknown): Promise<T> {
return this.request<T>('POST', path, body);
}
async patch<T>(path: string, body?: unknown): Promise<T> {
return this.request<T>('PATCH', path, body);
}
async delete<T>(path: string, body?: unknown): Promise<T> {
return this.request<T>('DELETE', path, body);
}
}
// Singleton instance
let clientInstance: PostcardAIClient | null = null;
function getClient(): PostcardAIClient {
if (!clientInstance) {
clientInstance = new PostcardAIClient();
}
return clientInstance;
}
export const client = {
get: <T>(path: string, params?: Record<string, unknown>) => getClient().get<T>(path, params),
post: <T>(path: string, body?: unknown) => getClient().post<T>(path, body),
patch: <T>(path: string, body?: unknown) => getClient().patch<T>(path, body),
delete: <T>(path: string, body?: unknown) => getClient().delete<T>(path, body),
};