@kodelyth/google-meet 2026.5.42 → 2026.6.2
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/package.json +17 -6
- package/doctor-contract-api.ts +0 -1
- package/google-meet.live.test.ts +0 -82
- package/index.create.test.ts +0 -671
- package/index.test.ts +0 -5051
- package/index.ts +0 -1224
- package/node-host.test.ts +0 -241
- package/src/agent-consult.ts +0 -158
- package/src/calendar.ts +0 -252
- package/src/cli.test.ts +0 -1234
- package/src/cli.ts +0 -2350
- package/src/config-compat.test.ts +0 -98
- package/src/config-compat.ts +0 -84
- package/src/config.ts +0 -589
- package/src/create.ts +0 -157
- package/src/drive.ts +0 -72
- package/src/google-api-errors.ts +0 -20
- package/src/meet.ts +0 -1024
- package/src/node-host.ts +0 -520
- package/src/oauth.test.ts +0 -73
- package/src/oauth.ts +0 -229
- package/src/realtime-node.ts +0 -780
- package/src/realtime.ts +0 -1334
- package/src/runtime.ts +0 -1008
- package/src/setup.ts +0 -285
- package/src/test-support/plugin-harness.ts +0 -232
- package/src/transports/chrome-browser-proxy.test.ts +0 -39
- package/src/transports/chrome-browser-proxy.ts +0 -204
- package/src/transports/chrome-create.ts +0 -364
- package/src/transports/chrome.test.ts +0 -12
- package/src/transports/chrome.ts +0 -1065
- package/src/transports/twilio.ts +0 -57
- package/src/transports/types.ts +0 -147
- package/src/voice-call-gateway.test.ts +0 -152
- package/src/voice-call-gateway.ts +0 -241
- package/tsconfig.json +0 -16
package/src/transports/twilio.ts
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { normalizeOptionalString } from "klaw/plugin-sdk/string-coerce-runtime";
|
|
2
|
-
|
|
3
|
-
const DTMF_PATTERN = /^[0-9*#wWpP,]+$/;
|
|
4
|
-
|
|
5
|
-
export function normalizeDialInNumber(value: unknown): string | undefined {
|
|
6
|
-
const normalized = normalizeOptionalString(value);
|
|
7
|
-
if (!normalized) {
|
|
8
|
-
return undefined;
|
|
9
|
-
}
|
|
10
|
-
const compact = normalized.replace(/[()\s.-]/g, "");
|
|
11
|
-
if (!/^\+?[0-9]{5,20}$/.test(compact)) {
|
|
12
|
-
throw new Error("dialInNumber must be a phone number");
|
|
13
|
-
}
|
|
14
|
-
return compact;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
function normalizeDtmfSequence(value: unknown): string | undefined {
|
|
18
|
-
const normalized = normalizeOptionalString(value);
|
|
19
|
-
if (!normalized) {
|
|
20
|
-
return undefined;
|
|
21
|
-
}
|
|
22
|
-
const compact = normalized.replace(/\s+/g, "");
|
|
23
|
-
if (!DTMF_PATTERN.test(compact)) {
|
|
24
|
-
throw new Error("dtmfSequence may only contain digits, *, #, comma, w, p");
|
|
25
|
-
}
|
|
26
|
-
return compact;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function buildMeetDtmfSequence(params: {
|
|
30
|
-
pin?: string;
|
|
31
|
-
dtmfSequence?: string;
|
|
32
|
-
}): string | undefined {
|
|
33
|
-
const explicit = normalizeDtmfSequence(params.dtmfSequence);
|
|
34
|
-
if (explicit) {
|
|
35
|
-
return explicit;
|
|
36
|
-
}
|
|
37
|
-
const pin = normalizeOptionalString(params.pin);
|
|
38
|
-
if (!pin) {
|
|
39
|
-
return undefined;
|
|
40
|
-
}
|
|
41
|
-
const compactPin = pin.replace(/\s+/g, "");
|
|
42
|
-
if (!/^[0-9]+#?$/.test(compactPin)) {
|
|
43
|
-
throw new Error("pin may only contain digits and an optional trailing #");
|
|
44
|
-
}
|
|
45
|
-
return compactPin.endsWith("#") ? compactPin : `${compactPin}#`;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function prefixDtmfWait(sequence: string | undefined, delayMs: number): string | undefined {
|
|
49
|
-
if (!sequence || delayMs <= 0) {
|
|
50
|
-
return sequence;
|
|
51
|
-
}
|
|
52
|
-
const waitCount = Math.ceil(delayMs / 500);
|
|
53
|
-
if (waitCount <= 0) {
|
|
54
|
-
return sequence;
|
|
55
|
-
}
|
|
56
|
-
return `${"w".repeat(waitCount)}${sequence}`;
|
|
57
|
-
}
|
package/src/transports/types.ts
DELETED
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import type { GoogleMeetMode, GoogleMeetModeInput, GoogleMeetTransport } from "../config.js";
|
|
2
|
-
|
|
3
|
-
type GoogleMeetSessionState = "active" | "ended";
|
|
4
|
-
|
|
5
|
-
export type GoogleMeetJoinRequest = {
|
|
6
|
-
url: string;
|
|
7
|
-
transport?: GoogleMeetTransport;
|
|
8
|
-
mode?: GoogleMeetModeInput;
|
|
9
|
-
message?: string;
|
|
10
|
-
requesterSessionKey?: string;
|
|
11
|
-
timeoutMs?: number;
|
|
12
|
-
dialInNumber?: string;
|
|
13
|
-
pin?: string;
|
|
14
|
-
dtmfSequence?: string;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
type GoogleMeetManualActionReason =
|
|
18
|
-
| "google-login-required"
|
|
19
|
-
| "meet-admission-required"
|
|
20
|
-
| "meet-permission-required"
|
|
21
|
-
| "meet-audio-choice-required"
|
|
22
|
-
| "browser-control-unavailable";
|
|
23
|
-
|
|
24
|
-
type GoogleMeetSpeechBlockedReason =
|
|
25
|
-
| GoogleMeetManualActionReason
|
|
26
|
-
| "not-in-call"
|
|
27
|
-
| "browser-unverified"
|
|
28
|
-
| "audio-bridge-unavailable"
|
|
29
|
-
| "meet-microphone-muted";
|
|
30
|
-
|
|
31
|
-
export type GoogleMeetChromeHealth = {
|
|
32
|
-
inCall?: boolean;
|
|
33
|
-
micMuted?: boolean;
|
|
34
|
-
lobbyWaiting?: boolean;
|
|
35
|
-
leaveReason?: string;
|
|
36
|
-
captioning?: boolean;
|
|
37
|
-
captionsEnabledAttempted?: boolean;
|
|
38
|
-
transcriptLines?: number;
|
|
39
|
-
lastCaptionAt?: string;
|
|
40
|
-
lastCaptionSpeaker?: string;
|
|
41
|
-
lastCaptionText?: string;
|
|
42
|
-
recentTranscript?: Array<{
|
|
43
|
-
at?: string;
|
|
44
|
-
speaker?: string;
|
|
45
|
-
text: string;
|
|
46
|
-
}>;
|
|
47
|
-
realtimeTranscriptLines?: number;
|
|
48
|
-
lastRealtimeTranscriptAt?: string;
|
|
49
|
-
lastRealtimeTranscriptRole?: "user" | "assistant";
|
|
50
|
-
lastRealtimeTranscriptText?: string;
|
|
51
|
-
recentRealtimeTranscript?: Array<{
|
|
52
|
-
at: string;
|
|
53
|
-
role: "user" | "assistant";
|
|
54
|
-
text: string;
|
|
55
|
-
}>;
|
|
56
|
-
lastRealtimeEventAt?: string;
|
|
57
|
-
lastRealtimeEventType?: string;
|
|
58
|
-
lastRealtimeEventDetail?: string;
|
|
59
|
-
recentRealtimeEvents?: Array<{
|
|
60
|
-
at: string;
|
|
61
|
-
direction: "client" | "server";
|
|
62
|
-
type: string;
|
|
63
|
-
detail?: string;
|
|
64
|
-
}>;
|
|
65
|
-
recentTalkEvents?: Array<{
|
|
66
|
-
id: string;
|
|
67
|
-
type: string;
|
|
68
|
-
sessionId: string;
|
|
69
|
-
turnId?: string;
|
|
70
|
-
seq: number;
|
|
71
|
-
timestamp: string;
|
|
72
|
-
final?: boolean;
|
|
73
|
-
}>;
|
|
74
|
-
manualActionRequired?: boolean;
|
|
75
|
-
manualActionReason?: GoogleMeetManualActionReason;
|
|
76
|
-
manualActionMessage?: string;
|
|
77
|
-
speechReady?: boolean;
|
|
78
|
-
speechBlockedReason?: GoogleMeetSpeechBlockedReason;
|
|
79
|
-
speechBlockedMessage?: string;
|
|
80
|
-
providerConnected?: boolean;
|
|
81
|
-
realtimeReady?: boolean;
|
|
82
|
-
audioInputActive?: boolean;
|
|
83
|
-
audioOutputActive?: boolean;
|
|
84
|
-
audioOutputRouted?: boolean;
|
|
85
|
-
audioOutputDeviceLabel?: string;
|
|
86
|
-
audioOutputRouteError?: string;
|
|
87
|
-
lastInputAt?: string;
|
|
88
|
-
lastOutputAt?: string;
|
|
89
|
-
lastSuppressedInputAt?: string;
|
|
90
|
-
lastClearAt?: string;
|
|
91
|
-
lastInputBytes?: number;
|
|
92
|
-
lastOutputBytes?: number;
|
|
93
|
-
suppressedInputBytes?: number;
|
|
94
|
-
consecutiveInputErrors?: number;
|
|
95
|
-
lastInputError?: string;
|
|
96
|
-
clearCount?: number;
|
|
97
|
-
queuedInputChunks?: number;
|
|
98
|
-
browserUrl?: string;
|
|
99
|
-
browserTitle?: string;
|
|
100
|
-
bridgeClosed?: boolean;
|
|
101
|
-
status?: string;
|
|
102
|
-
notes?: string[];
|
|
103
|
-
};
|
|
104
|
-
|
|
105
|
-
export type GoogleMeetSession = {
|
|
106
|
-
id: string;
|
|
107
|
-
url: string;
|
|
108
|
-
transport: GoogleMeetTransport;
|
|
109
|
-
mode: GoogleMeetMode;
|
|
110
|
-
state: GoogleMeetSessionState;
|
|
111
|
-
createdAt: string;
|
|
112
|
-
updatedAt: string;
|
|
113
|
-
participantIdentity: string;
|
|
114
|
-
realtime: {
|
|
115
|
-
enabled: boolean;
|
|
116
|
-
strategy?: string;
|
|
117
|
-
provider?: string;
|
|
118
|
-
model?: string;
|
|
119
|
-
transcriptionProvider?: string;
|
|
120
|
-
toolPolicy: string;
|
|
121
|
-
};
|
|
122
|
-
chrome?: {
|
|
123
|
-
audioBackend: "blackhole-2ch";
|
|
124
|
-
launched: boolean;
|
|
125
|
-
nodeId?: string;
|
|
126
|
-
browserProfile?: string;
|
|
127
|
-
audioBridge?: {
|
|
128
|
-
type: "command-pair" | "node-command-pair" | "external-command";
|
|
129
|
-
provider?: string;
|
|
130
|
-
};
|
|
131
|
-
health?: GoogleMeetChromeHealth;
|
|
132
|
-
};
|
|
133
|
-
twilio?: {
|
|
134
|
-
dialInNumber: string;
|
|
135
|
-
pinProvided: boolean;
|
|
136
|
-
dtmfSequence?: string;
|
|
137
|
-
voiceCallId?: string;
|
|
138
|
-
dtmfSent?: boolean;
|
|
139
|
-
introSent?: boolean;
|
|
140
|
-
};
|
|
141
|
-
notes: string[];
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
export type GoogleMeetJoinResult = {
|
|
145
|
-
session: GoogleMeetSession;
|
|
146
|
-
spoken?: boolean;
|
|
147
|
-
};
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { resolveGoogleMeetConfig } from "./config.js";
|
|
3
|
-
import {
|
|
4
|
-
endMeetVoiceCallGatewayCall,
|
|
5
|
-
getMeetVoiceCallGatewayCall,
|
|
6
|
-
joinMeetViaVoiceCallGateway,
|
|
7
|
-
} from "./voice-call-gateway.js";
|
|
8
|
-
|
|
9
|
-
const gatewayMocks = vi.hoisted(() => ({
|
|
10
|
-
request: vi.fn(),
|
|
11
|
-
stopAndWait: vi.fn(async () => {}),
|
|
12
|
-
startGatewayClientWhenEventLoopReady: vi.fn(async () => ({ ready: true, aborted: false })),
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
vi.mock("klaw/plugin-sdk/gateway-runtime", () => ({
|
|
16
|
-
GatewayClient: vi.fn(function MockGatewayClient(params: { onHelloOk?: () => void }) {
|
|
17
|
-
queueMicrotask(() => params.onHelloOk?.());
|
|
18
|
-
return {
|
|
19
|
-
request: gatewayMocks.request,
|
|
20
|
-
stopAndWait: gatewayMocks.stopAndWait,
|
|
21
|
-
};
|
|
22
|
-
}),
|
|
23
|
-
startGatewayClientWhenEventLoopReady: gatewayMocks.startGatewayClientWhenEventLoopReady,
|
|
24
|
-
}));
|
|
25
|
-
|
|
26
|
-
describe("Google Meet voice-call gateway", () => {
|
|
27
|
-
beforeEach(() => {
|
|
28
|
-
vi.useRealTimers();
|
|
29
|
-
gatewayMocks.request.mockReset();
|
|
30
|
-
gatewayMocks.request.mockResolvedValue({ callId: "call-1" });
|
|
31
|
-
gatewayMocks.stopAndWait.mockClear();
|
|
32
|
-
gatewayMocks.startGatewayClientWhenEventLoopReady.mockClear();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
afterEach(() => {
|
|
36
|
-
vi.useRealTimers();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
afterAll(() => {
|
|
40
|
-
vi.doUnmock("klaw/plugin-sdk/gateway-runtime");
|
|
41
|
-
vi.resetModules();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("starts Twilio Meet calls with pre-connect DTMF, then speaks the intro without TwiML fallback", async () => {
|
|
45
|
-
const config = resolveGoogleMeetConfig({
|
|
46
|
-
voiceCall: {
|
|
47
|
-
gatewayUrl: "ws://127.0.0.1:18789",
|
|
48
|
-
dtmfDelayMs: 1,
|
|
49
|
-
postDtmfSpeechDelayMs: 2,
|
|
50
|
-
},
|
|
51
|
-
realtime: { introMessage: "Say exactly: I'm here and listening." },
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const join = joinMeetViaVoiceCallGateway({
|
|
55
|
-
config,
|
|
56
|
-
dialInNumber: "+15551234567",
|
|
57
|
-
dtmfSequence: "123456#",
|
|
58
|
-
message: "Say exactly: I'm here and listening.",
|
|
59
|
-
requesterSessionKey: "agent:main:discord:channel:general",
|
|
60
|
-
sessionKey: "voice:google-meet:meet-1",
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
await join;
|
|
64
|
-
|
|
65
|
-
expect(gatewayMocks.request).toHaveBeenNthCalledWith(
|
|
66
|
-
1,
|
|
67
|
-
"voicecall.start",
|
|
68
|
-
{
|
|
69
|
-
to: "+15551234567",
|
|
70
|
-
mode: "conversation",
|
|
71
|
-
dtmfSequence: "123456#",
|
|
72
|
-
requesterSessionKey: "agent:main:discord:channel:general",
|
|
73
|
-
sessionKey: "voice:google-meet:meet-1",
|
|
74
|
-
},
|
|
75
|
-
{ timeoutMs: 30_000 },
|
|
76
|
-
);
|
|
77
|
-
expect(gatewayMocks.request).toHaveBeenNthCalledWith(
|
|
78
|
-
2,
|
|
79
|
-
"voicecall.speak",
|
|
80
|
-
{
|
|
81
|
-
callId: "call-1",
|
|
82
|
-
allowTwimlFallback: false,
|
|
83
|
-
message: "Say exactly: I'm here and listening.",
|
|
84
|
-
},
|
|
85
|
-
{ timeoutMs: 30_000 },
|
|
86
|
-
);
|
|
87
|
-
expect(gatewayMocks.request).toHaveBeenCalledTimes(2);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("skips the intro without failing when the realtime bridge is not ready", async () => {
|
|
91
|
-
gatewayMocks.request
|
|
92
|
-
.mockResolvedValueOnce({ callId: "call-1" })
|
|
93
|
-
.mockResolvedValueOnce({ success: false, error: "No active realtime bridge for call" });
|
|
94
|
-
const config = resolveGoogleMeetConfig({
|
|
95
|
-
voiceCall: {
|
|
96
|
-
gatewayUrl: "ws://127.0.0.1:18789",
|
|
97
|
-
dtmfDelayMs: 1,
|
|
98
|
-
postDtmfSpeechDelayMs: 1,
|
|
99
|
-
},
|
|
100
|
-
});
|
|
101
|
-
const logger = { info: vi.fn(), warn: vi.fn(), error: vi.fn(), debug: vi.fn() };
|
|
102
|
-
|
|
103
|
-
const result = await joinMeetViaVoiceCallGateway({
|
|
104
|
-
config,
|
|
105
|
-
dialInNumber: "+15551234567",
|
|
106
|
-
dtmfSequence: "123456#",
|
|
107
|
-
logger,
|
|
108
|
-
message: "Say exactly: I'm here and listening.",
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
expect(result.callId).toBe("call-1");
|
|
112
|
-
expect(result.dtmfSent).toBe(true);
|
|
113
|
-
expect(result.introSent).toBe(false);
|
|
114
|
-
expect(logger.warn).toHaveBeenCalledWith(
|
|
115
|
-
"[google-meet] Skipped intro speech because realtime bridge was not ready: No active realtime bridge for call",
|
|
116
|
-
);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("treats missing delegated calls as already ended", async () => {
|
|
120
|
-
gatewayMocks.request.mockRejectedValueOnce(new Error("Call not found"));
|
|
121
|
-
const config = resolveGoogleMeetConfig({
|
|
122
|
-
voiceCall: { gatewayUrl: "ws://127.0.0.1:18789" },
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
await expect(
|
|
126
|
-
endMeetVoiceCallGatewayCall({ config, callId: "call-1" }),
|
|
127
|
-
).resolves.toBeUndefined();
|
|
128
|
-
|
|
129
|
-
expect(gatewayMocks.request).toHaveBeenCalledWith(
|
|
130
|
-
"voicecall.end",
|
|
131
|
-
{ callId: "call-1" },
|
|
132
|
-
{ timeoutMs: 30_000 },
|
|
133
|
-
);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("reads delegated call status from the gateway", async () => {
|
|
137
|
-
gatewayMocks.request.mockResolvedValueOnce({ found: false });
|
|
138
|
-
const config = resolveGoogleMeetConfig({
|
|
139
|
-
voiceCall: { gatewayUrl: "ws://127.0.0.1:18789" },
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
await expect(getMeetVoiceCallGatewayCall({ config, callId: "call-1" })).resolves.toEqual({
|
|
143
|
-
found: false,
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
expect(gatewayMocks.request).toHaveBeenCalledWith(
|
|
147
|
-
"voicecall.status",
|
|
148
|
-
{ callId: "call-1" },
|
|
149
|
-
{ timeoutMs: 30_000 },
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
import { formatErrorMessage } from "klaw/plugin-sdk/error-runtime";
|
|
2
|
-
import {
|
|
3
|
-
GatewayClient,
|
|
4
|
-
startGatewayClientWhenEventLoopReady,
|
|
5
|
-
} from "klaw/plugin-sdk/gateway-runtime";
|
|
6
|
-
import type { RuntimeLogger } from "klaw/plugin-sdk/plugin-runtime";
|
|
7
|
-
import { sleep } from "klaw/plugin-sdk/runtime-env";
|
|
8
|
-
import type { GoogleMeetConfig } from "./config.js";
|
|
9
|
-
|
|
10
|
-
type VoiceCallGatewayClient = InstanceType<typeof GatewayClient>;
|
|
11
|
-
|
|
12
|
-
type VoiceCallStartResult = {
|
|
13
|
-
callId?: string;
|
|
14
|
-
initiated?: boolean;
|
|
15
|
-
error?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
type VoiceCallSpeakResult = {
|
|
19
|
-
success?: boolean;
|
|
20
|
-
error?: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
type VoiceCallStatusResult = {
|
|
24
|
-
found?: boolean;
|
|
25
|
-
call?: unknown;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
type VoiceCallMeetJoinResult = {
|
|
29
|
-
callId: string;
|
|
30
|
-
dtmfSent: boolean;
|
|
31
|
-
introSent: boolean;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
async function createConnectedGatewayClient(
|
|
35
|
-
config: GoogleMeetConfig,
|
|
36
|
-
): Promise<VoiceCallGatewayClient> {
|
|
37
|
-
let client: VoiceCallGatewayClient;
|
|
38
|
-
await new Promise<void>((resolve, reject) => {
|
|
39
|
-
const abortStart = new AbortController();
|
|
40
|
-
const timer = setTimeout(() => {
|
|
41
|
-
abortStart.abort();
|
|
42
|
-
reject(new Error("gateway connect timeout"));
|
|
43
|
-
}, config.voiceCall.requestTimeoutMs);
|
|
44
|
-
client = new GatewayClient({
|
|
45
|
-
url: config.voiceCall.gatewayUrl,
|
|
46
|
-
token: config.voiceCall.token,
|
|
47
|
-
requestTimeoutMs: config.voiceCall.requestTimeoutMs,
|
|
48
|
-
clientName: "cli",
|
|
49
|
-
clientDisplayName: "Google Meet plugin",
|
|
50
|
-
scopes: ["operator.write"],
|
|
51
|
-
onHelloOk: () => {
|
|
52
|
-
clearTimeout(timer);
|
|
53
|
-
resolve();
|
|
54
|
-
},
|
|
55
|
-
onConnectError: (err) => {
|
|
56
|
-
clearTimeout(timer);
|
|
57
|
-
abortStart.abort();
|
|
58
|
-
reject(err);
|
|
59
|
-
},
|
|
60
|
-
});
|
|
61
|
-
void startGatewayClientWhenEventLoopReady(client, {
|
|
62
|
-
timeoutMs: config.voiceCall.requestTimeoutMs,
|
|
63
|
-
signal: abortStart.signal,
|
|
64
|
-
})
|
|
65
|
-
.then((readiness) => {
|
|
66
|
-
if (!readiness.ready && !readiness.aborted) {
|
|
67
|
-
clearTimeout(timer);
|
|
68
|
-
reject(new Error("gateway event loop readiness timeout"));
|
|
69
|
-
}
|
|
70
|
-
})
|
|
71
|
-
.catch((err) => {
|
|
72
|
-
clearTimeout(timer);
|
|
73
|
-
reject(err instanceof Error ? err : new Error(String(err)));
|
|
74
|
-
});
|
|
75
|
-
});
|
|
76
|
-
return client!;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export function isVoiceCallMissingError(error: unknown): boolean {
|
|
80
|
-
const message = formatErrorMessage(error).toLowerCase();
|
|
81
|
-
return message.includes("call not found") || message.includes("call is not active");
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function joinMeetViaVoiceCallGateway(params: {
|
|
85
|
-
config: GoogleMeetConfig;
|
|
86
|
-
dialInNumber: string;
|
|
87
|
-
dtmfSequence?: string;
|
|
88
|
-
logger?: RuntimeLogger;
|
|
89
|
-
message?: string;
|
|
90
|
-
requesterSessionKey?: string;
|
|
91
|
-
sessionKey?: string;
|
|
92
|
-
}): Promise<VoiceCallMeetJoinResult> {
|
|
93
|
-
let client: VoiceCallGatewayClient | undefined;
|
|
94
|
-
|
|
95
|
-
try {
|
|
96
|
-
client = await createConnectedGatewayClient(params.config);
|
|
97
|
-
params.logger?.info(
|
|
98
|
-
`[google-meet] Delegating Twilio join to Voice Call (dtmf=${params.dtmfSequence ? "pre-connect" : "none"}, intro=${params.message ? "delayed" : "none"})`,
|
|
99
|
-
);
|
|
100
|
-
const start = (await client.request(
|
|
101
|
-
"voicecall.start",
|
|
102
|
-
{
|
|
103
|
-
to: params.dialInNumber,
|
|
104
|
-
mode: "conversation",
|
|
105
|
-
...(params.dtmfSequence ? { dtmfSequence: params.dtmfSequence } : {}),
|
|
106
|
-
...(params.requesterSessionKey ? { requesterSessionKey: params.requesterSessionKey } : {}),
|
|
107
|
-
...(params.sessionKey ? { sessionKey: params.sessionKey } : {}),
|
|
108
|
-
},
|
|
109
|
-
{ timeoutMs: params.config.voiceCall.requestTimeoutMs },
|
|
110
|
-
)) as VoiceCallStartResult;
|
|
111
|
-
if (!start.callId) {
|
|
112
|
-
throw new Error(start.error || "voicecall.start did not return callId");
|
|
113
|
-
}
|
|
114
|
-
params.logger?.info(
|
|
115
|
-
`[google-meet] Voice Call Twilio phone leg started: callId=${start.callId}`,
|
|
116
|
-
);
|
|
117
|
-
const dtmfSent = Boolean(params.dtmfSequence);
|
|
118
|
-
if (dtmfSent) {
|
|
119
|
-
params.logger?.info(
|
|
120
|
-
`[google-meet] Meet DTMF queued before realtime connect: callId=${start.callId} digits=${params.dtmfSequence?.length ?? 0}`,
|
|
121
|
-
);
|
|
122
|
-
}
|
|
123
|
-
let introSent = false;
|
|
124
|
-
if (params.message) {
|
|
125
|
-
const delayMs = params.dtmfSequence ? params.config.voiceCall.postDtmfSpeechDelayMs : 0;
|
|
126
|
-
if (delayMs > 0) {
|
|
127
|
-
params.logger?.info(
|
|
128
|
-
`[google-meet] Waiting ${delayMs}ms after Meet DTMF before speaking intro for callId=${start.callId}`,
|
|
129
|
-
);
|
|
130
|
-
await sleep(delayMs);
|
|
131
|
-
}
|
|
132
|
-
let spoken: VoiceCallSpeakResult;
|
|
133
|
-
try {
|
|
134
|
-
spoken = (await client.request(
|
|
135
|
-
"voicecall.speak",
|
|
136
|
-
{
|
|
137
|
-
callId: start.callId,
|
|
138
|
-
allowTwimlFallback: false,
|
|
139
|
-
message: params.message,
|
|
140
|
-
},
|
|
141
|
-
{ timeoutMs: params.config.voiceCall.requestTimeoutMs },
|
|
142
|
-
)) as VoiceCallSpeakResult;
|
|
143
|
-
} catch (err) {
|
|
144
|
-
params.logger?.warn?.(
|
|
145
|
-
`[google-meet] Skipped intro speech because realtime bridge was not ready: ${formatErrorMessage(err)}`,
|
|
146
|
-
);
|
|
147
|
-
spoken = { success: false };
|
|
148
|
-
}
|
|
149
|
-
if (spoken.success === false) {
|
|
150
|
-
params.logger?.warn?.(
|
|
151
|
-
`[google-meet] Skipped intro speech because realtime bridge was not ready: ${
|
|
152
|
-
spoken.error || "voicecall.speak failed"
|
|
153
|
-
}`,
|
|
154
|
-
);
|
|
155
|
-
} else {
|
|
156
|
-
introSent = true;
|
|
157
|
-
params.logger?.info(
|
|
158
|
-
`[google-meet] Intro speech requested after Meet dial sequence: callId=${start.callId}`,
|
|
159
|
-
);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return {
|
|
163
|
-
callId: start.callId,
|
|
164
|
-
dtmfSent,
|
|
165
|
-
introSent,
|
|
166
|
-
};
|
|
167
|
-
} finally {
|
|
168
|
-
await client?.stopAndWait({ timeoutMs: 1_000 });
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
export async function endMeetVoiceCallGatewayCall(params: {
|
|
173
|
-
config: GoogleMeetConfig;
|
|
174
|
-
callId: string;
|
|
175
|
-
}): Promise<void> {
|
|
176
|
-
let client: VoiceCallGatewayClient | undefined;
|
|
177
|
-
|
|
178
|
-
try {
|
|
179
|
-
client = await createConnectedGatewayClient(params.config);
|
|
180
|
-
try {
|
|
181
|
-
await client.request(
|
|
182
|
-
"voicecall.end",
|
|
183
|
-
{
|
|
184
|
-
callId: params.callId,
|
|
185
|
-
},
|
|
186
|
-
{ timeoutMs: params.config.voiceCall.requestTimeoutMs },
|
|
187
|
-
);
|
|
188
|
-
} catch (err) {
|
|
189
|
-
if (!isVoiceCallMissingError(err)) {
|
|
190
|
-
throw err;
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
} finally {
|
|
194
|
-
await client?.stopAndWait({ timeoutMs: 1_000 });
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
export async function getMeetVoiceCallGatewayCall(params: {
|
|
199
|
-
config: GoogleMeetConfig;
|
|
200
|
-
callId: string;
|
|
201
|
-
}): Promise<VoiceCallStatusResult> {
|
|
202
|
-
let client: VoiceCallGatewayClient | undefined;
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
client = await createConnectedGatewayClient(params.config);
|
|
206
|
-
return (await client.request(
|
|
207
|
-
"voicecall.status",
|
|
208
|
-
{
|
|
209
|
-
callId: params.callId,
|
|
210
|
-
},
|
|
211
|
-
{ timeoutMs: params.config.voiceCall.requestTimeoutMs },
|
|
212
|
-
)) as VoiceCallStatusResult;
|
|
213
|
-
} finally {
|
|
214
|
-
await client?.stopAndWait({ timeoutMs: 1_000 });
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
export async function speakMeetViaVoiceCallGateway(params: {
|
|
219
|
-
config: GoogleMeetConfig;
|
|
220
|
-
callId: string;
|
|
221
|
-
message: string;
|
|
222
|
-
}): Promise<void> {
|
|
223
|
-
let client: VoiceCallGatewayClient | undefined;
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
client = await createConnectedGatewayClient(params.config);
|
|
227
|
-
const spoken = (await client.request(
|
|
228
|
-
"voicecall.speak",
|
|
229
|
-
{
|
|
230
|
-
callId: params.callId,
|
|
231
|
-
message: params.message,
|
|
232
|
-
},
|
|
233
|
-
{ timeoutMs: params.config.voiceCall.requestTimeoutMs },
|
|
234
|
-
)) as VoiceCallSpeakResult;
|
|
235
|
-
if (spoken.success === false) {
|
|
236
|
-
throw new Error(spoken.error || "voicecall.speak failed");
|
|
237
|
-
}
|
|
238
|
-
} finally {
|
|
239
|
-
await client?.stopAndWait({ timeoutMs: 1_000 });
|
|
240
|
-
}
|
|
241
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../tsconfig.package-boundary.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "."
|
|
5
|
-
},
|
|
6
|
-
"include": ["./*.ts", "./src/**/*.ts"],
|
|
7
|
-
"exclude": [
|
|
8
|
-
"./**/*.test.ts",
|
|
9
|
-
"./dist/**",
|
|
10
|
-
"./node_modules/**",
|
|
11
|
-
"./src/test-support/**",
|
|
12
|
-
"./src/**/*test-helpers.ts",
|
|
13
|
-
"./src/**/*test-harness.ts",
|
|
14
|
-
"./src/**/*test-support.ts"
|
|
15
|
-
]
|
|
16
|
-
}
|