@openclaw/voice-call 2026.2.3 → 2026.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,6 +1,24 @@
1
1
  # Changelog
2
2
 
3
- ## 2026.2.3
3
+ ## 2026.2.6-3
4
+
5
+ ### Changes
6
+
7
+ - Version alignment with core OpenClaw release numbers.
8
+
9
+ ## 2026.2.6-2
10
+
11
+ ### Changes
12
+
13
+ - Version alignment with core OpenClaw release numbers.
14
+
15
+ ## 2026.2.6
16
+
17
+ ### Changes
18
+
19
+ - Version alignment with core OpenClaw release numbers.
20
+
21
+ ## 2026.2.4
4
22
 
5
23
  ### Changes
6
24
 
package/index.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import type { GatewayRequestHandlerOptions, OpenClawPluginApi } from "openclaw/plugin-sdk";
1
2
  import { Type } from "@sinclair/typebox";
2
3
  import type { CoreConfig } from "./src/core-bridge.js";
3
4
  import { registerVoiceCallCli } from "./src/cli.js";
@@ -144,7 +145,7 @@ const voiceCallPlugin = {
144
145
  name: "Voice Call",
145
146
  description: "Voice-call plugin with Telnyx/Twilio/Plivo providers",
146
147
  configSchema: voiceCallConfigSchema,
147
- register(api) {
148
+ register(api: OpenClawPluginApi) {
148
149
  const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig));
149
150
  const validation = validateProviderConfig(config);
150
151
 
@@ -188,142 +189,160 @@ const voiceCallPlugin = {
188
189
  respond(false, { error: err instanceof Error ? err.message : String(err) });
189
190
  };
190
191
 
191
- api.registerGatewayMethod("voicecall.initiate", async ({ params, respond }) => {
192
- try {
193
- const message = typeof params?.message === "string" ? params.message.trim() : "";
194
- if (!message) {
195
- respond(false, { error: "message required" });
196
- return;
197
- }
198
- const rt = await ensureRuntime();
199
- const to =
200
- typeof params?.to === "string" && params.to.trim()
201
- ? params.to.trim()
202
- : rt.config.toNumber;
203
- if (!to) {
204
- respond(false, { error: "to required" });
205
- return;
206
- }
207
- const mode =
208
- params?.mode === "notify" || params?.mode === "conversation" ? params.mode : undefined;
209
- const result = await rt.manager.initiateCall(to, undefined, {
210
- message,
211
- mode,
212
- });
213
- if (!result.success) {
214
- respond(false, { error: result.error || "initiate failed" });
215
- return;
192
+ api.registerGatewayMethod(
193
+ "voicecall.initiate",
194
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
195
+ try {
196
+ const message = typeof params?.message === "string" ? params.message.trim() : "";
197
+ if (!message) {
198
+ respond(false, { error: "message required" });
199
+ return;
200
+ }
201
+ const rt = await ensureRuntime();
202
+ const to =
203
+ typeof params?.to === "string" && params.to.trim()
204
+ ? params.to.trim()
205
+ : rt.config.toNumber;
206
+ if (!to) {
207
+ respond(false, { error: "to required" });
208
+ return;
209
+ }
210
+ const mode =
211
+ params?.mode === "notify" || params?.mode === "conversation" ? params.mode : undefined;
212
+ const result = await rt.manager.initiateCall(to, undefined, {
213
+ message,
214
+ mode,
215
+ });
216
+ if (!result.success) {
217
+ respond(false, { error: result.error || "initiate failed" });
218
+ return;
219
+ }
220
+ respond(true, { callId: result.callId, initiated: true });
221
+ } catch (err) {
222
+ sendError(respond, err);
216
223
  }
217
- respond(true, { callId: result.callId, initiated: true });
218
- } catch (err) {
219
- sendError(respond, err);
220
- }
221
- });
224
+ },
225
+ );
222
226
 
223
- api.registerGatewayMethod("voicecall.continue", async ({ params, respond }) => {
224
- try {
225
- const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
226
- const message = typeof params?.message === "string" ? params.message.trim() : "";
227
- if (!callId || !message) {
228
- respond(false, { error: "callId and message required" });
229
- return;
230
- }
231
- const rt = await ensureRuntime();
232
- const result = await rt.manager.continueCall(callId, message);
233
- if (!result.success) {
234
- respond(false, { error: result.error || "continue failed" });
235
- return;
227
+ api.registerGatewayMethod(
228
+ "voicecall.continue",
229
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
230
+ try {
231
+ const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
232
+ const message = typeof params?.message === "string" ? params.message.trim() : "";
233
+ if (!callId || !message) {
234
+ respond(false, { error: "callId and message required" });
235
+ return;
236
+ }
237
+ const rt = await ensureRuntime();
238
+ const result = await rt.manager.continueCall(callId, message);
239
+ if (!result.success) {
240
+ respond(false, { error: result.error || "continue failed" });
241
+ return;
242
+ }
243
+ respond(true, { success: true, transcript: result.transcript });
244
+ } catch (err) {
245
+ sendError(respond, err);
236
246
  }
237
- respond(true, { success: true, transcript: result.transcript });
238
- } catch (err) {
239
- sendError(respond, err);
240
- }
241
- });
247
+ },
248
+ );
242
249
 
243
- api.registerGatewayMethod("voicecall.speak", async ({ params, respond }) => {
244
- try {
245
- const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
246
- const message = typeof params?.message === "string" ? params.message.trim() : "";
247
- if (!callId || !message) {
248
- respond(false, { error: "callId and message required" });
249
- return;
250
- }
251
- const rt = await ensureRuntime();
252
- const result = await rt.manager.speak(callId, message);
253
- if (!result.success) {
254
- respond(false, { error: result.error || "speak failed" });
255
- return;
250
+ api.registerGatewayMethod(
251
+ "voicecall.speak",
252
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
253
+ try {
254
+ const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
255
+ const message = typeof params?.message === "string" ? params.message.trim() : "";
256
+ if (!callId || !message) {
257
+ respond(false, { error: "callId and message required" });
258
+ return;
259
+ }
260
+ const rt = await ensureRuntime();
261
+ const result = await rt.manager.speak(callId, message);
262
+ if (!result.success) {
263
+ respond(false, { error: result.error || "speak failed" });
264
+ return;
265
+ }
266
+ respond(true, { success: true });
267
+ } catch (err) {
268
+ sendError(respond, err);
256
269
  }
257
- respond(true, { success: true });
258
- } catch (err) {
259
- sendError(respond, err);
260
- }
261
- });
270
+ },
271
+ );
262
272
 
263
- api.registerGatewayMethod("voicecall.end", async ({ params, respond }) => {
264
- try {
265
- const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
266
- if (!callId) {
267
- respond(false, { error: "callId required" });
268
- return;
269
- }
270
- const rt = await ensureRuntime();
271
- const result = await rt.manager.endCall(callId);
272
- if (!result.success) {
273
- respond(false, { error: result.error || "end failed" });
274
- return;
273
+ api.registerGatewayMethod(
274
+ "voicecall.end",
275
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
276
+ try {
277
+ const callId = typeof params?.callId === "string" ? params.callId.trim() : "";
278
+ if (!callId) {
279
+ respond(false, { error: "callId required" });
280
+ return;
281
+ }
282
+ const rt = await ensureRuntime();
283
+ const result = await rt.manager.endCall(callId);
284
+ if (!result.success) {
285
+ respond(false, { error: result.error || "end failed" });
286
+ return;
287
+ }
288
+ respond(true, { success: true });
289
+ } catch (err) {
290
+ sendError(respond, err);
275
291
  }
276
- respond(true, { success: true });
277
- } catch (err) {
278
- sendError(respond, err);
279
- }
280
- });
292
+ },
293
+ );
281
294
 
282
- api.registerGatewayMethod("voicecall.status", async ({ params, respond }) => {
283
- try {
284
- const raw =
285
- typeof params?.callId === "string"
286
- ? params.callId.trim()
287
- : typeof params?.sid === "string"
288
- ? params.sid.trim()
289
- : "";
290
- if (!raw) {
291
- respond(false, { error: "callId required" });
292
- return;
293
- }
294
- const rt = await ensureRuntime();
295
- const call = rt.manager.getCall(raw) || rt.manager.getCallByProviderCallId(raw);
296
- if (!call) {
297
- respond(true, { found: false });
298
- return;
295
+ api.registerGatewayMethod(
296
+ "voicecall.status",
297
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
298
+ try {
299
+ const raw =
300
+ typeof params?.callId === "string"
301
+ ? params.callId.trim()
302
+ : typeof params?.sid === "string"
303
+ ? params.sid.trim()
304
+ : "";
305
+ if (!raw) {
306
+ respond(false, { error: "callId required" });
307
+ return;
308
+ }
309
+ const rt = await ensureRuntime();
310
+ const call = rt.manager.getCall(raw) || rt.manager.getCallByProviderCallId(raw);
311
+ if (!call) {
312
+ respond(true, { found: false });
313
+ return;
314
+ }
315
+ respond(true, { found: true, call });
316
+ } catch (err) {
317
+ sendError(respond, err);
299
318
  }
300
- respond(true, { found: true, call });
301
- } catch (err) {
302
- sendError(respond, err);
303
- }
304
- });
319
+ },
320
+ );
305
321
 
306
- api.registerGatewayMethod("voicecall.start", async ({ params, respond }) => {
307
- try {
308
- const to = typeof params?.to === "string" ? params.to.trim() : "";
309
- const message = typeof params?.message === "string" ? params.message.trim() : "";
310
- if (!to) {
311
- respond(false, { error: "to required" });
312
- return;
313
- }
314
- const rt = await ensureRuntime();
315
- const result = await rt.manager.initiateCall(to, undefined, {
316
- message: message || undefined,
317
- });
318
- if (!result.success) {
319
- respond(false, { error: result.error || "initiate failed" });
320
- return;
322
+ api.registerGatewayMethod(
323
+ "voicecall.start",
324
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
325
+ try {
326
+ const to = typeof params?.to === "string" ? params.to.trim() : "";
327
+ const message = typeof params?.message === "string" ? params.message.trim() : "";
328
+ if (!to) {
329
+ respond(false, { error: "to required" });
330
+ return;
331
+ }
332
+ const rt = await ensureRuntime();
333
+ const result = await rt.manager.initiateCall(to, undefined, {
334
+ message: message || undefined,
335
+ });
336
+ if (!result.success) {
337
+ respond(false, { error: result.error || "initiate failed" });
338
+ return;
339
+ }
340
+ respond(true, { callId: result.callId, initiated: true });
341
+ } catch (err) {
342
+ sendError(respond, err);
321
343
  }
322
- respond(true, { callId: result.callId, initiated: true });
323
- } catch (err) {
324
- sendError(respond, err);
325
- }
326
- });
344
+ },
345
+ );
327
346
 
328
347
  api.registerTool({
329
348
  name: "voice_call",
@@ -332,7 +351,7 @@ const voiceCallPlugin = {
332
351
  parameters: VoiceCallToolSchema,
333
352
  async execute(_toolCallId, params) {
334
353
  const json = (payload: unknown) => ({
335
- content: [{ type: "text", text: JSON.stringify(payload, null, 2) }],
354
+ content: [{ type: "text" as const, text: JSON.stringify(payload, null, 2) }],
336
355
  details: payload,
337
356
  });
338
357
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/voice-call",
3
- "version": "2026.2.3",
3
+ "version": "2026.2.9",
4
4
  "description": "OpenClaw voice-call plugin",
5
5
  "type": "module",
6
6
  "dependencies": {
package/src/cli.ts CHANGED
@@ -2,6 +2,7 @@ import type { Command } from "commander";
2
2
  import fs from "node:fs";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
+ import { sleep } from "openclaw/plugin-sdk";
5
6
  import type { VoiceCallConfig } from "./config.js";
6
7
  import type { VoiceCallRuntime } from "./runtime.js";
7
8
  import { resolveUserPath } from "./utils.js";
@@ -40,10 +41,6 @@ function resolveDefaultStorePath(config: VoiceCallConfig): string {
40
41
  return path.join(base, "calls.jsonl");
41
42
  }
42
43
 
43
- function sleep(ms: number): Promise<void> {
44
- return new Promise((resolve) => setTimeout(resolve, ms));
45
- }
46
-
47
44
  export function registerVoiceCallCli(params: {
48
45
  program: Command;
49
46
  config: VoiceCallConfig;
@@ -146,7 +146,7 @@ export async function generateVoiceResponse(
146
146
 
147
147
  const text = texts.join(" ") || null;
148
148
 
149
- if (!text && result.meta.aborted) {
149
+ if (!text && result.meta?.aborted) {
150
150
  return { text: null, error: "Response generation was aborted" };
151
151
  }
152
152
 
package/src/runtime.ts CHANGED
@@ -30,7 +30,7 @@ type Logger = {
30
30
  info: (message: string) => void;
31
31
  warn: (message: string) => void;
32
32
  error: (message: string) => void;
33
- debug: (message: string) => void;
33
+ debug?: (message: string) => void;
34
34
  };
35
35
 
36
36
  function isLoopbackBind(bind: string | undefined): boolean {
package/src/webhook.ts CHANGED
@@ -296,23 +296,48 @@ export class VoiceCallWebhookServer {
296
296
  }
297
297
 
298
298
  /**
299
- * Read request body as string.
299
+ * Read request body as string with timeout protection.
300
300
  */
301
- private readBody(req: http.IncomingMessage, maxBytes: number): Promise<string> {
301
+ private readBody(
302
+ req: http.IncomingMessage,
303
+ maxBytes: number,
304
+ timeoutMs = 30_000,
305
+ ): Promise<string> {
302
306
  return new Promise((resolve, reject) => {
307
+ let done = false;
308
+ const finish = (fn: () => void) => {
309
+ if (done) {
310
+ return;
311
+ }
312
+ done = true;
313
+ clearTimeout(timer);
314
+ fn();
315
+ };
316
+
317
+ const timer = setTimeout(() => {
318
+ finish(() => {
319
+ const err = new Error("Request body timeout");
320
+ req.destroy(err);
321
+ reject(err);
322
+ });
323
+ }, timeoutMs);
324
+
303
325
  const chunks: Buffer[] = [];
304
326
  let totalBytes = 0;
305
327
  req.on("data", (chunk: Buffer) => {
306
328
  totalBytes += chunk.length;
307
329
  if (totalBytes > maxBytes) {
308
- req.destroy();
309
- reject(new Error("PayloadTooLarge"));
330
+ finish(() => {
331
+ req.destroy();
332
+ reject(new Error("PayloadTooLarge"));
333
+ });
310
334
  return;
311
335
  }
312
336
  chunks.push(chunk);
313
337
  });
314
- req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
315
- req.on("error", reject);
338
+ req.on("end", () => finish(() => resolve(Buffer.concat(chunks).toString("utf-8"))));
339
+ req.on("error", (err) => finish(() => reject(err)));
340
+ req.on("close", () => finish(() => reject(new Error("Connection closed"))));
316
341
  });
317
342
  }
318
343