llmconveyors
v0.4.0
Published
Official TypeScript SDK for the LLM Conveyors API
Maintainers
Readme
llmconveyors
Official TypeScript SDK for the LLM Conveyors API.
Zero runtime dependencies. Full type safety. SSE streaming via async iterables. Node.js 18+.
Installation
npm install llmconveyors
# or
pnpm add llmconveyors
# or
yarn add llmconveyorsSetup
- Get an API key from Settings > API Keys in the web UI
- Create a
.envfile (see .env.example):
LLMC_API_KEY=llmc_your_api_key_here- Initialize the client:
import { LLMConveyors } from 'llmconveyors';
const client = new LLMConveyors({
apiKey: process.env.LLMC_API_KEY,
});Usage
There are three ways to run a generation, from simplest to most control:
1. One call with run() (recommended)
Generates, streams, handles phased interactions, and returns the final result:
const result = await client.agents.run('job-hunter', {
companyName: 'Stripe',
jobTitle: 'Senior Backend Engineer',
jobDescription: 'Build payment APIs...',
autoSelectContacts: true,
}, {
onProgress: (step, percent) => console.log(`${step}: ${percent}%`),
onChunk: (chunk) => process.stdout.write(chunk),
});
console.log(result.success); // true
console.log(result.artifacts); // [{ type: 'cv', ... }, ...]See examples/job-hunter.ts for a complete working example.
2. Manual streaming
For full control over each SSE event:
const { generationId } = await client.agents.generate('job-hunter', {
companyName: 'Stripe',
jobTitle: 'Senior Backend Engineer',
jobDescription: 'Build payment APIs...',
});
for await (const event of client.stream.generation(generationId)) {
switch (event.event) {
case 'progress': console.log(`${event.data.step}: ${event.data.percent}%`); break;
case 'chunk': process.stdout.write(event.data.chunk); break;
case 'complete': console.log('Done!', event.data.artifacts); break;
case 'error': console.error(event.data.code, event.data.message); break;
}
}See examples/manual-streaming.ts for phased interaction handling.
3. Polling (no SSE)
For environments where streaming is not available:
const { jobId } = await client.agents.generate('b2b-sales', {
companyName: 'Notion',
companyWebsite: 'https://notion.so',
});
const result = await client.agents.poll('b2b-sales', jobId, {
interval: 3000,
timeout: 120_000,
});
console.log(result.status); // 'completed'
console.log(result.artifacts); // [{ type: 'cold-email', ... }]See examples/b2b-sales.ts for a complete working example.
Agents
Job Hunter
Generates tailored CVs, cover letters, and cold emails for job applications.
const result = await client.agents.run('job-hunter', {
companyName: 'Stripe',
jobTitle: 'Senior Backend Engineer',
jobDescription: 'Build payment APIs...',
autoSelectContacts: true, // skip contact selection (headless)
// mode: 'cold_outreach', // or 'standard' (default)
// theme: 'modern', // resume template
// webhookUrl: 'https://...',
});Supports phased workflows: Phase A researches and finds contacts, Phase B generates documents. With autoSelectContacts: true, both phases run automatically. Without it, provide an interactionHandler:
const result = await client.agents.run('job-hunter', body, {
interactionHandler: async (interactionType, interactionData) => {
// interactionData contains contact candidates
// Return your selection
return { selectedContacts: ['contact_id_1'] };
},
});B2B Sales
Generates personalized sales outreach for B2B prospecting.
const result = await client.agents.run('b2b-sales', {
companyName: 'Notion',
companyWebsite: 'https://notion.so',
// model: 'pro', // or 'flash' (default)
// skipResearchCache: false,
// webhookUrl: 'https://...',
});Supports phased workflows with contact selection, same as Job Hunter.
Error Handling
All API errors are thrown as typed classes:
import { RateLimitError, ValidationError, LLMConveyorsError } from 'llmconveyors';
try {
await client.agents.generate('job-hunter', body);
} catch (err) {
if (err instanceof RateLimitError) {
console.log('Retry after:', err.retryAfter, 'seconds');
} else if (err instanceof ValidationError) {
console.log('Hint:', err.hint);
console.log('Field errors:', err.details?.fieldErrors);
} else if (err instanceof LLMConveyorsError) {
console.log(err.code, err.isRetryable());
}
}The SDK automatically retries on RATE_LIMITED, CONCURRENT_GENERATION_LIMIT, AI_PROVIDER_ERROR, AI_RATE_LIMITED, SERVICE_UNAVAILABLE, GENERATION_TIMEOUT, SERVER_RESTARTING, and STREAM_ERROR with exponential backoff and jitter.
Webhook Verification
Verify incoming webhook signatures using your webhook secret (not your API key):
import { verifyWebhookSignature, parseWebhookEvent } from 'llmconveyors';
app.post('/webhook', (req, res) => {
const isValid = verifyWebhookSignature(req.body, req.headers['x-webhook-signature'], secret);
if (!isValid) return res.status(401).send('Invalid signature');
const event = parseWebhookEvent(req.body);
// event.event: 'generation.completed' | 'generation.failed' | 'generation.awaiting_input'
// event.generationId, event.sessionId, event.agentType, event.timestamp (top-level)
// event.data: { status, artifacts?, error?, interactionData? }
res.sendStatus(200);
});All Resources
| Namespace | Methods |
|-----------|---------|
| client.agents | generate, generateCv, getStatus, interact, getManifest, run, poll |
| client.stream | generation, health |
| client.sessions | create, list, get, hydrate, download, delete, init, stats, log, initGenerationLog |
| client.upload | resume, jobFile, jobText |
| client.resume | parse, validate, render, preview, themes, importRxResume, exportRxResume, getMaster, upsertMaster, deleteMaster |
| client.ats | score |
| client.settings | getProfile, getPreferences, updatePreferences, createApiKey, listApiKeys, revokeApiKey, rotateApiKey, getApiKeyUsage, getProviderKeyStatus, getSupportedProviders, setProviderKey, removeProviderKey, getUsageLogs, getUsageSummary, getWebhookSecret, rotateWebhookSecret |
| client.privacy | listConsents, grantConsent, revokeConsent, exportData, deleteAccount |
| client.documents | download |
| client.logging | send |
| client.health | root, check, ready, live |
| client.content | save, deleteGeneration, researchSender, listSources, getSource, deleteSource |
| client.shares | create, getStats, getPublic, recordVisit, getShareStats |
| client.referral | getStats, getCode, setVanityCode |
Configuration
const client = new LLMConveyors({
apiKey: 'llmc_...', // Required, must start with llmc_
baseUrl: 'https://api.llmconveyors.com/api/v1', // Default
maxRetries: 3, // Default, for retryable errors
timeout: 30_000, // Default, ms per request
debug: (msg) => console.log(msg), // Optional debug logger
});Custom auth (Bearer tokens, SuperTokens, rotating sessions)
For flows where you do not have a static llmc_ API key — browser sessions,
Chrome extensions, server-to-server OAuth — pass a getAuthHeaders callback
instead of apiKey. The SDK calls it before every request (and every SSE
reconnect), so token rotation is transparent:
const client = new LLMConveyors({
baseUrl: 'https://api.llmconveyors.com/api/v1',
getAuthHeaders: async () => ({
Authorization: `Bearer ${await getAccessToken()}`,
}),
});On 401, the SDK calls the callback a second time and retries the request
once if the returned headers differ — so a refresh you do inside the callback
is picked up automatically. If the headers are unchanged, the 401 surfaces
as UnauthorizedError with no retry loop. The SDK stores zero auth state:
token lifecycle, storage, and refresh coordination all live in your code.
Examples
Working examples in the examples/ directory:
| File | What it shows |
|------|---------------|
| job-hunter.ts | Full run() workflow with callbacks and interaction handling |
| b2b-sales.ts | Generate + poll + session hydration |
| manual-streaming.ts | Raw SSE for await loop with manual interaction |
| smoke-test.ts | Quick connectivity test against all read endpoints |
Run any example:
cp .env.example .env
# Edit .env with your API key
npx tsx examples/job-hunter.tsRequirements
- Node.js 18+ (uses native
fetch,crypto,ReadableStream) - Zero runtime dependencies
License
MIT
