@openclaw/google-meet 2026.5.2 → 2026.5.3-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.
@@ -1,46 +0,0 @@
1
- import { normalizeOptionalString } from "openclaw/plugin-sdk/text-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
- }
@@ -1,113 +0,0 @@
1
- import type { GoogleMeetMode, GoogleMeetTransport } from "../config.js";
2
-
3
- type GoogleMeetSessionState = "active" | "ended";
4
-
5
- export type GoogleMeetJoinRequest = {
6
- url: string;
7
- transport?: GoogleMeetTransport;
8
- mode?: GoogleMeetMode;
9
- message?: string;
10
- timeoutMs?: number;
11
- dialInNumber?: string;
12
- pin?: string;
13
- dtmfSequence?: string;
14
- };
15
-
16
- type GoogleMeetManualActionReason =
17
- | "google-login-required"
18
- | "meet-admission-required"
19
- | "meet-permission-required"
20
- | "meet-audio-choice-required"
21
- | "browser-control-unavailable";
22
-
23
- type GoogleMeetSpeechBlockedReason =
24
- | GoogleMeetManualActionReason
25
- | "not-in-call"
26
- | "browser-unverified"
27
- | "audio-bridge-unavailable";
28
-
29
- export type GoogleMeetChromeHealth = {
30
- inCall?: boolean;
31
- micMuted?: boolean;
32
- lobbyWaiting?: boolean;
33
- leaveReason?: string;
34
- captioning?: boolean;
35
- captionsEnabledAttempted?: boolean;
36
- transcriptLines?: number;
37
- lastCaptionAt?: string;
38
- lastCaptionSpeaker?: string;
39
- lastCaptionText?: string;
40
- recentTranscript?: Array<{
41
- at?: string;
42
- speaker?: string;
43
- text: string;
44
- }>;
45
- manualActionRequired?: boolean;
46
- manualActionReason?: GoogleMeetManualActionReason;
47
- manualActionMessage?: string;
48
- speechReady?: boolean;
49
- speechBlockedReason?: GoogleMeetSpeechBlockedReason;
50
- speechBlockedMessage?: string;
51
- providerConnected?: boolean;
52
- realtimeReady?: boolean;
53
- audioInputActive?: boolean;
54
- audioOutputActive?: boolean;
55
- lastInputAt?: string;
56
- lastOutputAt?: string;
57
- lastSuppressedInputAt?: string;
58
- lastClearAt?: string;
59
- lastInputBytes?: number;
60
- lastOutputBytes?: number;
61
- suppressedInputBytes?: number;
62
- consecutiveInputErrors?: number;
63
- lastInputError?: string;
64
- clearCount?: number;
65
- queuedInputChunks?: number;
66
- browserUrl?: string;
67
- browserTitle?: string;
68
- bridgeClosed?: boolean;
69
- status?: string;
70
- notes?: string[];
71
- };
72
-
73
- export type GoogleMeetSession = {
74
- id: string;
75
- url: string;
76
- transport: GoogleMeetTransport;
77
- mode: GoogleMeetMode;
78
- state: GoogleMeetSessionState;
79
- createdAt: string;
80
- updatedAt: string;
81
- participantIdentity: string;
82
- realtime: {
83
- enabled: boolean;
84
- provider?: string;
85
- model?: string;
86
- toolPolicy: string;
87
- };
88
- chrome?: {
89
- audioBackend: "blackhole-2ch";
90
- launched: boolean;
91
- nodeId?: string;
92
- browserProfile?: string;
93
- audioBridge?: {
94
- type: "command-pair" | "node-command-pair" | "external-command";
95
- provider?: string;
96
- };
97
- health?: GoogleMeetChromeHealth;
98
- };
99
- twilio?: {
100
- dialInNumber: string;
101
- pinProvided: boolean;
102
- dtmfSequence?: string;
103
- voiceCallId?: string;
104
- dtmfSent?: boolean;
105
- introSent?: boolean;
106
- };
107
- notes: string[];
108
- };
109
-
110
- export type GoogleMeetJoinResult = {
111
- session: GoogleMeetSession;
112
- spoken?: boolean;
113
- };
@@ -1,79 +0,0 @@
1
- import { describe, expect, it, vi, beforeEach } from "vitest";
2
- import { resolveGoogleMeetConfig } from "./config.js";
3
- import { joinMeetViaVoiceCallGateway } from "./voice-call-gateway.js";
4
-
5
- const gatewayMocks = vi.hoisted(() => ({
6
- request: vi.fn(),
7
- stopAndWait: vi.fn(async () => {}),
8
- startGatewayClientWhenEventLoopReady: vi.fn(async () => ({ ready: true, aborted: false })),
9
- }));
10
-
11
- vi.mock("openclaw/plugin-sdk/gateway-runtime", () => ({
12
- GatewayClient: vi.fn(function MockGatewayClient(params: { onHelloOk?: () => void }) {
13
- queueMicrotask(() => params.onHelloOk?.());
14
- return {
15
- request: gatewayMocks.request,
16
- stopAndWait: gatewayMocks.stopAndWait,
17
- };
18
- }),
19
- startGatewayClientWhenEventLoopReady: gatewayMocks.startGatewayClientWhenEventLoopReady,
20
- }));
21
-
22
- describe("Google Meet voice-call gateway", () => {
23
- beforeEach(() => {
24
- vi.useRealTimers();
25
- gatewayMocks.request.mockReset();
26
- gatewayMocks.request.mockResolvedValue({ callId: "call-1" });
27
- gatewayMocks.stopAndWait.mockClear();
28
- gatewayMocks.startGatewayClientWhenEventLoopReady.mockClear();
29
- });
30
-
31
- it("starts Twilio Meet calls, sends delayed DTMF, then speaks the intro", async () => {
32
- const config = resolveGoogleMeetConfig({
33
- voiceCall: {
34
- gatewayUrl: "ws://127.0.0.1:18789",
35
- dtmfDelayMs: 1,
36
- postDtmfSpeechDelayMs: 2,
37
- },
38
- realtime: { introMessage: "Say exactly: I'm here and listening." },
39
- });
40
-
41
- const join = joinMeetViaVoiceCallGateway({
42
- config,
43
- dialInNumber: "+15551234567",
44
- dtmfSequence: "123456#",
45
- message: "Say exactly: I'm here and listening.",
46
- });
47
-
48
- await join;
49
-
50
- expect(gatewayMocks.request).toHaveBeenNthCalledWith(
51
- 1,
52
- "voicecall.start",
53
- {
54
- to: "+15551234567",
55
- mode: "conversation",
56
- },
57
- { timeoutMs: 30_000 },
58
- );
59
- expect(gatewayMocks.request).toHaveBeenNthCalledWith(
60
- 2,
61
- "voicecall.dtmf",
62
- {
63
- callId: "call-1",
64
- digits: "123456#",
65
- },
66
- { timeoutMs: 30_000 },
67
- );
68
- expect(gatewayMocks.request).toHaveBeenNthCalledWith(
69
- 3,
70
- "voicecall.speak",
71
- {
72
- callId: "call-1",
73
- message: "Say exactly: I'm here and listening.",
74
- },
75
- { timeoutMs: 30_000 },
76
- );
77
- expect(gatewayMocks.request).toHaveBeenCalledTimes(3);
78
- });
79
- });
@@ -1,213 +0,0 @@
1
- import {
2
- GatewayClient,
3
- startGatewayClientWhenEventLoopReady,
4
- } from "openclaw/plugin-sdk/gateway-runtime";
5
- import type { RuntimeLogger } from "openclaw/plugin-sdk/plugin-runtime";
6
- import type { GoogleMeetConfig } from "./config.js";
7
-
8
- type VoiceCallGatewayClient = InstanceType<typeof GatewayClient>;
9
-
10
- type VoiceCallStartResult = {
11
- callId?: string;
12
- initiated?: boolean;
13
- error?: string;
14
- };
15
-
16
- type VoiceCallSpeakResult = {
17
- success?: boolean;
18
- error?: string;
19
- };
20
-
21
- type VoiceCallDtmfResult = {
22
- success?: boolean;
23
- error?: string;
24
- };
25
-
26
- type VoiceCallMeetJoinResult = {
27
- callId: string;
28
- dtmfSent: boolean;
29
- introSent: boolean;
30
- };
31
-
32
- function sleep(ms: number): Promise<void> {
33
- if (ms <= 0) {
34
- return Promise.resolve();
35
- }
36
- return new Promise((resolve) => setTimeout(resolve, ms));
37
- }
38
-
39
- async function createConnectedGatewayClient(
40
- config: GoogleMeetConfig,
41
- ): Promise<VoiceCallGatewayClient> {
42
- let client: VoiceCallGatewayClient;
43
- await new Promise<void>((resolve, reject) => {
44
- const abortStart = new AbortController();
45
- const timer = setTimeout(() => {
46
- abortStart.abort();
47
- reject(new Error("gateway connect timeout"));
48
- }, config.voiceCall.requestTimeoutMs);
49
- client = new GatewayClient({
50
- url: config.voiceCall.gatewayUrl,
51
- token: config.voiceCall.token,
52
- requestTimeoutMs: config.voiceCall.requestTimeoutMs,
53
- clientName: "cli",
54
- clientDisplayName: "Google Meet plugin",
55
- scopes: ["operator.write"],
56
- onHelloOk: () => {
57
- clearTimeout(timer);
58
- resolve();
59
- },
60
- onConnectError: (err) => {
61
- clearTimeout(timer);
62
- abortStart.abort();
63
- reject(err);
64
- },
65
- });
66
- void startGatewayClientWhenEventLoopReady(client, {
67
- timeoutMs: config.voiceCall.requestTimeoutMs,
68
- signal: abortStart.signal,
69
- })
70
- .then((readiness) => {
71
- if (!readiness.ready && !readiness.aborted) {
72
- clearTimeout(timer);
73
- reject(new Error("gateway event loop readiness timeout"));
74
- }
75
- })
76
- .catch((err) => {
77
- clearTimeout(timer);
78
- reject(err instanceof Error ? err : new Error(String(err)));
79
- });
80
- });
81
- return client!;
82
- }
83
-
84
- export async function joinMeetViaVoiceCallGateway(params: {
85
- config: GoogleMeetConfig;
86
- dialInNumber: string;
87
- dtmfSequence?: string;
88
- logger?: RuntimeLogger;
89
- message?: string;
90
- }): Promise<VoiceCallMeetJoinResult> {
91
- let client: VoiceCallGatewayClient | undefined;
92
-
93
- try {
94
- client = await createConnectedGatewayClient(params.config);
95
- params.logger?.info(
96
- `[google-meet] Delegating Twilio join to Voice Call (dtmf=${params.dtmfSequence ? "post-connect" : "none"}, intro=${params.message ? "delayed" : "none"})`,
97
- );
98
- const start = (await client.request(
99
- "voicecall.start",
100
- {
101
- to: params.dialInNumber,
102
- mode: "conversation",
103
- },
104
- { timeoutMs: params.config.voiceCall.requestTimeoutMs },
105
- )) as VoiceCallStartResult;
106
- if (!start.callId) {
107
- throw new Error(start.error || "voicecall.start did not return callId");
108
- }
109
- params.logger?.info(
110
- `[google-meet] Voice Call Twilio phone leg started: callId=${start.callId}`,
111
- );
112
- let dtmfSent = false;
113
- if (params.dtmfSequence) {
114
- const delayMs = params.config.voiceCall.dtmfDelayMs;
115
- params.logger?.info(
116
- `[google-meet] Waiting ${delayMs}ms before sending Meet DTMF for callId=${start.callId}`,
117
- );
118
- await sleep(delayMs);
119
- const dtmf = (await client.request(
120
- "voicecall.dtmf",
121
- {
122
- callId: start.callId,
123
- digits: params.dtmfSequence,
124
- },
125
- { timeoutMs: params.config.voiceCall.requestTimeoutMs },
126
- )) as VoiceCallDtmfResult;
127
- if (dtmf.success === false) {
128
- throw new Error(dtmf.error || "voicecall.dtmf failed");
129
- }
130
- dtmfSent = true;
131
- params.logger?.info(
132
- `[google-meet] Meet DTMF sent after phone leg connected: callId=${start.callId} digits=${params.dtmfSequence.length}`,
133
- );
134
- }
135
- let introSent = false;
136
- if (params.message) {
137
- const delayMs = params.dtmfSequence ? params.config.voiceCall.postDtmfSpeechDelayMs : 0;
138
- if (delayMs > 0) {
139
- params.logger?.info(
140
- `[google-meet] Waiting ${delayMs}ms after Meet DTMF before speaking intro for callId=${start.callId}`,
141
- );
142
- await sleep(delayMs);
143
- }
144
- const spoken = (await client.request(
145
- "voicecall.speak",
146
- {
147
- callId: start.callId,
148
- message: params.message,
149
- },
150
- { timeoutMs: params.config.voiceCall.requestTimeoutMs },
151
- )) as VoiceCallSpeakResult;
152
- if (spoken.success === false) {
153
- throw new Error(spoken.error || "voicecall.speak failed");
154
- }
155
- introSent = true;
156
- params.logger?.info(
157
- `[google-meet] Intro speech requested after Meet dial sequence: callId=${start.callId}`,
158
- );
159
- }
160
- return {
161
- callId: start.callId,
162
- dtmfSent,
163
- introSent,
164
- };
165
- } finally {
166
- await client?.stopAndWait({ timeoutMs: 1_000 });
167
- }
168
- }
169
-
170
- export async function endMeetVoiceCallGatewayCall(params: {
171
- config: GoogleMeetConfig;
172
- callId: string;
173
- }): Promise<void> {
174
- let client: VoiceCallGatewayClient | undefined;
175
-
176
- try {
177
- client = await createConnectedGatewayClient(params.config);
178
- await client.request(
179
- "voicecall.end",
180
- {
181
- callId: params.callId,
182
- },
183
- { timeoutMs: params.config.voiceCall.requestTimeoutMs },
184
- );
185
- } finally {
186
- await client?.stopAndWait({ timeoutMs: 1_000 });
187
- }
188
- }
189
-
190
- export async function speakMeetViaVoiceCallGateway(params: {
191
- config: GoogleMeetConfig;
192
- callId: string;
193
- message: string;
194
- }): Promise<void> {
195
- let client: VoiceCallGatewayClient | undefined;
196
-
197
- try {
198
- client = await createConnectedGatewayClient(params.config);
199
- const spoken = (await client.request(
200
- "voicecall.speak",
201
- {
202
- callId: params.callId,
203
- message: params.message,
204
- },
205
- { timeoutMs: params.config.voiceCall.requestTimeoutMs },
206
- )) as VoiceCallSpeakResult;
207
- if (spoken.success === false) {
208
- throw new Error(spoken.error || "voicecall.speak failed");
209
- }
210
- } finally {
211
- await client?.stopAndWait({ timeoutMs: 1_000 });
212
- }
213
- }
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
- }