@openclaw/voice-call 2026.9.6 → 2026.9.7

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 (62) hide show
  1. package/assets/icon.png +0 -0
  2. package/dist/.setup/{cli-D6j7ppuN.mjs → cli-D6uwMXyS.mjs} +6 -10
  3. package/dist/.setup/{config-72FnMWHb.mjs → config-Ds7xoZbI.mjs} +27 -30
  4. package/dist/.setup/{guarded-json-api-exU4Hutw.mjs → guarded-json-api-Bqc6QS8A.mjs} +23 -17
  5. package/dist/.setup/{mock-CcYEAUhV.mjs → mock-ClWW6Ufu.mjs} +34 -53
  6. package/dist/.setup/{plivo-BGJda1Uz.mjs → plivo-69ObrQPk.mjs} +17 -42
  7. package/dist/.setup/realtime-agent-context.legacy-DAEWY27o.mjs +53 -0
  8. package/dist/.setup/{realtime-handler-DlFmdSPn.mjs → realtime-handler-BgDPcPZt.mjs} +34 -53
  9. package/dist/.setup/{response-generator-hZyGIjO4.mjs → response-generator-BzKrJMEb.mjs} +2 -2
  10. package/dist/.setup/{runtime-entry-dDRm6RgA.mjs → runtime-entry-BNOQRXii.mjs} +82 -297
  11. package/dist/.setup/{telnyx-BV56PXr-.mjs → telnyx-DjFGCfy1.mjs} +19 -55
  12. package/dist/.setup/{twilio-BMb-7cu-.mjs → twilio-Bp3gfmUB.mjs} +29 -78
  13. package/dist/.setup/{store-path-B1j1up4_.mjs → utils-B5pnM_Ij.mjs} +14 -6
  14. package/dist/api.js +5 -1
  15. package/dist/doctor-contract-api.js +186 -15
  16. package/dist/index.js +6 -42
  17. package/dist/runtime-entry.js +1 -1
  18. package/node_modules/typebox/build/compile/code.mjs +1 -2
  19. package/node_modules/typebox/build/compile/validator.mjs +5 -1
  20. package/node_modules/typebox/build/format/iri.mjs +16 -9
  21. package/node_modules/typebox/build/guard/emit.mjs +2 -2
  22. package/node_modules/typebox/build/guard/guard.d.mts +7 -5
  23. package/node_modules/typebox/build/guard/guard.mjs +14 -9
  24. package/node_modules/typebox/build/guard/index.mjs +6 -0
  25. package/node_modules/typebox/build/guard/unicode/unicode.d.mts +6 -0
  26. package/node_modules/typebox/build/guard/unicode/unicode.mjs +35 -0
  27. package/node_modules/typebox/build/guard/{string.mjs → unicode/unicode_segment.mjs} +16 -19
  28. package/node_modules/typebox/build/schema/build.mjs +2 -3
  29. package/node_modules/typebox/build/schema/engine/_pathing.d.mts +1 -0
  30. package/node_modules/typebox/build/schema/engine/_pathing.mjs +11 -0
  31. package/node_modules/typebox/build/schema/engine/_refine.mjs +13 -5
  32. package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +2 -1
  33. package/node_modules/typebox/build/schema/engine/dependencies.mjs +2 -1
  34. package/node_modules/typebox/build/schema/engine/dependentSchemas.mjs +2 -1
  35. package/node_modules/typebox/build/schema/engine/dynamicRef.d.mts +1 -1
  36. package/node_modules/typebox/build/schema/engine/dynamicRef.mjs +2 -2
  37. package/node_modules/typebox/build/schema/engine/format.mjs +9 -2
  38. package/node_modules/typebox/build/schema/engine/patternProperties.mjs +3 -2
  39. package/node_modules/typebox/build/schema/engine/properties.mjs +3 -2
  40. package/node_modules/typebox/build/schema/engine/propertyNames.mjs +2 -1
  41. package/node_modules/typebox/build/schema/engine/recursiveRef.d.mts +1 -1
  42. package/node_modules/typebox/build/schema/engine/recursiveRef.mjs +2 -2
  43. package/node_modules/typebox/build/schema/engine/ref.d.mts +1 -1
  44. package/node_modules/typebox/build/schema/engine/ref.mjs +2 -2
  45. package/node_modules/typebox/build/schema/engine/schema.mjs +24 -24
  46. package/node_modules/typebox/build/schema/parse.mjs +3 -0
  47. package/node_modules/typebox/build/schema/pointer/pointer.d.mts +1 -6
  48. package/node_modules/typebox/build/schema/pointer/pointer.mjs +47 -64
  49. package/node_modules/typebox/build/system/settings/internal.d.mts +2 -0
  50. package/node_modules/typebox/build/system/settings/internal.mjs +20 -0
  51. package/node_modules/typebox/build/system/settings/settings.d.mts +11 -0
  52. package/node_modules/typebox/build/system/settings/settings.mjs +14 -0
  53. package/node_modules/typebox/build/type/extends/inference.d.mts +1 -1
  54. package/node_modules/typebox/build/type/extends/inference.mjs +15 -2
  55. package/node_modules/typebox/build/type/extends/tuple.d.mts +2 -1
  56. package/node_modules/typebox/build/type/extends/tuple.mjs +21 -1
  57. package/node_modules/typebox/build/value/parse/parse.mjs +10 -4
  58. package/node_modules/typebox/package.json +1 -1
  59. package/node_modules/typebox/readme.md +3 -1
  60. package/package.json +5 -5
  61. package/dist/runtime-api.js +0 -6
  62. /package/node_modules/typebox/build/guard/{string.d.mts → unicode/unicode_segment.d.mts} +0 -0
@@ -1,4 +1,4 @@
1
- import { s as verifyTelnyxWebhook, t as guardedJsonApiRequest } from "./guarded-json-api-exU4Hutw.mjs";
1
+ import { c as verifyTelnyxWebhook, n as readProviderCallStatus, t as guardedJsonApiRequest } from "./guarded-json-api-Bqc6QS8A.mjs";
2
2
  import crypto from "node:crypto";
3
3
  //#region extensions/voice-call/src/providers/telnyx.ts
4
4
  function normalizeTelnyxDirection(direction) {
@@ -30,9 +30,6 @@ var TelnyxProvider = class {
30
30
  this.publicKey = config.publicKey;
31
31
  this.options = options;
32
32
  }
33
- /**
34
- * Make an authenticated request to the Telnyx API.
35
- */
36
33
  async apiRequest(endpoint, body, options) {
37
34
  return await guardedJsonApiRequest({
38
35
  url: `${this.baseUrl}${endpoint}`,
@@ -48,9 +45,6 @@ var TelnyxProvider = class {
48
45
  errorPrefix: "Telnyx API error"
49
46
  });
50
47
  }
51
- /**
52
- * Verify Telnyx webhook signature using Ed25519.
53
- */
54
48
  verifyWebhook(ctx) {
55
49
  const result = verifyTelnyxWebhook(ctx, this.publicKey, { skipVerification: this.options.skipVerification });
56
50
  return {
@@ -61,9 +55,6 @@ var TelnyxProvider = class {
61
55
  releaseReplay: result.releaseReplay
62
56
  };
63
57
  }
64
- /**
65
- * Parse Telnyx webhook event into normalized format.
66
- */
67
58
  parseWebhookEvent(ctx, options) {
68
59
  try {
69
60
  const data = JSON.parse(ctx.rawBody).data;
@@ -83,9 +74,6 @@ var TelnyxProvider = class {
83
74
  };
84
75
  }
85
76
  }
86
- /**
87
- * Convert Telnyx event to normalized event format.
88
- */
89
77
  normalizeEvent(data, dedupeKey) {
90
78
  let callId = "";
91
79
  if (data.payload?.client_state) callId = decodeClientStateBase64(data.payload.client_state) ?? data.payload.client_state;
@@ -190,9 +178,6 @@ var TelnyxProvider = class {
190
178
  status: "initiated"
191
179
  };
192
180
  }
193
- /**
194
- * Hang up a call via Telnyx API.
195
- */
196
181
  async hangupCall(input) {
197
182
  await this.apiRequest(`/calls/${input.providerCallId}/actions/hangup`, { command_id: crypto.randomUUID() }, { allowNotFound: true });
198
183
  }
@@ -203,9 +188,6 @@ var TelnyxProvider = class {
203
188
  };
204
189
  await this.apiRequest(`/calls/${input.providerCallId}/actions/answer`, body);
205
190
  }
206
- /**
207
- * Play TTS audio via Telnyx speak action.
208
- */
209
191
  async playTts(input) {
210
192
  await this.apiRequest(`/calls/${input.providerCallId}/actions/speak`, {
211
193
  command_id: crypto.randomUUID(),
@@ -214,57 +196,39 @@ var TelnyxProvider = class {
214
196
  language: input.locale || "en-US"
215
197
  });
216
198
  }
217
- /**
218
- * Start transcription (STT) via Telnyx.
219
- */
220
199
  async startListening(input) {
221
200
  await this.apiRequest(`/calls/${input.providerCallId}/actions/transcription_start`, {
222
201
  command_id: crypto.randomUUID(),
223
202
  language: input.language || "en"
224
203
  });
225
204
  }
226
- /**
227
- * Stop transcription via Telnyx.
228
- */
229
205
  async stopListening(input) {
230
206
  await this.apiRequest(`/calls/${input.providerCallId}/actions/transcription_stop`, { command_id: crypto.randomUUID() }, { allowNotFound: true });
231
207
  }
232
208
  async getCallStatus(input) {
233
- try {
234
- const data = await guardedJsonApiRequest({
235
- url: `${this.baseUrl}/calls/${input.providerCallId}`,
236
- method: "GET",
237
- headers: {
238
- Authorization: `Bearer ${this.apiKey}`,
239
- "Content-Type": "application/json"
240
- },
241
- allowNotFound: true,
242
- allowedHostnames: [this.apiHost],
243
- auditContext: "telnyx-get-call-status",
244
- errorPrefix: "Telnyx get call status error"
245
- });
246
- if (!data) return {
247
- status: "not-found",
248
- isTerminal: true
249
- };
250
- const state = data.data?.state ?? "unknown";
209
+ return readProviderCallStatus(() => guardedJsonApiRequest({
210
+ url: `${this.baseUrl}/calls/${input.providerCallId}`,
211
+ method: "GET",
212
+ headers: {
213
+ Authorization: `Bearer ${this.apiKey}`,
214
+ "Content-Type": "application/json"
215
+ },
216
+ allowNotFound: true,
217
+ allowedHostnames: [this.apiHost],
218
+ auditContext: "telnyx-get-call-status",
219
+ errorPrefix: "Telnyx get call status error"
220
+ }), (data) => {
221
+ const status = data.data?.state ?? "unknown";
251
222
  const isAlive = data.data?.is_alive;
252
- if (isAlive === void 0) return {
253
- status: state,
223
+ return isAlive === void 0 ? {
224
+ status,
254
225
  isTerminal: false,
255
226
  isUnknown: true
256
- };
257
- return {
258
- status: state,
227
+ } : {
228
+ status,
259
229
  isTerminal: !isAlive
260
230
  };
261
- } catch {
262
- return {
263
- status: "error",
264
- isTerminal: false,
265
- isUnknown: true
266
- };
267
- }
231
+ });
268
232
  }
269
233
  };
270
234
  function buildTelnyxStreamingFields(streamUrl, streamAuthToken) {
@@ -1,8 +1,7 @@
1
- import { fetchWithSsrFGuard } from "../runtime-api.js";
2
- import "../api.js";
1
+ import { fetchWithSsrFGuard } from "../api.js";
3
2
  import { n as requireSupportedTwilioApiHostname, r as resolveTwilioApiBaseUrl } from "./twilio-region-4fkgz3UG.mjs";
4
- import { a as normalizeProviderStatus, b as escapeXml, h as chunkAudio, i as mapProviderStatusToEndReason, l as getHeader, r as isProviderStatusTerminal, x as mapVoiceToPolly } from "./runtime-entry-dDRm6RgA.mjs";
5
- import { c as verifyTwilioWebhook, i as readVoiceCallProviderJsonResponse, n as cancelProviderResponseBody, r as readProviderErrorResponseSnippet, t as guardedJsonApiRequest } from "./guarded-json-api-exU4Hutw.mjs";
3
+ import { a as normalizeProviderStatus, b as mapVoiceToPolly, h as chunkAudio, i as mapProviderStatusToEndReason, l as getHeader, r as isProviderStatusTerminal, y as escapeXml } from "./runtime-entry-BNOQRXii.mjs";
4
+ import { a as readVoiceCallProviderJsonResponse, i as readProviderErrorResponseSnippet, l as verifyTwilioWebhook, n as readProviderCallStatus, r as cancelProviderResponseBody, t as guardedJsonApiRequest } from "./guarded-json-api-Bqc6QS8A.mjs";
6
5
  import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
7
6
  import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
8
7
  import crypto from "node:crypto";
@@ -75,10 +74,6 @@ async function twilioApiRequest(params) {
75
74
  }
76
75
  //#endregion
77
76
  //#region extensions/voice-call/src/providers/twilio/twiml-policy.ts
78
- /** Return true for Twilio outbound call directions. */
79
- function isOutboundDirection(direction) {
80
- return direction?.startsWith("outbound") ?? false;
81
- }
82
77
  /** Read the Twilio request fields needed by TwiML decision logic. */
83
78
  function readTwimlRequestView(ctx) {
84
79
  const params = new URLSearchParams(ctx.rawBody);
@@ -95,23 +90,17 @@ function readTwimlRequestView(ctx) {
95
90
  /** Decide the TwiML response kind for a Twilio webhook request. */
96
91
  function decideTwimlResponse(input) {
97
92
  if (input.callIdFromQuery && !input.isStatusCallback) {
98
- if (input.hasStoredTwiml) return {
99
- kind: "stored",
100
- consumeStoredTwimlCallId: input.callIdFromQuery
101
- };
102
- if (isOutboundDirection(input.direction)) return input.canStream ? { kind: "stream" } : { kind: "pause" };
93
+ if (input.hasStoredTwiml) return "stored";
94
+ if (input.direction?.startsWith("outbound")) return input.canStream ? "stream" : "pause";
103
95
  }
104
- if (input.isStatusCallback) return { kind: "empty" };
96
+ if (input.isStatusCallback) return "empty";
105
97
  if (input.direction === "inbound") {
106
- if (input.hasActiveStreams) return { kind: "queue" };
107
- if (input.canStream && input.callSid) return {
108
- kind: "stream",
109
- activateStreamCallSid: input.callSid
110
- };
111
- return { kind: "pause" };
98
+ if (input.hasActiveStreams) return "queue";
99
+ if (input.canStream && input.callSid) return "stream";
100
+ return "pause";
112
101
  }
113
- if (input.callStatus !== "in-progress") return { kind: "empty" };
114
- return input.canStream ? { kind: "stream" } : { kind: "pause" };
102
+ if (input.callStatus !== "in-progress") return "empty";
103
+ return input.canStream ? "stream" : "pause";
115
104
  }
116
105
  //#endregion
117
106
  //#region extensions/voice-call/src/providers/twilio/webhook.ts
@@ -155,15 +144,6 @@ function createTwilioRequestDedupeKey(ctx, verifiedRequestKey) {
155
144
  return `twilio:fallback:${crypto.createHash("sha256").update(`${signature}\n${callSid}\n${callStatus}\n${direction}\n${callId}\n${flow}\n${turnToken}\n${ctx.rawBody}`).digest("hex")}`;
156
145
  }
157
146
  var TwilioProvider = class TwilioProvider {
158
- /**
159
- * Delete stored TwiML for a given `callId`.
160
- *
161
- * We keep TwiML in-memory only long enough to satisfy the initial Twilio
162
- * webhook request before streaming. Subsequent webhooks should not reuse it.
163
- */
164
- deleteStoredTwiml(callId) {
165
- this.twimlStorage.delete(callId);
166
- }
167
147
  /**
168
148
  * Release all process-local metadata owned by one Twilio call.
169
149
  * Terminal webhooks can be replayed, so this must stay idempotent.
@@ -174,7 +154,7 @@ var TwilioProvider = class TwilioProvider {
174
154
  if (!resolvedCallId && webhookUrl) try {
175
155
  resolvedCallId = new URL(webhookUrl).searchParams.get("callId") || void 0;
176
156
  } catch {}
177
- if (resolvedCallId) this.deleteStoredTwiml(resolvedCallId);
157
+ if (resolvedCallId) this.twimlStorage.delete(resolvedCallId);
178
158
  this.callWebhookUrls.delete(providerCallId);
179
159
  this.callStreamMap.delete(providerCallId);
180
160
  this.streamAuthTokens.delete(providerCallId);
@@ -246,9 +226,6 @@ var TwilioProvider = class TwilioProvider {
246
226
  if (!streamSid || !this.mediaStreamHandler) return;
247
227
  this.mediaStreamHandler.clearTtsQueue(streamSid, reason);
248
228
  }
249
- /**
250
- * Make an authenticated request to the Twilio API.
251
- */
252
229
  async apiRequest(endpoint, params, options) {
253
230
  return await twilioApiRequest({
254
231
  baseUrl: this.baseUrl,
@@ -287,9 +264,6 @@ var TwilioProvider = class TwilioProvider {
287
264
  options: this.options
288
265
  });
289
266
  }
290
- /**
291
- * Parse Twilio webhook event into normalized format.
292
- */
293
267
  parseWebhookEvent(ctx, options) {
294
268
  try {
295
269
  const params = new URLSearchParams(ctx.rawBody);
@@ -316,9 +290,6 @@ var TwilioProvider = class TwilioProvider {
316
290
  };
317
291
  }
318
292
  }
319
- /**
320
- * Parse Twilio direction to normalized format.
321
- */
322
293
  static parseDirection(direction) {
323
294
  if (direction === "inbound") return "inbound";
324
295
  if (direction === "outbound-api" || direction === "outbound-dial") return "outbound";
@@ -328,9 +299,6 @@ var TwilioProvider = class TwilioProvider {
328
299
  if (!trimmed || !/^\d+(?:\.\d+)?$/.test(trimmed)) return .9;
329
300
  return Number(trimmed);
330
301
  }
331
- /**
332
- * Convert Twilio webhook params to normalized event format.
333
- */
334
302
  normalizeEvent(params, options) {
335
303
  const callSid = params.get("CallSid") || "";
336
304
  const callIdOverride = options?.callIdOverride;
@@ -408,15 +376,15 @@ var TwilioProvider = class TwilioProvider {
408
376
  if (!ctx) return TwilioProvider.EMPTY_TWIML;
409
377
  const view = readTwimlRequestView(ctx);
410
378
  const storedTwiml = view.callIdFromQuery ? this.twimlStorage.get(view.callIdFromQuery) : void 0;
411
- const decision = decideTwimlResponse({
379
+ switch (decideTwimlResponse({
412
380
  ...view,
413
381
  hasStoredTwiml: Boolean(storedTwiml),
414
382
  hasActiveStreams: this.activeStreamCalls.size > 0,
415
383
  canStream: Boolean(view.callSid && this.getStreamUrl())
416
- });
417
- if (decision.consumeStoredTwimlCallId) this.deleteStoredTwiml(decision.consumeStoredTwimlCallId);
418
- switch (decision.kind) {
419
- case "stored": return storedTwiml ?? TwilioProvider.EMPTY_TWIML;
384
+ })) {
385
+ case "stored":
386
+ if (view.callIdFromQuery) this.twimlStorage.delete(view.callIdFromQuery);
387
+ return storedTwiml ?? TwilioProvider.EMPTY_TWIML;
420
388
  case "queue": return TwilioProvider.QUEUE_TWIML;
421
389
  case "pause": return TwilioProvider.PAUSE_TWIML;
422
390
  case "stream": {
@@ -431,7 +399,7 @@ var TwilioProvider = class TwilioProvider {
431
399
  if (!view.callIdFromQuery || view.isStatusCallback) return null;
432
400
  const storedTwiml = this.twimlStorage.get(view.callIdFromQuery);
433
401
  if (!storedTwiml) return null;
434
- this.deleteStoredTwiml(view.callIdFromQuery);
402
+ this.twimlStorage.delete(view.callIdFromQuery);
435
403
  console.log(`[voice-call] Twilio initial TwiML consumed for call ${view.callIdFromQuery} (kind=pre-connect, callSid=${view.callSid ?? "unknown"})`);
436
404
  return storedTwiml;
437
405
  }
@@ -516,9 +484,6 @@ var TwilioProvider = class TwilioProvider {
516
484
  status: result.status === "queued" ? "queued" : "initiated"
517
485
  };
518
486
  }
519
- /**
520
- * Hang up a call via Twilio API.
521
- */
522
487
  async hangupCall(input) {
523
488
  await this.apiRequest(`/Calls/${input.providerCallId}.json`, { Status: "completed" }, { allowNotFound: true });
524
489
  this.releaseCallState(input.providerCallId, input.callId);
@@ -621,7 +586,7 @@ var TwilioProvider = class TwilioProvider {
621
586
  for (const chunk of chunkAudio(muLawAudio, CHUNK_SIZE)) {
622
587
  if (signal.aborted) break;
623
588
  chunkAttempts += 1;
624
- if (!handler.sendAudio(streamSid, chunk).sent) {
589
+ if (!handler.sendAudio(streamSid, chunk)) {
625
590
  handler.clearAudio(streamSid);
626
591
  throw new Error(`Telephony stream playback failed: audio chunk ${chunkAttempts} not delivered`);
627
592
  }
@@ -642,9 +607,6 @@ var TwilioProvider = class TwilioProvider {
642
607
  await handler.sendMarkAndWait(streamSid, markName, muLawAudio.length / 8, signal);
643
608
  });
644
609
  }
645
- /**
646
- * Start listening for speech via Twilio <Gather>.
647
- */
648
610
  async startListening(input) {
649
611
  const webhookUrl = this.callWebhookUrls.get(input.providerCallId);
650
612
  if (!webhookUrl) throw new Error("Missing webhook URL for this call (provider state not initialized)");
@@ -662,32 +624,21 @@ var TwilioProvider = class TwilioProvider {
662
624
  */
663
625
  async stopListening(_input) {}
664
626
  async getCallStatus(input) {
665
- try {
666
- const data = await guardedJsonApiRequest({
667
- url: `${this.baseUrl}/Calls/${input.providerCallId}.json`,
668
- method: "GET",
669
- headers: { Authorization: `Basic ${Buffer.from(`${this.accountSid}:${this.authToken}`).toString("base64")}` },
670
- allowNotFound: true,
671
- allowedHostnames: [new URL(this.baseUrl).hostname],
672
- auditContext: "twilio-get-call-status",
673
- errorPrefix: "Twilio get call status error"
674
- });
675
- if (!data) return {
676
- status: "not-found",
677
- isTerminal: true
678
- };
627
+ return readProviderCallStatus(() => guardedJsonApiRequest({
628
+ url: `${this.baseUrl}/Calls/${input.providerCallId}.json`,
629
+ method: "GET",
630
+ headers: { Authorization: `Basic ${Buffer.from(`${this.accountSid}:${this.authToken}`).toString("base64")}` },
631
+ allowNotFound: true,
632
+ allowedHostnames: [new URL(this.baseUrl).hostname],
633
+ auditContext: "twilio-get-call-status",
634
+ errorPrefix: "Twilio get call status error"
635
+ }), (data) => {
679
636
  const status = normalizeProviderStatus(data.status);
680
637
  return {
681
638
  status,
682
639
  isTerminal: isProviderStatusTerminal(status)
683
640
  };
684
- } catch {
685
- return {
686
- status: "error",
687
- isTerminal: false,
688
- isUnknown: true
689
- };
690
- }
641
+ });
691
642
  }
692
643
  };
693
644
  //#endregion
@@ -3,6 +3,7 @@ import { createHash, randomUUID } from "node:crypto";
3
3
  import path from "node:path";
4
4
  import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
5
5
  import { resolveStateDir } from "openclaw/plugin-sdk/state-paths";
6
+ import os from "node:os";
6
7
  //#region extensions/voice-call/src/types.ts
7
8
  const ProviderNameSchema = z.enum([
8
9
  "telnyx",
@@ -373,11 +374,6 @@ async function loadActiveCallsFromStore(storePath, stateRuntime) {
373
374
  } catch (err) {
374
375
  console.error("[voice-call] Failed to read SQLite call records:", err);
375
376
  }
376
- if (calls.length === 0) return {
377
- activeCalls: /* @__PURE__ */ new Map(),
378
- providerCallIdMap: /* @__PURE__ */ new Map(),
379
- processedEventIds: /* @__PURE__ */ new Set()
380
- };
381
377
  const callMap = /* @__PURE__ */ new Map();
382
378
  for (const call of calls) {
383
379
  callMap.delete(call.callId);
@@ -426,4 +422,16 @@ function resolveDefaultVoiceCallStoreDir(env = process.env) {
426
422
  return path.join(resolveStateDir(env), "voice-calls");
427
423
  }
428
424
  //#endregion
429
- export { releaseRejectedProviderCall as _, CALL_RECORD_EVENT_META_MAX_ENTRIES as a, setVoiceCallStateRuntime as b, buildVoiceCallLegacyJsonlEventKey as c, getCallHistoryFromStore as d, loadActiveCallsFromStore as f, appendCallReplayKey as g, resolveVoiceCallLegacyCallLogPath as h, CALL_RECORD_EVENT_CHUNKS_NAMESPACE as i, encodeCallRecordEvent as l, persistCallRecord as m, CALL_RECORD_CHUNK_MAX_ENTRIES as n, MAX_CALL_RECORD_EVENTS as o, parseVoiceCallRecordLine as p, CALL_RECORD_EVENTS_NAMESPACE as r, buildChunkKey as s, resolveDefaultVoiceCallStoreDir as t, findCallInStore as u, rememberManagerReplayKey as v, TerminalStates as x, reserveRejectedProviderCall as y };
425
+ //#region extensions/voice-call/src/utils.ts
426
+ /** Runtime uses the OS home; Doctor supplies the home from its migration environment. */
427
+ function resolveUserPath(input, homedir = os.homedir) {
428
+ const trimmed = input.trim();
429
+ if (!trimmed) return trimmed;
430
+ if (trimmed.startsWith("~")) {
431
+ const expanded = trimmed.replace(/^~(?=$|[\\/])/, homedir);
432
+ return path.resolve(expanded);
433
+ }
434
+ return path.resolve(trimmed);
435
+ }
436
+ //#endregion
437
+ export { TerminalStates as S, appendCallReplayKey as _, CALL_RECORD_EVENT_CHUNKS_NAMESPACE as a, reserveRejectedProviderCall as b, buildChunkKey as c, findCallInStore as d, getCallHistoryFromStore as f, resolveVoiceCallLegacyCallLogPath as g, persistCallRecord as h, CALL_RECORD_EVENTS_NAMESPACE as i, buildVoiceCallLegacyJsonlEventKey as l, parseVoiceCallRecordLine as m, resolveDefaultVoiceCallStoreDir as n, CALL_RECORD_EVENT_META_MAX_ENTRIES as o, loadActiveCallsFromStore as p, CALL_RECORD_CHUNK_MAX_ENTRIES as r, MAX_CALL_RECORD_EVENTS as s, resolveUserPath as t, encodeCallRecordEvent as u, releaseRejectedProviderCall as v, setVoiceCallStateRuntime as x, rememberManagerReplayKey as y };
package/dist/api.js CHANGED
@@ -1,2 +1,6 @@
1
- import { TtsAutoSchema, TtsConfigSchema, TtsModeSchema, TtsProviderSchema, definePluginEntry, fetchWithSsrFGuard, isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, sendHttpRequestRejection, sleep } from "./runtime-api.js";
1
+ import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
2
+ import { isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, sendHttpRequestRejection } from "openclaw/plugin-sdk/webhook-request-guards";
3
+ import { fetchWithSsrFGuard, isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
4
+ import { TtsAutoSchema, TtsConfigSchema, TtsModeSchema, TtsProviderSchema } from "openclaw/plugin-sdk/tts-runtime";
5
+ import { sleep } from "openclaw/plugin-sdk/runtime-env";
2
6
  export { TtsAutoSchema, TtsConfigSchema, TtsModeSchema, TtsProviderSchema, definePluginEntry, fetchWithSsrFGuard, isBlockedHostnameOrIp, isRequestBodyLimitError, readRequestBodyWithLimit, requestBodyErrorToText, sendHttpRequestRejection, sleep };
@@ -1,23 +1,194 @@
1
- import { a as CALL_RECORD_EVENT_META_MAX_ENTRIES, c as buildVoiceCallLegacyJsonlEventKey, h as resolveVoiceCallLegacyCallLogPath, i as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, l as encodeCallRecordEvent, n as CALL_RECORD_CHUNK_MAX_ENTRIES, o as MAX_CALL_RECORD_EVENTS, p as parseVoiceCallRecordLine, r as CALL_RECORD_EVENTS_NAMESPACE, s as buildChunkKey, t as resolveDefaultVoiceCallStoreDir } from "./.setup/store-path-B1j1up4_.mjs";
1
+ import { a as CALL_RECORD_EVENT_CHUNKS_NAMESPACE, c as buildChunkKey, g as resolveVoiceCallLegacyCallLogPath, i as CALL_RECORD_EVENTS_NAMESPACE, l as buildVoiceCallLegacyJsonlEventKey, m as parseVoiceCallRecordLine, n as resolveDefaultVoiceCallStoreDir, o as CALL_RECORD_EVENT_META_MAX_ENTRIES, r as CALL_RECORD_CHUNK_MAX_ENTRIES, s as MAX_CALL_RECORD_EVENTS, t as resolveUserPath, u as encodeCallRecordEvent } from "./.setup/utils-B5pnM_Ij.mjs";
2
+ import "node:module";
2
3
  import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
3
4
  import { asOptionalRecord } from "openclaw/plugin-sdk/string-coerce-runtime";
4
5
  import { existsSync } from "node:fs";
5
- import path from "node:path";
6
6
  import os from "node:os";
7
7
  import fs$1 from "node:fs/promises";
8
- import { archiveLegacyStateSource } from "openclaw/plugin-sdk/runtime-doctor-migrations";
9
- //#region extensions/voice-call/doctor-contract-api.ts
10
- /** Resolve home from doctor env with OS fallback. */
11
- function resolveHome(env) {
12
- return env.HOME?.trim() || os.homedir();
8
+ //#region \0rolldown/runtime.js
9
+ var __defProp = Object.defineProperty;
10
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
11
+ var __getOwnPropNames = Object.getOwnPropertyNames;
12
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
13
+ var __exportAll = (all, no_symbols) => {
14
+ let target = {};
15
+ for (var name in all) __defProp(target, name, {
16
+ get: all[name],
17
+ enumerable: true
18
+ });
19
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
20
+ return target;
21
+ };
22
+ var __copyProps = (to, from, except, desc) => {
23
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
24
+ key = keys[i];
25
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
26
+ get: ((k) => from[k]).bind(null, key),
27
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
28
+ });
29
+ }
30
+ return to;
31
+ };
32
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
33
+ //#endregion
34
+ //#region packages/normalization-core/src/record-coerce.ts
35
+ /** Type guard for non-array object records at browser-safe boundaries. */
36
+ function isRecord$1(value) {
37
+ return value !== null && typeof value === "object" && !Array.isArray(value);
13
38
  }
14
- /** Resolve config paths, including "~", against the doctor env home. */
15
- function resolveUserPath(input, env) {
16
- const trimmed = input.trim();
17
- if (!trimmed) return trimmed;
18
- if (trimmed.startsWith("~")) return path.resolve(trimmed.replace(/^~(?=$|[\\/])/, () => resolveHome(env)));
19
- return path.resolve(trimmed);
39
+ /** Returns a non-array record or null. */
40
+ function asNullableRecord$1(value) {
41
+ return isRecord$1(value) ? value : null;
42
+ }
43
+ //#endregion
44
+ //#region src/config/channel-config-normalization.ts
45
+ /** Applies one channel-specific doctor migration to every object-shaped account. */
46
+ function normalizeChannelAccounts(params) {
47
+ const rawAccounts = asNullableRecord$1(params.entry.accounts);
48
+ if (!rawAccounts) return {
49
+ entry: params.entry,
50
+ changed: false
51
+ };
52
+ let changed = false;
53
+ const accounts = { ...rawAccounts };
54
+ for (const [accountId, value] of Object.entries(rawAccounts)) {
55
+ const account = asNullableRecord$1(value);
56
+ if (!account) continue;
57
+ const normalized = params.normalizeAccount({
58
+ account,
59
+ accountId,
60
+ pathPrefix: `${params.pathPrefix}.accounts.${accountId}`,
61
+ changes: params.changes
62
+ });
63
+ if (normalized.changed) {
64
+ accounts[accountId] = normalized.entry;
65
+ changed = true;
66
+ }
67
+ }
68
+ return changed ? {
69
+ entry: {
70
+ ...params.entry,
71
+ accounts
72
+ },
73
+ changed: true
74
+ } : {
75
+ entry: params.entry,
76
+ changed: false
77
+ };
20
78
  }
79
+ /** Applies the same channel-specific doctor migration at root and account scope. */
80
+ function normalizeChannelConfigEntries(params) {
81
+ const changes = params.changes ?? [];
82
+ const channels = params.cfg.channels;
83
+ const entry = asNullableRecord$1(channels?.[params.channelId]);
84
+ if (!entry) return {
85
+ config: params.cfg,
86
+ changes
87
+ };
88
+ const channelPath = `channels.${params.channelId}`;
89
+ const root = params.normalizeEntry({
90
+ entry,
91
+ pathPrefix: channelPath,
92
+ changes
93
+ });
94
+ const accounts = normalizeChannelAccounts({
95
+ entry: root.entry,
96
+ pathPrefix: channelPath,
97
+ changes,
98
+ normalizeAccount: (accountParams) => params.normalizeEntry({
99
+ entry: accountParams.account,
100
+ accountId: accountParams.accountId,
101
+ pathPrefix: accountParams.pathPrefix,
102
+ changes: accountParams.changes
103
+ })
104
+ });
105
+ if (!root.changed && !accounts.changed) return {
106
+ config: params.cfg,
107
+ changes
108
+ };
109
+ return {
110
+ config: {
111
+ ...params.cfg,
112
+ channels: {
113
+ ...channels,
114
+ [params.channelId]: accounts.entry
115
+ }
116
+ },
117
+ changes
118
+ };
119
+ }
120
+ //#endregion
121
+ //#region src/plugin-sdk/legacy-webhook-listener-migration.ts
122
+ /** Preserve explicitly configured webhook listeners while moving ingress onto Gateway routes. */
123
+ function createLegacyWebhookListenerDoctorContract(params) {
124
+ const portKey = params.portKey ?? "webhookPort";
125
+ const hostKey = params.hostKey === void 0 ? "webhookHost" : params.hostKey;
126
+ const source = (entry) => params.webhookKey ? asNullableRecord$1(entry[params.webhookKey]) : entry;
127
+ const hasLegacy = (value) => {
128
+ const entry = asNullableRecord$1(value);
129
+ const listener = entry && source(entry);
130
+ return Boolean(listener && (Object.hasOwn(listener, portKey) || hostKey && Object.hasOwn(listener, hostKey)));
131
+ };
132
+ const prefix = `channels.${params.channelKey}`;
133
+ return {
134
+ legacyConfigRules: [{
135
+ path: ["channels", params.channelKey],
136
+ message: `${prefix} webhook listeners moved to Gateway routes. Run "openclaw doctor --fix" to preserve explicitly configured listener settings as legacyWebhook.`,
137
+ match: (value) => {
138
+ const accounts = asNullableRecord$1(asNullableRecord$1(value)?.accounts);
139
+ return hasLegacy(value) || Object.values(accounts ?? {}).some(hasLegacy);
140
+ }
141
+ }],
142
+ normalizeCompatibilityConfig: ({ cfg }) => {
143
+ const root = asNullableRecord$1(asNullableRecord$1(cfg.channels)?.[params.channelKey]);
144
+ const inherited = root && source(root);
145
+ const canonicalRoot = asNullableRecord$1(root?.legacyWebhook);
146
+ return normalizeChannelConfigEntries({
147
+ cfg,
148
+ channelId: params.channelKey,
149
+ normalizeEntry: ({ entry, accountId, pathPrefix, changes }) => {
150
+ const listener = source(entry);
151
+ if (!listener || !hasLegacy(entry)) return {
152
+ entry,
153
+ changed: false
154
+ };
155
+ const next = { ...entry };
156
+ const port = Object.hasOwn(listener, portKey) ? listener[portKey] : (accountId ? canonicalRoot?.port ?? inherited?.[portKey] : void 0) ?? params.defaultPort;
157
+ const inheritedHost = accountId ? canonicalRoot ? canonicalRoot.host : (hostKey ? inherited?.[hostKey] : void 0) ?? params.defaultHost : params.defaultHost;
158
+ const host = hostKey ? listener[hostKey] ?? inheritedHost : inheritedHost;
159
+ const legacyPath = [pathPrefix, params.webhookKey].filter(Boolean).join(".");
160
+ if (Object.hasOwn(entry, "legacyWebhook")) changes.push(`Removed ${legacyPath} legacy listener keys; ${pathPrefix}.legacyWebhook is already configured.`);
161
+ else if (accountId && root?.legacyWebhook === false) changes.push(`Removed ${legacyPath} legacy listener keys; ${prefix}.legacyWebhook: false keeps this account's inherited listener disabled.`);
162
+ else {
163
+ next.legacyWebhook = {
164
+ port,
165
+ ...host !== void 0 ? { host } : {}
166
+ };
167
+ changes.push(`Moved ${legacyPath} listener settings to ${pathPrefix}.legacyWebhook. Point the external callback or reverse proxy at the Gateway port and webhook path, verify delivery, then set legacyWebhook: false to disable legacy forwarding.`);
168
+ }
169
+ const updated = params.webhookKey ? { ...listener } : next;
170
+ delete updated[portKey];
171
+ if (hostKey) delete updated[hostKey];
172
+ if (params.webhookKey) {
173
+ if (Object.keys(updated).length) next[params.webhookKey] = updated;
174
+ else delete next[params.webhookKey];
175
+ }
176
+ return {
177
+ entry: next,
178
+ changed: true
179
+ };
180
+ }
181
+ });
182
+ }
183
+ };
184
+ }
185
+ //#endregion
186
+ //#region \0openclaw:bundled-sdk:openclaw/plugin-sdk/runtime-doctor-migrations
187
+ var runtime_doctor_migrations_exports = /* @__PURE__ */ __exportAll({ createLegacyWebhookListenerDoctorContract: () => createLegacyWebhookListenerDoctorContract });
188
+ import * as import_openclaw_plugin_sdk_runtime_doctor_migrations from "openclaw/plugin-sdk/runtime-doctor-migrations";
189
+ __reExport(runtime_doctor_migrations_exports, import_openclaw_plugin_sdk_runtime_doctor_migrations);
190
+ //#endregion
191
+ //#region extensions/voice-call/doctor-contract-api.ts
21
192
  /** Read the configured voice-call store path from either package id. */
22
193
  function getVoiceCallConfigStore(config) {
23
194
  for (const pluginId of ["voice-call", "@openclaw/voice-call"]) {
@@ -48,7 +219,7 @@ function resolveSessionStoreAgentIds(params) {
48
219
  /** Resolve the voice-call store path used by legacy and plugin-state call records. */
49
220
  function resolveVoiceCallStorePath(params) {
50
221
  const configuredStore = getVoiceCallConfigStore(params.config);
51
- if (configuredStore) return resolveUserPath(configuredStore, params.env);
222
+ if (configuredStore) return resolveUserPath(configuredStore, () => params.env.HOME?.trim() || os.homedir());
52
223
  return resolveDefaultVoiceCallStoreDir(params.env);
53
224
  }
54
225
  function resolveVoiceCallStateDatabaseEnv(params) {
@@ -226,7 +397,7 @@ const stateMigrations = [{
226
397
  warnings
227
398
  };
228
399
  }
229
- await archiveLegacyStateSource({
400
+ await (0, runtime_doctor_migrations_exports.archiveLegacyStateSource)({
230
401
  filePath,
231
402
  label: "Voice Call call-log",
232
403
  changes,