@openclaw/voice-call 2026.10.10 → 2026.11.5-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.
- package/dist/.setup/{carrier-api-BgJdQC-e.mjs → carrier-api-CZQtPnYQ.mjs} +15 -28
- package/dist/.setup/{cli-CI9QzIaT.mjs → cli-BUVmNuk8.mjs} +113 -61
- package/dist/.setup/{config-BxESUUSm.mjs → config-D_w9HzxP.mjs} +55 -111
- package/dist/.setup/{mock-BO3pptb-.mjs → mock-fux1a_i_.mjs} +3 -4
- package/dist/.setup/{plivo-C5cLYAup.mjs → plivo-Cpxxx8BT.mjs} +6 -21
- package/dist/.setup/{realtime-handler-B2JJ-8wv.mjs → realtime-handler-BkoZ-ZYd.mjs} +190 -265
- package/dist/.setup/{response-generator-B2nVYBf_.mjs → response-generator-COluXT2F.mjs} +11 -15
- package/dist/.setup/{runtime-entry-CKPPin7r.mjs → runtime-entry-Br7ttAwq.mjs} +382 -522
- package/dist/.setup/{state-retention-api-y5WYfZsQ.mjs → state-retention-api-BDJR8dnG.mjs} +1 -1
- package/dist/.setup/{store-path-Bn3bLr8y.mjs → store-path-C0yJCDE8.mjs} +2 -2
- package/dist/.setup/{telnyx-ZopMHuZl.mjs → telnyx-DdFzdUEf.mjs} +4 -5
- package/dist/.setup/{twilio-BGLnhzpD.mjs → twilio-BqUbz9jp.mjs} +7 -15
- package/dist/.setup/webhook-replay-DnlMxw4z.mjs +30 -0
- package/dist/.source-module-urls/8b7cb5a3dc0f034090cbbda318ba348c0d48853d04f0e3b500529cf7666abe03.json +1 -1
- package/dist/doctor-contract-api.js +21 -2
- package/dist/index.js +106 -195
- package/dist/runtime-entry.js +1 -1
- package/dist/setup-api.js +1 -2
- package/dist/state-retention-api.js +1 -1
- package/node_modules/typebox/build/format/duration.d.mts +2 -2
- package/node_modules/typebox/build/format/duration.mjs +2 -2
- package/node_modules/typebox/build/format/idn_email.d.mts +1 -0
- package/node_modules/typebox/build/format/idn_email.mjs +1 -0
- package/node_modules/typebox/build/format/idn_hostname.d.mts +1 -1
- package/node_modules/typebox/build/format/idn_hostname.mjs +1 -1
- package/node_modules/typebox/build/format/ipv4.d.mts +2 -2
- package/node_modules/typebox/build/format/ipv4.mjs +2 -2
- package/node_modules/typebox/build/format/ipv6.d.mts +1 -1
- package/node_modules/typebox/build/format/ipv6.mjs +1 -1
- package/node_modules/typebox/build/format/relative_json_pointer.d.mts +2 -2
- package/node_modules/typebox/build/format/relative_json_pointer.mjs +2 -2
- package/node_modules/typebox/build/schema/build.mjs +1 -0
- package/node_modules/typebox/build/schema/engine/_unique.d.mts +4 -2
- package/node_modules/typebox/build/schema/engine/_unique.mjs +7 -3
- package/node_modules/typebox/build/schema/engine/additionalItems.mjs +3 -3
- package/node_modules/typebox/build/schema/engine/additionalProperties.d.mts +0 -3
- package/node_modules/typebox/build/schema/engine/additionalProperties.mjs +15 -17
- package/node_modules/typebox/build/schema/engine/contains.mjs +3 -3
- package/node_modules/typebox/build/schema/engine/index.d.mts +1 -0
- package/node_modules/typebox/build/schema/engine/index.mjs +1 -0
- package/node_modules/typebox/build/schema/engine/items.mjs +7 -4
- package/node_modules/typebox/build/schema/engine/maxContains.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/minContains.mjs +3 -3
- package/node_modules/typebox/build/schema/engine/oneOf.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/patternProperties.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/propertyNames.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/unevaluatedItems.mjs +2 -2
- package/node_modules/typebox/build/schema/engine/unevaluatedProperties.mjs +2 -2
- package/node_modules/typebox/build/system/hashing/hash.mjs +63 -34
- package/node_modules/typebox/build/system/locale/en_US.mjs +2 -1
- package/node_modules/typebox/build/system/memory/assign.d.mts +2 -8
- package/node_modules/typebox/build/system/memory/assign.mjs +13 -6
- package/node_modules/typebox/build/system/memory/clone.d.mts +4 -5
- package/node_modules/typebox/build/system/memory/clone.mjs +8 -96
- package/node_modules/typebox/build/system/memory/create.d.mts +1 -5
- package/node_modules/typebox/build/system/memory/create.mjs +18 -13
- package/node_modules/typebox/build/system/memory/deep.d.mts +2 -0
- package/node_modules/typebox/build/system/memory/deep.mjs +108 -0
- package/node_modules/typebox/build/system/memory/discard.d.mts +1 -1
- package/node_modules/typebox/build/system/memory/discard.mjs +9 -8
- package/node_modules/typebox/build/system/memory/finalize.d.mts +4 -0
- package/node_modules/typebox/build/system/memory/finalize.mjs +30 -0
- package/node_modules/typebox/build/system/memory/keys.d.mts +2 -0
- package/node_modules/typebox/build/system/memory/keys.mjs +8 -0
- package/node_modules/typebox/build/system/memory/update.d.mts +1 -4
- package/node_modules/typebox/build/system/memory/update.mjs +43 -16
- package/node_modules/typebox/build/type/engine/call/index.d.mts +1 -0
- package/node_modules/typebox/build/type/engine/call/index.mjs +1 -0
- package/node_modules/typebox/build/type/engine/evaluate/composite.d.mts +2 -24
- package/node_modules/typebox/build/type/engine/evaluate/composite.mjs +2 -29
- package/node_modules/typebox/build/type/engine/evaluate/index.d.mts +1 -0
- package/node_modules/typebox/build/type/engine/evaluate/index.mjs +1 -0
- package/node_modules/typebox/build/type/engine/evaluate/narrow.d.mts +5 -1
- package/node_modules/typebox/build/type/engine/evaluate/narrow.mjs +6 -1
- package/node_modules/typebox/build/type/engine/evaluate/property.d.mts +30 -0
- package/node_modules/typebox/build/type/engine/evaluate/property.mjs +29 -0
- package/node_modules/typebox/build/type/engine/immutable/instantiate_add.d.mts +1 -3
- package/node_modules/typebox/build/type/engine/immutable/instantiate_add.mjs +1 -4
- package/node_modules/typebox/build/type/engine/object/from_intersect.d.mts +2 -2
- package/node_modules/typebox/build/type/engine/object/from_intersect.mjs +2 -2
- package/node_modules/typebox/build/type/engine/object/from_union.d.mts +2 -2
- package/node_modules/typebox/build/type/engine/object/from_union.mjs +2 -2
- package/node_modules/typebox/build/type/engine/optional/instantiate_add.d.mts +1 -3
- package/node_modules/typebox/build/type/engine/optional/instantiate_add.mjs +1 -4
- package/node_modules/typebox/build/type/engine/readonly/instantiate_add.d.mts +1 -3
- package/node_modules/typebox/build/type/engine/readonly/instantiate_add.mjs +1 -4
- package/node_modules/typebox/build/type/engine/this/index.d.mts +1 -0
- package/node_modules/typebox/build/type/engine/this/index.mjs +1 -0
- package/node_modules/typebox/build/value/clone/clone.d.mts +4 -3
- package/node_modules/typebox/build/value/clone/clone.mjs +5 -7
- package/node_modules/typebox/build/value/repair/from_cyclic.d.mts +2 -0
- package/node_modules/typebox/build/value/repair/from_cyclic.mjs +6 -0
- package/node_modules/typebox/build/value/shared/union_priority_sort.mjs +25 -14
- package/node_modules/typebox/package.json +1 -1
- package/node_modules/typebox/readme.md +1 -1
- package/node_modules/zod/package.json +1 -1
- package/node_modules/zod/src/v4/classic/tests/array.test.ts +27 -0
- package/node_modules/zod/src/v4/classic/tests/map.test.ts +7 -0
- package/node_modules/zod/src/v4/classic/tests/set.test.ts +6 -0
- package/node_modules/zod/src/v4/core/compile.ts +9 -0
- package/node_modules/zod/src/v4/core/schemas.ts +35 -1
- package/node_modules/zod/src/v4/core/util.ts +13 -0
- package/node_modules/zod/src/v4/core/versions.ts +1 -1
- package/node_modules/zod/v4/core/compile.cjs +8 -0
- package/node_modules/zod/v4/core/compile.js +8 -0
- package/node_modules/zod/v4/core/schemas.cjs +30 -0
- package/node_modules/zod/v4/core/schemas.d.cts +3 -1
- package/node_modules/zod/v4/core/schemas.d.ts +3 -1
- package/node_modules/zod/v4/core/schemas.js +30 -0
- package/node_modules/zod/v4/core/util.cjs +15 -0
- package/node_modules/zod/v4/core/util.d.cts +2 -0
- package/node_modules/zod/v4/core/util.d.ts +2 -0
- package/node_modules/zod/v4/core/util.js +14 -0
- package/node_modules/zod/v4/core/versions.cjs +1 -1
- package/node_modules/zod/v4/core/versions.js +1 -1
- package/openclaw.plugin.json +9 -2
- package/package.json +6 -6
- package/dist/.setup/webhook-replay-B0Eo3Ll5.mjs +0 -42
- package/node_modules/typebox/build/system/memory/freeze.d.mts +0 -4
- package/node_modules/typebox/build/system/memory/freeze.mjs +0 -6
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
import { fetchWithSsrFGuard } from "../api.js";
|
|
2
|
-
import {
|
|
3
|
-
import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-
|
|
2
|
+
import { n as normalizeProxyIp, s as getHeader } from "./runtime-entry-Br7ttAwq.mjs";
|
|
3
|
+
import { n as reserveWebhookReplay, t as createWebhookReplayCache } from "./webhook-replay-DnlMxw4z.mjs";
|
|
4
|
+
import crypto from "node:crypto";
|
|
4
5
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
5
6
|
import { isLoopbackHost } from "openclaw/plugin-sdk/gateway-runtime";
|
|
6
7
|
import { normalizeLowercaseStringOrEmpty, normalizeStringEntries } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
7
|
-
import crypto from "node:crypto";
|
|
8
8
|
import { isIP } from "node:net";
|
|
9
|
+
import { sha256Hex } from "openclaw/plugin-sdk/node-crypto-runtime";
|
|
9
10
|
import { redactSensitiveText, safeEqualSecret } from "openclaw/plugin-sdk/security-runtime";
|
|
10
11
|
import { readResponseTextPrefix, readResponseWithLimit } from "openclaw/plugin-sdk/response-limit-runtime";
|
|
11
12
|
// extensions/voice-call/src/webhook-security.ts
|
|
12
13
|
const twilioReplayCache = createWebhookReplayCache();
|
|
13
14
|
const plivoReplayCache = createWebhookReplayCache();
|
|
14
15
|
const telnyxReplayCache = createWebhookReplayCache();
|
|
15
|
-
function sha256Hex(input) {
|
|
16
|
-
return crypto.createHash("sha256").update(input).digest("hex");
|
|
17
|
-
}
|
|
18
16
|
function skipWebhookVerification(provider, ctx, cache) {
|
|
19
17
|
const replayKey = `${provider}:skip:${sha256Hex(`${ctx.method}\n${ctx.url}\n${ctx.rawBody}`)}`;
|
|
20
18
|
return {
|
|
@@ -254,9 +252,8 @@ function verifyTelnyxWebhook(ctx, publicKey, options) {
|
|
|
254
252
|
ok: false,
|
|
255
253
|
reason: "Invalid signature"
|
|
256
254
|
};
|
|
257
|
-
const maxSkewMs = options?.maxSkewMs ?? 3e5;
|
|
258
255
|
const eventTimeMs = eventTimeSec * 1e3;
|
|
259
|
-
if (Math.abs(Date.now() - eventTimeMs) >
|
|
256
|
+
if (Math.abs(Date.now() - eventTimeMs) > 3e5) return {
|
|
260
257
|
ok: false,
|
|
261
258
|
reason: "Timestamp too old"
|
|
262
259
|
};
|
|
@@ -297,7 +294,6 @@ function verifyTwilioWebhook(ctx, authToken, options) {
|
|
|
297
294
|
});
|
|
298
295
|
return {
|
|
299
296
|
ok: true,
|
|
300
|
-
verificationUrl: candidateUrl,
|
|
301
297
|
...reserveWebhookReplay(twilioReplayCache, replayKey)
|
|
302
298
|
};
|
|
303
299
|
};
|
|
@@ -317,11 +313,9 @@ function verifyTwilioWebhook(ctx, authToken, options) {
|
|
|
317
313
|
const result = verifyUrl(candidateUrl);
|
|
318
314
|
if (result) return result;
|
|
319
315
|
}
|
|
320
|
-
const isNgrokFreeTier = verificationUrl.includes(".ngrok-free.app") || verificationUrl.includes(".ngrok.io");
|
|
321
316
|
return {
|
|
322
317
|
ok: false,
|
|
323
|
-
reason: `Invalid signature for URL: ${redactTwilioVerificationUrlForDiagnostics(verificationUrl)}
|
|
324
|
-
isNgrokFreeTier
|
|
318
|
+
reason: `Invalid signature for URL: ${redactTwilioVerificationUrlForDiagnostics(verificationUrl)}`
|
|
325
319
|
};
|
|
326
320
|
}
|
|
327
321
|
function normalizeSignatureBase64(input) {
|
|
@@ -352,19 +346,13 @@ function constructPlivoV3BaseUrl(params) {
|
|
|
352
346
|
function verifyPlivoSignature(params) {
|
|
353
347
|
const hmacBase = `${params.baseUrl}${params.version === "v3" ? "." : ""}${params.nonce}`;
|
|
354
348
|
const expected = normalizeSignatureBase64(crypto.createHmac("sha256", params.authToken).update(hmacBase).digest("base64"));
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
ok,
|
|
358
|
-
version: params.version,
|
|
359
|
-
verificationUrl: params.verificationUrl
|
|
360
|
-
};
|
|
361
|
-
if (!ok) return {
|
|
362
|
-
...result,
|
|
349
|
+
if (!params.signatures.map(normalizeSignatureBase64).some((signature) => safeEqualSecret(expected, signature))) return {
|
|
350
|
+
ok: false,
|
|
363
351
|
reason: `Invalid Plivo ${params.version.toUpperCase()} signature`
|
|
364
352
|
};
|
|
365
353
|
const replayKey = `plivo:${params.version}:${sha256Hex(`${params.baseUrl}\n${params.nonce}`)}`;
|
|
366
354
|
return {
|
|
367
|
-
|
|
355
|
+
ok: true,
|
|
368
356
|
...reserveWebhookReplay(plivoReplayCache, replayKey)
|
|
369
357
|
};
|
|
370
358
|
}
|
|
@@ -400,8 +388,6 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
400
388
|
const method = ctx.method === "GET" || ctx.method === "POST" ? ctx.method : null;
|
|
401
389
|
if (!method) return {
|
|
402
390
|
ok: false,
|
|
403
|
-
version: "v3",
|
|
404
|
-
verificationUrl,
|
|
405
391
|
reason: `Unsupported HTTP method for Plivo V3 signature: ${ctx.method}`
|
|
406
392
|
};
|
|
407
393
|
const postParams = new URLSearchParams(ctx.rawBody);
|
|
@@ -413,7 +399,6 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
413
399
|
return verifyPlivoSignature({
|
|
414
400
|
authToken,
|
|
415
401
|
version: "v3",
|
|
416
|
-
verificationUrl,
|
|
417
402
|
signatures: normalizeStringEntries(signatureV3.split(",")),
|
|
418
403
|
nonce: nonceV3,
|
|
419
404
|
baseUrl
|
|
@@ -422,17 +407,19 @@ function verifyPlivoWebhook(ctx, authToken, options) {
|
|
|
422
407
|
if (signatureV2 && nonceV2) return verifyPlivoSignature({
|
|
423
408
|
authToken,
|
|
424
409
|
version: "v2",
|
|
425
|
-
verificationUrl,
|
|
426
410
|
signatures: [signatureV2],
|
|
427
411
|
nonce: nonceV2,
|
|
428
412
|
baseUrl: getBaseUrlNoQuery(verificationUrl)
|
|
429
413
|
});
|
|
430
414
|
return {
|
|
431
415
|
ok: false,
|
|
432
|
-
reason: "Missing Plivo signature headers (V3 or V2)"
|
|
433
|
-
verificationUrl
|
|
416
|
+
reason: "Missing Plivo signature headers (V3 or V2)"
|
|
434
417
|
};
|
|
435
418
|
}
|
|
419
|
+
// extensions/voice-call/src/providers/base.ts
|
|
420
|
+
function assertVoiceCallProviderSetting(value, message) {
|
|
421
|
+
if (!value) throw new Error(message);
|
|
422
|
+
}
|
|
436
423
|
// extensions/voice-call/src/providers/shared/guarded-json-api.ts
|
|
437
424
|
const VOICE_CALL_PROVIDER_API_TIMEOUT_MS = 3e4;
|
|
438
425
|
const PROVIDER_JSON_RESPONSE_MAX_BYTES = 1048576;
|
|
@@ -524,4 +511,4 @@ function createCarrierApi(name, baseUrl, authorization, options = {}) {
|
|
|
524
511
|
}
|
|
525
512
|
};
|
|
526
513
|
}
|
|
527
|
-
export {
|
|
514
|
+
export { verifyTelnyxWebhook as a, verifyPlivoWebhook as i, assertVoiceCallProviderSetting as n, verifyTwilioWebhook as o, reconstructWebhookUrl as r, createCarrierApi as t };
|
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { g as
|
|
3
|
-
import {
|
|
4
|
-
import { r as resolveUserPath, t as resolveDefaultVoiceCallStoreDir } from "./store-path-Bn3bLr8y.mjs";
|
|
1
|
+
import { c as resolveVoiceCallStreamExposurePaths, h as CallBriefSchema, l as validateProviderConfig } from "./config-D_w9HzxP.mjs";
|
|
2
|
+
import { _ as getCallHistoryFromStore, c as resolveWebhookExposureStatus, d as setupTailscaleExposureRoutes, g as findCallInStore, h as MAX_CALL_RECORD_EVENTS, l as cleanupTailscaleExposureRoute, u as getTailscaleSelfInfo, v as loadActiveCallsFromStore, x as resolveVoiceCallAgentId, y as setVoiceCallStateRuntime } from "./runtime-entry-Br7ttAwq.mjs";
|
|
3
|
+
import { r as resolveUserPath, t as resolveDefaultVoiceCallStoreDir } from "./store-path-C0yJCDE8.mjs";
|
|
5
4
|
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
6
5
|
import { callGatewayFromCli, isGatewayClientRequestError, isGatewayTransportError, redactSensitiveUrlLikeString } from "openclaw/plugin-sdk/gateway-runtime";
|
|
7
|
-
import { isRecord, normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
6
|
import { MAX_TCP_PORT, MAX_TIMER_TIMEOUT_MS, addTimerTimeoutGraceMs, clampTimerTimeoutMs, parseStrictNonNegativeInteger } from "openclaw/plugin-sdk/number-runtime";
|
|
7
|
+
import { isRecord, normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
|
8
|
+
import { sleepWithAbort } from "openclaw/plugin-sdk/runtime-env";
|
|
9
9
|
import fs from "node:fs";
|
|
10
10
|
import path from "node:path";
|
|
11
|
-
import {
|
|
11
|
+
import { format } from "node:util";
|
|
12
12
|
import { createDeferred } from "openclaw/plugin-sdk/concurrency-runtime";
|
|
13
|
+
import { realpath } from "node:fs/promises";
|
|
13
14
|
import { readRegularFile } from "openclaw/plugin-sdk/file-access-runtime";
|
|
14
15
|
import { once } from "node:events";
|
|
15
|
-
import {
|
|
16
|
+
import { setTimeout } from "node:timers/promises";
|
|
16
17
|
// extensions/voice-call/src/cli-command-io.ts
|
|
17
18
|
function writeCliLine(...values) {
|
|
18
19
|
process.stdout.write(`${format(...values)}\n`);
|
|
@@ -28,8 +29,9 @@ function parseCliInteger(raw, optionName, opts) {
|
|
|
28
29
|
}
|
|
29
30
|
// extensions/voice-call/src/cli-call-log.ts
|
|
30
31
|
const READ_BYTES = 65536;
|
|
31
|
-
async function writeLogChunk(chunk) {
|
|
32
|
-
|
|
32
|
+
async function writeLogChunk(chunk, signal) {
|
|
33
|
+
signal.throwIfAborted();
|
|
34
|
+
if (!process.stdout.write(chunk)) await once(process.stdout, "drain", { signal });
|
|
33
35
|
}
|
|
34
36
|
function* readLogLine({ fd, start, end }) {
|
|
35
37
|
for (let offset = start; offset < end;) {
|
|
@@ -40,20 +42,21 @@ function* readLogLine({ fd, start, end }) {
|
|
|
40
42
|
yield chunk.subarray(0, bytesRead);
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
|
-
async function* readLogLines(file, last, pollMs) {
|
|
45
|
+
async function* readLogLines(file, last, pollMs, signal) {
|
|
44
46
|
let initial = true;
|
|
45
47
|
let offset = 0;
|
|
46
48
|
let lineStart = 0;
|
|
47
49
|
let previous;
|
|
48
50
|
const buffer = Buffer.alloc(READ_BYTES);
|
|
49
51
|
for (;;) {
|
|
52
|
+
signal?.throwIfAborted();
|
|
50
53
|
let fd;
|
|
51
54
|
try {
|
|
52
55
|
const flags = process.platform === "win32" ? "r" : fs.constants.O_RDONLY | fs.constants.O_NONBLOCK;
|
|
53
56
|
fd = fs.openSync(file, flags);
|
|
54
57
|
} catch (error) {
|
|
55
58
|
if (initial || pollMs === void 0 || !isRecord(error) || error.code !== "ENOENT") throw error;
|
|
56
|
-
await
|
|
59
|
+
await setTimeout(pollMs, void 0, { signal });
|
|
57
60
|
continue;
|
|
58
61
|
}
|
|
59
62
|
try {
|
|
@@ -95,7 +98,7 @@ async function* readLogLines(file, last, pollMs) {
|
|
|
95
98
|
}
|
|
96
99
|
initial = false;
|
|
97
100
|
if (pollMs === void 0) return;
|
|
98
|
-
await
|
|
101
|
+
await setTimeout(pollMs, void 0, { signal });
|
|
99
102
|
}
|
|
100
103
|
}
|
|
101
104
|
function summarizeSeries(values) {
|
|
@@ -142,24 +145,43 @@ function registerVoiceCallLogs(params) {
|
|
|
142
145
|
const file = options.file;
|
|
143
146
|
const since = parseCliInteger(options.since, "--since", { min: 0 });
|
|
144
147
|
const pollMs = parseCliInteger(options.poll, "--poll", { min: 50 });
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
148
|
+
const cancellation = new AbortController();
|
|
149
|
+
const { signal } = cancellation;
|
|
150
|
+
const interrupt = (exitCode) => {
|
|
151
|
+
if (!signal.aborted) {
|
|
152
|
+
process.exitCode = exitCode;
|
|
153
|
+
cancellation.abort();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
const onSigint = () => interrupt(130);
|
|
157
|
+
const onSigterm = () => interrupt(143);
|
|
158
|
+
process.on("SIGINT", onSigint);
|
|
159
|
+
process.on("SIGTERM", onSigterm);
|
|
160
|
+
try {
|
|
161
|
+
if (fs.existsSync(file) && path.basename(file) !== "calls.jsonl") for await (const line of readLogLines(file, since, pollMs, signal)) {
|
|
162
|
+
for (const chunk of readLogLine(line)) await writeLogChunk(chunk, signal);
|
|
163
|
+
await writeLogChunk("\n", signal);
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
params.ensureHistoryStateRuntime();
|
|
167
|
+
let initial = true;
|
|
168
|
+
let seen = /* @__PURE__ */ new Set();
|
|
169
|
+
while (!signal.aborted) {
|
|
170
|
+
const lines = (await getCallHistoryFromStore(path.dirname(file), MAX_CALL_RECORD_EVENTS)).map((call) => JSON.stringify(call));
|
|
171
|
+
for (const line of initial ? lines.slice(Math.max(0, lines.length - since)) : lines) if (!seen.has(line)) {
|
|
172
|
+
await writeLogChunk(`${line}\n`, signal);
|
|
173
|
+
seen.add(line);
|
|
174
|
+
}
|
|
175
|
+
seen = new Set(lines);
|
|
176
|
+
initial = false;
|
|
177
|
+
await setTimeout(pollMs, void 0, { signal });
|
|
158
178
|
}
|
|
159
|
-
seen = new Set(lines);
|
|
160
|
-
initial = false;
|
|
161
|
-
await sleep(pollMs);
|
|
162
179
|
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
if (!signal.aborted || !(error instanceof Error) || error.name !== "AbortError") throw error;
|
|
182
|
+
} finally {
|
|
183
|
+
process.off("SIGINT", onSigint);
|
|
184
|
+
process.off("SIGTERM", onSigterm);
|
|
163
185
|
}
|
|
164
186
|
});
|
|
165
187
|
params.root.command("latency").description("Summarize turn latency metrics from voice-call history or a custom JSONL log").option("--file <path>", "Path to calls.jsonl", params.defaultFile).option("--last <n>", "Analyze last N records", "200").action(async (options) => {
|
|
@@ -187,9 +209,6 @@ const VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS = 5e3;
|
|
|
187
209
|
const VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS = 3e4;
|
|
188
210
|
const VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS = 1e4;
|
|
189
211
|
const VOICE_CALL_GATEWAY_POLL_INTERVAL_MS = 1e3;
|
|
190
|
-
function isGatewayUnavailableForLocalFallback(err) {
|
|
191
|
-
return isGatewayTransportError(err) && err.kind === "closed" && (err.code === void 0 || err.code === 1006);
|
|
192
|
-
}
|
|
193
212
|
function isGatewayCredentialFailure(err) {
|
|
194
213
|
return err instanceof Error && (err.name === "GatewayCredentialsRequiredError" || err.name === "GatewayExplicitAuthRequiredError" || err.name === "GatewaySecretRefUnavailableError");
|
|
195
214
|
}
|
|
@@ -207,21 +226,23 @@ function gatewayOperationalError(err) {
|
|
|
207
226
|
})();
|
|
208
227
|
return new Error(redactSensitiveUrlLikeString(detail));
|
|
209
228
|
}
|
|
210
|
-
function isUnknownMethod(err, method) {
|
|
211
|
-
return formatErrorMessage(err).includes(`unknown method: ${method}`);
|
|
212
|
-
}
|
|
213
229
|
async function callVoiceCallGateway(method, params, opts) {
|
|
214
230
|
try {
|
|
231
|
+
opts?.signal?.throwIfAborted();
|
|
215
232
|
const timeoutMs = typeof opts?.timeoutMs === "number" && Number.isFinite(opts.timeoutMs) ? Math.max(1, Math.ceil(opts.timeoutMs)) : VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS;
|
|
216
233
|
return {
|
|
217
234
|
ok: true,
|
|
218
235
|
payload: await callGatewayFromCli(method, {
|
|
219
236
|
json: true,
|
|
220
237
|
timeout: String(timeoutMs)
|
|
221
|
-
}, params, {
|
|
238
|
+
}, params, {
|
|
239
|
+
progress: false,
|
|
240
|
+
...opts?.signal ? { signal: opts.signal } : {}
|
|
241
|
+
})
|
|
222
242
|
};
|
|
223
243
|
} catch (err) {
|
|
224
|
-
if (
|
|
244
|
+
if (opts?.signal?.aborted && err instanceof Error && err.name === "AbortError") throw err;
|
|
245
|
+
if (isGatewayTransportError(err) && err.kind === "closed" && (err.code === void 0 || err.code === 1006)) return {
|
|
225
246
|
ok: false,
|
|
226
247
|
error: err
|
|
227
248
|
};
|
|
@@ -234,25 +255,31 @@ function resolveOperationTimeout(config) {
|
|
|
234
255
|
function resolveContinueTimeout(config) {
|
|
235
256
|
return clampTimerTimeoutMs(config.transcriptTimeoutMs + VOICE_CALL_GATEWAY_OPERATION_TIMEOUT_MS + VOICE_CALL_GATEWAY_TRANSCRIPT_BUFFER_MS) ?? 1;
|
|
236
257
|
}
|
|
237
|
-
async function pollContinueGateway(payload, fallbackTimeoutMs) {
|
|
258
|
+
async function pollContinueGateway(payload, fallbackTimeoutMs, signal) {
|
|
259
|
+
signal?.throwIfAborted();
|
|
238
260
|
if (!isRecord(payload) || typeof payload.operationId !== "string") return payload;
|
|
239
261
|
if (!payload.operationId) throw new Error("voicecall gateway response missing operationId");
|
|
240
262
|
const operationId = payload.operationId;
|
|
241
263
|
const timeoutMs = typeof payload.pollTimeoutMs === "number" ? clampTimerTimeoutMs(payload.pollTimeoutMs) ?? fallbackTimeoutMs : fallbackTimeoutMs;
|
|
242
264
|
const deadlineMs = performance.now() + (clampTimerTimeoutMs(timeoutMs) ?? MAX_TIMER_TIMEOUT_MS);
|
|
243
265
|
for (;;) {
|
|
266
|
+
signal?.throwIfAborted();
|
|
244
267
|
const remainingMs = deadlineMs - performance.now();
|
|
245
268
|
if (remainingMs <= 0) break;
|
|
246
|
-
const gateway = await callVoiceCallGateway("voicecall.continue.result", { operationId }, {
|
|
269
|
+
const gateway = await callVoiceCallGateway("voicecall.continue.result", { operationId }, {
|
|
270
|
+
timeoutMs: Math.min(VOICE_CALL_GATEWAY_DEFAULT_TIMEOUT_MS, remainingMs),
|
|
271
|
+
signal
|
|
272
|
+
});
|
|
247
273
|
if (!gateway.ok) throw new Error(`gateway unavailable while waiting for voicecall continue result: ${formatErrorMessage(gateway.error)}`);
|
|
248
274
|
const result = gateway.payload;
|
|
249
275
|
if (!isRecord(result)) throw new Error("voicecall gateway response missing operation status");
|
|
250
|
-
if (result.status === "completed") return result.result;
|
|
251
276
|
if (result.status === "failed") throw new Error(typeof result.error === "string" ? result.error : "continue failed");
|
|
252
|
-
if (result.status !== "pending") throw new Error("voicecall gateway response has unknown operation status");
|
|
277
|
+
if (result.status !== "pending" && result.status !== "completed") throw new Error("voicecall gateway response has unknown operation status");
|
|
278
|
+
signal?.throwIfAborted();
|
|
279
|
+
if (result.status === "completed") return result.result;
|
|
253
280
|
const sleepMs = Math.min(VOICE_CALL_GATEWAY_POLL_INTERVAL_MS, deadlineMs - performance.now());
|
|
254
281
|
if (sleepMs <= 0) break;
|
|
255
|
-
await
|
|
282
|
+
await sleepWithAbort(sleepMs, signal);
|
|
256
283
|
}
|
|
257
284
|
throw new Error("voicecall continue timed out waiting for gateway operation");
|
|
258
285
|
}
|
|
@@ -265,14 +292,20 @@ async function ensureStandaloneRuntime(params) {
|
|
|
265
292
|
}
|
|
266
293
|
}
|
|
267
294
|
async function runGatewayManagerCommand(params) {
|
|
295
|
+
params.signal?.throwIfAborted();
|
|
268
296
|
const gateway = await params.gatewayCall();
|
|
297
|
+
params.signal?.throwIfAborted();
|
|
269
298
|
if (gateway.ok) {
|
|
270
|
-
|
|
299
|
+
const payload = params.resolveGatewayPayload ? await params.resolveGatewayPayload(gateway.payload) : gateway.payload;
|
|
300
|
+
params.signal?.throwIfAborted();
|
|
301
|
+
writeCliJson(payload);
|
|
271
302
|
return;
|
|
272
303
|
}
|
|
273
304
|
const runtime = await ensureStandaloneRuntime(params);
|
|
305
|
+
params.signal?.throwIfAborted();
|
|
274
306
|
const result = await params.managerFallback(runtime.manager);
|
|
275
307
|
if (!result.success) throw new Error(result.error || `${params.failureLabel} failed`);
|
|
308
|
+
params.signal?.throwIfAborted();
|
|
276
309
|
writeCliJson(result);
|
|
277
310
|
}
|
|
278
311
|
function readGatewayCallId(payload, invalidCallIdMessage) {
|
|
@@ -305,9 +338,21 @@ async function initiateVoiceCall(params) {
|
|
|
305
338
|
return result.callId;
|
|
306
339
|
}
|
|
307
340
|
// extensions/voice-call/src/cli.ts
|
|
341
|
+
const briefFileDecoder = new TextDecoder("utf-8", {
|
|
342
|
+
fatal: true,
|
|
343
|
+
ignoreBOM: true
|
|
344
|
+
});
|
|
308
345
|
async function readCliBrief(options) {
|
|
309
346
|
if (options.brief && options.briefFile) throw new Error("Use either --brief or --brief-file");
|
|
310
|
-
|
|
347
|
+
let raw = options.brief;
|
|
348
|
+
if (options.briefFile) {
|
|
349
|
+
const { buffer } = await readRegularFile({ filePath: await realpath(resolveUserPath(options.briefFile)) });
|
|
350
|
+
try {
|
|
351
|
+
raw = briefFileDecoder.decode(buffer);
|
|
352
|
+
} catch (error) {
|
|
353
|
+
throw new Error(`Brief file must be valid UTF-8: ${options.briefFile}. Re-save it as UTF-8 and retry.`, { cause: error });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
311
356
|
if (raw === void 0) return;
|
|
312
357
|
if (raw.length > 16e3) throw new Error("Brief input is too large");
|
|
313
358
|
const value = options.briefFile || raw.trimStart().startsWith("{") || raw.trimStart().startsWith("[") ? JSON.parse(raw) : { task: raw };
|
|
@@ -375,29 +420,40 @@ function writeSetupStatus(status) {
|
|
|
375
420
|
writeCliLine("Voice Call setup: %s", status.ok ? "OK" : "needs attention");
|
|
376
421
|
for (const check of status.checks) writeCliLine("%s %s: %s", check.ok ? "OK" : "FAIL", check.id, check.message);
|
|
377
422
|
}
|
|
378
|
-
async function runWithStandaloneRuntime(createRuntime, run) {
|
|
423
|
+
async function runWithStandaloneRuntime(createRuntime, run, interruptGateway = false) {
|
|
379
424
|
let runtime;
|
|
380
425
|
const interrupted = createDeferred();
|
|
426
|
+
const cancellation = new AbortController();
|
|
381
427
|
let exitSignal;
|
|
382
428
|
const interrupt = (signal) => {
|
|
383
429
|
exitSignal ??= signal;
|
|
384
430
|
process.exitCode = exitSignal === "SIGINT" ? 130 : 143;
|
|
431
|
+
cancellation.abort();
|
|
385
432
|
interrupted.resolve();
|
|
386
433
|
};
|
|
387
434
|
const onSigint = () => interrupt("SIGINT");
|
|
388
435
|
const onSigterm = () => interrupt("SIGTERM");
|
|
436
|
+
const listen = () => {
|
|
437
|
+
process.on("SIGINT", onSigint);
|
|
438
|
+
process.on("SIGTERM", onSigterm);
|
|
439
|
+
};
|
|
389
440
|
try {
|
|
441
|
+
if (interruptGateway) listen();
|
|
390
442
|
await run(async () => {
|
|
391
443
|
if (!runtime) {
|
|
392
|
-
|
|
393
|
-
process.on("SIGTERM", onSigterm);
|
|
444
|
+
if (!interruptGateway) listen();
|
|
394
445
|
runtime = createRuntime();
|
|
395
446
|
}
|
|
396
447
|
const active = await runtime;
|
|
397
|
-
if (exitSignal)
|
|
448
|
+
if (exitSignal) {
|
|
449
|
+
if (interruptGateway) cancellation.signal.throwIfAborted();
|
|
450
|
+
throw new Error(`Voice call command interrupted by ${exitSignal}`);
|
|
451
|
+
}
|
|
398
452
|
return active;
|
|
399
|
-
});
|
|
453
|
+
}, interruptGateway ? cancellation.signal : void 0);
|
|
400
454
|
if (runtime) await interrupted.promise;
|
|
455
|
+
} catch (error) {
|
|
456
|
+
if (!interruptGateway || !cancellation.signal.aborted || !(error instanceof Error) || error.name !== "AbortError") throw error;
|
|
401
457
|
} finally {
|
|
402
458
|
try {
|
|
403
459
|
await (await runtime?.catch(() => void 0))?.stop();
|
|
@@ -496,19 +552,15 @@ function registerVoiceCallCli(params) {
|
|
|
496
552
|
message: options.message
|
|
497
553
|
};
|
|
498
554
|
const continueTimeoutMs = resolveContinueTimeout(config);
|
|
499
|
-
return
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
return callVoiceCallGateway("voicecall.continue", gatewayParams, { timeoutMs: continueTimeoutMs });
|
|
506
|
-
}
|
|
507
|
-
},
|
|
508
|
-
resolveGatewayPayload: (payload) => pollContinueGateway(payload, continueTimeoutMs),
|
|
555
|
+
return runWithStandaloneRuntime(createRuntime, (ensureRuntime, signal) => runGatewayManagerCommand({
|
|
556
|
+
config,
|
|
557
|
+
ensureRuntime,
|
|
558
|
+
signal,
|
|
559
|
+
gatewayCall: () => callVoiceCallGateway("voicecall.continue.start", gatewayParams, { timeoutMs: resolveOperationTimeout(config) }),
|
|
560
|
+
resolveGatewayPayload: (payload) => pollContinueGateway(payload, continueTimeoutMs, signal),
|
|
509
561
|
managerFallback: (manager) => manager.continueCall(options.callId, options.message),
|
|
510
562
|
failureLabel: "continue"
|
|
511
|
-
});
|
|
563
|
+
}), true);
|
|
512
564
|
});
|
|
513
565
|
root.command("speak").description("Speak a message without waiting for response").requiredOption("--call-id <id>", "Call ID").requiredOption("--message <text>", "Message to speak").action((options) => managerAction({
|
|
514
566
|
gatewayCall: () => callVoiceCallGateway("voicecall.speak", {
|
|
@@ -575,11 +627,11 @@ function registerVoiceCallCli(params) {
|
|
|
575
627
|
});
|
|
576
628
|
root.command("expose").description("Enable/disable Tailscale serve/funnel for the webhook").option("--mode <mode>", "off | serve (tailnet) | funnel (public)", "funnel").option("--path <path>", "Tailscale path to expose (recommend matching serve.path)").option("--port <port>", "Local webhook port").option("--serve-path <path>", "Local webhook path").action(async (options) => {
|
|
577
629
|
const mode = resolveMode(options.mode ?? "funnel");
|
|
578
|
-
const servePort = parseCliInteger(options.port ?? String(config.serve.port
|
|
630
|
+
const servePort = parseCliInteger(options.port ?? String(config.serve.port), "--port", {
|
|
579
631
|
min: 1,
|
|
580
632
|
max: MAX_TCP_PORT
|
|
581
633
|
});
|
|
582
|
-
const servePath = options.servePath ?? config.serve.path
|
|
634
|
+
const servePath = options.servePath ?? config.serve.path;
|
|
583
635
|
const tsPath = options.path ?? config.tailscale?.path ?? servePath;
|
|
584
636
|
const streamExposurePaths = resolveVoiceCallStreamExposurePaths(config, {
|
|
585
637
|
publicWebhookPath: tsPath,
|