ClawOps Node.js SDK
ClawOps Voice API의 공식 Node.js/TypeScript 라이브러리입니다.

설치
npm install @teamlearners/clawops
npm install @teamlearners/clawops ws openai
npm install @teamlearners/clawops ws @google/genai
npm install @teamlearners/clawops ws @deepgram/sdk openai elevenlabs
npm install @teamlearners/clawops ws @deepgram/sdk @anthropic-ai/sdk elevenlabs
AI Agent (음성 에이전트)
ClawOpsAgent를 사용하면 한 줄로 인바운드 전화를 AI로 처리할 수 있습니다. ngrok 없이 WebSocket 역방향 연결로 동작합니다.
import { ClawOpsAgent, OpenAIRealtime } from '@teamlearners/clawops/agent';
const agent = new ClawOpsAgent({
from: '07012341234',
session: new OpenAIRealtime({
systemPrompt: '친절한 상담원입니다. 고객의 질문에 답변해주세요.',
voice: 'marin',
language: 'ko',
}),
});
agent.tool('check_order', '주문 상태를 확인합니다.', { orderId: { type: 'string' } }, async ({ orderId }) => {
return '배송 완료';
});
agent.on('call_start', async (call) => {
console.log(`통화 시작: ${call.fromNumber} -> ${call.toNumber}`);
});
await agent.serve();
Outbound 발신 Prewarm (낮은 첫 음성 latency)
outbound 통화에서 상대 응답 직후 첫 음성까지의 지연을 줄이기 위해, ClawOpsAgent 는
control WS 의 call.outbound_ready 이벤트 수신 즉시 LLM WebSocket 을 미리 연결하고
첫 audio delta 를 메모리에 누적한다 (prewarm + first-audio prebuffer). media WS 가 연결되면
누적된 chunk 를 flush 하여 사용자가 첫 음성을 빠르게 듣게 한다.
const agent = new ClawOpsAgent({
from: '07012341234',
session: new OpenAIRealtime({ systemPrompt: '...' }),
prewarmEnabled: true,
});
비용/효과 검증 단계에서는 prewarmEnabled: false 로 비활성화할 수 있다. 동작 측정은
[PREWARM-T] 로그 마커(start / done / failed / attach / first-audio)를 grep
하여 elapsed 를 계산한다.
한계 / 비목표
- 동시 outbound 통화 1건 가정 —
ClawOpsAgent 1 인스턴스의 session 객체는 prewarm 시
단일 BufferingCall 을 공유한다. 같은 인스턴스로 동시 outbound 통화를 발신하면 prewarm
race 가 발생할 수 있다. 다중 동시 outbound 가 필요하면 통화별로 별도 ClawOpsAgent
인스턴스를 사용하거나, session factory 패턴 도입이 필요하다 (후속 과제).
- Session 타입별 효과 차이 — Realtime (OpenAI / Gemini) 에서 LLM WS handshake +
session.update 가 prewarm 으로 숨겨지므로 latency 절감 효과가 가장 크다. 반면
PipelineSession 은 STT / LLM / TTS 가 lazy 연결되므로, prewarm 단계에서는 STT 루프
기동과 greeting kickoff 정도만 선행되어 latency 절감 효과가 제한적이다.
Call Transfer (통화 전환)
AI가 통화 중 다른 번호로 전환할 수 있습니다. Blind(즉시)와 Warm(안내 후) 모드를 지원합니다.
import { ClawOpsAgent, OpenAIRealtime, BuiltinTool } from '@teamlearners/clawops/agent';
const agent = new ClawOpsAgent({
from: '07012341234',
session: new OpenAIRealtime({
systemPrompt: '고객 문의를 처리하고, 필요하면 상담원에게 전환하세요.',
}),
builtinTools: [BuiltinTool.HANG_UP, BuiltinTool.TRANSFER_CALL],
});
agent.on('call_start', async (call) => {
if (shouldTransfer) {
await call.transfer('01012345678', { mode: 'warm', whisper: 'VIP 고객입니다.' });
}
});
await agent.serve();
MCP 서버 연동
MCP 서버를 연결하여 AI에게 외부 도구를 제공할 수 있습니다.
npm install @teamlearners/clawops ws @modelcontextprotocol/sdk
import { ClawOpsAgent, OpenAIRealtime, mcpServerStdio, mcpServerHTTP } from '@teamlearners/clawops/agent';
const agent = new ClawOpsAgent({
from: '07012341234',
session: new OpenAIRealtime({
systemPrompt: '상담원입니다.',
}),
mcpServers: [
mcpServerStdio('npx', { args: ['@modelcontextprotocol/server-google'], env: { GOOGLE_API_KEY: '...' } }),
mcpServerHTTP('https://my-mcp-server.com', { headers: { Authorization: 'Bearer token' } }),
],
});
await agent.serve();
MCP 서버는 전화가 올 때마다 자동으로 시작되고, 통화 종료 시 정리됩니다. MCP 서버가 제공하는 도구는 agent.tool()로 등록한 도구와 함께 세션에 자동 등록됩니다.
OpenTelemetry Tracing
통화 흐름, MCP 도구 호출, LLM 세션을 OpenTelemetry로 추적할 수 있습니다.
npm install @teamlearners/clawops ws @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-grpc
import { NodeTracerProvider } from '@opentelemetry/sdk-trace-node';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
const provider = new NodeTracerProvider();
provider.addSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter()));
provider.register();
import { ClawOpsAgent, OpenAIRealtime, setTracingConfig } from '@teamlearners/clawops/agent';
setTracingConfig({ enabled: true, serviceName: 'my-call-center' });
const agent = new ClawOpsAgent({
from: '07012341234',
session: new OpenAIRealtime({ systemPrompt: '상담원입니다.' }),
});
Span 계층:
call → mcp.connect → llm.session → tool.call → mcp.call_tool
자세한 사용법은 Agent 문서 를 참고하세요. (Tool, 이벤트, 통화 녹음, 파이프라인 모드, 커스텀 제공자, MCP 연동, Tracing 등)
REST API 사용법
import ClawOps from '@teamlearners/clawops';
const client = new ClawOps({
apiKey: 'sk_...',
accountId: 'AC1a2b3c4d',
});
통화 (Calls)
const call = await client.calls.create({
to: '01012345678',
from: '07052358010',
url: 'https://my-app.com/twiml',
statusCallback: 'https://my-app.com/status',
statusCallbackEvent: 'initiated ringing answered completed',
});
console.log(call.callId);
const amdCall = await client.calls.create({
to: '01012345678',
from: '07052358010',
url: 'https://my-app.com/twiml',
machineDetection: 'Enable',
});
const done = await client.calls.get(amdCall.callId);
console.log(done.answeredBy);
const aiCall = await client.calls.create({
to: '01012345678',
from: '07052358010',
ai: {
provider: 'openai',
model: 'gpt-realtime',
apiKey: process.env.OPENAI_API_KEY!,
voice: 'marin',
messages: [{ role: 'system', content: '당신은 예약 확인 AI입니다.' }],
},
});
const page = await client.calls.list({ status: 'completed', page: 0, pageSize: 20 });
for (const call of page) {
console.log(call.callId, call.status);
}
for await (const call of (await client.calls.list()).autoPagingIter()) {
console.log(call.callId);
}
const detail = await client.calls.get('CAabcdef1234567890');
await client.calls.update('CAabcdef1234567890', { status: 'completed' });
const state = await client.calls.getTranscript('CAabcdef1234567890');
if (state.status === 'completed') {
for (const seg of state.segments ?? []) {
console.log(`[${seg.speaker}] ${seg.text}`);
}
} else if (state.status === 'not_requested') {
await client.calls.requestTranscript('CAabcdef1234567890');
}
const summary = await client.calls.getSummary('CAabcdef1234567890');
if (summary.status === 'completed') {
console.log(summary.resultJson);
}
통화 녹음 (Recordings)
콘솔에서 들리는 것과 동일한 서버측 MixMonitor 원본(WAV PCM 16bit mono 8kHz)을 다운로드합니다. SDK 측 mix.wav가 아닌 서버에서 합성된 파일이라 싱크/볼륨이 정상입니다.
import { writeFile } from 'node:fs/promises';
const list = await client.calls.list({ pageSize: 10 });
for (const call of list.data) {
if (!call.recordingUrl) continue;
const rec = await client.recordings.download(call.callId);
await writeFile(rec.filename ?? `${call.callId}.wav`, Buffer.from(rec.data));
console.log(rec.contentType, rec.data.byteLength, 'bytes');
}
녹음이 없는 통화(recordingUrl: null)에 호출하면 NotFoundError(404) 가 발생합니다.
await client.recordings.delete('CAabcdef1234567890');
전화번호 (Numbers)
const number = await client.numbers.create({ source: 'pool' });
console.log(number.phoneNumber);
const numbers = await client.numbers.list();
await client.numbers.update('07012340001', { webhookUrl: 'https://my-app.com/webhook' });
await client.numbers.delete('07012340001');
메시지 (Messages)
const msg = await client.messages.create({
to: '01012345678',
from: '07052358010',
body: '안녕하세요',
});
console.log(msg.messageId);
const mms = await client.messages.create({
to: '01012345678',
from: '07052358010',
body: '사진 첨부',
type: 'mms',
subject: '제목',
});
const lms = await client.messages.create({
to: '01012345678',
from: '07052358010',
body: '긴 내용의 메시지입니다...',
type: 'lms',
subject: '알림',
});
const msgPage = await client.messages.list({ type: 'sms', status: 'sent', page: 0, pageSize: 20 });
for (const m of msgPage) {
console.log(m.messageId, m.status);
}
for await (const m of (await client.messages.list()).autoPagingIter()) {
console.log(m.messageId);
}
const detail = await client.messages.get('MG0123456789abcdef');
멀티 계정 접근
const other = client.accounts('AC_other_account_id');
await other.calls.list();
await other.numbers.list();
await other.messages.list();
웹훅 서명 검증
client.webhooks.verify({
url: 'https://my-app.com/webhook',
params: { CallId: 'CA...', CallStatus: 'completed' },
signature: request.headers['x-signature'],
signingKey: 'your_account_signing_key',
});
서명이 유효하지 않으면 WebhookVerificationError가 발생합니다.
에러 처리
import ClawOps, { BadRequestError, AuthenticationError, NotFoundError } from '@teamlearners/clawops';
const client = new ClawOps();
try {
const call = await client.calls.create({ to: '01012345678', from: '07052358010', url: 'https://...' });
} catch (e) {
if (e instanceof BadRequestError) {
console.log(`잘못된 요청: ${e.statusCode} - ${JSON.stringify(e.body)}`);
} else if (e instanceof AuthenticationError) {
console.log(`유효하지 않은 API 키: ${e.statusCode}`);
} else if (e instanceof NotFoundError) {
console.log(`리소스를 찾을 수 없음: ${e.statusCode}`);
}
}
모든 에러는 ClawOpsError를 상속합니다. HTTP 에러는 statusCode, body 속성을 제공합니다.
BadRequestError | 400 |
AuthenticationError | 401 |
PermissionDeniedError | 403 |
NotFoundError | 404 |
ConflictError | 409 |
UnprocessableEntityError | 422 |
InternalServerError | 500+ |
ServiceUnavailableError | 503 |
설정
재시도
기본적으로 408, 409, 429, 500+ 에러 시 지수 백오프로 최대 2회 재시도합니다.
const client = new ClawOps({ maxRetries: 5 });
const client = new ClawOps({ maxRetries: 0 });
타임아웃
기본 타임아웃은 600초입니다. 클라이언트 단위로 변경할 수 있습니다:
const client = new ClawOps({ timeout: 30_000 });
커스텀 fetch
프록시 등 고급 설정이 필요한 경우 커스텀 fetch 함수를 주입할 수 있습니다:
import { ProxyAgent } from 'undici';
const dispatcher = new ProxyAgent('http://proxy.example.com:8080');
const client = new ClawOps({
fetch: (url, init) => fetch(url, { ...init, dispatcher }),
});
환경변수
CLAWOPS_API_KEY | API 키 (sk_...) | 예 (생성자에 전달하지 않은 경우) |
CLAWOPS_ACCOUNT_ID | 기본 계정 ID (AC...) | 예 (생성자에 전달하지 않은 경우) |
CLAWOPS_BASE_URL | API 기본 URL | 아니오 (기본값: https://api.claw-ops.com) |
OPENAI_API_KEY | OpenAI API 키 | OpenAI Realtime 사용 시 |
GOOGLE_API_KEY | Google API 키 | Gemini Realtime 사용 시 |
문서
다른 언어
요구사항
- Node.js 18+
zod >= 3.23
ws >= 8.0 (Agent 사용 시)
라이선스
Apache-2.0