
Research
/Security News
OpenAPI React Query Codegen Compromised in Mini Shai-Hulud npm Supply Chain Attack
Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.
@teamlearners/clawops
Advanced tools
ClawOps Voice API의 공식 Node.js/TypeScript 라이브러리입니다.
# REST API SDK만 사용
npm install @teamlearners/clawops
# AI Agent 포함 (필요한 프로바이더를 함께 설치)
npm install @teamlearners/clawops ws openai # OpenAI Realtime 모드
npm install @teamlearners/clawops ws @google/genai # Gemini Realtime 모드
npm install @teamlearners/clawops ws @deepgram/sdk openai elevenlabs # Pipeline 모드 (OpenAI LLM)
npm install @teamlearners/clawops ws @deepgram/sdk @anthropic-ai/sdk elevenlabs # Pipeline 모드 (Anthropic LLM)
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(); // Ctrl+C로 종료
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, // default true
});
비용/효과 검증 단계에서는 prewarmEnabled: false 로 비활성화할 수 있다. 동작 측정은
[PREWARM-T] 로그 마커(start / done / failed / attach / first-audio)를 grep
하여 elapsed 를 계산한다.
한계 / 비목표
ClawOpsAgent 1 인스턴스의 session 객체는 prewarm 시
단일 BufferingCall 을 공유한다. 같은 인스턴스로 동시 outbound 통화를 발신하면 prewarm
race 가 발생할 수 있다. 다중 동시 outbound 가 필요하면 통화별로 별도 ClawOpsAgent
인스턴스를 사용하거나, session factory 패턴 도입이 필요하다 (후속 과제).PipelineSession 은 STT / LLM / TTS 가 lazy 연결되므로, prewarm 단계에서는 STT 루프
기동과 greeting kickoff 정도만 선행되어 latency 절감 효과가 제한적이다.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 서버를 연결하여 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(); // Ctrl+C로 종료
MCP 서버는 전화가 올 때마다 자동으로 시작되고, 통화 종료 시 정리됩니다. MCP 서버가 제공하는 도구는 agent.tool()로 등록한 도구와 함께 세션에 자동 등록됩니다.
통화 흐름, 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 등)
import ClawOps from '@teamlearners/clawops';
const client = new ClawOps({
apiKey: 'sk_...', // 또는 CLAWOPS_API_KEY 환경변수 사용
accountId: 'AC1a2b3c4d', // 또는 CLAWOPS_ACCOUNT_ID 환경변수 사용
});
// 발신 전화 생성
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);
// 음성사서함 감지(AMD) — Enable=결과만 통보(통화 계속), Hangup=사서함이면 자동 종료
const amdCall = await client.calls.create({
to: '01012345678',
from: '07052358010',
url: 'https://my-app.com/twiml',
machineDetection: 'Enable',
});
// 통화 종료 후 결과 확인: human(사람) / machine(자동응답기) / unknown(판정 불가)
const done = await client.calls.get(amdCall.callId);
console.log(done.answeredBy);
// statusCallback 을 설정했다면 completed 이벤트 payload 의 AnsweredBy 로도 통보됩니다.
// 통화 목록 조회 (페이지네이션)
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');
// 연결 실패 사유 확인 — status가 'failed' 인 경우는 결번·망 오류·시스템 오류를 모두 포함하는
// 대분류라, 다시 걸어도 소용없는 번호를 가려내려면 hangupCause 를 봅니다.
const DO_NOT_RETRY = ['invalid_number', 'number_changed', 'incompatible_destination'];
if (detail.status !== 'completed') {
if (DO_NOT_RETRY.includes(detail.hangupCause ?? '')) {
console.log(`결번 — 목록에서 제외: ${detail.to}`); // hangupCauseQ850=1, sipResponseCode=404
} else if (detail.hangupSource === 'app' || detail.hangupSource === 'system') {
console.log('ClawOps 측 오류 — 재시도');
} else {
console.log(`일시적 사유(${detail.hangupCause}) — 나중에 재시도`);
}
}
// 통화 종료
await client.calls.update('CAabcdef1234567890', { status: 'completed' });
// 통화 전사 상태 조회 (completed 시 segments 까지 inline)
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') {
// 조직 설정 off 거나 아직 요청 안 된 상태 — 명시 요청 (사용량 과금)
await client.calls.requestTranscript('CAabcdef1234567890');
}
// 통화 요약 상태 조회 (completed 시 resultJson 까지 inline)
const summary = await client.calls.getSummary('CAabcdef1234567890');
if (summary.status === 'completed') {
console.log(summary.resultJson); // { coreSummary, decisions, followUps, sentiment }
}
콘솔에서 들리는 것과 동일한 서버측 MixMonitor 원본(WAV PCM 16bit mono 8kHz)을 다운로드합니다. SDK 측 mix.wav가 아닌 서버에서 합성된 파일이라 싱크/볼륨이 정상입니다.
import { writeFile } from 'node:fs/promises';
// callList 응답의 recordingUrl 필드로 녹음 유무 확인 가능
const list = await client.calls.list({ pageSize: 10 });
for (const call of list.data) {
if (!call.recordingUrl) continue; // failed/no-answer 등은 null
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');
// 번호 발급 — 풀에서 자동 배정되며 어떤 번호가 나올지는 지정할 수 없다
const number = await client.numbers.create();
console.log(number.number, number.routingType); // 07012340001 webhook
// 번호 목록 조회 (페이지네이션 없음 — 보유한 번호가 전부 반환된다)
const numbers = await client.numbers.list();
// 발급 직후에는 webhookUrl 이 비어 있어 걸려온 전화가 거절된다. 착신 라우팅을 지정한다.
// 매니지드 에이전트가 받도록
await client.numbers.update('07012340001', {
routingType: 'agent',
agentId: 'AG7c2f9b1e4a6d',
});
// 콜 플로우(ARS)가 받도록
await client.numbers.update('07012340001', {
routingType: 'callflow',
callFlowId: 'CF41b8e07d9c25',
});
// 내 서버의 VoiceML 이 받도록
await client.numbers.update('07012340001', {
routingType: 'webhook',
webhookUrl: 'https://my-app.com/voice',
});
// 보유한 다른 번호로 착신전환 (같은 계정의 번호만 가능)
await client.numbers.update('07012340001', {
routingType: 'forward',
forwardTo: '07012340002',
});
// 소프트폰 단말 착신 (sip_trunk 부가서비스 + 등록 단말 필요)
const creds = await client.sipCredentials.list({ status: 'active' });
await client.numbers.update('07012340001', {
routingType: 'softphone',
sipCredentialId: creds[0].id,
});
// 외부 PBX 로 (sip_trunk 부가서비스 + 활성 라우트 1개 이상 필요)
const endpoints = await client.sipEndpoints.list({ status: 'active' });
await client.numbers.update('07012340001', { routingType: 'sip', sipEndpointId: endpoints[0].id });
// 수신 통화 상태 통지
await client.numbers.update('07012340001', {
statusCallback: 'https://my-app.com/call-status',
statusCallbackEvents: 'initiated ringing answered completed',
});
// 번호 반납 — 되돌릴 수 없고 같은 번호를 다시 받는다는 보장이 없다
await client.numbers.delete('07012340001');
라우팅을 바꾸면 다른 라우팅 필드는 서버에서 자동으로 비워집니다. agent 에서 webhook 으로
되돌리면 agentId 가 null 이 되므로, 다시 agent 로 돌아갈 때 agentId 를 새로 지정해야
합니다.
// SMS 발송
const msg = await client.messages.create({
to: '01012345678',
from: '07052358010',
body: '안녕하세요',
});
console.log(msg.messageId);
// MMS 발송
const mms = await client.messages.create({
to: '01012345678',
from: '07052358010',
body: '사진 첨부',
type: 'mms',
subject: '제목',
});
// LMS (장문 문자) 발송
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');
이미 솔라피 SDK 로 작성된 코드를 그대로 두고 문자(SMS/LMS/MMS)만 ClawOps 로 보냅니다. 알림톡·친구톡·RCS 는 기존 솔라피 계정으로 계속 나갑니다.
npm install @teamlearners/clawops solapi
바꾸는 곳은 인스턴스를 만드는 한 줄뿐입니다.
import { ClawOps } from '@teamlearners/clawops';
import { ClawOpsMessageService } from '@teamlearners/clawops/solapi';
import { SolapiMessageService } from 'solapi';
const messageService = new ClawOpsMessageService({
clawops: new ClawOps({ apiKey: process.env.CLAWOPS_API_KEY, accountId: process.env.CLAWOPS_ACCOUNT_ID }),
solapi: new SolapiMessageService(SOLAPI_KEY, SOLAPI_SECRET), // 알림톡을 계속 쓸 때만
from: '07052358010', // ClawOps 에 등록된 번호
});
// 이 아래 호출부는 기존 코드 그대로입니다
await messageService.send({ to: '01012345678', from: '07052358010', text: '인증번호는 123456 입니다' });
messageService 의 타입은 SolapiMessageService 와 동일해서 기존 코드의 타입 자리에 그대로 들어갑니다.
send 만 가로채고 getBalance()·getKakaoChannels() 같은 나머지 메서드는 주입한 솔라피 인스턴스로 그대로 전달합니다.
원본 인스턴스는 수정하지 않습니다.
| 메시지 | 어디로 |
|---|---|
SMS / LMS / MMS | ClawOps |
ATA(알림톡) · CTA/CTI(친구톡) · RCS_* · NSA · FAX · VOICE · BMS_* | 솔라피 (요청을 손대지 않고 그대로 전달) |
type 미지정 | kakaoOptions·rcsOptions 같은 vendor 옵션이 있으면 솔라피, 없으면 ClawOps |
솔라피는 type 을 필수로 요구하지 않고 vendor 옵션으로 추론합니다. 그래서 우리도 타입 문자열이
아니라 실제로 실린 옵션으로 가릅니다 — send({ to, from, kakaoOptions }) 처럼 type 을 생략한
알림톡도 솔라피로 갑니다.
type 을 SMS/LMS 로 지정하면 그대로 따르고, 지정하지 않고 vendor 옵션도 없으면 서버와 같은
규칙으로 고릅니다 — subject 가 있으면 lms, 본문이 200 byte(UTF-8)를 넘으면 lms, 아니면 sms.
imageId 는 솔라피에 업로드된 파일 ID 라 ClawOps 로 옮길 수 없습니다. 이미지가 붙은 메시지는
조용히 텍스트만 보내지 않고 에러를 던집니다. 첨부가 필요하면 client.messages.create 의
mediaUrl 로 직접 발송하십시오. 첨부 없는 MMS 는 위 규칙에 따라 sms/lms 로 나갑니다.
거절은 모두 SolapiBridgeError(ClawOpsError 하위)로 던지므로 SDK 의 다른 에러와 함께 잡을 수 있습니다.
솔라피의 대체발송은 솔라피에 등록된 발신번호가 있어야 동작합니다. 그 번호가 없으면 알림톡이 실패해도 문자가 나가지 않습니다. 이때 대체발송을 ClawOps 가 대신합니다.
의도는 솔라피 API 의 값 그대로 읽습니다 — 별도 옵션이 필요 없습니다.
| 보낸 값 | 동작 |
|---|---|
from 있음 + disableSms 생략/false | 알림톡 실패 시 ClawOps 문자로 대체발송 |
disableSms: true | 대체발송하지 않음 |
from 없음 | 대체발송하지 않음 (솔라피 규칙과 동일) |
솔라피로 요청을 넘길 때 두 가지를 조정합니다.
from 을 제외합니다. 솔라피에 등록되지 않은 번호가 실리면 알림톡 자체가 접수 거부됩니다.disableSms 를 true 로 보냅니다. 솔라피가 중복으로 문자를 발송하지 않도록.대체발송 문구는 다음 순서로 정해집니다.
customFields 에 지정한 문구 (기본 키 clawopsFallbackText) — 문자 전용 문구를 직접 넣을 때kakaoOptions.templateId 가 있으면 그 템플릿 본문을 조회해 variables 로 치환 — 알림톡은 보통 text 없이 보내므로 기본 경로입니다. type 을 생략해도 동작합니다text 가 본문입니다await messageService.send({
to: '01012345678',
from: '07052358010',
type: 'ATA',
kakaoOptions: { pfId: 'KA01PF...', templateId: 'TPL_001', variables: { 고객명: '홍길동', 주문번호: 'A-1024' } },
// 문자로 나갈 때만 다른 문구를 쓰고 싶다면
customFields: { clawopsFallbackText: '[상점명] 홍길동님 주문 A-1024 가 접수되었습니다.' },
});
치환되지 않은 변수(#{...})가 남으면 발송하지 않고 onBlocked 로 알립니다.
#{주문번호} 가 그대로 찍힌 문자가 나가는 것을 막기 위해서입니다.
const messageService = new ClawOpsMessageService({
clawops, solapi, from: '07052358010',
fallback: {
enabled: true, // 기본 true. false 면 대체발송하지 않는다
field: 'clawopsFallbackText', // customFields 키를 바꾸고 싶을 때
onFallback: (e) => logger.info({ to: e.to, source: e.source }, '문자로 대체발송'),
onBlocked: (e) => logger.warn({ to: e.to, reason: e.reason }, '대체발송 못 함'),
},
});
대체발송된 건은 솔라피와 같은 의미로 groupInfo.count.sentReplacement 에 집계됩니다.
mode: 'sweep'위까지는 접수 실패만 다룹니다. 그런데 실제로 대체발송이 필요한 건 대부분 접수 이후에
판명됩니다 — 수신자가 카카오톡을 쓰지 않거나(3104) 알림톡을 차단한 경우(3107)는
접수가 성공하고 이통사 리포트에서만 드러납니다.
const messageService = new ClawOpsMessageService({
clawops, solapi, from: '07052358010',
fallback: {
enabled: true,
mode: 'sweep', // 리포트를 주기적으로 훑는다
intervalMs: 5 * 60_000, // 기본 5분
lookbackMs: 60 * 60_000, // 커서가 없을 때 거슬러 볼 구간. 기본 1시간
on: ['3104', '3107', '3102'], // 기본값 (수신자 사유만)
types: ['ATA'], // 훑을 타입. 친구톡까지 보려면 ['ATA','CTA','CTI']
onFallback: (e) => logger.info({ messageId: e.messageId, statusCode: e.statusCode }, '대체발송'),
onBlocked: (e) => logger.warn({ messageId: e.messageId, reason: e.reason }, '대체발송 못 함'),
},
});
켜기만 하면 됩니다. 커서·저장소·크론이 필요 없습니다. 발송할 때 customFields 에 마커를
심어 두고, 스윕이 그 마커가 있는 실패 건만 골라 문자를 보냅니다. 고객이 솔라피로 직접 보낸
알림톡은 마커가 없어 건드리지 않습니다.
같은 건을 두 번 보내지 않도록 두 장치가 다르게 동작합니다 — 커서와 처리 집합이 재조회를 막고(정상 운영 중에는 요청이 아예 안 나갑니다), 멱등키가 프로세스 재시작처럼 커서가 비는 순간의 안전망입니다.
알림톡 실패는 3XXX 로만 판정합니다 — 모르는 코드가 오면 실패로 단정하지 않고 다음 스윕에서 다시 봅니다.
ClawOps 가 대체 문자를 거절하면 onBlocked 에 reason: 'send_rejected' 로 알리고, 다음 스윕이
다시 시도합니다(멱등키가 같아 중복 발송은 되지 않습니다).
기본 대상은 수신자 사유만입니다. 설정 오류(3101 발신프로필 무효 · 3105 미등록 템플릿 ·
3106 유효하지 않은 채널)를 문자로 덮으면 알림톡이 깨진 걸 모르게 되고, 3108(발송 가능 시간
아님)을 대체하면 21시 이후 발송이 되어 야간 규제에 걸립니다. 이런 건은 보내지 않고
onBlocked 에 reason: 'code_not_eligible' 로 알리기만 합니다.
상주 프로세스가 없다면(서버리스·크론) 같은 일을 하는 함수를 직접 부르십시오.
import { sweepFailedAlimtalk } from '@teamlearners/clawops/solapi';
const result = await sweepFailedAlimtalk({
clawops, solapi, from: '07052358010',
cursor: await loadCursor(), // 없으면 lookback 만큼 거슬러 본다
});
await saveCursor(result.cursor); // 직렬화 가능한 값만 담겨 있다
솔라피를 아예 쓰지 않는다면 solapi 를 넘기지 않아도 됩니다.
이때는 타입에서도 send 만 노출되어, 솔라피 전용 메서드를 부르면 컴파일 단계에서 막힙니다.
const messageService = new ClawOpsMessageService({ clawops, from: '07052358010' });
await messageService.send({ to: '01012345678', text: '문자' });
from·disableSms
도 건드리지 않으므로, 솔라피 자체 대체발송이 그대로 동작합니다: fallback 을 껐을 때,
브랜드메시지(kakaoOptions.bms), 그리고 customFields 가 이미 10개(솔라피 상한)를 다 썼을 때.mode: 'sweep' 은 한 인스턴스에서만 켜십시오. 여러 프로세스가 동시에 스윕하면 같은 건을
함께 집어 문자가 두 번 나갈 수 있습니다 — 멱등키는 순차 재시도를 막을 뿐, 완전히 동시에 들어온
두 요청은 통과합니다. 다중 인스턴스라면 크론에서 sweepFailedAlimtalk() 를 한 번만 부르십시오.scheduledDate
또는 allowDuplicates: false 가 오면 조용히 무시하지 않고 에러를 던집니다.failedMessageList 에 statusCode: 'CLAWOPS' 로 담깁니다. 솔라피 상태 코드가 아닙니다.groupInfo 중 balance·point·price·countForCharge 는 ClawOps 에
대응 개념이 없어 0/빈 값이고, groupId 는 CLAWOPS- 로 시작하는 자체 값이라
getGroupMessages() 로 조회되지 않습니다.groupInfo 를 이어받되, 접수 집계
(total·registeredSuccess·registeredFailed)는 우리가 보낸 문자까지 합쳐 다시 셉니다.
발송 단계 집계(sentSuccess 등)는 솔라피 값 그대로입니다.010-1111-2222 로 보내도 됩니다).disableSms: true 로 두거나 customFields 로 문구를 조정하십시오.// 다른 계정의 리소스에 접근
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 }); // 30초 (밀리초)
프록시 등 고급 설정이 필요한 경우 커스텀 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 사용 시 |
| 언어 | 패키지 | 저장소 |
|---|---|---|
| Python | clawops | sdk-python |
zod >= 3.23ws >= 8.0 (Agent 사용 시)Apache-2.0
FAQs
Official Node.js/TypeScript SDK for the ClawOps Voice API
The npm package @teamlearners/clawops receives a total of 624 weekly downloads. As such, @teamlearners/clawops popularity was classified as not popular.
We found that @teamlearners/clawops demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Research
/Security News
Ten malicious OpenAPI React Query Codegen versions were published to npm in the Mini Shai-Hulud attack, all with valid provenance.

Security News
Socket joins more than 100 technology, cybersecurity, and financial organizations calling for a global surge in cyber defense.

Product
Enterprise security teams can now detect malware, credential theft, suspicious network activity, and risky updates across Microsoft Edge extensions.