@openclaw/voice-call 2026.1.29 → 2026.2.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 (41) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +13 -9
  3. package/index.ts +45 -49
  4. package/openclaw.plugin.json +11 -53
  5. package/package.json +6 -3
  6. package/src/cli.ts +80 -113
  7. package/src/config.test.ts +1 -4
  8. package/src/config.ts +88 -110
  9. package/src/core-bridge.ts +14 -12
  10. package/src/manager/context.ts +1 -1
  11. package/src/manager/events.ts +18 -9
  12. package/src/manager/lookup.ts +3 -1
  13. package/src/manager/outbound.ts +46 -19
  14. package/src/manager/state.ts +4 -6
  15. package/src/manager/store.ts +6 -3
  16. package/src/manager/timers.ts +11 -8
  17. package/src/manager.test.ts +7 -10
  18. package/src/manager.ts +53 -75
  19. package/src/media-stream.test.ts +0 -1
  20. package/src/media-stream.ts +12 -26
  21. package/src/providers/mock.ts +13 -16
  22. package/src/providers/plivo.test.ts +0 -1
  23. package/src/providers/plivo.ts +27 -29
  24. package/src/providers/stt-openai-realtime.ts +8 -8
  25. package/src/providers/telnyx.ts +5 -11
  26. package/src/providers/tts-openai.ts +9 -14
  27. package/src/providers/twilio/api.ts +9 -12
  28. package/src/providers/twilio/webhook.ts +2 -4
  29. package/src/providers/twilio.test.ts +1 -5
  30. package/src/providers/twilio.ts +34 -46
  31. package/src/response-generator.ts +7 -20
  32. package/src/runtime.ts +12 -25
  33. package/src/telephony-audio.ts +14 -12
  34. package/src/telephony-tts.ts +21 -12
  35. package/src/tunnel.ts +7 -24
  36. package/src/types.ts +0 -1
  37. package/src/utils.ts +3 -1
  38. package/src/voice-mapping.ts +3 -1
  39. package/src/webhook-security.test.ts +12 -21
  40. package/src/webhook-security.ts +25 -29
  41. package/src/webhook.ts +22 -57
package/src/cli.ts CHANGED
@@ -1,9 +1,7 @@
1
+ import type { Command } from "commander";
1
2
  import fs from "node:fs";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
4
-
5
- import type { Command } from "commander";
6
-
7
5
  import type { VoiceCallConfig } from "./config.js";
8
6
  import type { VoiceCallRuntime } from "./runtime.js";
9
7
  import { resolveUserPath } from "./utils.js";
@@ -21,7 +19,9 @@ type Logger = {
21
19
 
22
20
  function resolveMode(input: string): "off" | "serve" | "funnel" {
23
21
  const raw = input.trim().toLowerCase();
24
- if (raw === "serve" || raw === "off") return raw;
22
+ if (raw === "serve" || raw === "off") {
23
+ return raw;
24
+ }
25
25
  return "funnel";
26
26
  }
27
27
 
@@ -31,10 +31,7 @@ function resolveDefaultStorePath(config: VoiceCallConfig): string {
31
31
  const existing =
32
32
  [resolvedPreferred].find((dir) => {
33
33
  try {
34
- return (
35
- fs.existsSync(path.join(dir, "calls.jsonl")) ||
36
- fs.existsSync(dir)
37
- );
34
+ return fs.existsSync(path.join(dir, "calls.jsonl")) || fs.existsSync(dir);
38
35
  } catch {
39
36
  return false;
40
37
  }
@@ -62,10 +59,7 @@ export function registerVoiceCallCli(params: {
62
59
  root
63
60
  .command("call")
64
61
  .description("Initiate an outbound voice call")
65
- .requiredOption(
66
- "-m, --message <text>",
67
- "Message to speak when call connects",
68
- )
62
+ .requiredOption("-m, --message <text>", "Message to speak when call connects")
69
63
  .option(
70
64
  "-t, --to <phone>",
71
65
  "Phone number to call (E.164 format, uses config toNumber if not set)",
@@ -75,27 +69,23 @@ export function registerVoiceCallCli(params: {
75
69
  "Call mode: notify (hangup after message) or conversation (stay open)",
76
70
  "conversation",
77
71
  )
78
- .action(
79
- async (options: { message: string; to?: string; mode?: string }) => {
80
- const rt = await ensureRuntime();
81
- const to = options.to ?? rt.config.toNumber;
82
- if (!to) {
83
- throw new Error("Missing --to and no toNumber configured");
84
- }
85
- const result = await rt.manager.initiateCall(to, undefined, {
86
- message: options.message,
87
- mode:
88
- options.mode === "notify" || options.mode === "conversation"
89
- ? options.mode
90
- : undefined,
91
- });
92
- if (!result.success) {
93
- throw new Error(result.error || "initiate failed");
94
- }
95
- // eslint-disable-next-line no-console
96
- console.log(JSON.stringify({ callId: result.callId }, null, 2));
97
- },
98
- );
72
+ .action(async (options: { message: string; to?: string; mode?: string }) => {
73
+ const rt = await ensureRuntime();
74
+ const to = options.to ?? rt.config.toNumber;
75
+ if (!to) {
76
+ throw new Error("Missing --to and no toNumber configured");
77
+ }
78
+ const result = await rt.manager.initiateCall(to, undefined, {
79
+ message: options.message,
80
+ mode:
81
+ options.mode === "notify" || options.mode === "conversation" ? options.mode : undefined,
82
+ });
83
+ if (!result.success) {
84
+ throw new Error(result.error || "initiate failed");
85
+ }
86
+ // eslint-disable-next-line no-console
87
+ console.log(JSON.stringify({ callId: result.callId }, null, 2));
88
+ });
99
89
 
100
90
  root
101
91
  .command("start")
@@ -107,23 +97,19 @@ export function registerVoiceCallCli(params: {
107
97
  "Call mode: notify (hangup after message) or conversation (stay open)",
108
98
  "conversation",
109
99
  )
110
- .action(
111
- async (options: { to: string; message?: string; mode?: string }) => {
112
- const rt = await ensureRuntime();
113
- const result = await rt.manager.initiateCall(options.to, undefined, {
114
- message: options.message,
115
- mode:
116
- options.mode === "notify" || options.mode === "conversation"
117
- ? options.mode
118
- : undefined,
119
- });
120
- if (!result.success) {
121
- throw new Error(result.error || "initiate failed");
122
- }
123
- // eslint-disable-next-line no-console
124
- console.log(JSON.stringify({ callId: result.callId }, null, 2));
125
- },
126
- );
100
+ .action(async (options: { to: string; message?: string; mode?: string }) => {
101
+ const rt = await ensureRuntime();
102
+ const result = await rt.manager.initiateCall(options.to, undefined, {
103
+ message: options.message,
104
+ mode:
105
+ options.mode === "notify" || options.mode === "conversation" ? options.mode : undefined,
106
+ });
107
+ if (!result.success) {
108
+ throw new Error(result.error || "initiate failed");
109
+ }
110
+ // eslint-disable-next-line no-console
111
+ console.log(JSON.stringify({ callId: result.callId }, null, 2));
112
+ });
127
113
 
128
114
  root
129
115
  .command("continue")
@@ -132,10 +118,7 @@ export function registerVoiceCallCli(params: {
132
118
  .requiredOption("--message <text>", "Message to speak")
133
119
  .action(async (options: { callId: string; message: string }) => {
134
120
  const rt = await ensureRuntime();
135
- const result = await rt.manager.continueCall(
136
- options.callId,
137
- options.message,
138
- );
121
+ const result = await rt.manager.continueCall(options.callId, options.message);
139
122
  if (!result.success) {
140
123
  throw new Error(result.error || "continue failed");
141
124
  }
@@ -185,86 +168,70 @@ export function registerVoiceCallCli(params: {
185
168
 
186
169
  root
187
170
  .command("tail")
188
- .description(
189
- "Tail voice-call JSONL logs (prints new lines; useful during provider tests)",
190
- )
171
+ .description("Tail voice-call JSONL logs (prints new lines; useful during provider tests)")
191
172
  .option("--file <path>", "Path to calls.jsonl", resolveDefaultStorePath(config))
192
173
  .option("--since <n>", "Print last N lines first", "25")
193
174
  .option("--poll <ms>", "Poll interval in ms", "250")
194
- .action(
195
- async (options: { file: string; since?: string; poll?: string }) => {
196
- const file = options.file;
197
- const since = Math.max(0, Number(options.since ?? 0));
198
- const pollMs = Math.max(50, Number(options.poll ?? 250));
175
+ .action(async (options: { file: string; since?: string; poll?: string }) => {
176
+ const file = options.file;
177
+ const since = Math.max(0, Number(options.since ?? 0));
178
+ const pollMs = Math.max(50, Number(options.poll ?? 250));
199
179
 
200
- if (!fs.existsSync(file)) {
201
- logger.error(`No log file at ${file}`);
202
- process.exit(1);
203
- }
180
+ if (!fs.existsSync(file)) {
181
+ logger.error(`No log file at ${file}`);
182
+ process.exit(1);
183
+ }
204
184
 
205
- const initial = fs.readFileSync(file, "utf8");
206
- const lines = initial.split("\n").filter(Boolean);
207
- for (const line of lines.slice(Math.max(0, lines.length - since))) {
208
- // eslint-disable-next-line no-console
209
- console.log(line);
210
- }
185
+ const initial = fs.readFileSync(file, "utf8");
186
+ const lines = initial.split("\n").filter(Boolean);
187
+ for (const line of lines.slice(Math.max(0, lines.length - since))) {
188
+ // eslint-disable-next-line no-console
189
+ console.log(line);
190
+ }
211
191
 
212
- let offset = Buffer.byteLength(initial, "utf8");
192
+ let offset = Buffer.byteLength(initial, "utf8");
213
193
 
214
- for (;;) {
215
- try {
216
- const stat = fs.statSync(file);
217
- if (stat.size < offset) {
218
- offset = 0;
219
- }
220
- if (stat.size > offset) {
221
- const fd = fs.openSync(file, "r");
222
- try {
223
- const buf = Buffer.alloc(stat.size - offset);
224
- fs.readSync(fd, buf, 0, buf.length, offset);
225
- offset = stat.size;
226
- const text = buf.toString("utf8");
227
- for (const line of text.split("\n").filter(Boolean)) {
228
- // eslint-disable-next-line no-console
229
- console.log(line);
230
- }
231
- } finally {
232
- fs.closeSync(fd);
194
+ for (;;) {
195
+ try {
196
+ const stat = fs.statSync(file);
197
+ if (stat.size < offset) {
198
+ offset = 0;
199
+ }
200
+ if (stat.size > offset) {
201
+ const fd = fs.openSync(file, "r");
202
+ try {
203
+ const buf = Buffer.alloc(stat.size - offset);
204
+ fs.readSync(fd, buf, 0, buf.length, offset);
205
+ offset = stat.size;
206
+ const text = buf.toString("utf8");
207
+ for (const line of text.split("\n").filter(Boolean)) {
208
+ // eslint-disable-next-line no-console
209
+ console.log(line);
233
210
  }
211
+ } finally {
212
+ fs.closeSync(fd);
234
213
  }
235
- } catch {
236
- // ignore and retry
237
214
  }
238
- await sleep(pollMs);
215
+ } catch {
216
+ // ignore and retry
239
217
  }
240
- },
241
- );
218
+ await sleep(pollMs);
219
+ }
220
+ });
242
221
 
243
222
  root
244
223
  .command("expose")
245
224
  .description("Enable/disable Tailscale serve/funnel for the webhook")
246
225
  .option("--mode <mode>", "off | serve (tailnet) | funnel (public)", "funnel")
247
- .option(
248
- "--path <path>",
249
- "Tailscale path to expose (recommend matching serve.path)",
250
- )
226
+ .option("--path <path>", "Tailscale path to expose (recommend matching serve.path)")
251
227
  .option("--port <port>", "Local webhook port")
252
228
  .option("--serve-path <path>", "Local webhook path")
253
229
  .action(
254
- async (options: {
255
- mode?: string;
256
- port?: string;
257
- path?: string;
258
- servePath?: string;
259
- }) => {
230
+ async (options: { mode?: string; port?: string; path?: string; servePath?: string }) => {
260
231
  const mode = resolveMode(options.mode ?? "funnel");
261
232
  const servePort = Number(options.port ?? config.serve.port ?? 3334);
262
- const servePath = String(
263
- options.servePath ?? config.serve.path ?? "/voice/webhook",
264
- );
265
- const tsPath = String(
266
- options.path ?? config.tailscale?.path ?? servePath,
267
- );
233
+ const servePath = String(options.servePath ?? config.serve.path ?? "/voice/webhook");
234
+ const tsPath = String(options.path ?? config.tailscale?.path ?? servePath);
268
235
 
269
236
  const localUrl = `http://127.0.0.1:${servePort}`;
270
237
 
@@ -1,10 +1,7 @@
1
1
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
-
3
2
  import { validateProviderConfig, resolveVoiceCallConfig, type VoiceCallConfig } from "./config.js";
4
3
 
5
- function createBaseConfig(
6
- provider: "telnyx" | "twilio" | "plivo" | "mock",
7
- ): VoiceCallConfig {
4
+ function createBaseConfig(provider: "telnyx" | "twilio" | "plivo" | "mock"): VoiceCallConfig {
8
5
  return {
9
6
  enabled: true,
10
7
  provider,
package/src/config.ts CHANGED
@@ -23,12 +23,7 @@ export const E164Schema = z
23
23
  * - "pairing": Unknown callers can request pairing (future)
24
24
  * - "open": Accept all inbound calls (dangerous!)
25
25
  */
26
- export const InboundPolicySchema = z.enum([
27
- "disabled",
28
- "allowlist",
29
- "pairing",
30
- "open",
31
- ]);
26
+ export const InboundPolicySchema = z.enum(["disabled", "allowlist", "pairing", "open"]);
32
27
  export type InboundPolicy = z.infer<typeof InboundPolicySchema>;
33
28
 
34
29
  // -----------------------------------------------------------------------------
@@ -37,33 +32,33 @@ export type InboundPolicy = z.infer<typeof InboundPolicySchema>;
37
32
 
38
33
  export const TelnyxConfigSchema = z
39
34
  .object({
40
- /** Telnyx API v2 key */
41
- apiKey: z.string().min(1).optional(),
42
- /** Telnyx connection ID (from Call Control app) */
43
- connectionId: z.string().min(1).optional(),
44
- /** Public key for webhook signature verification */
45
- publicKey: z.string().min(1).optional(),
46
- })
35
+ /** Telnyx API v2 key */
36
+ apiKey: z.string().min(1).optional(),
37
+ /** Telnyx connection ID (from Call Control app) */
38
+ connectionId: z.string().min(1).optional(),
39
+ /** Public key for webhook signature verification */
40
+ publicKey: z.string().min(1).optional(),
41
+ })
47
42
  .strict();
48
43
  export type TelnyxConfig = z.infer<typeof TelnyxConfigSchema>;
49
44
 
50
45
  export const TwilioConfigSchema = z
51
46
  .object({
52
- /** Twilio Account SID */
53
- accountSid: z.string().min(1).optional(),
54
- /** Twilio Auth Token */
55
- authToken: z.string().min(1).optional(),
56
- })
47
+ /** Twilio Account SID */
48
+ accountSid: z.string().min(1).optional(),
49
+ /** Twilio Auth Token */
50
+ authToken: z.string().min(1).optional(),
51
+ })
57
52
  .strict();
58
53
  export type TwilioConfig = z.infer<typeof TwilioConfigSchema>;
59
54
 
60
55
  export const PlivoConfigSchema = z
61
56
  .object({
62
- /** Plivo Auth ID (starts with MA/SA) */
63
- authId: z.string().min(1).optional(),
64
- /** Plivo Auth Token */
65
- authToken: z.string().min(1).optional(),
66
- })
57
+ /** Plivo Auth ID (starts with MA/SA) */
58
+ authId: z.string().min(1).optional(),
59
+ /** Plivo Auth Token */
60
+ authToken: z.string().min(1).optional(),
61
+ })
67
62
  .strict();
68
63
  export type PlivoConfig = z.infer<typeof PlivoConfigSchema>;
69
64
 
@@ -190,9 +185,7 @@ export const VoiceCallTailscaleConfigSchema = z
190
185
  })
191
186
  .strict()
192
187
  .default({ mode: "off", path: "/voice/webhook" });
193
- export type VoiceCallTailscaleConfig = z.infer<
194
- typeof VoiceCallTailscaleConfigSchema
195
- >;
188
+ export type VoiceCallTailscaleConfig = z.infer<typeof VoiceCallTailscaleConfigSchema>;
196
189
 
197
190
  // -----------------------------------------------------------------------------
198
191
  // Tunnel Configuration (unified ngrok/tailscale)
@@ -207,9 +200,7 @@ export const VoiceCallTunnelConfigSchema = z
207
200
  * - "tailscale-serve": Tailscale serve (private to tailnet)
208
201
  * - "tailscale-funnel": Tailscale funnel (public HTTPS)
209
202
  */
210
- provider: z
211
- .enum(["none", "ngrok", "tailscale-serve", "tailscale-funnel"])
212
- .default("none"),
203
+ provider: z.enum(["none", "ngrok", "tailscale-serve", "tailscale-funnel"]).default("none"),
213
204
  /** ngrok auth token (optional, enables longer sessions and more features) */
214
205
  ngrokAuthToken: z.string().min(1).optional(),
215
206
  /** ngrok custom domain (paid feature, e.g., "myapp.ngrok.io") */
@@ -283,9 +274,7 @@ export const VoiceCallStreamingConfigSchema = z
283
274
  vadThreshold: 0.5,
284
275
  streamPath: "/voice/stream",
285
276
  });
286
- export type VoiceCallStreamingConfig = z.infer<
287
- typeof VoiceCallStreamingConfigSchema
288
- >;
277
+ export type VoiceCallStreamingConfig = z.infer<typeof VoiceCallStreamingConfigSchema>;
289
278
 
290
279
  // -----------------------------------------------------------------------------
291
280
  // Main Voice Call Configuration
@@ -293,90 +282,90 @@ export type VoiceCallStreamingConfig = z.infer<
293
282
 
294
283
  export const VoiceCallConfigSchema = z
295
284
  .object({
296
- /** Enable voice call functionality */
297
- enabled: z.boolean().default(false),
285
+ /** Enable voice call functionality */
286
+ enabled: z.boolean().default(false),
298
287
 
299
- /** Active provider (telnyx, twilio, plivo, or mock) */
300
- provider: z.enum(["telnyx", "twilio", "plivo", "mock"]).optional(),
288
+ /** Active provider (telnyx, twilio, plivo, or mock) */
289
+ provider: z.enum(["telnyx", "twilio", "plivo", "mock"]).optional(),
301
290
 
302
- /** Telnyx-specific configuration */
303
- telnyx: TelnyxConfigSchema.optional(),
291
+ /** Telnyx-specific configuration */
292
+ telnyx: TelnyxConfigSchema.optional(),
304
293
 
305
- /** Twilio-specific configuration */
306
- twilio: TwilioConfigSchema.optional(),
294
+ /** Twilio-specific configuration */
295
+ twilio: TwilioConfigSchema.optional(),
307
296
 
308
- /** Plivo-specific configuration */
309
- plivo: PlivoConfigSchema.optional(),
297
+ /** Plivo-specific configuration */
298
+ plivo: PlivoConfigSchema.optional(),
310
299
 
311
- /** Phone number to call from (E.164) */
312
- fromNumber: E164Schema.optional(),
300
+ /** Phone number to call from (E.164) */
301
+ fromNumber: E164Schema.optional(),
313
302
 
314
- /** Default phone number to call (E.164) */
315
- toNumber: E164Schema.optional(),
303
+ /** Default phone number to call (E.164) */
304
+ toNumber: E164Schema.optional(),
316
305
 
317
- /** Inbound call policy */
318
- inboundPolicy: InboundPolicySchema.default("disabled"),
306
+ /** Inbound call policy */
307
+ inboundPolicy: InboundPolicySchema.default("disabled"),
319
308
 
320
- /** Allowlist of phone numbers for inbound calls (E.164) */
321
- allowFrom: z.array(E164Schema).default([]),
309
+ /** Allowlist of phone numbers for inbound calls (E.164) */
310
+ allowFrom: z.array(E164Schema).default([]),
322
311
 
323
- /** Greeting message for inbound calls */
324
- inboundGreeting: z.string().optional(),
312
+ /** Greeting message for inbound calls */
313
+ inboundGreeting: z.string().optional(),
325
314
 
326
- /** Outbound call configuration */
327
- outbound: OutboundConfigSchema,
315
+ /** Outbound call configuration */
316
+ outbound: OutboundConfigSchema,
328
317
 
329
- /** Maximum call duration in seconds */
330
- maxDurationSeconds: z.number().int().positive().default(300),
318
+ /** Maximum call duration in seconds */
319
+ maxDurationSeconds: z.number().int().positive().default(300),
331
320
 
332
- /** Silence timeout for end-of-speech detection (ms) */
333
- silenceTimeoutMs: z.number().int().positive().default(800),
321
+ /** Silence timeout for end-of-speech detection (ms) */
322
+ silenceTimeoutMs: z.number().int().positive().default(800),
334
323
 
335
- /** Timeout for user transcript (ms) */
336
- transcriptTimeoutMs: z.number().int().positive().default(180000),
324
+ /** Timeout for user transcript (ms) */
325
+ transcriptTimeoutMs: z.number().int().positive().default(180000),
337
326
 
338
- /** Ring timeout for outbound calls (ms) */
339
- ringTimeoutMs: z.number().int().positive().default(30000),
327
+ /** Ring timeout for outbound calls (ms) */
328
+ ringTimeoutMs: z.number().int().positive().default(30000),
340
329
 
341
- /** Maximum concurrent calls */
342
- maxConcurrentCalls: z.number().int().positive().default(1),
330
+ /** Maximum concurrent calls */
331
+ maxConcurrentCalls: z.number().int().positive().default(1),
343
332
 
344
- /** Webhook server configuration */
345
- serve: VoiceCallServeConfigSchema,
333
+ /** Webhook server configuration */
334
+ serve: VoiceCallServeConfigSchema,
346
335
 
347
- /** Tailscale exposure configuration (legacy, prefer tunnel config) */
348
- tailscale: VoiceCallTailscaleConfigSchema,
336
+ /** Tailscale exposure configuration (legacy, prefer tunnel config) */
337
+ tailscale: VoiceCallTailscaleConfigSchema,
349
338
 
350
- /** Tunnel configuration (unified ngrok/tailscale) */
351
- tunnel: VoiceCallTunnelConfigSchema,
339
+ /** Tunnel configuration (unified ngrok/tailscale) */
340
+ tunnel: VoiceCallTunnelConfigSchema,
352
341
 
353
- /** Real-time audio streaming configuration */
354
- streaming: VoiceCallStreamingConfigSchema,
342
+ /** Real-time audio streaming configuration */
343
+ streaming: VoiceCallStreamingConfigSchema,
355
344
 
356
- /** Public webhook URL override (if set, bypasses tunnel auto-detection) */
357
- publicUrl: z.string().url().optional(),
345
+ /** Public webhook URL override (if set, bypasses tunnel auto-detection) */
346
+ publicUrl: z.string().url().optional(),
358
347
 
359
- /** Skip webhook signature verification (development only, NOT for production) */
360
- skipSignatureVerification: z.boolean().default(false),
348
+ /** Skip webhook signature verification (development only, NOT for production) */
349
+ skipSignatureVerification: z.boolean().default(false),
361
350
 
362
- /** STT configuration */
363
- stt: SttConfigSchema,
351
+ /** STT configuration */
352
+ stt: SttConfigSchema,
364
353
 
365
- /** TTS override (deep-merges with core messages.tts) */
366
- tts: TtsConfigSchema,
354
+ /** TTS override (deep-merges with core messages.tts) */
355
+ tts: TtsConfigSchema,
367
356
 
368
- /** Store path for call logs */
369
- store: z.string().optional(),
357
+ /** Store path for call logs */
358
+ store: z.string().optional(),
370
359
 
371
- /** Model for generating voice responses (e.g., "anthropic/claude-sonnet-4", "openai/gpt-4o") */
372
- responseModel: z.string().default("openai/gpt-4o-mini"),
360
+ /** Model for generating voice responses (e.g., "anthropic/claude-sonnet-4", "openai/gpt-4o") */
361
+ responseModel: z.string().default("openai/gpt-4o-mini"),
373
362
 
374
- /** System prompt for voice responses */
375
- responseSystemPrompt: z.string().optional(),
363
+ /** System prompt for voice responses */
364
+ responseSystemPrompt: z.string().optional(),
376
365
 
377
- /** Timeout for response generation in ms (default 30s) */
378
- responseTimeoutMs: z.number().int().positive().default(30000),
379
- })
366
+ /** Timeout for response generation in ms (default 30s) */
367
+ responseTimeoutMs: z.number().int().positive().default(30000),
368
+ })
380
369
  .strict();
381
370
 
382
371
  export type VoiceCallConfig = z.infer<typeof VoiceCallConfigSchema>;
@@ -395,30 +384,23 @@ export function resolveVoiceCallConfig(config: VoiceCallConfig): VoiceCallConfig
395
384
  // Telnyx
396
385
  if (resolved.provider === "telnyx") {
397
386
  resolved.telnyx = resolved.telnyx ?? {};
398
- resolved.telnyx.apiKey =
399
- resolved.telnyx.apiKey ?? process.env.TELNYX_API_KEY;
400
- resolved.telnyx.connectionId =
401
- resolved.telnyx.connectionId ?? process.env.TELNYX_CONNECTION_ID;
402
- resolved.telnyx.publicKey =
403
- resolved.telnyx.publicKey ?? process.env.TELNYX_PUBLIC_KEY;
387
+ resolved.telnyx.apiKey = resolved.telnyx.apiKey ?? process.env.TELNYX_API_KEY;
388
+ resolved.telnyx.connectionId = resolved.telnyx.connectionId ?? process.env.TELNYX_CONNECTION_ID;
389
+ resolved.telnyx.publicKey = resolved.telnyx.publicKey ?? process.env.TELNYX_PUBLIC_KEY;
404
390
  }
405
391
 
406
392
  // Twilio
407
393
  if (resolved.provider === "twilio") {
408
394
  resolved.twilio = resolved.twilio ?? {};
409
- resolved.twilio.accountSid =
410
- resolved.twilio.accountSid ?? process.env.TWILIO_ACCOUNT_SID;
411
- resolved.twilio.authToken =
412
- resolved.twilio.authToken ?? process.env.TWILIO_AUTH_TOKEN;
395
+ resolved.twilio.accountSid = resolved.twilio.accountSid ?? process.env.TWILIO_ACCOUNT_SID;
396
+ resolved.twilio.authToken = resolved.twilio.authToken ?? process.env.TWILIO_AUTH_TOKEN;
413
397
  }
414
398
 
415
399
  // Plivo
416
400
  if (resolved.provider === "plivo") {
417
401
  resolved.plivo = resolved.plivo ?? {};
418
- resolved.plivo.authId =
419
- resolved.plivo.authId ?? process.env.PLIVO_AUTH_ID;
420
- resolved.plivo.authToken =
421
- resolved.plivo.authToken ?? process.env.PLIVO_AUTH_TOKEN;
402
+ resolved.plivo.authId = resolved.plivo.authId ?? process.env.PLIVO_AUTH_ID;
403
+ resolved.plivo.authToken = resolved.plivo.authToken ?? process.env.PLIVO_AUTH_TOKEN;
422
404
  }
423
405
 
424
406
  // Tunnel Config
@@ -427,13 +409,9 @@ export function resolveVoiceCallConfig(config: VoiceCallConfig): VoiceCallConfig
427
409
  allowNgrokFreeTierLoopbackBypass: false,
428
410
  };
429
411
  resolved.tunnel.allowNgrokFreeTierLoopbackBypass =
430
- resolved.tunnel.allowNgrokFreeTierLoopbackBypass ||
431
- resolved.tunnel.allowNgrokFreeTier ||
432
- false;
433
- resolved.tunnel.ngrokAuthToken =
434
- resolved.tunnel.ngrokAuthToken ?? process.env.NGROK_AUTHTOKEN;
435
- resolved.tunnel.ngrokDomain =
436
- resolved.tunnel.ngrokDomain ?? process.env.NGROK_DOMAIN;
412
+ resolved.tunnel.allowNgrokFreeTierLoopbackBypass || resolved.tunnel.allowNgrokFreeTier || false;
413
+ resolved.tunnel.ngrokAuthToken = resolved.tunnel.ngrokAuthToken ?? process.env.NGROK_AUTHTOKEN;
414
+ resolved.tunnel.ngrokDomain = resolved.tunnel.ngrokDomain ?? process.env.NGROK_DOMAIN;
437
415
 
438
416
  return resolved;
439
417
  }
@@ -1,7 +1,6 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
3
  import { fileURLToPath, pathToFileURL } from "node:url";
4
-
5
4
  import type { VoiceCallTtsConfig } from "./config.js";
6
5
 
7
6
  export type CoreConfig = {
@@ -51,10 +50,7 @@ type CoreAgentDeps = {
51
50
  ensureAgentWorkspace: (params?: { dir: string }) => Promise<void>;
52
51
  resolveStorePath: (store?: string, opts?: { agentId?: string }) => string;
53
52
  loadSessionStore: (storePath: string) => Record<string, unknown>;
54
- saveSessionStore: (
55
- storePath: string,
56
- store: Record<string, unknown>,
57
- ) => Promise<void>;
53
+ saveSessionStore: (storePath: string, store: Record<string, unknown>) => Promise<void>;
58
54
  resolveSessionFilePath: (
59
55
  sessionId: string,
60
56
  entry: unknown,
@@ -75,19 +71,25 @@ function findPackageRoot(startDir: string, name: string): string | null {
75
71
  if (fs.existsSync(pkgPath)) {
76
72
  const raw = fs.readFileSync(pkgPath, "utf8");
77
73
  const pkg = JSON.parse(raw) as { name?: string };
78
- if (pkg.name === name) return dir;
74
+ if (pkg.name === name) {
75
+ return dir;
76
+ }
79
77
  }
80
78
  } catch {
81
79
  // ignore parse errors and keep walking
82
80
  }
83
81
  const parent = path.dirname(dir);
84
- if (parent === dir) return null;
82
+ if (parent === dir) {
83
+ return null;
84
+ }
85
85
  dir = parent;
86
86
  }
87
87
  }
88
88
 
89
89
  function resolveOpenClawRoot(): string {
90
- if (coreRootCache) return coreRootCache;
90
+ if (coreRootCache) {
91
+ return coreRootCache;
92
+ }
91
93
  const override = process.env.OPENCLAW_ROOT?.trim();
92
94
  if (override) {
93
95
  coreRootCache = override;
@@ -116,9 +118,7 @@ function resolveOpenClawRoot(): string {
116
118
  }
117
119
  }
118
120
 
119
- throw new Error(
120
- "Unable to resolve core root. Set OPENCLAW_ROOT to the package root.",
121
- );
121
+ throw new Error("Unable to resolve core root. Set OPENCLAW_ROOT to the package root.");
122
122
  }
123
123
 
124
124
  async function importCoreModule<T>(relativePath: string): Promise<T> {
@@ -133,7 +133,9 @@ async function importCoreModule<T>(relativePath: string): Promise<T> {
133
133
  }
134
134
 
135
135
  export async function loadCoreAgentDeps(): Promise<CoreAgentDeps> {
136
- if (coreDepsPromise) return coreDepsPromise;
136
+ if (coreDepsPromise) {
137
+ return coreDepsPromise;
138
+ }
137
139
 
138
140
  coreDepsPromise = (async () => {
139
141
  const [
@@ -1,6 +1,6 @@
1
- import type { CallId, CallRecord } from "../types.js";
2
1
  import type { VoiceCallConfig } from "../config.js";
3
2
  import type { VoiceCallProvider } from "../providers/base.js";
3
+ import type { CallId, CallRecord } from "../types.js";
4
4
 
5
5
  export type TranscriptWaiter = {
6
6
  resolve: (text: string) => void;