@openclaw/voice-call 2026.3.13 → 2026.5.2-beta.1

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.
Files changed (103) hide show
  1. package/README.md +27 -5
  2. package/api.ts +16 -0
  3. package/cli-metadata.ts +10 -0
  4. package/config-api.ts +12 -0
  5. package/index.test.ts +943 -0
  6. package/index.ts +379 -149
  7. package/openclaw.plugin.json +384 -157
  8. package/package.json +35 -5
  9. package/runtime-api.ts +20 -0
  10. package/runtime-entry.ts +1 -0
  11. package/setup-api.ts +47 -0
  12. package/src/allowlist.test.ts +18 -0
  13. package/src/cli.ts +533 -68
  14. package/src/config-compat.test.ts +120 -0
  15. package/src/config-compat.ts +227 -0
  16. package/src/config.test.ts +273 -12
  17. package/src/config.ts +355 -72
  18. package/src/core-bridge.ts +2 -147
  19. package/src/deep-merge.test.ts +40 -0
  20. package/src/gateway-continue-operation.ts +200 -0
  21. package/src/http-headers.ts +6 -3
  22. package/src/manager/context.ts +6 -5
  23. package/src/manager/events.test.ts +243 -19
  24. package/src/manager/events.ts +61 -31
  25. package/src/manager/lifecycle.ts +53 -0
  26. package/src/manager/lookup.test.ts +52 -0
  27. package/src/manager/outbound.test.ts +528 -0
  28. package/src/manager/outbound.ts +163 -57
  29. package/src/manager/store.ts +18 -6
  30. package/src/manager/timers.test.ts +129 -0
  31. package/src/manager/timers.ts +4 -3
  32. package/src/manager/twiml.test.ts +13 -0
  33. package/src/manager/twiml.ts +8 -0
  34. package/src/manager.closed-loop.test.ts +30 -12
  35. package/src/manager.inbound-allowlist.test.ts +77 -10
  36. package/src/manager.notify.test.ts +344 -20
  37. package/src/manager.restore.test.ts +95 -8
  38. package/src/manager.test-harness.ts +8 -6
  39. package/src/manager.ts +79 -5
  40. package/src/media-stream.test.ts +578 -81
  41. package/src/media-stream.ts +235 -54
  42. package/src/providers/base.ts +19 -0
  43. package/src/providers/mock.ts +7 -1
  44. package/src/providers/plivo.test.ts +50 -6
  45. package/src/providers/plivo.ts +14 -6
  46. package/src/providers/shared/call-status.ts +2 -1
  47. package/src/providers/shared/guarded-json-api.test.ts +106 -0
  48. package/src/providers/shared/guarded-json-api.ts +1 -1
  49. package/src/providers/telnyx.test.ts +178 -6
  50. package/src/providers/telnyx.ts +40 -3
  51. package/src/providers/twilio/api.test.ts +145 -0
  52. package/src/providers/twilio/api.ts +67 -16
  53. package/src/providers/twilio/twiml-policy.ts +6 -10
  54. package/src/providers/twilio/webhook.ts +1 -1
  55. package/src/providers/twilio.test.ts +425 -25
  56. package/src/providers/twilio.ts +230 -77
  57. package/src/providers/twilio.types.ts +17 -0
  58. package/src/realtime-defaults.ts +3 -0
  59. package/src/realtime-fast-context.test.ts +88 -0
  60. package/src/realtime-fast-context.ts +165 -0
  61. package/src/realtime-transcription.runtime.ts +4 -0
  62. package/src/realtime-voice.runtime.ts +5 -0
  63. package/src/response-generator.test.ts +321 -0
  64. package/src/response-generator.ts +213 -53
  65. package/src/response-model.test.ts +71 -0
  66. package/src/response-model.ts +23 -0
  67. package/src/runtime.test.ts +429 -0
  68. package/src/runtime.ts +270 -24
  69. package/src/telephony-audio.test.ts +61 -0
  70. package/src/telephony-audio.ts +1 -79
  71. package/src/telephony-tts.test.ts +133 -12
  72. package/src/telephony-tts.ts +155 -2
  73. package/src/test-fixtures.ts +28 -7
  74. package/src/tts-provider-voice.test.ts +34 -0
  75. package/src/tts-provider-voice.ts +21 -0
  76. package/src/tunnel.test.ts +166 -0
  77. package/src/tunnel.ts +1 -1
  78. package/src/types.ts +24 -37
  79. package/src/utils.test.ts +17 -0
  80. package/src/voice-mapping.test.ts +34 -0
  81. package/src/voice-mapping.ts +3 -2
  82. package/src/webhook/realtime-handler.test.ts +598 -0
  83. package/src/webhook/realtime-handler.ts +485 -0
  84. package/src/webhook/stale-call-reaper.test.ts +88 -0
  85. package/src/webhook/stale-call-reaper.ts +5 -0
  86. package/src/webhook/tailscale.test.ts +214 -0
  87. package/src/webhook/tailscale.ts +19 -5
  88. package/src/webhook-exposure.test.ts +33 -0
  89. package/src/webhook-exposure.ts +84 -0
  90. package/src/webhook-security.test.ts +172 -21
  91. package/src/webhook-security.ts +43 -29
  92. package/src/webhook.hangup-once.lifecycle.test.ts +135 -0
  93. package/src/webhook.test.ts +1145 -27
  94. package/src/webhook.ts +523 -102
  95. package/src/webhook.types.ts +5 -0
  96. package/src/websocket-test-support.ts +72 -0
  97. package/tsconfig.json +16 -0
  98. package/CHANGELOG.md +0 -121
  99. package/src/providers/index.ts +0 -10
  100. package/src/providers/stt-openai-realtime.test.ts +0 -42
  101. package/src/providers/stt-openai-realtime.ts +0 -311
  102. package/src/providers/tts-openai.test.ts +0 -43
  103. package/src/providers/tts-openai.ts +0 -221
package/src/types.ts CHANGED
@@ -1,11 +1,11 @@
1
- import { z } from "zod";
1
+ import { z } from "openclaw/plugin-sdk/zod";
2
2
  import type { CallMode } from "./config.js";
3
3
 
4
4
  // -----------------------------------------------------------------------------
5
5
  // Provider Identifiers
6
6
  // -----------------------------------------------------------------------------
7
7
 
8
- export const ProviderNameSchema = z.enum(["telnyx", "twilio", "plivo", "mock"]);
8
+ const ProviderNameSchema = z.enum(["telnyx", "twilio", "plivo", "mock"]);
9
9
  export type ProviderName = z.infer<typeof ProviderNameSchema>;
10
10
 
11
11
  // -----------------------------------------------------------------------------
@@ -16,13 +16,13 @@ export type ProviderName = z.infer<typeof ProviderNameSchema>;
16
16
  export type CallId = string;
17
17
 
18
18
  /** Provider-specific call identifier */
19
- export type ProviderCallId = string;
19
+ type ProviderCallId = string;
20
20
 
21
21
  // -----------------------------------------------------------------------------
22
22
  // Call Lifecycle States
23
23
  // -----------------------------------------------------------------------------
24
24
 
25
- export const CallStateSchema = z.enum([
25
+ const CallStateSchema = z.enum([
26
26
  // Non-terminal states
27
27
  "initiated",
28
28
  "ringing",
@@ -55,7 +55,7 @@ export const TerminalStates = new Set<CallState>([
55
55
  "voicemail",
56
56
  ]);
57
57
 
58
- export const EndReasonSchema = z.enum([
58
+ const EndReasonSchema = z.enum([
59
59
  "completed",
60
60
  "hangup-user",
61
61
  "hangup-bot",
@@ -87,7 +87,7 @@ const BaseEventSchema = z.object({
87
87
  to: z.string().optional(),
88
88
  });
89
89
 
90
- export const NormalizedEventSchema = z.discriminatedUnion("type", [
90
+ const NormalizedEventSchema = z.discriminatedUnion("type", [
91
91
  BaseEventSchema.extend({
92
92
  type: z.literal("call.initiated"),
93
93
  }),
@@ -134,14 +134,13 @@ export type NormalizedEvent = z.infer<typeof NormalizedEventSchema>;
134
134
  // Call Direction
135
135
  // -----------------------------------------------------------------------------
136
136
 
137
- export const CallDirectionSchema = z.enum(["outbound", "inbound"]);
138
- export type CallDirection = z.infer<typeof CallDirectionSchema>;
137
+ const CallDirectionSchema = z.enum(["outbound", "inbound"]);
139
138
 
140
139
  // -----------------------------------------------------------------------------
141
140
  // Call Record
142
141
  // -----------------------------------------------------------------------------
143
142
 
144
- export const TranscriptEntrySchema = z.object({
143
+ const TranscriptEntrySchema = z.object({
145
144
  timestamp: z.number(),
146
145
  speaker: z.enum(["bot", "user"]),
147
146
  text: z.string(),
@@ -212,8 +211,10 @@ export type InitiateCallInput = {
212
211
  to: string;
213
212
  webhookUrl: string;
214
213
  clientState?: Record<string, string>;
215
- /** Inline TwiML to execute (skips webhook, used for notify mode) */
214
+ /** Inline TwiML to execute without fetching webhook TwiML. */
216
215
  inlineTwiml?: string;
216
+ /** TwiML to serve once before normal webhook-driven call handling resumes. */
217
+ preConnectTwiml?: string;
217
218
  };
218
219
 
219
220
  export type InitiateCallResult = {
@@ -227,6 +228,11 @@ export type HangupCallInput = {
227
228
  reason: EndReason;
228
229
  };
229
230
 
231
+ export type AnswerCallInput = {
232
+ callId: CallId;
233
+ providerCallId: ProviderCallId;
234
+ };
235
+
230
236
  export type PlayTtsInput = {
231
237
  callId: CallId;
232
238
  providerCallId: ProviderCallId;
@@ -235,6 +241,12 @@ export type PlayTtsInput = {
235
241
  locale?: string;
236
242
  };
237
243
 
244
+ export type SendDtmfInput = {
245
+ callId: CallId;
246
+ providerCallId: ProviderCallId;
247
+ digits: string;
248
+ };
249
+
238
250
  export type StartListeningInput = {
239
251
  callId: CallId;
240
252
  providerCallId: ProviderCallId;
@@ -274,31 +286,6 @@ export type OutboundCallOptions = {
274
286
  message?: string;
275
287
  /** Call mode (overrides config default) */
276
288
  mode?: CallMode;
277
- };
278
-
279
- // -----------------------------------------------------------------------------
280
- // Tool Result Types
281
- // -----------------------------------------------------------------------------
282
-
283
- export type InitiateCallToolResult = {
284
- success: boolean;
285
- callId?: string;
286
- status?: "initiated" | "queued" | "no-answer" | "busy" | "failed";
287
- error?: string;
288
- };
289
-
290
- export type ContinueCallToolResult = {
291
- success: boolean;
292
- transcript?: string;
293
- error?: string;
294
- };
295
-
296
- export type SpeakToUserToolResult = {
297
- success: boolean;
298
- error?: string;
299
- };
300
-
301
- export type EndCallToolResult = {
302
- success: boolean;
303
- error?: string;
289
+ /** DTMF digits to send after the call is connected */
290
+ dtmfSequence?: string;
304
291
  };
@@ -0,0 +1,17 @@
1
+ import os from "node:os";
2
+ import path from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import { resolveUserPath } from "./utils.js";
5
+
6
+ describe("resolveUserPath", () => {
7
+ it("returns trimmed empty input unchanged", () => {
8
+ expect(resolveUserPath(" ")).toBe("");
9
+ });
10
+
11
+ it("expands tildes and resolves relative paths", () => {
12
+ expect(resolveUserPath("~/voice-call/config.json")).toBe(
13
+ path.resolve(os.homedir(), "voice-call/config.json"),
14
+ );
15
+ expect(resolveUserPath("./voice-call")).toBe(path.resolve("./voice-call"));
16
+ });
17
+ });
@@ -0,0 +1,34 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ DEFAULT_POLLY_VOICE,
4
+ escapeXml,
5
+ getOpenAiVoiceNames,
6
+ isOpenAiVoice,
7
+ mapVoiceToPolly,
8
+ } from "./voice-mapping.js";
9
+
10
+ describe("voice mapping", () => {
11
+ it("escapes xml-special characters", () => {
12
+ expect(escapeXml(`5 < 6 & "quote" 'apostrophe' > 4`)).toBe(
13
+ "5 &lt; 6 &amp; &quot;quote&quot; &apos;apostrophe&apos; &gt; 4",
14
+ );
15
+ });
16
+
17
+ it("maps openai voices, passes through provider voices, and falls back to default", () => {
18
+ expect(mapVoiceToPolly("alloy")).toBe("Polly.Joanna");
19
+ expect(mapVoiceToPolly("ECHO")).toBe("Polly.Matthew");
20
+ expect(mapVoiceToPolly("Polly.Brian")).toBe("Polly.Brian");
21
+ expect(mapVoiceToPolly("Google.en-US-Standard-C")).toBe("Google.en-US-Standard-C");
22
+ expect(mapVoiceToPolly("unknown")).toBe(DEFAULT_POLLY_VOICE);
23
+ expect(mapVoiceToPolly(undefined)).toBe(DEFAULT_POLLY_VOICE);
24
+ });
25
+
26
+ it("detects known openai voices and lists them", () => {
27
+ expect(isOpenAiVoice("nova")).toBe(true);
28
+ expect(isOpenAiVoice("NOVA")).toBe(true);
29
+ expect(isOpenAiVoice("Polly.Joanna")).toBe(false);
30
+ expect(getOpenAiVoiceNames()).toEqual(
31
+ expect.arrayContaining(["alloy", "echo", "fable", "nova", "onyx", "shimmer"]),
32
+ );
33
+ });
34
+ });
@@ -1,6 +1,7 @@
1
1
  /**
2
2
  * Voice mapping and XML utilities for voice call providers.
3
3
  */
4
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
4
5
 
5
6
  /**
6
7
  * Escape XML special characters for TwiML and other XML responses.
@@ -49,14 +50,14 @@ export function mapVoiceToPolly(voice: string | undefined): string {
49
50
  }
50
51
 
51
52
  // Map OpenAI voices to Polly equivalents
52
- return OPENAI_TO_POLLY_MAP[voice.toLowerCase()] || DEFAULT_POLLY_VOICE;
53
+ return OPENAI_TO_POLLY_MAP[normalizeLowercaseStringOrEmpty(voice)] || DEFAULT_POLLY_VOICE;
53
54
  }
54
55
 
55
56
  /**
56
57
  * Check if a voice name is a known OpenAI voice.
57
58
  */
58
59
  export function isOpenAiVoice(voice: string): boolean {
59
- return voice.toLowerCase() in OPENAI_TO_POLLY_MAP;
60
+ return normalizeLowercaseStringOrEmpty(voice) in OPENAI_TO_POLLY_MAP;
60
61
  }
61
62
 
62
63
  /**