@openclaw/google-meet 2026.7.2-beta.7 → 2026.8.1-beta.3

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/index.js CHANGED
@@ -1,2327 +1,16 @@
1
- import { a as buildGoogleMeetCalendarDayWindow, c as normalizeMeetUrl, i as resolveGoogleMeetGatewayOperationTimeoutMs, n as DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND, o as findGoogleMeetCalendarEvent, r as resolveGoogleMeetConfig, s as listGoogleMeetCalendarEvents, t as DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND } from "./config-BcaS1BDr.js";
2
1
  import { t as GOOGLE_MEET_CLI_DESCRIPTOR } from "./cli-output-mode-BwH_6tH8.js";
3
- import { _ as endGoogleMeetActiveConference, a as isRecoverableMeetTab, c as readMeetAuthUser, d as resolveChromeNodeInfo, f as GOOGLE_MEET_NODE_COMMAND, h as fetchGoogleMeetAttendance, i as isEnglishMeetTab, l as callBrowserProxyOnNode, m as fetchGoogleMeetArtifacts, n as isGoogleMeetBrowserManualActionError, o as isSameMeetUrlForReuse, p as buildGoogleMeetPreflightReport, r as forceMeetEnglishUi, s as normalizeMeetUrlForReuse, t as createMeetWithBrowserProxyOnNode, u as resolveChromeNode, v as fetchGoogleMeetSpace, y as fetchLatestGoogleMeetConferenceRecord } from "./chrome-create-DMKruRFW.js";
4
- import { optionalPositiveIntegerSchema, readPositiveIntegerParam } from "openclaw/plugin-sdk/channel-actions";
5
- import { ErrorCodes, GatewayClient, callGatewayFromCli, errorShape, startGatewayClientWhenEventLoopReady } from "openclaw/plugin-sdk/gateway-runtime";
2
+ import { n as GOOGLE_MEET_NODE_COMMAND } from "./google-meet-platform-constants-Bs5iAg3E.js";
3
+ import { r as resolveGoogleMeetConfig } from "./config-wBK8loBb.js";
4
+ import { a as createLazyGoogleMeetNodeInvokePolicy, c as loadGoogleMeetCliModule, d as normalizeMode, f as normalizeTransport, h as shouldJoinCreatedMeet, i as createGoogleMeetRuntimeAccessor, l as loadGoogleMeetNodeHostModule, m as sendGoogleMeetGatewayError, n as assertGoogleMeetAgentToolActionSupported, o as formatGoogleMeetGatewayError, p as resolveMeetingInput, r as callGoogleMeetGatewayFromTool, s as keepTrustedToolAgentId, t as asParamRecord, u as loadGoogleMeetPluginHelpers } from "./plugin-registration-Csi-NdQU.js";
6
5
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
7
- import { normalizeAgentId, parseAgentSessionKey } from "openclaw/plugin-sdk/routing";
8
- import { asRecord, normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
6
+ import { normalizeOptionalString } from "openclaw/plugin-sdk/string-coerce-runtime";
9
7
  import { jsonResult } from "openclaw/plugin-sdk/tool-results";
10
- import { createMeetingTranscriptSourceProvider, resolveTranscriptsConfig } from "openclaw/plugin-sdk/transcripts";
11
- import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
12
- import { spawnSync } from "node:child_process";
13
- import { MeetingPlatformAdapter, MeetingSessionRuntime, addMeetingSetupCheck, createLocalMeetingRealtimeAudioTransport, createMeetingBrowserNodeInvokePolicy, createMeetingNodeHost, createMeetingRealtimeEngineBindings, createMeetingSession, createMeetingSetupStatus, createMeetingVoiceCallGateway, createNodeMeetingRealtimeAudioTransport, endMeetingVoiceCallGatewayCall, getMeetingVoiceCallGatewayCall, isMeetingVoiceCallMissingError, joinMeetingViaVoiceCallGateway, leaveMeetingWithBrowser, openMeetingWithBrowser, readMeetingTranscriptWithBrowser, recoverMeetingBrowserTab, resolveLocalMeetingBrowserRequest, speakMeetingViaVoiceCallGateway, startMeetingAgentRealtimeEngine, startMeetingRealtimeEngine } from "openclaw/plugin-sdk/meeting-runtime";
14
- import { addTimerTimeoutGraceMs } from "openclaw/plugin-sdk/number-runtime";
15
- import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
16
- import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
17
- import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
18
- import fs from "node:fs";
19
- import os from "node:os";
20
- import path from "node:path";
8
+ import { optionalPositiveIntegerSchema } from "openclaw/plugin-sdk/channel-actions";
21
9
  import { Type } from "typebox";
22
- //#region extensions/google-meet/src/transports/chrome-audio-device.ts
23
- const GOOGLE_MEET_SYSTEM_PROFILER_COMMAND = "/usr/sbin/system_profiler";
24
- function outputMentionsBlackHole2ch(output) {
25
- return /\bBlackHole\s+2ch\b/i.test(output);
26
- }
27
- //#endregion
28
- //#region extensions/google-meet/src/transports/types.ts
29
- const GOOGLE_MEET_TRANSCRIPT_MAX_LINES = 2e3;
30
- //#endregion
31
- //#region extensions/google-meet/src/transports/google-meet-page-scripts.ts
32
- const GOOGLE_MEET_CAPTION_SETTLE_MS = 1e3;
33
- function meetStatusScript(params) {
34
- return `async () => {
35
- const text = (node) => (node?.innerText || node?.textContent || "").trim();
36
- const manualActionFor = (reason, message) => ({ reason, message });
37
- const allowMicrophone = ${JSON.stringify(params.allowMicrophone)};
38
- const captionSessionId = ${JSON.stringify(params.captionSessionId)};
39
- const captureCaptions = ${JSON.stringify(params.captureCaptions)};
40
- const readOnly = ${JSON.stringify(Boolean(params.readOnly))};
41
- const buttons = [...document.querySelectorAll('button')];
42
- const buttonLabel = (button) =>
43
- [
44
- button.getAttribute("aria-label"),
45
- button.getAttribute("data-tooltip"),
46
- text(button),
47
- ]
48
- .filter(Boolean)
49
- .join(" ");
50
- const buttonLabels = buttons.map(buttonLabel).filter(Boolean);
51
- const notes = [];
52
- let audioOutputRouted;
53
- let audioOutputDeviceLabel;
54
- let audioOutputRouteError;
55
- const findButton = (pattern) =>
56
- buttons.find((button) => {
57
- const label = buttonLabel(button);
58
- return pattern.test(label) && !button.disabled;
59
- });
60
- const findCallControlButton = (pattern) =>
61
- buttons.find((button) => {
62
- const label = buttonLabel(button);
63
- return pattern.test(label) && !/remotely mute|someone else/i.test(label) && !button.disabled;
64
- });
65
- const input = [...document.querySelectorAll('input')].find((el) =>
66
- /your name/i.test(el.getAttribute('aria-label') || el.placeholder || '')
67
- );
68
- if (!readOnly && ${JSON.stringify(params.autoJoin)} && input && !input.value) {
69
- input.focus();
70
- input.value = ${JSON.stringify(params.guestName)};
71
- input.dispatchEvent(new Event('input', { bubbles: true }));
72
- input.dispatchEvent(new Event('change', { bubbles: true }));
73
- }
74
- const pageText = text(document.body).toLowerCase();
75
- const permissionText = [pageText, ...buttonLabels].join("\\n");
76
- const host = location.hostname.toLowerCase();
77
- const pageUrl = location.href;
78
- const permissionNeeded = /permission needed|microphone problem|speaker problem|allow.*(microphone|camera)|blocked.*(microphone|camera)|permission.*(microphone|camera|speaker)/i.test(permissionText);
79
- let mic = findCallControlButton(/^\\s*turn (?:off|on) microphone\\b/i);
80
- if (!mic) {
81
- const callControls = document.querySelector('[role="region"][aria-label="Call controls"]');
82
- mic = [...(callControls?.querySelectorAll('button') || [])].find((button) =>
83
- /^\\s*turn (?:off|on) microphone\\b/i.test(buttonLabel(button))
84
- );
85
- }
86
- if (!readOnly && allowMicrophone && mic && /turn on microphone/i.test(buttonLabel(mic))) {
87
- mic.click();
88
- notes.push("Attempted to turn on the Meet microphone for talk-back mode.");
89
- }
90
- if (!readOnly && !allowMicrophone && mic && /turn off microphone/i.test(mic.getAttribute('aria-label') || text(mic))) {
91
- mic.click();
92
- notes.push("Muted Meet microphone for observe-only mode.");
93
- }
94
- const joinElsewhere = findButton(/join here too/i);
95
- const join = !readOnly && ${JSON.stringify(params.autoJoin)}
96
- ? findButton(/join now|ask to join/i)
97
- : null;
98
- if (join) join.click();
99
- const microphoneChoice = findButton(/\\buse microphone\\b/i);
100
- const noMicrophoneChoice = findButton(/\\b(continue|join|use) without (microphone|mic)\\b|\\bnot now\\b/i);
101
- if (!readOnly && allowMicrophone && microphoneChoice) {
102
- microphoneChoice.click();
103
- notes.push("Accepted Meet microphone prompt with browser automation.");
104
- } else if (!readOnly && !allowMicrophone && noMicrophoneChoice) {
105
- noMicrophoneChoice.click();
106
- notes.push("Skipped Meet microphone prompt for observe-only mode.");
107
- }
108
- const inCall = buttons.some((button) => /leave call/i.test(button.getAttribute('aria-label') || text(button)));
109
- const routeMeetAudioOutput = async () => {
110
- if (
111
- !allowMicrophone ||
112
- typeof navigator === 'undefined' ||
113
- !navigator.mediaDevices?.enumerateDevices
114
- ) return;
115
- const mediaElements = [...document.querySelectorAll('audio, video')]
116
- .filter((el) => typeof el.setSinkId === 'function');
117
- if (mediaElements.length === 0) return;
118
- try {
119
- const devices = await navigator.mediaDevices.enumerateDevices();
120
- const output = devices.find((device) =>
121
- device.kind === 'audiooutput' && /\\bBlackHole\\s+2ch\\b/i.test(device.label || '')
122
- ) || devices.find((device) =>
123
- device.kind === 'audiooutput' && /\\bBlackHole\\b/i.test(device.label || '')
124
- );
125
- if (!output?.deviceId) {
126
- if (devices.some((device) => device.kind === 'audiooutput')) {
127
- notes.push("BlackHole 2ch speaker output was not visible to Meet.");
128
- }
129
- return;
130
- }
131
- let routed = 0;
132
- for (const element of mediaElements) {
133
- if (element.sinkId !== output.deviceId) {
134
- if (readOnly) {
135
- continue;
136
- }
137
- await element.setSinkId(output.deviceId);
138
- routed += 1;
139
- }
140
- }
141
- audioOutputRouted = mediaElements.some((element) => element.sinkId === output.deviceId);
142
- audioOutputDeviceLabel = output.label || "BlackHole 2ch";
143
- if (!readOnly && audioOutputRouted) {
144
- notes.push(
145
- routed > 0
146
- ? \`Routed Meet media output to \${audioOutputDeviceLabel}.\`
147
- : \`Meet media output already routed to \${audioOutputDeviceLabel}.\`
148
- );
149
- }
150
- } catch (error) {
151
- audioOutputRouteError = error?.message || String(error);
152
- notes.push(\`Could not route Meet speaker output to BlackHole 2ch: \${audioOutputRouteError}\`);
153
- }
154
- };
155
- if (inCall) {
156
- await routeMeetAudioOutput();
157
- }
158
- let captioning = false;
159
- let captionsEnabledAttempted = false;
160
- let transcriptLines = 0;
161
- let lastCaptionAt;
162
- let lastCaptionSpeaker;
163
- let lastCaptionText;
164
- let recentTranscript = [];
165
- const captionSelector = '[role="region"][aria-label*="aption" i], [aria-live="polite"][role="region"], div[aria-live="polite"]';
166
- const captionState = (() => {
167
- if (!captureCaptions) return undefined;
168
- const w = window;
169
- if (!inCall && !w.__openclawMeetCaptions) return undefined;
170
- // A reused tab starts a fresh logical transcript for each OpenClaw session.
171
- // Status refreshes omit the id, so they preserve the active page-owned buffer.
172
- if (!w.__openclawMeetCaptions || (captionSessionId && w.__openclawMeetCaptions.sessionId !== captionSessionId)) {
173
- if (w.__openclawMeetCaptions?.settleTimer !== undefined) {
174
- clearTimeout(w.__openclawMeetCaptions.settleTimer);
175
- }
176
- w.__openclawMeetCaptions?.observer?.disconnect?.();
177
- w.__openclawMeetCaptions = {
178
- sessionId: captionSessionId,
179
- // Epochs cross document lifetimes in the runtime transcript cursor.
180
- // Strong UUIDs keep a reloaded page distinct from its prior buffer.
181
- epoch: crypto.randomUUID(),
182
- enabledAttempted: false,
183
- observerInstalled: false,
184
- observer: undefined,
185
- droppedLines: 0,
186
- lines: [],
187
- settleTimer: undefined,
188
- visible: []
189
- };
190
- }
191
- return w.__openclawMeetCaptions;
192
- })();
193
- const normalizeCaption = (speaker, captionText) => {
194
- if (!captionState) return;
195
- const clean = String(captionText || "").replace(/\\s+/g, " ").trim();
196
- const cleanSpeaker = String(speaker || "").replace(/\\s+/g, " ").trim();
197
- if (!clean || clean.length < 2) return undefined;
198
- if (/^(turn on captions|turn off captions|captions)$/i.test(clean)) return undefined;
199
- return { speaker: cleanSpeaker || undefined, text: clean };
200
- };
201
- const commitLines = (state, entries) => {
202
- state.lines.push(...entries.map((entry) => ({
203
- at: entry.at,
204
- speaker: entry.speaker,
205
- text: entry.text
206
- })));
207
- const excess = state.lines.length - ${GOOGLE_MEET_TRANSCRIPT_MAX_LINES};
208
- if (excess > 0) {
209
- state.lines.splice(0, excess);
210
- state.droppedLines = (state.droppedLines || 0) + excess;
211
- }
212
- };
213
- const scrapeCaptions = () => {
214
- if (!captionState) return;
215
- const regions = [...document.querySelectorAll(captionSelector)];
216
- const rows = [];
217
- for (const region of regions) {
218
- const raw = text(region);
219
- if (!raw) continue;
220
- const pieces = raw.split(/\\n+/).map((part) => part.trim()).filter(Boolean);
221
- const row = pieces.length >= 2
222
- ? normalizeCaption(pieces[0], pieces.slice(1).join(" "))
223
- : normalizeCaption("", pieces[0] || raw);
224
- if (row) rows.push({ ...row, node: region });
225
- }
226
- if (rows.length === 0) {
227
- // Meet briefly removes caption rows while rerendering. Keep them mutable
228
- // for one settle window so a DOM gap cannot fabricate a repeated line.
229
- if (captionState.visible.length > 0 && captionState.settleTimer === undefined) {
230
- const pendingState = captionState;
231
- pendingState.settleTimer = setTimeout(() => {
232
- if (window.__openclawMeetCaptions !== pendingState) return;
233
- commitLines(pendingState, pendingState.visible);
234
- pendingState.visible = [];
235
- pendingState.settleTimer = undefined;
236
- }, ${GOOGLE_MEET_CAPTION_SETTLE_MS});
237
- }
238
- return;
239
- }
240
- if (captionState.settleTimer !== undefined) {
241
- clearTimeout(captionState.settleTimer);
242
- captionState.settleTimer = undefined;
243
- }
244
- const previous = Array.isArray(captionState.visible) ? captionState.visible : [];
245
- const unmatchedPrevious = [...previous];
246
- const nextVisible = [];
247
- const now = Date.now();
248
- for (let index = 0; index < rows.length; index += 1) {
249
- const row = rows[index];
250
- const priorIndex = unmatchedPrevious.findIndex((candidate) => {
251
- const sameTextLifecycle =
252
- candidate.text === row.text ||
253
- row.text.startsWith(candidate.text) ||
254
- candidate.text.startsWith(row.text);
255
- const sameDomLifecycle =
256
- candidate.node === row.node || now - candidate.seenAt <= ${GOOGLE_MEET_CAPTION_SETTLE_MS};
257
- return candidate.speaker === row.speaker && sameTextLifecycle && sameDomLifecycle;
258
- });
259
- const prior = priorIndex >= 0 ? unmatchedPrevious.splice(priorIndex, 1)[0] : undefined;
260
- const sameSpeaker = Boolean(prior) && prior.speaker === row.speaker;
261
- if (sameSpeaker && prior.text === row.text) {
262
- prior.node = row.node;
263
- prior.seenAt = now;
264
- nextVisible.push(prior);
265
- continue;
266
- }
267
- if (sameSpeaker && row.text.startsWith(prior.text)) {
268
- prior.text = row.text;
269
- prior.node = row.node;
270
- prior.seenAt = now;
271
- nextVisible.push(prior);
272
- continue;
273
- }
274
- if (sameSpeaker && prior.text.startsWith(row.text)) {
275
- prior.node = row.node;
276
- prior.seenAt = now;
277
- nextVisible.push(prior);
278
- continue;
279
- }
280
- const entry = {
281
- at: new Date().toISOString(),
282
- node: row.node,
283
- seenAt: now,
284
- speaker: row.speaker,
285
- text: row.text
286
- };
287
- nextVisible.push(entry);
288
- }
289
- commitLines(captionState, unmatchedPrevious);
290
- captionState.visible = nextVisible;
291
- };
292
- if (captionState) {
293
- if (!readOnly && inCall && !captionState.enabledAttempted) {
294
- const captionButton = findButton(/turn on captions|show captions|captions/i);
295
- const captionLabel = captionButton ? (captionButton.getAttribute("aria-label") || captionButton.getAttribute("data-tooltip") || text(captionButton)) : "";
296
- if (captionButton) {
297
- captionState.enabledAttempted = true;
298
- captionsEnabledAttempted = true;
299
- if (!/turn off captions|hide captions/i.test(captionLabel)) {
300
- captionButton.click();
301
- notes.push("Attempted to enable Meet captions for observe-only transcript health.");
302
- }
303
- }
304
- } else if (captionState.enabledAttempted) {
305
- captionsEnabledAttempted = true;
306
- }
307
- if (inCall && !captionState.observerInstalled) {
308
- captionState.observerInstalled = true;
309
- captionState.observer = new MutationObserver(scrapeCaptions);
310
- captionState.observer.observe(document.body, {
311
- childList: true,
312
- subtree: true,
313
- characterData: true
314
- });
315
- notes.push("Installed Meet caption observer for observe-only transcript health.");
316
- }
317
- if (inCall) {
318
- scrapeCaptions();
319
- }
320
- const committedLines = Array.isArray(captionState.lines) ? captionState.lines : [];
321
- const visibleLines = Array.isArray(captionState.visible) ? captionState.visible : [];
322
- const lines = [...committedLines, ...visibleLines];
323
- const last = lines[lines.length - 1];
324
- captioning = document.querySelector(captionSelector) !== null || lines.length > 0;
325
- transcriptLines = (captionState.droppedLines || 0) + lines.length;
326
- lastCaptionAt = last?.at;
327
- lastCaptionSpeaker = last?.speaker;
328
- lastCaptionText = last?.text;
329
- recentTranscript = lines.slice(-5);
330
- }
331
- const lobbyWaiting = !inCall && /asking to be let in|you.?ll join when someone lets you in|waiting to be let in|ask to join/i.test(pageText);
332
- const leaveReason = !inCall && /you left the meeting|you.?ve left the meeting|removed from the meeting|you were removed|call ended|meeting ended/i.test(pageText)
333
- ? pageText.match(/you left the meeting|you.?ve left the meeting|removed from the meeting|you were removed|call ended|meeting ended/i)?.[0]
334
- : undefined;
335
- let manualAction;
336
- if (!inCall && (host === "accounts.google.com" || /use your google account|to continue to google meet|choose an account|sign in to (join|continue)/i.test(pageText))) {
337
- manualAction = manualActionFor("google-login-required", "Sign in to Google in the OpenClaw browser profile, then retry the Meet join.");
338
- } else if (!inCall && joinElsewhere) {
339
- manualAction = manualActionFor("meet-session-conflict", "Meet is already active in another tab or device. Leave that session or reuse an English-pinned tab before retrying.");
340
- } else if (!inCall && /asking to be let in|you.?ll join when someone lets you in|waiting to be let in|ask to join/i.test(pageText)) {
341
- manualAction = manualActionFor("meet-admission-required", "Admit the OpenClaw browser participant in Google Meet, then retry speech.");
342
- } else if (permissionNeeded) {
343
- manualAction = manualActionFor("meet-permission-required", allowMicrophone ? "Allow microphone/camera/speaker permissions for Meet in the OpenClaw browser profile, then retry." : "Join without microphone/camera permissions in the OpenClaw browser profile, then retry.");
344
- } else if (!inCall && (allowMicrophone ? !microphoneChoice : !noMicrophoneChoice) && /do you want people to hear you in the meeting/i.test(pageText)) {
345
- manualAction = manualActionFor("meet-audio-choice-required", allowMicrophone ? "Meet is showing the microphone choice. Click Use microphone in the OpenClaw browser profile, then retry." : "Meet is showing the microphone choice. Choose the no-microphone option in the OpenClaw browser profile, then retry.");
346
- }
347
- return JSON.stringify({
348
- clickedJoin: Boolean(join),
349
- clickedMicrophoneChoice: Boolean(allowMicrophone && microphoneChoice),
350
- inCall,
351
- micMuted: mic ? /turn on microphone/i.test(buttonLabel(mic)) : undefined,
352
- lobbyWaiting,
353
- leaveReason,
354
- captioning,
355
- captionsEnabledAttempted,
356
- transcriptLines,
357
- lastCaptionAt,
358
- lastCaptionSpeaker,
359
- lastCaptionText,
360
- recentTranscript,
361
- audioOutputRouted,
362
- audioOutputDeviceLabel,
363
- audioOutputRouteError,
364
- manualAction,
365
- title: document.title,
366
- url: pageUrl,
367
- notes
368
- });
369
- }`;
370
- }
371
- function meetTranscriptScript(meetingUrl, meetingSessionId, finalize) {
372
- const expectedMeetingUrl = normalizeMeetUrlForReuse(meetingUrl);
373
- return `() => {
374
- const expectedMeetingUrl = ${JSON.stringify(expectedMeetingUrl)};
375
- const expectedSessionId = ${JSON.stringify(meetingSessionId)};
376
- let currentMeetingUrl;
377
- try {
378
- const currentUrl = new URL(location.href);
379
- currentMeetingUrl = currentUrl.origin + currentUrl.pathname.toLowerCase().replace(/\\/$/, "");
380
- } catch {
381
- return JSON.stringify({ urlMatched: false });
382
- }
383
- if (!expectedMeetingUrl || currentMeetingUrl !== expectedMeetingUrl) {
384
- return JSON.stringify({ urlMatched: false });
385
- }
386
- const state = window.__openclawMeetCaptions;
387
- if (state?.sessionId && state.sessionId !== expectedSessionId) {
388
- return JSON.stringify({ urlMatched: true, sessionMatched: false });
389
- }
390
- if (${JSON.stringify(finalize)} && Array.isArray(state?.visible) && state.visible.length > 0) {
391
- if (state.settleTimer !== undefined) clearTimeout(state.settleTimer);
392
- state.settleTimer = undefined;
393
- state.lines = Array.isArray(state.lines) ? state.lines : [];
394
- state.lines.push(...state.visible.map((entry) => ({
395
- at: entry.at,
396
- speaker: entry.speaker,
397
- text: entry.text
398
- })));
399
- state.visible = [];
400
- const excess = state.lines.length - ${GOOGLE_MEET_TRANSCRIPT_MAX_LINES};
401
- if (excess > 0) {
402
- state.lines.splice(0, excess);
403
- state.droppedLines = (state.droppedLines || 0) + excess;
404
- }
405
- }
406
- const lines = Array.isArray(state?.lines) ? state.lines : [];
407
- return JSON.stringify({
408
- urlMatched: true,
409
- sessionMatched: true,
410
- epoch: typeof state?.epoch === "string" ? state.epoch : undefined,
411
- droppedLines: Number.isFinite(state?.droppedLines) ? Math.max(0, Math.trunc(state.droppedLines)) : 0,
412
- lines: lines.map((line) => ({
413
- at: typeof line?.at === "string" ? line.at : undefined,
414
- speaker: typeof line?.speaker === "string" ? line.speaker : undefined,
415
- text: typeof line?.text === "string" ? line.text : ""
416
- })).filter((line) => line.text)
417
- });
418
- }`;
419
- }
420
- function meetLeaveScript(meetingUrl) {
421
- const expectedMeetingUrl = normalizeMeetUrlForReuse(meetingUrl);
422
- return `() => {
423
- const expectedMeetingUrl = ${JSON.stringify(expectedMeetingUrl)};
424
- let currentMeetingUrl;
425
- try {
426
- const currentUrl = new URL(location.href);
427
- currentMeetingUrl = currentUrl.origin + currentUrl.pathname.toLowerCase().replace(/\\/$/, "");
428
- } catch {
429
- return JSON.stringify({ departed: false });
430
- }
431
- if (!expectedMeetingUrl) {
432
- return JSON.stringify({ departed: false });
433
- }
434
- if (currentMeetingUrl !== expectedMeetingUrl) {
435
- return JSON.stringify({ departed: true, urlMatched: false });
436
- }
437
- const text = (node) => (node?.innerText || node?.textContent || "").trim();
438
- // Locale-independent fallback: Meet renders the leave control as a Material
439
- // Symbols icon whose ligature text is "call_end" in every UI language, so a
440
- // localized aria-label (e.g. "Anruf verlassen") still resolves to the button.
441
- const hasLeaveIcon = (button) => {
442
- const icon = button.querySelector ? button.querySelector("i") : null;
443
- return icon ? (icon.textContent || "").trim() === "call_end" : false;
444
- };
445
- const buttons = [...document.querySelectorAll('button')];
446
- const label = (button) => [
447
- button.getAttribute("aria-label"),
448
- button.getAttribute("data-tooltip"),
449
- text(button),
450
- ]
451
- .filter(Boolean)
452
- .join(" ");
453
- const postCall = buttons.some((button) => /\\b(rejoin|return to home screen)\\b/i.test(label(button)));
454
- if (postCall) {
455
- return JSON.stringify({ departed: true, urlMatched: true });
456
- }
457
- // Managed join tabs are reused only after the English-tab gate or opened
458
- // through the English-UI helper, so follow-up labels are pinned to English.
459
- const confirmation = buttons.find((button) => {
460
- return !button.disabled && /\\bleave meeting\\b/i.test(label(button));
461
- });
462
- if (confirmation) {
463
- confirmation.click();
464
- return JSON.stringify({ departed: false, leaveAction: "confirm", urlMatched: true });
465
- }
466
- const leave = buttons.find((button) => {
467
- if (button.disabled) return false;
468
- return /leave call/i.test(label(button)) || hasLeaveIcon(button);
469
- });
470
- if (leave) {
471
- leave.click();
472
- return JSON.stringify({ departed: false, leaveAction: "leave", urlMatched: true });
473
- }
474
- return JSON.stringify({ departed: false, urlMatched: true });
475
- }`;
476
- }
477
- //#endregion
478
- //#region extensions/google-meet/src/transports/twilio.ts
479
- const DTMF_PATTERN = /^[0-9*#wWpP,]+$/;
480
- function normalizeDialInNumber(value) {
481
- const normalized = normalizeOptionalString(value);
482
- if (!normalized) return;
483
- const compact = normalized.replace(/[()\s.-]/g, "");
484
- if (!/^\+?[0-9]{5,20}$/.test(compact)) throw new Error("dialInNumber must be a phone number");
485
- return compact;
486
- }
487
- function normalizeDtmfSequence(value) {
488
- const normalized = normalizeOptionalString(value);
489
- if (!normalized) return;
490
- const compact = normalized.replace(/\s+/g, "");
491
- if (!DTMF_PATTERN.test(compact)) throw new Error("dtmfSequence may only contain digits, *, #, comma, w, p");
492
- return compact;
493
- }
494
- function buildMeetDtmfSequence(params) {
495
- const explicit = normalizeDtmfSequence(params.dtmfSequence);
496
- if (explicit) return explicit;
497
- const pin = normalizeOptionalString(params.pin);
498
- if (!pin) return;
499
- const compactPin = pin.replace(/\s+/g, "");
500
- if (!/^[0-9]+#?$/.test(compactPin)) throw new Error("pin may only contain digits and an optional trailing #");
501
- return compactPin.endsWith("#") ? compactPin : `${compactPin}#`;
502
- }
503
- function prefixDtmfWait(sequence, delayMs) {
504
- if (!sequence || delayMs <= 0) return sequence;
505
- const waitCount = Math.ceil(delayMs / 500);
506
- if (waitCount <= 0) return sequence;
507
- return `${"w".repeat(waitCount)}${sequence}`;
508
- }
509
- //#endregion
510
- //#region extensions/google-meet/src/transports/google-meet-platform-adapter.ts
511
- function parsePermissionGrantNotes(result) {
512
- const record = result && typeof result === "object" ? result : {};
513
- const unsupportedPermissions = Array.isArray(record.unsupportedPermissions) ? record.unsupportedPermissions.filter((value) => typeof value === "string") : [];
514
- const notes = ["Granted Meet microphone/camera permissions through browser control."];
515
- if (unsupportedPermissions.includes("speakerSelection")) notes.push("Chrome did not accept the optional Meet speaker-selection permission.");
516
- return notes;
517
- }
518
- const manualActionCategories = /* @__PURE__ */ new Map([
519
- ["browser-control-unavailable", "browser-control-unavailable"],
520
- ["google-login-required", "login-required"],
521
- ["meet-admission-required", "admission-required"],
522
- ["meet-audio-choice-required", "audio-choice-required"],
523
- ["meet-locale-required", "locale-required"],
524
- ["meet-permission-required", "permission-required"],
525
- ["meet-session-conflict", "session-conflict"]
526
- ]);
527
- function classifyManualActionReason(reason) {
528
- return manualActionCategories.get(reason) ?? "custom";
529
- }
530
- const GOOGLE_MEET_PLATFORM_ADAPTER = MeetingPlatformAdapter.create({
531
- id: "google-meet",
532
- displayName: "Google Meet",
533
- browserLabel: "Meet",
534
- logScope: "[google-meet]",
535
- agentConsult: {
536
- surface: "a private Google Meet",
537
- userLabel: "Participant",
538
- assistantLabel: "Agent",
539
- questionSourceLabel: "participant",
540
- workingResponseLabel: "participant",
541
- extraSystemPrompt: [
542
- "You are a behind-the-scenes consultant for a live meeting voice agent.",
543
- "Prioritize a fast, speakable answer over exhaustive investigation.",
544
- "For tool-backed status checks, prefer one or two bounded read-only queries before answering.",
545
- "Do not print secret values or dump environment variables; only check whether required configuration is present.",
546
- "Be accurate, brief, and speakable."
547
- ].join(" ")
548
- },
549
- session: {
550
- idPrefix: "meet",
551
- participantIdentity: (transport) => transport === "twilio" ? "Twilio phone participant" : transport === "chrome-node" ? "signed-in Google Chrome profile on a paired node" : "signed-in Google Chrome profile"
552
- },
553
- nodeCommandName: GOOGLE_MEET_NODE_COMMAND,
554
- nodeConfigPath: "plugins.entries.google-meet.config.chromeNode.node",
555
- urls: {
556
- validateAndNormalize: normalizeMeetUrl,
557
- normalizeForReuse: normalizeMeetUrlForReuse,
558
- isSameMeeting: isSameMeetUrlForReuse,
559
- buildJoinUrl: (session) => forceMeetEnglishUi(session.url),
560
- accountHint: readMeetAuthUser,
561
- isPreferredJoinUrl: isEnglishMeetTab,
562
- isRecoverableTab: isRecoverableMeetTab,
563
- localeAction: (tab) => {
564
- if (!normalizeMeetUrlForReuse(tab.url) || isEnglishMeetTab(tab.url)) return;
565
- return {
566
- category: "locale-required",
567
- reason: "meet-locale-required",
568
- message: "The existing Meet tab is not pinned to English. Open the meeting with ?hl=en, then retry recovery."
569
- };
570
- }
571
- },
572
- browser: {
573
- allowsMicrophone: MeetingPlatformAdapter.isTalkBackMode,
574
- buildStatusJoinScript: (params) => meetStatusScript({
575
- allowMicrophone: MeetingPlatformAdapter.isTalkBackMode(params.mode),
576
- autoJoin: params.autoJoin,
577
- captionSessionId: params.meetingSessionId || void 0,
578
- captureCaptions: params.captureCaptions,
579
- guestName: params.guestName,
580
- readOnly: params.readOnly
581
- }),
582
- browserControlUnavailable: () => ({
583
- category: "browser-control-unavailable",
584
- reason: "browser-control-unavailable",
585
- message: "Open the OpenClaw browser profile, finish Google Meet login, admission, or permission prompts, then retry."
586
- }),
587
- buildLeaveScript: meetLeaveScript,
588
- captions: {
589
- enabled: (_mode) => true,
590
- buildTranscriptScript: ({ finalize, meetingSessionId, meetingUrl }) => meetTranscriptScript(meetingUrl, meetingSessionId, finalize)
591
- },
592
- permissions: ({ allowMicrophone }) => allowMicrophone ? {
593
- origin: "https://meet.google.com",
594
- permissions: ["audioCapture", "videoCapture"],
595
- optionalPermissions: ["speakerSelection"]
596
- } : void 0,
597
- permissionNotes: ({ allowMicrophone, error, result }) => {
598
- if (!allowMicrophone) return ["Observe-only mode skips Meet microphone/camera permission grants."];
599
- if (error) return [`Could not grant Meet media permissions automatically: ${formatErrorMessage(error)}`];
600
- return parsePermissionGrantNotes(result);
601
- }
602
- },
603
- create: { browser: createMeetWithBrowserProxyOnNode },
604
- dialIn: { buildPlan: (params) => {
605
- const number = normalizeDialInNumber(params.dialInNumber ?? params.defaultDialInNumber);
606
- const pin = params.pin ?? params.defaultPin;
607
- const rawDtmfSequence = buildMeetDtmfSequence({
608
- pin,
609
- dtmfSequence: params.dtmfSequence ?? params.defaultDtmfSequence
610
- });
611
- return {
612
- number,
613
- pin,
614
- dtmfSequence: params.dtmfSequence || params.defaultDtmfSequence ? rawDtmfSequence : prefixDtmfWait(rawDtmfSequence, params.dtmfDelayMs)
615
- };
616
- } },
617
- parsing: {
618
- classifyManualActionReason,
619
- displayName: "Meet",
620
- invalidTranscriptMessage: "Google Meet transcript payload is invalid.",
621
- malformedStatusMessage: "Google Meet browser status JSON is malformed.",
622
- malformedTranscriptMessage: "Google Meet transcript JSON is malformed.",
623
- statusFields: (parsed) => ({ leaveReason: typeof parsed.leaveReason === "string" ? parsed.leaveReason : void 0 })
624
- }
625
- });
626
- //#endregion
627
- //#region extensions/google-meet/src/node-host.ts
628
- function assertBlackHoleAvailable(timeoutMs) {
629
- if (process.platform !== "darwin") throw new Error("Chrome Meet transport with blackhole-2ch audio is currently macOS-only");
630
- const result = spawnSync(GOOGLE_MEET_SYSTEM_PROFILER_COMMAND, ["SPAudioDataType"], {
631
- encoding: "utf8",
632
- timeout: timeoutMs
633
- });
634
- const stderr = result.stderr ?? (result.error ? result.error instanceof Error ? result.error.message : String(result.error) : "");
635
- const output = `${result.stdout ?? ""}\n${stderr}`;
636
- if ((typeof result.status === "number" ? result.status : result.error ? 1 : 0) !== 0 || !outputMentionsBlackHole2ch(output)) throw new Error("BlackHole 2ch audio device not found on the node.");
637
- }
638
- function normalizeMeetKey(value) {
639
- if (!value) return;
640
- try {
641
- const url = new URL(value);
642
- if (url.hostname.toLowerCase() !== "meet.google.com") return value;
643
- return /^\/([a-z]{3}-[a-z]{4}-[a-z]{3})(?:$|[/?#])/i.exec(url.pathname)?.[1]?.toLowerCase() ?? value;
644
- } catch {
645
- return value;
646
- }
647
- }
648
- const googleMeetNodeHost = createMeetingNodeHost({
649
- commandName: GOOGLE_MEET_NODE_COMMAND,
650
- displayName: "Google Meet",
651
- browserLabel: "Meet",
652
- bridgeIdPrefix: "meet_node_",
653
- defaultAudioInputCommand: DEFAULT_GOOGLE_MEET_AUDIO_INPUT_COMMAND,
654
- defaultAudioOutputCommand: DEFAULT_GOOGLE_MEET_AUDIO_OUTPUT_COMMAND,
655
- talkBackModes: /* @__PURE__ */ new Set([
656
- "agent",
657
- "bidi",
658
- "realtime"
659
- ]),
660
- agentMode: "agent",
661
- normalizeUrl: (url) => GOOGLE_MEET_PLATFORM_ADAPTER.urls.validateAndNormalize(url),
662
- normalizeMeetingKey: normalizeMeetKey,
663
- assertAudioAvailable: assertBlackHoleAvailable,
664
- browser: {
665
- application: "Google Chrome",
666
- buildProfileArgs: (profile) => ["--args", `--profile-directory=${profile}`],
667
- openedStatus: "chrome-opened",
668
- openedNotes: ["Browser page control is handled by OpenClaw browser automation when using chrome-node."]
669
- }
670
- });
671
- async function handleGoogleMeetNodeHostCommand(paramsJSON) {
672
- return await googleMeetNodeHost.handleCommand(paramsJSON);
673
- }
674
- //#endregion
675
- //#region extensions/google-meet/src/node-invoke-policy.ts
676
- const GOOGLE_MEET_CHROME_NODE_COMMAND = GOOGLE_MEET_NODE_COMMAND;
677
- const START_MODES = /* @__PURE__ */ new Set([
678
- "agent",
679
- "bidi",
680
- "realtime",
681
- "transcribe"
682
- ]);
683
- function createGoogleMeetChromeNodeInvokePolicy(config) {
684
- return createMeetingBrowserNodeInvokePolicy({
685
- commandName: GOOGLE_MEET_CHROME_NODE_COMMAND,
686
- displayName: "Google Meet",
687
- deniedCode: "GOOGLE_MEET_NODE_POLICY_DENIED",
688
- supportedModes: START_MODES,
689
- normalizeUrl: (url) => GOOGLE_MEET_PLATFORM_ADAPTER.urls.validateAndNormalize(url),
690
- start: config.chrome
691
- });
692
- }
693
- //#endregion
694
- //#region extensions/google-meet/src/runtime-probes.ts
695
- function resolveProbeTimeoutMs(input, fallback) {
696
- if (input === void 0) return Math.min(Math.max(fallback, 1), 12e4);
697
- if (!Number.isFinite(input) || input <= 0) throw new Error("timeoutMs must be a positive number");
698
- return Math.min(Math.trunc(input), 12e4);
699
- }
700
- const probes = MeetingPlatformAdapter.createRuntimeProbes({
701
- defaultSpeechMessage: "Say exactly: Google Meet speech test complete.",
702
- invalidRequest: (message) => new Error(message),
703
- resolveTimeoutMs: resolveProbeTimeoutMs,
704
- shouldWaitForListening: (session) => Boolean((session.transport === "chrome" || session.transport === "chrome-node") && session.chrome?.launched),
705
- talkBackMode: MeetingPlatformAdapter.isTalkBackMode,
706
- normalizeUrl: normalizeMeetUrl,
707
- resolveRequestMode: (mode) => mode === "realtime" ? "agent" : mode,
708
- defaultTransport: (config) => config.defaultTransport,
709
- validateListeningTransport: (transport) => {
710
- if (transport === "twilio") throw new Error("test_listen supports chrome or chrome-node transports");
711
- },
712
- resolveSpeechTimeoutMs: (_request, config) => Math.min(config.chrome.joinTimeoutMs, 5e3),
713
- refreshCaptionHealth: async (context, session) => await context.refreshCaptionHealth(session),
714
- speechModeError: "test_speech requires mode: agent or bidi; use join mode: transcribe for observe-only sessions.",
715
- listeningModeError: "test_listen requires mode: transcribe; use test_speech for talk-back sessions."
716
- });
717
- const testGoogleMeetListening = probes.testListening;
718
- const testGoogleMeetSpeech = probes.testSpeech;
719
- //#endregion
720
- //#region extensions/google-meet/src/runtime-session.ts
721
- function resolveTransport(input, config) {
722
- return input ?? config.defaultTransport;
723
- }
724
- function resolveMode(input, config) {
725
- return input === "realtime" ? "agent" : input ?? config.defaultMode;
726
- }
727
- function withSessionAgentConfig(config, agentId) {
728
- return config.realtime.agentId === agentId ? config : {
729
- ...config,
730
- realtime: {
731
- ...config.realtime,
732
- agentId
733
- }
734
- };
735
- }
736
- function isBrowserTransport(transport) {
737
- return transport === "chrome" || transport === "chrome-node";
738
- }
739
- function noteSession(session, note) {
740
- session.notes = [...session.notes.filter((item) => item !== note), note];
741
- }
742
- //#endregion
743
- //#region extensions/google-meet/src/setup.ts
744
- function resolveUserPath(input) {
745
- if (input === "~") return os.homedir();
746
- if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
747
- return input;
748
- }
749
- function isProviderUnreachableWebhookUrl(webhookUrl) {
750
- try {
751
- return isBlockedHostnameOrIp(new URL(webhookUrl).hostname);
752
- } catch {
753
- return false;
754
- }
755
- }
756
- function resolveVoiceCallSetupValue(configured, fallback) {
757
- return normalizeOptionalString(configured) ?? normalizeOptionalString(fallback);
758
- }
759
- function getVoiceCallWebhookExposureCheck(voiceCallConfig) {
760
- const publicUrl = normalizeOptionalString(voiceCallConfig.publicUrl);
761
- const tunnel = asRecord(voiceCallConfig.tunnel);
762
- const tailscale = asRecord(voiceCallConfig.tailscale);
763
- const tunnelProvider = normalizeOptionalString(tunnel.provider);
764
- const tailscaleMode = normalizeOptionalString(tailscale.mode);
765
- if (publicUrl) {
766
- const ok = !isProviderUnreachableWebhookUrl(publicUrl);
767
- return {
768
- id: "twilio-voice-call-webhook",
769
- ok,
770
- message: ok ? `Voice-call public webhook URL configured: ${publicUrl}` : `Voice-call publicUrl is local/private and cannot be reached by Twilio: ${publicUrl}`
771
- };
772
- }
773
- if (tunnelProvider && tunnelProvider !== "none") return {
774
- id: "twilio-voice-call-webhook",
775
- ok: true,
776
- message: "Voice-call webhook exposure configured through tunnel"
777
- };
778
- if (tailscaleMode && tailscaleMode !== "off") return {
779
- id: "twilio-voice-call-webhook",
780
- ok: true,
781
- message: "Voice-call webhook exposure configured through Tailscale"
782
- };
783
- return {
784
- id: "twilio-voice-call-webhook",
785
- ok: false,
786
- message: "Set plugins.entries.voice-call.config.publicUrl or configure voice-call tunnel/tailscale exposure for Twilio dialing"
787
- };
788
- }
789
- function getGoogleMeetSetupStatus(config, options) {
790
- const checks = [];
791
- const env = options?.env ?? process.env;
792
- const fullConfig = asRecord(options?.fullConfig);
793
- const mode = options?.mode ?? config.defaultMode;
794
- const transport = options?.transport ?? config.defaultTransport;
795
- const needsChromeRealtimeAudio = MeetingPlatformAdapter.isTalkBackMode(mode) && (transport === "chrome" || transport === "chrome-node");
796
- const pluginEntries = asRecord(asRecord(fullConfig.plugins).entries);
797
- const pluginAllow = asRecord(fullConfig.plugins).allow;
798
- const voiceCallEntry = asRecord(pluginEntries["voice-call"]);
799
- const voiceCallConfig = asRecord(voiceCallEntry.config);
800
- const voiceCallTwilioConfig = asRecord(voiceCallConfig.twilio);
801
- if (config.auth.tokenPath) {
802
- const tokenPath = resolveUserPath(config.auth.tokenPath);
803
- checks.push({
804
- id: "google-oauth-token",
805
- ok: fs.existsSync(tokenPath),
806
- message: fs.existsSync(tokenPath) ? "Google OAuth token file found" : `Google OAuth token file missing at ${config.auth.tokenPath}`
807
- });
808
- } else checks.push({
809
- id: "google-oauth-token",
810
- ok: true,
811
- message: "Google OAuth token path not configured; Chrome profile auth will be used"
812
- });
813
- checks.push({
814
- id: "chrome-profile",
815
- ok: true,
816
- message: config.chrome.browserProfile ? "Local Chrome uses the OpenClaw browser profile; chrome.browserProfile is passed to chrome-node hosts" : "Local Chrome uses the OpenClaw browser profile; configure browser.defaultProfile to choose another profile"
817
- });
818
- if (needsChromeRealtimeAudio) {
819
- const hasCommandPair = Boolean(config.chrome.audioInputCommand && config.chrome.audioOutputCommand);
820
- const hasExternalBridge = Boolean(config.chrome.audioBridgeCommand);
821
- const agentModeExternalBridgeInvalid = mode === "agent" && hasExternalBridge;
822
- checks.push({
823
- id: "audio-bridge",
824
- ok: mode === "agent" ? hasCommandPair && !agentModeExternalBridgeInvalid : hasExternalBridge || hasCommandPair,
825
- message: agentModeExternalBridgeInvalid ? "Chrome agent mode requires chrome.audioInputCommand and chrome.audioOutputCommand; chrome.audioBridgeCommand is bidi-only" : hasExternalBridge ? "Chrome audio bridge command configured" : hasCommandPair ? `Chrome command-pair talk-back audio bridge configured (${config.chrome.audioFormat})` : "Chrome talk-back audio bridge not configured"
826
- });
827
- } else if (transport === "chrome" || transport === "chrome-node") checks.push({
828
- id: "audio-bridge",
829
- ok: true,
830
- message: "Chrome observe-only mode does not require a realtime audio bridge"
831
- });
832
- checks.push({
833
- id: "guest-join-defaults",
834
- ok: Boolean(config.chrome.guestName && config.chrome.autoJoin && config.chrome.reuseExistingTab),
835
- message: config.chrome.guestName && config.chrome.autoJoin && config.chrome.reuseExistingTab ? "Guest auto-join and tab reuse defaults are enabled" : "Set chrome.guestName, chrome.autoJoin, and chrome.reuseExistingTab for unattended guest joins"
836
- });
837
- checks.push({
838
- id: "chrome-node-target",
839
- ok: config.defaultTransport !== "chrome-node" || Boolean(config.chromeNode.node),
840
- message: config.defaultTransport === "chrome-node" && !config.chromeNode.node ? "chrome-node default should pin chromeNode.node when multiple nodes may be connected" : config.chromeNode.node ? `Chrome node pinned to ${config.chromeNode.node}` : "Chrome node not pinned; automatic selection works when exactly one capable node is connected"
841
- });
842
- if (needsChromeRealtimeAudio) checks.push({
843
- id: "intro-after-in-call",
844
- ok: config.chrome.waitForInCallMs > 0,
845
- message: config.chrome.waitForInCallMs > 0 ? `Realtime intro waits up to ${config.chrome.waitForInCallMs}ms for the Meet tab to be in-call` : "Set chrome.waitForInCallMs to delay realtime intro until the Meet tab is in-call"
846
- });
847
- if (transport === "twilio") {
848
- const hasRequestDialPlan = Boolean(options?.twilioDialInNumber);
849
- const hasDefaultDialPlan = Boolean(config.twilio.defaultDialInNumber);
850
- const hasDialPlan = hasRequestDialPlan || hasDefaultDialPlan;
851
- checks.push({
852
- id: "twilio-dial-plan",
853
- ok: hasDialPlan,
854
- message: hasRequestDialPlan ? "Twilio request includes a Meet dial-in number" : hasDefaultDialPlan ? "Twilio default Meet dial-in number is configured" : "Twilio joins require a Meet dial-in phone number; pass dialInNumber with optional pin/dtmfSequence or configure twilio.defaultDialInNumber"
855
- });
856
- }
857
- if (config.voiceCall.enabled && (transport === "twilio" || Boolean(config.twilio.defaultDialInNumber) || Object.hasOwn(pluginEntries, "voice-call"))) {
858
- const voiceCallAllowed = !Array.isArray(pluginAllow) || pluginAllow.includes("voice-call");
859
- const voiceCallEnabled = Object.hasOwn(pluginEntries, "voice-call") && voiceCallEntry.enabled !== false;
860
- checks.push({
861
- id: "twilio-voice-call-plugin",
862
- ok: voiceCallAllowed && voiceCallEnabled,
863
- message: voiceCallAllowed && voiceCallEnabled ? "Twilio transport can delegate dialing to the voice-call plugin" : "Enable plugins.entries.voice-call and include voice-call in plugins.allow for Twilio dialing"
864
- });
865
- if ((normalizeOptionalString(voiceCallConfig.provider) ?? "twilio") === "twilio") {
866
- const accountSid = resolveVoiceCallSetupValue(voiceCallTwilioConfig.accountSid, env.TWILIO_ACCOUNT_SID);
867
- const authToken = resolveVoiceCallSetupValue(voiceCallTwilioConfig.authToken, env.TWILIO_AUTH_TOKEN);
868
- const fromNumber = resolveVoiceCallSetupValue(voiceCallConfig.fromNumber, env.TWILIO_FROM_NUMBER);
869
- const twilioReady = Boolean(accountSid && authToken && fromNumber);
870
- checks.push({
871
- id: "twilio-voice-call-credentials",
872
- ok: twilioReady,
873
- message: twilioReady ? "Twilio voice-call credentials are configured" : "Set TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN, and TWILIO_FROM_NUMBER or configure voice-call Twilio credentials"
874
- });
875
- checks.push(getVoiceCallWebhookExposureCheck(voiceCallConfig));
876
- }
877
- }
878
- return createMeetingSetupStatus(checks);
879
- }
880
- function addGoogleMeetSetupCheck(status, check) {
881
- return addMeetingSetupCheck(status, check);
882
- }
883
- //#endregion
884
- //#region extensions/google-meet/src/transports/chrome.ts
885
- function shouldCaptureCaptions(mode, fullConfig) {
886
- return mode === "transcribe" || !fullConfig || resolveTranscriptsConfig(fullConfig.transcripts).enabled;
887
- }
888
- async function assertBlackHole2chAvailable(params) {
889
- if (process.platform !== "darwin") throw new Error("Chrome Meet transport with blackhole-2ch audio is currently macOS-only");
890
- const result = await params.runtime.system.runCommandWithTimeout([GOOGLE_MEET_SYSTEM_PROFILER_COMMAND, "SPAudioDataType"], { timeoutMs: params.timeoutMs });
891
- const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
892
- if (result.code !== 0 || !outputMentionsBlackHole2ch(output)) {
893
- const hint = params.runtime.system.formatNativeDependencyHint?.({
894
- packageName: "BlackHole 2ch",
895
- downloadCommand: "brew install blackhole-2ch"
896
- }) ?? "";
897
- throw new Error([
898
- "BlackHole 2ch audio device not found.",
899
- "Install BlackHole 2ch and route Chrome input/output through the OpenClaw audio bridge.",
900
- hint
901
- ].filter(Boolean).join(" "));
902
- }
903
- }
904
- async function launchChromeMeet(params) {
905
- const checkRealtimeAudioPrerequisites = async () => {
906
- if (!MeetingPlatformAdapter.isTalkBackMode(params.mode)) return;
907
- await assertBlackHole2chAvailable({
908
- runtime: params.runtime,
909
- timeoutMs: Math.min(params.config.chrome.joinTimeoutMs, 1e4)
910
- });
911
- if (params.config.chrome.audioBridgeHealthCommand) {
912
- const health = await params.runtime.system.runCommandWithTimeout(params.config.chrome.audioBridgeHealthCommand, { timeoutMs: params.config.chrome.joinTimeoutMs });
913
- if (health.code !== 0) throw new Error(`Chrome audio bridge health check failed: ${health.stderr || health.stdout || health.code}`);
914
- }
915
- };
916
- const startRealtimeAudioBridge = async () => {
917
- if (!MeetingPlatformAdapter.isTalkBackMode(params.mode)) return;
918
- if (params.config.chrome.audioBridgeCommand) {
919
- if (params.mode === "agent") throw new Error("Chrome agent mode requires chrome.audioInputCommand and chrome.audioOutputCommand so OpenClaw can run STT and regular TTS directly.");
920
- const bridge = await params.runtime.system.runCommandWithTimeout(params.config.chrome.audioBridgeCommand, { timeoutMs: params.config.chrome.joinTimeoutMs });
921
- if (bridge.code !== 0) throw new Error(`failed to start Chrome audio bridge: ${bridge.stderr || bridge.stdout || bridge.code}`);
922
- return { type: "external-command" };
923
- }
924
- if (!params.config.chrome.audioInputCommand || !params.config.chrome.audioOutputCommand) throw new Error("Chrome talk-back mode requires chrome.audioInputCommand and chrome.audioOutputCommand, or chrome.audioBridgeCommand for an external bridge.");
925
- const transport = createLocalMeetingRealtimeAudioTransport({
926
- inputCommand: params.config.chrome.audioInputCommand,
927
- outputCommand: params.config.chrome.audioOutputCommand,
928
- audioFormat: params.config.chrome.audioFormat,
929
- bargeInInputCommand: params.config.chrome.bargeInInputCommand,
930
- bargeInRmsThreshold: params.config.chrome.bargeInRmsThreshold,
931
- bargeInPeakThreshold: params.config.chrome.bargeInPeakThreshold,
932
- bargeInCooldownMs: params.config.chrome.bargeInCooldownMs,
933
- logger: params.logger,
934
- logScope: GOOGLE_MEET_PLATFORM_ADAPTER.logScope
935
- });
936
- const bindings = createMeetingRealtimeEngineBindings({
937
- platform: GOOGLE_MEET_PLATFORM_ADAPTER,
938
- ...params
939
- });
940
- const engine = params.mode === "agent" ? await startMeetingAgentRealtimeEngine({
941
- config: params.config,
942
- fullConfig: params.fullConfig,
943
- runtime: params.runtime,
944
- platform: bindings.platform,
945
- meetingSessionId: params.meetingSessionId,
946
- requesterSessionKey: params.requesterSessionKey,
947
- transport,
948
- logger: params.logger,
949
- consultAgent: bindings.consultAgent
950
- }) : await startMeetingRealtimeEngine({
951
- config: {
952
- ...params.config,
953
- realtime: {
954
- ...params.config.realtime,
955
- strategy: "bidi"
956
- }
957
- },
958
- fullConfig: params.fullConfig,
959
- runtime: params.runtime,
960
- ...bindings,
961
- meetingSessionId: params.meetingSessionId,
962
- requesterSessionKey: params.requesterSessionKey,
963
- transport,
964
- logger: params.logger
965
- });
966
- return {
967
- type: "command-pair",
968
- inputCommand: params.config.chrome.audioInputCommand,
969
- outputCommand: params.config.chrome.audioOutputCommand,
970
- ...engine
971
- };
972
- };
973
- await checkRealtimeAudioPrerequisites();
974
- if (!params.config.chrome.launch) return {
975
- launched: false,
976
- audioBridge: await startRealtimeAudioBridge()
977
- };
978
- const result = await openMeetingWithBrowser({
979
- adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
980
- callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
981
- config: params.config.chrome,
982
- session: {
983
- captureCaptions: shouldCaptureCaptions(params.mode, params.fullConfig),
984
- meetingSessionId: params.meetingSessionId,
985
- mode: params.mode,
986
- url: params.url
987
- }
988
- });
989
- const audioBridge = MeetingPlatformAdapter.isTalkBackMode(params.mode) && result.browser?.inCall === true && result.browser.micMuted === false && result.browser.manualAction === void 0 ? await startRealtimeAudioBridge() : void 0;
990
- return {
991
- ...result,
992
- audioBridge
993
- };
994
- }
995
- function parseNodeStartResult(raw) {
996
- const value = raw && typeof raw === "object" && "payload" in raw ? raw.payload : raw;
997
- if (!value || typeof value !== "object") throw new Error("Google Meet node returned an invalid start result.");
998
- return value;
999
- }
1000
- async function leaveChromeMeet(params) {
1001
- return await leaveMeetingWithBrowser({
1002
- adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
1003
- callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
1004
- launch: params.config.chrome.launch,
1005
- meetingSessionId: params.meetingSessionId,
1006
- meetingUrl: params.meetingUrl,
1007
- tab: params.tab,
1008
- timeoutMs: params.config.chrome.joinTimeoutMs
1009
- });
1010
- }
1011
- async function readChromeMeetTranscript(params) {
1012
- return await readMeetingTranscriptWithBrowser({
1013
- adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
1014
- callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
1015
- finalize: params.finalize === true,
1016
- meetingUrl: params.meetingUrl,
1017
- meetingSessionId: params.meetingSessionId,
1018
- tab: params.tab,
1019
- timeoutMs: Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4)
1020
- });
1021
- }
1022
- async function readChromeMeetTranscriptOnNode(params) {
1023
- const nodeId = params.nodeId ?? await resolveChromeNode({
1024
- runtime: params.runtime,
1025
- requestedNode: params.config.chromeNode.node
1026
- });
1027
- const timeoutMs = Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4);
1028
- return await readMeetingTranscriptWithBrowser({
1029
- adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
1030
- callBrowser: async (request) => await callBrowserProxyOnNode({
1031
- runtime: params.runtime,
1032
- nodeId,
1033
- method: request.method,
1034
- path: request.path,
1035
- body: request.body,
1036
- timeoutMs: request.timeoutMs
1037
- }),
1038
- finalize: params.finalize === true,
1039
- meetingUrl: params.meetingUrl,
1040
- meetingSessionId: params.meetingSessionId,
1041
- tab: params.tab,
1042
- timeoutMs
1043
- });
1044
- }
1045
- async function leaveChromeMeetOnNode(params) {
1046
- const nodeId = params.nodeId ?? await resolveChromeNode({
1047
- runtime: params.runtime,
1048
- requestedNode: params.config.chromeNode.node
1049
- });
1050
- return await leaveMeetingWithBrowser({
1051
- adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
1052
- callBrowser: async (request) => await callBrowserProxyOnNode({
1053
- runtime: params.runtime,
1054
- nodeId,
1055
- method: request.method,
1056
- path: request.path,
1057
- body: request.body,
1058
- timeoutMs: request.timeoutMs
1059
- }),
1060
- launch: params.config.chrome.launch,
1061
- meetingSessionId: params.meetingSessionId,
1062
- meetingUrl: params.meetingUrl,
1063
- tab: params.tab,
1064
- timeoutMs: params.config.chrome.joinTimeoutMs
1065
- });
1066
- }
1067
- async function openMeetWithBrowserProxy(params) {
1068
- return await openMeetingWithBrowser({
1069
- adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
1070
- callBrowser: async (request) => await callBrowserProxyOnNode({
1071
- runtime: params.runtime,
1072
- nodeId: params.nodeId,
1073
- method: request.method,
1074
- path: request.path,
1075
- body: request.body,
1076
- timeoutMs: request.timeoutMs
1077
- }),
1078
- config: params.config.chrome,
1079
- session: {
1080
- captureCaptions: params.captureCaptions,
1081
- mode: params.mode,
1082
- meetingSessionId: params.meetingSessionId,
1083
- url: params.url
1084
- }
1085
- });
1086
- }
1087
- async function recoverCurrentMeetTab(params) {
1088
- return {
1089
- transport: "chrome",
1090
- ...await recoverMeetingBrowserTab({
1091
- adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
1092
- callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
1093
- captureCaptions: shouldCaptureCaptions(params.mode ?? "bidi", params.fullConfig),
1094
- config: params.config.chrome,
1095
- locationLabel: "in local Chrome",
1096
- mode: params.mode ?? "bidi",
1097
- readOnly: params.readOnly,
1098
- requestedMeetingUrl: params.url,
1099
- trackedMeetingUrl: params.trackedMeetingUrl,
1100
- trackedTargetId: params.trackedTargetId
1101
- })
1102
- };
1103
- }
1104
- async function recoverCurrentMeetTabOnNode(params) {
1105
- const nodeId = await resolveChromeNode({
1106
- runtime: params.runtime,
1107
- requestedNode: params.config.chromeNode.node
1108
- });
1109
- return {
1110
- transport: "chrome-node",
1111
- nodeId,
1112
- ...await recoverMeetingBrowserTab({
1113
- adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
1114
- callBrowser: async (request) => await callBrowserProxyOnNode({
1115
- runtime: params.runtime,
1116
- nodeId,
1117
- method: request.method,
1118
- path: request.path,
1119
- body: request.body,
1120
- timeoutMs: request.timeoutMs
1121
- }),
1122
- captureCaptions: shouldCaptureCaptions(params.mode ?? "bidi", params.fullConfig),
1123
- config: params.config.chrome,
1124
- locationLabel: "on the selected Chrome node",
1125
- mode: params.mode ?? "bidi",
1126
- readOnly: params.readOnly,
1127
- requestedMeetingUrl: params.url,
1128
- trackedMeetingUrl: params.trackedMeetingUrl,
1129
- trackedTargetId: params.trackedTargetId
1130
- })
1131
- };
1132
- }
1133
- async function launchChromeMeetOnNode(params) {
1134
- const nodeId = await resolveChromeNode({
1135
- runtime: params.runtime,
1136
- requestedNode: params.config.chromeNode.node
1137
- });
1138
- try {
1139
- await params.runtime.nodes.invoke({
1140
- nodeId,
1141
- command: GOOGLE_MEET_NODE_COMMAND,
1142
- params: {
1143
- action: "stopByUrl",
1144
- url: params.url,
1145
- mode: params.mode
1146
- },
1147
- timeoutMs: 5e3
1148
- });
1149
- } catch (error) {
1150
- params.logger.debug?.(`[google-meet] node bridge cleanup before join ignored: ${error instanceof Error ? error.message : String(error)}`);
1151
- }
1152
- const browserControl = await openMeetWithBrowserProxy({
1153
- runtime: params.runtime,
1154
- nodeId,
1155
- config: params.config,
1156
- captureCaptions: shouldCaptureCaptions(params.mode, params.fullConfig),
1157
- mode: params.mode,
1158
- meetingSessionId: params.meetingSessionId,
1159
- url: params.url
1160
- });
1161
- if (params.config.chrome.launch && MeetingPlatformAdapter.isTalkBackMode(params.mode) && (browserControl.browser?.inCall !== true || browserControl.browser.micMuted !== false || browserControl.browser.manualAction)) return {
1162
- nodeId,
1163
- launched: browserControl.launched,
1164
- browser: browserControl.browser,
1165
- tab: browserControl.tab
1166
- };
1167
- const result = parseNodeStartResult(await params.runtime.nodes.invoke({
1168
- nodeId,
1169
- command: GOOGLE_MEET_NODE_COMMAND,
1170
- params: {
1171
- action: "start",
1172
- url: params.url,
1173
- mode: params.mode,
1174
- launch: false,
1175
- browserProfile: params.config.chrome.browserProfile,
1176
- joinTimeoutMs: params.config.chrome.joinTimeoutMs,
1177
- audioInputCommand: params.config.chrome.audioInputCommand,
1178
- audioOutputCommand: params.config.chrome.audioOutputCommand,
1179
- audioBridgeCommand: params.config.chrome.audioBridgeCommand,
1180
- audioBridgeHealthCommand: params.config.chrome.audioBridgeHealthCommand
1181
- },
1182
- timeoutMs: addTimerTimeoutGraceMs(params.config.chrome.joinTimeoutMs) ?? 1
1183
- }));
1184
- if (result.audioBridge?.type === "node-command-pair") {
1185
- if (!result.bridgeId) throw new Error("Google Meet node did not return an audio bridge id.");
1186
- const transport = createNodeMeetingRealtimeAudioTransport({
1187
- runtime: params.runtime,
1188
- nodeId,
1189
- bridgeId: result.bridgeId,
1190
- audioFormat: params.config.chrome.audioFormat,
1191
- logger: params.logger,
1192
- commandName: GOOGLE_MEET_NODE_COMMAND,
1193
- logScope: GOOGLE_MEET_PLATFORM_ADAPTER.logScope,
1194
- logPrefix: params.mode === "agent" ? "node agent" : "node"
1195
- });
1196
- const bindings = createMeetingRealtimeEngineBindings({
1197
- platform: GOOGLE_MEET_PLATFORM_ADAPTER,
1198
- ...params
1199
- });
1200
- const engine = params.mode === "agent" ? await startMeetingAgentRealtimeEngine({
1201
- config: params.config,
1202
- fullConfig: params.fullConfig,
1203
- runtime: params.runtime,
1204
- platform: bindings.platform,
1205
- meetingSessionId: params.meetingSessionId,
1206
- requesterSessionKey: params.requesterSessionKey,
1207
- logPrefix: "node",
1208
- transport,
1209
- logger: params.logger,
1210
- consultAgent: bindings.consultAgent
1211
- }) : await startMeetingRealtimeEngine({
1212
- config: {
1213
- ...params.config,
1214
- realtime: {
1215
- ...params.config.realtime,
1216
- strategy: "bidi"
1217
- }
1218
- },
1219
- fullConfig: params.fullConfig,
1220
- runtime: params.runtime,
1221
- ...bindings,
1222
- meetingSessionId: params.meetingSessionId,
1223
- requesterSessionKey: params.requesterSessionKey,
1224
- logPrefix: "node",
1225
- talkSessionId: `google-meet:${params.meetingSessionId}:${result.bridgeId}:node-realtime`,
1226
- talkContext: {
1227
- nodeId,
1228
- bridgeId: result.bridgeId
1229
- },
1230
- transport,
1231
- logger: params.logger
1232
- });
1233
- const bridge = {
1234
- type: "node-command-pair",
1235
- nodeId,
1236
- bridgeId: result.bridgeId,
1237
- ...engine
1238
- };
1239
- return {
1240
- nodeId,
1241
- launched: browserControl.launched || result.launched === true,
1242
- audioBridge: bridge,
1243
- browser: browserControl.browser ?? result.browser,
1244
- tab: browserControl.tab
1245
- };
1246
- }
1247
- if (result.audioBridge?.type === "external-command") return {
1248
- nodeId,
1249
- launched: browserControl.launched || result.launched === true,
1250
- audioBridge: { type: "external-command" },
1251
- browser: browserControl.browser ?? result.browser,
1252
- tab: browserControl.tab
1253
- };
1254
- return {
1255
- nodeId,
1256
- launched: browserControl.launched || result.launched === true,
1257
- browser: browserControl.browser ?? result.browser,
1258
- tab: browserControl.tab
1259
- };
1260
- }
1261
- //#endregion
1262
- //#region extensions/google-meet/src/runtime-setup.ts
1263
- function collectChromeAudioCommands(config) {
1264
- return uniqueStrings((config.chrome.audioBridgeCommand ? [config.chrome.audioBridgeCommand[0]] : [
1265
- config.chrome.audioInputCommand?.[0],
1266
- config.chrome.audioOutputCommand?.[0],
1267
- config.chrome.bargeInInputCommand?.[0]
1268
- ]).filter((value) => Boolean(value?.trim())));
1269
- }
1270
- async function commandExists(runtime, command) {
1271
- return (await runtime.system.runCommandWithTimeout([
1272
- "/bin/sh",
1273
- "-lc",
1274
- "command -v \"$1\" >/dev/null 2>&1",
1275
- "sh",
1276
- command
1277
- ], { timeoutMs: 5e3 })).code === 0;
1278
- }
1279
- async function getGoogleMeetRuntimeSetupStatus(params) {
1280
- const options = params.options ?? {};
1281
- const transport = options.transport ?? params.config.defaultTransport;
1282
- const mode = options.mode === "realtime" ? "agent" : options.mode ?? params.config.defaultMode;
1283
- const twilioDialInNumber = transport === "twilio" ? normalizeDialInNumber(options.dialInNumber) : void 0;
1284
- const shouldCheckChromeNode = transport === "chrome-node" || !options.transport && Boolean(params.config.chromeNode.node);
1285
- let status = getGoogleMeetSetupStatus(params.config, {
1286
- fullConfig: params.fullConfig,
1287
- mode,
1288
- transport,
1289
- twilioDialInNumber
1290
- });
1291
- if (shouldCheckChromeNode) try {
1292
- const node = await resolveChromeNodeInfo({
1293
- runtime: params.runtime,
1294
- requestedNode: params.config.chromeNode.node
1295
- });
1296
- const label = node.displayName ?? node.remoteIp ?? node.nodeId ?? "connected node";
1297
- status = addGoogleMeetSetupCheck(status, {
1298
- id: "chrome-node-connected",
1299
- ok: true,
1300
- message: `Connected Google Meet node ready: ${label}`
1301
- });
1302
- } catch (error) {
1303
- status = addGoogleMeetSetupCheck(status, {
1304
- id: "chrome-node-connected",
1305
- ok: false,
1306
- message: formatErrorMessage(error)
1307
- });
1308
- }
1309
- if (transport !== "chrome" || mode !== "agent" && mode !== "bidi") return status;
1310
- try {
1311
- await assertBlackHole2chAvailable({
1312
- runtime: params.runtime,
1313
- timeoutMs: Math.min(params.config.chrome.joinTimeoutMs, 1e4)
1314
- });
1315
- status = addGoogleMeetSetupCheck(status, {
1316
- id: "chrome-local-audio-device",
1317
- ok: true,
1318
- message: "BlackHole 2ch audio device found"
1319
- });
1320
- } catch (error) {
1321
- status = addGoogleMeetSetupCheck(status, {
1322
- id: "chrome-local-audio-device",
1323
- ok: false,
1324
- message: formatErrorMessage(error)
1325
- });
1326
- }
1327
- const commands = collectChromeAudioCommands(params.config);
1328
- const missingCommands = [];
1329
- for (const command of commands) try {
1330
- if (!await commandExists(params.runtime, command)) missingCommands.push(command);
1331
- } catch {
1332
- missingCommands.push(command);
1333
- }
1334
- return addGoogleMeetSetupCheck(status, {
1335
- id: "chrome-local-audio-commands",
1336
- ok: commands.length > 0 && missingCommands.length === 0,
1337
- message: commands.length === 0 ? "Chrome talk-back audio commands are not configured" : missingCommands.length === 0 ? `Chrome audio command${commands.length === 1 ? "" : "s"} available: ${commands.join(", ")}` : `Chrome audio command${missingCommands.length === 1 ? "" : "s"} missing: ${missingCommands.join(", ")}`
1338
- });
1339
- }
1340
- //#endregion
1341
- //#region extensions/google-meet/src/voice-call-gateway.ts
1342
- const GOOGLE_MEET_VOICE_CALL_SURFACE = {
1343
- clientDisplayName: "Google Meet plugin",
1344
- configPath: "google-meet voiceCall.gatewayUrl",
1345
- logScope: "[google-meet]",
1346
- meetingLabel: "Meet",
1347
- providerLabel: "Twilio"
1348
- };
1349
- async function createConnectedGatewayClient(params) {
1350
- let client;
1351
- await new Promise((resolve, reject) => {
1352
- const abortStart = new AbortController();
1353
- const timer = setTimeout(() => {
1354
- abortStart.abort();
1355
- reject(/* @__PURE__ */ new Error("gateway connect timeout"));
1356
- }, params.config.requestTimeoutMs);
1357
- client = new GatewayClient({
1358
- url: params.config.gatewayUrl,
1359
- token: params.config.token,
1360
- requestTimeoutMs: params.config.requestTimeoutMs,
1361
- clientName: "cli",
1362
- clientDisplayName: params.surface.clientDisplayName,
1363
- scopes: ["operator.write"],
1364
- onHelloOk: () => {
1365
- clearTimeout(timer);
1366
- resolve();
1367
- },
1368
- onConnectError: (error) => {
1369
- clearTimeout(timer);
1370
- abortStart.abort();
1371
- reject(error);
1372
- }
1373
- });
1374
- startGatewayClientWhenEventLoopReady(client, {
1375
- timeoutMs: params.config.requestTimeoutMs,
1376
- signal: abortStart.signal
1377
- }).then((readiness) => {
1378
- if (!readiness.ready && !readiness.aborted) {
1379
- clearTimeout(timer);
1380
- reject(/* @__PURE__ */ new Error("gateway event loop readiness timeout"));
1381
- }
1382
- }).catch((error) => {
1383
- clearTimeout(timer);
1384
- reject(error instanceof Error ? error : new Error(String(error)));
1385
- });
1386
- });
1387
- return client;
1388
- }
1389
- function createVoiceCallGateway(params) {
1390
- return createMeetingVoiceCallGateway({
1391
- config: params.config.voiceCall,
1392
- runtime: params.runtime,
1393
- surface: GOOGLE_MEET_VOICE_CALL_SURFACE,
1394
- connectClient: createConnectedGatewayClient
1395
- });
1396
- }
1397
- const isVoiceCallMissingError = isMeetingVoiceCallMissingError;
1398
- async function joinMeetViaVoiceCallGateway(params) {
1399
- return await joinMeetingViaVoiceCallGateway({
1400
- ...params,
1401
- config: params.config.voiceCall,
1402
- surface: GOOGLE_MEET_VOICE_CALL_SURFACE
1403
- });
1404
- }
1405
- async function endMeetVoiceCallGatewayCall(params) {
1406
- await endMeetingVoiceCallGatewayCall(params);
1407
- }
1408
- async function getMeetVoiceCallGatewayCall(params) {
1409
- return await getMeetingVoiceCallGatewayCall(params);
1410
- }
1411
- async function speakMeetViaVoiceCallGateway(params) {
1412
- await speakMeetingViaVoiceCallGateway(params);
1413
- }
1414
- //#endregion
1415
- //#region extensions/google-meet/src/runtime.ts
1416
- const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
1417
- var GoogleMeetRuntime = class {
1418
- #createdBrowserTabs;
1419
- #agentId;
1420
- #voiceCallGateway;
1421
- #sessions;
1422
- constructor(params) {
1423
- this.params = params;
1424
- this.#createdBrowserTabs = /* @__PURE__ */ new Map();
1425
- this.transcriptSourceRuntime = () => this.#sessions;
1426
- const adapter = GOOGLE_MEET_PLATFORM_ADAPTER;
1427
- this.#agentId = resolveDefaultAgentId(params.fullConfig);
1428
- this.#voiceCallGateway = createVoiceCallGateway(params);
1429
- this.#sessions = new MeetingSessionRuntime({
1430
- logger: params.logger,
1431
- logScope: "[google-meet]",
1432
- formatError: formatErrorMessage,
1433
- reuseExistingBrowserTab: params.config.chrome.reuseExistingTab,
1434
- waitForInCallMs: params.config.chrome.waitForInCallMs,
1435
- joinTimeoutMs: params.config.chrome.joinTimeoutMs,
1436
- defaultSpeechInstructions: params.config.realtime.introMessage,
1437
- transientSpeechBlockedReasons: /* @__PURE__ */ new Set([
1438
- "not-in-call",
1439
- "browser-unverified",
1440
- "meet-microphone-muted"
1441
- ]),
1442
- messages: {
1443
- previousBrowserLeaveFailed: "Could not leave the previous Meet browser tab before reassignment.",
1444
- reassignedSessionNote: "Ended before the same Meet tab was reassigned to another agent.",
1445
- reusedSessionNote: "Reused existing active Meet session.",
1446
- replacementBrowserLeaveFailed: "Could not leave the previous Meet browser tab before reassignment.",
1447
- speechBlockedFallback: "Realtime speech blocked until Google Meet is ready.",
1448
- speech: {
1449
- audioBridgeUnavailable: "Realtime speech requires an active Chrome audio bridge.",
1450
- browserUnverified: "Google Meet browser state has not been verified yet.",
1451
- microphoneMuted: "Turn on the OpenClaw Google Meet microphone before asking OpenClaw to speak.",
1452
- microphoneMutedReason: "meet-microphone-muted",
1453
- notInCall: "Google Meet has not reported that the browser participant is in the call.",
1454
- notInCallReason: "not-in-call",
1455
- browserUnverifiedReason: "browser-unverified",
1456
- audioBridgeUnavailableReason: "audio-bridge-unavailable"
1457
- }
1458
- },
1459
- resolveJoin: (request) => ({
1460
- url: adapter.urls.validateAndNormalize(request.url),
1461
- transport: resolveTransport(request.transport, params.config),
1462
- mode: resolveMode(request.mode, params.config),
1463
- agentId: normalizeAgentId(request.agentId ?? params.config.realtime.agentId ?? this.#agentId)
1464
- }),
1465
- createSession: ({ resolved, createdAt }) => createMeetingSession({
1466
- platform: adapter,
1467
- config: params.config,
1468
- resolved,
1469
- createdAt
1470
- }),
1471
- resolveSpeechInstructions: (request) => request.message ?? params.config.realtime.introMessage,
1472
- isBrowserTransport,
1473
- isTalkBackMode: (mode) => MeetingPlatformAdapter.isTalkBackMode(mode),
1474
- isTranscribeMode: (mode) => mode === "transcribe",
1475
- sameMeetingUrl: (left, right) => adapter.urls.isSameMeeting(left, right),
1476
- normalizeMeetingUrlForReuse: (url) => adapter.urls.normalizeForReuse(url),
1477
- getBrowser: (session) => session.chrome ? {
1478
- launched: session.chrome.launched,
1479
- nodeId: session.chrome.nodeId,
1480
- tab: session.chrome.browserTab,
1481
- health: session.chrome.health,
1482
- hasAudioBridge: Boolean(session.chrome.audioBridge)
1483
- } : void 0,
1484
- setBrowserTab: (session, tab) => {
1485
- if (session.chrome) session.chrome.browserTab = tab;
1486
- },
1487
- setBrowserHealth: (session, health) => {
1488
- if (session.chrome) session.chrome.health = health;
1489
- },
1490
- joinTransport: async ({ request, session, context }) => await this.#joinTransport(request, session, context),
1491
- releaseBrowserTab: async (session) => await this.#releaseBrowserTab(session),
1492
- refreshBrowserHealth: async (session, options) => await this.#refreshBrowserHealth(session, options),
1493
- refreshStatus: async (session) => await this.#refreshStatus(session),
1494
- refreshReusableSession: async (session, _request, _resolved) => {
1495
- if (session.transport === "twilio") await this.#refreshTwilioVoiceCallStatus(session);
1496
- },
1497
- ensureRealtimeBridge: async (session) => await this.#ensureChromeRealtimeBridge(session),
1498
- captureTranscript: async (session, options) => await this.#captureTranscript(session, options),
1499
- speakViaTransport: async (session, instructions) => await this.#speakViaTransport(session, instructions),
1500
- durableTranscripts: {
1501
- config: params.fullConfig.transcripts,
1502
- providerId: "google-meet",
1503
- providerName: "Google Meet"
1504
- }
1505
- });
1506
- }
1507
- list() {
1508
- return this.#sessions.list();
1509
- }
1510
- async status(sessionId) {
1511
- return await this.#sessions.status(sessionId);
1512
- }
1513
- async transcript(sessionId, options = {}) {
1514
- return await this.#sessions.transcript(sessionId, options);
1515
- }
1516
- async setupStatus(options = {}) {
1517
- return await getGoogleMeetRuntimeSetupStatus({
1518
- config: this.params.config,
1519
- fullConfig: this.params.fullConfig,
1520
- runtime: this.params.runtime,
1521
- options
1522
- });
1523
- }
1524
- async createViaBrowser() {
1525
- const result = await GOOGLE_MEET_PLATFORM_ADAPTER.create.browser({
1526
- runtime: this.params.runtime,
1527
- config: this.params.config
1528
- });
1529
- if (result.openedByPlugin && result.targetId) this.#createdBrowserTabs.set(`${result.nodeId}:${result.targetId}`, result.meetingUri);
1530
- return result;
1531
- }
1532
- async recoverCurrentTab(request = {}) {
1533
- const transport = resolveTransport(request.transport, this.params.config);
1534
- if (transport === "twilio") throw new Error("recover_current_tab only supports chrome or chrome-node transports");
1535
- const url = request.url ? GOOGLE_MEET_PLATFORM_ADAPTER.urls.validateAndNormalize(request.url) : void 0;
1536
- return transport === "chrome-node" ? await recoverCurrentMeetTabOnNode({
1537
- runtime: this.params.runtime,
1538
- config: this.params.config,
1539
- fullConfig: this.params.fullConfig,
1540
- url
1541
- }) : await recoverCurrentMeetTab({
1542
- runtime: this.params.runtime,
1543
- config: this.params.config,
1544
- fullConfig: this.params.fullConfig,
1545
- url
1546
- });
1547
- }
1548
- async join(request) {
1549
- return await this.#sessions.join(request);
1550
- }
1551
- async leave(sessionId, options) {
1552
- return await this.#sessions.leave(sessionId, options);
1553
- }
1554
- async speak(sessionId, instructions) {
1555
- return await this.#sessions.speak(sessionId, instructions);
1556
- }
1557
- async testSpeech(request) {
1558
- return await testGoogleMeetSpeech(this.#probeContext(), request);
1559
- }
1560
- async testListen(request) {
1561
- return await testGoogleMeetListening(this.#probeContext(), request);
1562
- }
1563
- #probeContext() {
1564
- return {
1565
- config: this.params.config,
1566
- resolveAgentId: (request) => normalizeAgentId(request.agentId ?? this.params.config.realtime.agentId ?? this.#agentId),
1567
- list: () => this.list(),
1568
- join: async (request) => await this.join(request),
1569
- isReusable: (session, resolved) => this.#sessions.isReusableSession(session, resolved),
1570
- hasHealthHandle: (sessionId) => this.#sessions.hasHealthHandle(sessionId),
1571
- refreshHealth: (sessionId) => this.#sessions.refreshHealth(sessionId),
1572
- refreshCaptionHealth: async (session) => await this.#sessions.refreshCaptionHealth(session)
1573
- };
1574
- }
1575
- async #joinTransport(request, session, context) {
1576
- if (isBrowserTransport(session.transport)) {
1577
- const chromeConfig = withSessionAgentConfig(this.params.config, session.agentId);
1578
- const result = session.transport === "chrome-node" ? await launchChromeMeetOnNode({
1579
- runtime: this.params.runtime,
1580
- config: chromeConfig,
1581
- fullConfig: this.params.fullConfig,
1582
- meetingSessionId: session.id,
1583
- requesterSessionKey: request.requesterSessionKey,
1584
- mode: session.mode,
1585
- url: session.url,
1586
- logger: this.params.logger
1587
- }) : await launchChromeMeet({
1588
- runtime: this.params.runtime,
1589
- config: chromeConfig,
1590
- fullConfig: this.params.fullConfig,
1591
- meetingSessionId: session.id,
1592
- requesterSessionKey: request.requesterSessionKey,
1593
- mode: session.mode,
1594
- url: session.url,
1595
- logger: this.params.logger
1596
- });
1597
- const nodeId = "nodeId" in result ? result.nodeId : void 0;
1598
- let tab = result.tab;
1599
- const createdKey = session.transport === "chrome-node" && nodeId && tab ? `${nodeId}:${tab.targetId}` : void 0;
1600
- const createdUrl = createdKey ? this.#createdBrowserTabs.get(createdKey) : void 0;
1601
- if (createdKey) this.#createdBrowserTabs.delete(createdKey);
1602
- if (tab && GOOGLE_MEET_PLATFORM_ADAPTER.urls.isSameMeeting(createdUrl, session.url)) tab = {
1603
- ...tab,
1604
- openedByPlugin: true
1605
- };
1606
- tab = context.inheritedBrowserTab({
1607
- session,
1608
- transport: session.transport,
1609
- nodeId,
1610
- meetingUrl: session.url,
1611
- tab
1612
- });
1613
- session.chrome = {
1614
- audioBackend: this.params.config.chrome.audioBackend,
1615
- launched: result.launched,
1616
- nodeId,
1617
- browserProfile: this.params.config.chrome.browserProfile,
1618
- browserTab: tab,
1619
- health: result.browser
1620
- };
1621
- const handles = this.#attachChromeAudioBridge(session, result.audioBridge);
1622
- if (handles) context.attachRuntimeHandles(session, handles);
1623
- session.notes.push(result.audioBridge ? session.transport === "chrome-node" ? "Chrome node transport joins as the signed-in Google profile on the selected node and routes realtime audio through the node bridge." : "Chrome transport joins as the signed-in Google profile and routes realtime audio through the configured bridge." : MeetingPlatformAdapter.isTalkBackMode(session.mode) ? "Chrome transport joins as the signed-in Google profile and expects BlackHole 2ch audio routing." : "Chrome transport joins as the signed-in Google profile without starting the realtime audio bridge.");
1624
- this.#sessions.refreshSpeechReadiness(session);
1625
- return {};
1626
- }
1627
- const dialPlan = GOOGLE_MEET_PLATFORM_ADAPTER.dialIn.buildPlan({
1628
- dialInNumber: request.dialInNumber,
1629
- defaultDialInNumber: this.params.config.twilio.defaultDialInNumber,
1630
- pin: request.pin,
1631
- defaultPin: this.params.config.twilio.defaultPin,
1632
- dtmfSequence: request.dtmfSequence,
1633
- defaultDtmfSequence: this.params.config.twilio.defaultDtmfSequence,
1634
- dtmfDelayMs: this.params.config.voiceCall.dtmfDelayMs
1635
- });
1636
- const dialInNumber = dialPlan.number;
1637
- if (!dialInNumber) throw new Error("Twilio transport requires a Meet dial-in phone number. Google Meet URLs do not include dial-in details; pass dialInNumber with optional pin/dtmfSequence, configure twilio.defaultDialInNumber, or use chrome/chrome-node transport.");
1638
- const dtmfSequence = dialPlan.dtmfSequence;
1639
- const delegatedAgentId = Boolean(normalizeOptionalString(request.agentId) || normalizeOptionalString(this.params.config.realtime.agentId)) ? session.agentId : void 0;
1640
- const voiceCallResult = this.params.config.voiceCall.enabled ? await joinMeetViaVoiceCallGateway({
1641
- config: this.params.config,
1642
- gateway: this.#voiceCallGateway,
1643
- dialInNumber,
1644
- dtmfSequence,
1645
- logger: this.params.logger,
1646
- ...request.requesterSessionKey ? { requesterSessionKey: request.requesterSessionKey } : {},
1647
- agentId: delegatedAgentId,
1648
- sessionKey: delegatedAgentId ? `agent:${delegatedAgentId}:google-meet:${session.id}` : `voice:google-meet:${session.id}`,
1649
- message: MeetingPlatformAdapter.isTalkBackMode(session.mode) ? request.message ?? this.params.config.voiceCall.introMessage ?? this.params.config.realtime.introMessage : void 0
1650
- }) : void 0;
1651
- session.twilio = {
1652
- dialInNumber,
1653
- pinProvided: Boolean(dialPlan.pin),
1654
- dtmfSequence,
1655
- voiceCallId: voiceCallResult?.callId,
1656
- dtmfSent: voiceCallResult?.dtmfSent,
1657
- introSent: voiceCallResult?.introSent
1658
- };
1659
- if (voiceCallResult?.callId) context.attachRuntimeHandles(session, { stop: async () => {
1660
- await endMeetVoiceCallGatewayCall({
1661
- gateway: this.#voiceCallGateway,
1662
- callId: voiceCallResult.callId
1663
- });
1664
- } });
1665
- session.notes.push(this.params.config.voiceCall.enabled ? dtmfSequence ? "Twilio transport delegated the phone leg to the voice-call plugin, then queued configured DTMF before realtime connect." : "Twilio transport delegated the call to the voice-call plugin without configured DTMF." : "Twilio transport is an explicit dial plan; voice-call delegation is disabled.");
1666
- return { delegatedSpoken: Boolean(voiceCallResult?.introSent) };
1667
- }
1668
- #attachChromeAudioBridge(session, audioBridge) {
1669
- if (!session.chrome || !audioBridge) return;
1670
- session.chrome.audioBridge = {
1671
- type: audioBridge.type,
1672
- provider: audioBridge.type === "command-pair" || audioBridge.type === "node-command-pair" ? audioBridge.providerId : void 0
1673
- };
1674
- return audioBridge.type === "command-pair" || audioBridge.type === "node-command-pair" ? {
1675
- stop: audioBridge.stop,
1676
- speak: audioBridge.speak,
1677
- getHealth: audioBridge.getHealth
1678
- } : void 0;
1679
- }
1680
- async #ensureChromeRealtimeBridge(session) {
1681
- if (!MeetingPlatformAdapter.isTalkBackMode(session.mode) || !isBrowserTransport(session.transport) || session.state !== "active" || !session.chrome || session.chrome.audioBridge || session.chrome.health?.inCall !== true || session.chrome.health.micMuted !== false || session.chrome.health.manualAction) return;
1682
- const config = withSessionAgentConfig(this.params.config, session.agentId);
1683
- const recoveryConfig = {
1684
- ...config,
1685
- chrome: {
1686
- ...config.chrome,
1687
- launch: false
1688
- },
1689
- ...session.chrome.nodeId ? { chromeNode: {
1690
- ...config.chromeNode,
1691
- node: session.chrome.nodeId
1692
- } } : {}
1693
- };
1694
- const result = session.transport === "chrome-node" ? await launchChromeMeetOnNode({
1695
- runtime: this.params.runtime,
1696
- config: recoveryConfig,
1697
- fullConfig: this.params.fullConfig,
1698
- meetingSessionId: session.id,
1699
- mode: session.mode,
1700
- url: session.url,
1701
- logger: this.params.logger
1702
- }) : await launchChromeMeet({
1703
- runtime: this.params.runtime,
1704
- config: recoveryConfig,
1705
- fullConfig: this.params.fullConfig,
1706
- meetingSessionId: session.id,
1707
- mode: session.mode,
1708
- url: session.url,
1709
- logger: this.params.logger
1710
- });
1711
- session.updatedAt = nowIso();
1712
- return this.#attachChromeAudioBridge(session, result.audioBridge);
1713
- }
1714
- async #refreshBrowserHealth(session, options = {}) {
1715
- try {
1716
- const result = session.transport === "chrome-node" ? await recoverCurrentMeetTabOnNode({
1717
- runtime: this.params.runtime,
1718
- config: this.params.config,
1719
- fullConfig: this.params.fullConfig,
1720
- mode: session.mode,
1721
- readOnly: options.readOnly,
1722
- trackedMeetingUrl: session.url,
1723
- trackedTargetId: session.chrome?.browserTab?.targetId,
1724
- url: session.url
1725
- }) : await recoverCurrentMeetTab({
1726
- runtime: this.params.runtime,
1727
- config: this.params.config,
1728
- fullConfig: this.params.fullConfig,
1729
- mode: session.mode,
1730
- readOnly: options.readOnly,
1731
- trackedMeetingUrl: session.url,
1732
- trackedTargetId: session.chrome?.browserTab?.targetId,
1733
- url: session.url
1734
- });
1735
- if (result.found && session.chrome) {
1736
- if (result.targetId) {
1737
- const currentTab = session.chrome.browserTab;
1738
- session.chrome.browserTab = {
1739
- targetId: result.targetId,
1740
- openedByPlugin: result.targetId === currentTab?.targetId ? currentTab.openedByPlugin : false
1741
- };
1742
- }
1743
- if (result.browser) session.chrome.health = {
1744
- ...session.chrome.health,
1745
- ...result.browser
1746
- };
1747
- session.updatedAt = nowIso();
1748
- }
1749
- } catch (error) {
1750
- this.params.logger.debug?.(`[google-meet] browser readiness refresh ignored: ${formatErrorMessage(error)}`);
1751
- }
1752
- }
1753
- async #refreshStatus(session) {
1754
- if (isBrowserTransport(session.transport)) await this.#sessions.refreshBrowserHealth(session, {
1755
- force: true,
1756
- readOnly: true
1757
- });
1758
- else if (session.transport === "twilio") await this.#refreshTwilioVoiceCallStatus(session);
1759
- else this.#sessions.refreshSpeechReadiness(session);
1760
- }
1761
- async #refreshTwilioVoiceCallStatus(session) {
1762
- const callId = session.twilio?.voiceCallId;
1763
- if (!callId || session.state !== "active") {
1764
- this.#sessions.refreshSpeechReadiness(session);
1765
- return;
1766
- }
1767
- try {
1768
- if ((await getMeetVoiceCallGatewayCall({
1769
- gateway: this.#voiceCallGateway,
1770
- callId
1771
- })).found === false) this.#sessions.markSessionEnded(session, "Voice Call is no longer active.");
1772
- } catch (error) {
1773
- this.params.logger.debug?.(`[google-meet] voice-call status refresh ignored: ${formatErrorMessage(error)}`);
1774
- }
1775
- this.#sessions.refreshSpeechReadiness(session);
1776
- }
1777
- async #speakViaTransport(session, instructions) {
1778
- if (session.transport !== "twilio" || !session.twilio?.voiceCallId) return;
1779
- try {
1780
- await speakMeetViaVoiceCallGateway({
1781
- gateway: this.#voiceCallGateway,
1782
- callId: session.twilio.voiceCallId,
1783
- message: instructions || this.params.config.voiceCall.introMessage || this.params.config.realtime.introMessage || ""
1784
- });
1785
- } catch (error) {
1786
- if (!isVoiceCallMissingError(error)) throw error;
1787
- this.#sessions.markSessionEnded(session, "Voice Call is no longer active.");
1788
- return {
1789
- handled: true,
1790
- spoken: false
1791
- };
1792
- }
1793
- session.twilio.introSent = true;
1794
- session.updatedAt = nowIso();
1795
- return {
1796
- handled: true,
1797
- spoken: true
1798
- };
1799
- }
1800
- async #captureTranscript(session, options = {}) {
1801
- const tab = session.chrome?.browserTab;
1802
- if (!tab) return;
1803
- return session.transport === "chrome-node" ? await readChromeMeetTranscriptOnNode({
1804
- runtime: this.params.runtime,
1805
- nodeId: session.chrome?.nodeId,
1806
- config: this.params.config,
1807
- ...options.finalize === void 0 ? {} : { finalize: options.finalize },
1808
- meetingUrl: session.url,
1809
- meetingSessionId: session.id,
1810
- tab
1811
- }) : await readChromeMeetTranscript({
1812
- runtime: this.params.runtime,
1813
- config: this.params.config,
1814
- ...options.finalize === void 0 ? {} : { finalize: options.finalize },
1815
- meetingUrl: session.url,
1816
- meetingSessionId: session.id,
1817
- tab
1818
- });
1819
- }
1820
- async #releaseBrowserTab(session) {
1821
- if (!isBrowserTransport(session.transport)) return;
1822
- const tab = session.chrome?.browserTab;
1823
- if (!tab) {
1824
- noteSession(session, "No tracked Meet browser tab for this session; close the Meet tab manually if it is still in the call.");
1825
- session.browserLeft = false;
1826
- return false;
1827
- }
1828
- if (this.list().some((other) => other.id !== session.id && other.state === "active" && isBrowserTransport(other.transport) && other.chrome?.browserTab?.targetId === tab.targetId && other.chrome?.nodeId === session.chrome?.nodeId)) {
1829
- noteSession(session, "Kept the shared Meet tab open because another active session uses it.");
1830
- session.browserLeft = void 0;
1831
- return;
1832
- }
1833
- let left;
1834
- try {
1835
- const result = session.transport === "chrome-node" ? await leaveChromeMeetOnNode({
1836
- runtime: this.params.runtime,
1837
- nodeId: session.chrome?.nodeId,
1838
- config: this.params.config,
1839
- meetingSessionId: session.id,
1840
- meetingUrl: session.url,
1841
- tab
1842
- }) : await leaveChromeMeet({
1843
- runtime: this.params.runtime,
1844
- config: this.params.config,
1845
- meetingSessionId: session.id,
1846
- meetingUrl: session.url,
1847
- tab
1848
- });
1849
- noteSession(session, result.note);
1850
- left = result.left;
1851
- } catch (error) {
1852
- noteSession(session, `Browser control could not leave the Meet tab: ${formatErrorMessage(error)}`);
1853
- left = false;
1854
- }
1855
- if (session.chrome && left) {
1856
- session.chrome.browserTab = void 0;
1857
- if (session.chrome.health) session.chrome.health = {
1858
- ...session.chrome.health,
1859
- captioning: false,
1860
- audioOutputRouted: false,
1861
- providerConnected: false,
1862
- realtimeReady: false,
1863
- audioInputActive: false,
1864
- audioOutputActive: false
1865
- };
1866
- }
1867
- session.browserLeft = left;
1868
- return left;
1869
- }
1870
- };
1871
- //#endregion
1872
- //#region extensions/google-meet/src/plugin-helpers.ts
1873
- const loadGoogleMeetCreateModule = createLazyRuntimeModule(() => import("./create-af1MDlus.js").then((n) => n.t));
1874
- const loadGoogleMeetCliModule = createLazyRuntimeModule(() => import("./cli-DCcRyaMf.js"));
1875
- function asParamRecord(params) {
1876
- return params && typeof params === "object" && !Array.isArray(params) ? params : {};
1877
- }
1878
- function normalizeTransport(value) {
1879
- return value === "chrome" || value === "chrome-node" || value === "twilio" ? value : void 0;
1880
- }
1881
- function normalizeMode(value) {
1882
- if (value === "realtime") return "agent";
1883
- return value === "agent" || value === "bidi" || value === "transcribe" ? value : void 0;
1884
- }
1885
- function resolveMeetingInput(config, value) {
1886
- const meeting = normalizeOptionalString(value) ?? config.defaults.meeting;
1887
- if (!meeting) throw new Error("Meeting input is required");
1888
- return meeting;
1889
- }
1890
- function shouldJoinCreatedMeet(raw) {
1891
- return raw.join !== false && raw.join !== "false";
1892
- }
1893
- const googleMeetToolDeps = {
1894
- callGatewayFromCli,
1895
- platform: () => process.platform
1896
- };
1897
- const testing = {
1898
- setCallGatewayFromCliForTests(next) {
1899
- googleMeetToolDeps.callGatewayFromCli = next ?? callGatewayFromCli;
1900
- },
1901
- setPlatformForTests(next) {
1902
- googleMeetToolDeps.platform = next ?? (() => process.platform);
1903
- },
1904
- isGoogleMeetAgentToolActionUnsupportedOnHost,
1905
- resolveGoogleMeetGatewayOperationTimeoutMs
1906
- };
1907
- function googleMeetGatewayMethodForToolAction(action) {
1908
- switch (action) {
1909
- case "recover_current_tab": return "googlemeet.recoverCurrentTab";
1910
- case "setup_status": return "googlemeet.setup";
1911
- case "test_speech": return "googlemeet.testSpeech";
1912
- case "test_listen": return "googlemeet.testListen";
1913
- case "end_active_conference": return "googlemeet.endActiveConference";
1914
- default: return `googlemeet.${action}`;
1915
- }
1916
- }
1917
- function isGoogleMeetAgentToolActionUnsupportedOnHost(params) {
1918
- if ((params.platform ?? googleMeetToolDeps.platform()) === "darwin") return false;
1919
- const action = params.raw.action;
1920
- if (action !== "join" && action !== "test_speech" && !(action === "create" && shouldJoinCreatedMeet(params.raw))) return false;
1921
- const transport = normalizeTransport(params.raw.transport) ?? params.config.defaultTransport;
1922
- const mode = action === "test_speech" ? "agent" : normalizeMode(params.raw.mode) ?? params.config.defaultMode;
1923
- return transport === "chrome" && MeetingPlatformAdapter.isTalkBackMode(mode);
1924
- }
1925
- function assertGoogleMeetAgentToolActionSupported(params) {
1926
- if (!isGoogleMeetAgentToolActionUnsupportedOnHost(params)) return;
1927
- throw new Error("Google Meet local Chrome talk-back audio is macOS-only. On this host, use mode: transcribe, transport: twilio, or transport: chrome-node backed by a macOS node.");
1928
- }
1929
- function readGatewayErrorDetails(err) {
1930
- if (!err || typeof err !== "object" || !("details" in err)) return;
1931
- return err.details;
1932
- }
1933
- async function callGoogleMeetGatewayFromTool(params) {
1934
- try {
1935
- if (params.runtime) return await params.runtime.gateway.request(googleMeetGatewayMethodForToolAction(params.action), params.raw, {
1936
- timeoutMs: resolveGoogleMeetGatewayOperationTimeoutMs(params.config),
1937
- scopes: ["operator.admin"]
1938
- });
1939
- return await googleMeetToolDeps.callGatewayFromCli(googleMeetGatewayMethodForToolAction(params.action), {
1940
- json: true,
1941
- timeout: String(resolveGoogleMeetGatewayOperationTimeoutMs(params.config))
1942
- }, params.raw, {
1943
- progress: false,
1944
- scopes: ["operator.admin"]
1945
- });
1946
- } catch (err) {
1947
- const details = readGatewayErrorDetails(err);
1948
- if (details && typeof details === "object") return details;
1949
- throw err;
1950
- }
1951
- }
1952
- function keepTrustedToolAgentId(raw, client) {
1953
- const { agentId: rawAgentId, ...rest } = raw;
1954
- if (client?.internal?.pluginRuntimeOwnerId !== "google-meet") return rest;
1955
- const agentId = normalizeOptionalString(rawAgentId);
1956
- return agentId ? {
1957
- ...rest,
1958
- agentId
1959
- } : rest;
1960
- }
1961
- async function createMeetFromParams(params) {
1962
- return (await loadGoogleMeetCreateModule()).createMeetFromParams(params);
1963
- }
1964
- async function createAndJoinMeetFromParams(params) {
1965
- return (await loadGoogleMeetCreateModule()).createAndJoinMeetFromParams(params);
1966
- }
1967
- async function resolveGoogleMeetTokenFromParams(config, raw) {
1968
- const { resolveGoogleMeetAccessToken } = await import("./oauth-DOfMKlGp.js");
1969
- return resolveGoogleMeetAccessToken({
1970
- clientId: normalizeOptionalString(raw.clientId) ?? config.oauth.clientId,
1971
- clientSecret: normalizeOptionalString(raw.clientSecret) ?? config.oauth.clientSecret,
1972
- refreshToken: normalizeOptionalString(raw.refreshToken) ?? config.oauth.refreshToken,
1973
- accessToken: normalizeOptionalString(raw.accessToken) ?? config.oauth.accessToken,
1974
- expiresAt: typeof raw.expiresAt === "number" ? raw.expiresAt : config.oauth.expiresAt
1975
- });
1976
- }
1977
- function wantsCalendarLookup(raw) {
1978
- return raw.today === true || Boolean(normalizeOptionalString(raw.event));
1979
- }
1980
- async function resolveMeetingFromParams(params) {
1981
- if (wantsCalendarLookup(params.raw)) {
1982
- const window = params.raw.today === true ? buildGoogleMeetCalendarDayWindow() : {};
1983
- const calendarEvent = await findGoogleMeetCalendarEvent({
1984
- accessToken: params.accessToken,
1985
- calendarId: normalizeOptionalString(params.raw.calendarId),
1986
- eventQuery: normalizeOptionalString(params.raw.event),
1987
- ...window
1988
- });
1989
- return {
1990
- meeting: calendarEvent.meetingUri,
1991
- calendarEvent
1992
- };
1993
- }
1994
- return { meeting: resolveMeetingInput(params.config, params.raw.meeting) };
1995
- }
1996
- async function resolveSpaceFromParams(config, raw) {
1997
- const token = await resolveGoogleMeetTokenFromParams(config, raw);
1998
- const { meeting, calendarEvent } = await resolveMeetingFromParams({
1999
- config,
2000
- raw,
2001
- accessToken: token.accessToken
2002
- });
2003
- return {
2004
- meeting,
2005
- token,
2006
- space: await fetchGoogleMeetSpace({
2007
- accessToken: token.accessToken,
2008
- meeting
2009
- }),
2010
- calendarEvent
2011
- };
2012
- }
2013
- async function resolveArtifactQueryFromParams(config, raw) {
2014
- const meeting = normalizeOptionalString(raw.meeting) ?? config.defaults.meeting;
2015
- const conferenceRecord = normalizeOptionalString(raw.conferenceRecord);
2016
- const token = await resolveGoogleMeetTokenFromParams(config, raw);
2017
- const resolvedMeeting = conferenceRecord ? { meeting } : wantsCalendarLookup(raw) ? await resolveMeetingFromParams({
2018
- config,
2019
- raw,
2020
- accessToken: token.accessToken
2021
- }) : { meeting };
2022
- if (!resolvedMeeting.meeting && !conferenceRecord) throw new Error("Meeting input, calendar lookup, or conferenceRecord required");
2023
- return {
2024
- token,
2025
- meeting: resolvedMeeting.meeting,
2026
- calendarEvent: resolvedMeeting.calendarEvent,
2027
- conferenceRecord,
2028
- pageSize: readPositiveIntegerParam(raw, "pageSize"),
2029
- includeTranscriptEntries: raw.includeTranscriptEntries !== false,
2030
- includeDocumentBodies: raw.includeDocumentBodies === true,
2031
- allConferenceRecords: raw.includeAllConferenceRecords === true,
2032
- mergeDuplicateParticipants: raw.mergeDuplicateParticipants !== false,
2033
- lateAfterMinutes: readPositiveIntegerParam(raw, "lateAfterMinutes"),
2034
- earlyBeforeMinutes: readPositiveIntegerParam(raw, "earlyBeforeMinutes")
2035
- };
2036
- }
2037
- async function exportGoogleMeetBundleFromParams(config, raw) {
2038
- const resolved = await resolveArtifactQueryFromParams(config, raw);
2039
- const [artifacts, attendance] = await Promise.all([fetchGoogleMeetArtifacts({
2040
- accessToken: resolved.token.accessToken,
2041
- meeting: resolved.meeting,
2042
- conferenceRecord: resolved.conferenceRecord,
2043
- pageSize: resolved.pageSize,
2044
- includeTranscriptEntries: resolved.includeTranscriptEntries,
2045
- includeDocumentBodies: resolved.includeDocumentBodies,
2046
- allConferenceRecords: resolved.allConferenceRecords
2047
- }), fetchGoogleMeetAttendance({
2048
- accessToken: resolved.token.accessToken,
2049
- meeting: resolved.meeting,
2050
- conferenceRecord: resolved.conferenceRecord,
2051
- pageSize: resolved.pageSize,
2052
- allConferenceRecords: resolved.allConferenceRecords,
2053
- mergeDuplicateParticipants: resolved.mergeDuplicateParticipants,
2054
- lateAfterMinutes: resolved.lateAfterMinutes,
2055
- earlyBeforeMinutes: resolved.earlyBeforeMinutes
2056
- })]);
2057
- const { buildGoogleMeetExportManifest, googleMeetExportFileNames, writeMeetExportBundle } = await loadGoogleMeetCliModule();
2058
- const calendarId = normalizeOptionalString(raw.calendarId);
2059
- const request = {
2060
- ...resolved.meeting ? { meeting: resolved.meeting } : {},
2061
- ...resolved.conferenceRecord ? { conferenceRecord: resolved.conferenceRecord } : {},
2062
- ...resolved.calendarEvent?.event.id ? { calendarEventId: resolved.calendarEvent.event.id } : {},
2063
- ...resolved.calendarEvent?.event.summary ? { calendarEventSummary: resolved.calendarEvent.event.summary } : {},
2064
- ...calendarId ? { calendarId } : {},
2065
- ...resolved.pageSize !== void 0 ? { pageSize: resolved.pageSize } : {},
2066
- includeTranscriptEntries: resolved.includeTranscriptEntries,
2067
- includeDocumentBodies: resolved.includeDocumentBodies,
2068
- allConferenceRecords: resolved.allConferenceRecords,
2069
- mergeDuplicateParticipants: resolved.mergeDuplicateParticipants,
2070
- ...resolved.lateAfterMinutes !== void 0 ? { lateAfterMinutes: resolved.lateAfterMinutes } : {},
2071
- ...resolved.earlyBeforeMinutes !== void 0 ? { earlyBeforeMinutes: resolved.earlyBeforeMinutes } : {}
2072
- };
2073
- const tokenSource = resolved.token.refreshed ? "refresh-token" : "cached-access-token";
2074
- if (raw.dryRun === true) return {
2075
- dryRun: true,
2076
- manifest: buildGoogleMeetExportManifest({
2077
- artifacts,
2078
- attendance,
2079
- files: googleMeetExportFileNames(),
2080
- request,
2081
- tokenSource,
2082
- ...resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {}
2083
- }),
2084
- ...resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {},
2085
- tokenSource
2086
- };
2087
- const outputDir = normalizeOptionalString(raw.outputDir) ?? normalizeOptionalString(raw.output);
2088
- return {
2089
- ...await writeMeetExportBundle({
2090
- ...outputDir ? { outputDir } : {},
2091
- artifacts,
2092
- attendance,
2093
- zip: raw.zip === true,
2094
- request,
2095
- tokenSource,
2096
- ...resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {}
2097
- }),
2098
- ...resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {},
2099
- tokenSource
2100
- };
2101
- }
2102
- function createGoogleMeetRuntimeAccessor(params) {
2103
- let runtime = null;
2104
- return async () => {
2105
- if (!params.config.enabled) throw new Error("Google Meet plugin disabled in plugin config");
2106
- if (!runtime) runtime = new GoogleMeetRuntime({
2107
- config: params.config,
2108
- fullConfig: params.api.config,
2109
- runtime: params.api.runtime,
2110
- logger: params.api.logger
2111
- });
2112
- return runtime;
2113
- };
2114
- }
2115
- function formatGoogleMeetGatewayError(err) {
2116
- return isGoogleMeetBrowserManualActionError(err) ? err.payload : { error: formatErrorMessage(err) };
2117
- }
2118
- function sendGoogleMeetGatewayError(respond, err, code = ErrorCodes.UNAVAILABLE) {
2119
- const payload = formatGoogleMeetGatewayError(err);
2120
- respond(false, payload, errorShape(code, typeof payload.error === "string" ? payload.error : "Google Meet request failed", { details: payload }));
2121
- }
2122
- //#endregion
2123
10
  //#region extensions/google-meet/src/plugin-schema.ts
2124
- const googleMeetConfigSchema = {
2125
- parse(value) {
2126
- return resolveGoogleMeetConfig(value);
2127
- },
2128
- uiHints: {
2129
- "defaults.meeting": {
2130
- label: "Default Meeting",
2131
- help: "Meet URL, meeting code, or spaces/{id} used when CLI commands omit a meeting."
2132
- },
2133
- "preview.enrollmentAcknowledged": {
2134
- label: "Preview Acknowledged",
2135
- help: "Confirms you understand the Google Meet Media API is still Developer Preview.",
2136
- advanced: true
2137
- },
2138
- defaultTransport: {
2139
- label: "Default Transport",
2140
- help: "Chrome uses a signed-in browser profile. Chrome-node runs Chrome on a paired node. Twilio uses Meet dial-in numbers."
2141
- },
2142
- defaultMode: {
2143
- label: "Default Mode",
2144
- help: "Agent uses realtime transcription plus regular OpenClaw TTS. Bidi uses the realtime voice model directly. Transcribe observes only."
2145
- },
2146
- "chrome.audioBackend": {
2147
- label: "Chrome Audio Backend",
2148
- help: "BlackHole 2ch is required for local duplex audio routing."
2149
- },
2150
- "chrome.launch": { label: "Launch Chrome" },
2151
- "chrome.browserProfile": {
2152
- label: "Chrome Profile",
2153
- advanced: true
2154
- },
2155
- "chrome.guestName": {
2156
- label: "Guest Name",
2157
- help: "Used when Chrome lands on the signed-out Meet guest-name screen."
2158
- },
2159
- "chrome.reuseExistingTab": {
2160
- label: "Reuse Existing Meet Tab",
2161
- help: "Avoids opening duplicate tabs for the same Meet URL."
2162
- },
2163
- "chrome.autoJoin": {
2164
- label: "Auto Join Guest Screen",
2165
- help: "Best-effort guest-name fill and Join Now click through OpenClaw browser automation."
2166
- },
2167
- "chrome.waitForInCallMs": {
2168
- label: "Wait For In-Call (ms)",
2169
- help: "Waits for Chrome to report that the Meet tab is in-call before the realtime intro speaks.",
2170
- advanced: true
2171
- },
2172
- "chrome.audioFormat": {
2173
- label: "Audio Format",
2174
- help: "Command-pair audio format. PCM16 24 kHz is the default Chrome/Meet path; G.711 mu-law 8 kHz remains available for legacy command pairs.",
2175
- advanced: true
2176
- },
2177
- "chrome.audioBufferBytes": {
2178
- label: "Audio Buffer Bytes",
2179
- help: "SoX processing buffer for generated Chrome command-pair audio commands. Lower values reduce latency but may underrun on busy hosts.",
2180
- advanced: true
2181
- },
2182
- "chrome.audioInputCommand": {
2183
- label: "Audio Input Command",
2184
- help: "Command that writes meeting audio to stdout in chrome.audioFormat.",
2185
- advanced: true
2186
- },
2187
- "chrome.audioOutputCommand": {
2188
- label: "Audio Output Command",
2189
- help: "Command that reads assistant audio from stdin in chrome.audioFormat.",
2190
- advanced: true
2191
- },
2192
- "chrome.bargeInInputCommand": {
2193
- label: "Barge-In Input Command",
2194
- help: "Optional Gateway-hosted microphone command that writes signed 16-bit little-endian mono PCM for human interruption detection while assistant playback is active.",
2195
- advanced: true
2196
- },
2197
- "chrome.bargeInRmsThreshold": {
2198
- label: "Barge-In RMS Threshold",
2199
- help: "RMS level on chrome.bargeInInputCommand that counts as a human interruption.",
2200
- advanced: true
2201
- },
2202
- "chrome.bargeInPeakThreshold": {
2203
- label: "Barge-In Peak Threshold",
2204
- help: "Peak level on chrome.bargeInInputCommand that counts as a human interruption.",
2205
- advanced: true
2206
- },
2207
- "chrome.bargeInCooldownMs": {
2208
- label: "Barge-In Cooldown (ms)",
2209
- help: "Minimum delay between repeated barge-in clears.",
2210
- advanced: true
2211
- },
2212
- "chrome.audioBridgeCommand": {
2213
- label: "Audio Bridge Command",
2214
- advanced: true
2215
- },
2216
- "chrome.audioBridgeHealthCommand": {
2217
- label: "Audio Bridge Health Command",
2218
- advanced: true
2219
- },
2220
- "chromeNode.node": {
2221
- label: "Chrome Node",
2222
- help: "Node id/name/IP that owns Chrome, BlackHole, and SoX for chrome-node transport.",
2223
- advanced: true
2224
- },
2225
- "twilio.defaultDialInNumber": {
2226
- label: "Default Dial-In Number",
2227
- placeholder: "+15551234567"
2228
- },
2229
- "twilio.defaultPin": {
2230
- label: "Default PIN",
2231
- advanced: true
2232
- },
2233
- "twilio.defaultDtmfSequence": {
2234
- label: "Default DTMF Sequence",
2235
- advanced: true
2236
- },
2237
- "voiceCall.enabled": { label: "Delegate To Voice Call" },
2238
- "voiceCall.gatewayUrl": {
2239
- label: "Voice Call Gateway URL",
2240
- advanced: true
2241
- },
2242
- "voiceCall.token": {
2243
- label: "Voice Call Gateway Token",
2244
- sensitive: true,
2245
- advanced: true
2246
- },
2247
- "voiceCall.requestTimeoutMs": {
2248
- label: "Voice Call Request Timeout (ms)",
2249
- advanced: true
2250
- },
2251
- "voiceCall.dtmfDelayMs": {
2252
- label: "DTMF Wait Before PIN (ms)",
2253
- help: "Leading Twilio wait time before playing a PIN-derived Meet DTMF sequence. Increase it if Meet asks for the PIN after DTMF was sent.",
2254
- advanced: true
2255
- },
2256
- "voiceCall.postDtmfSpeechDelayMs": {
2257
- label: "Post-DTMF Speech Delay (ms)",
2258
- help: "Delay before requesting the realtime intro greeting after Voice Call starts the Twilio leg.",
2259
- advanced: true
2260
- },
2261
- "voiceCall.introMessage": {
2262
- label: "Voice Call Intro Message",
2263
- advanced: true
2264
- },
2265
- "realtime.strategy": {
2266
- label: "Realtime Strategy",
2267
- help: "Legacy realtime alias setting. Use mode=agent or mode=bidi for new Meet joins."
2268
- },
2269
- "realtime.provider": {
2270
- label: "Speech Provider",
2271
- help: "Compatibility fallback for both realtime transcription and bidi voice. Prefer realtime.transcriptionProvider and realtime.voiceProvider for new configs."
2272
- },
2273
- "realtime.transcriptionProvider": {
2274
- label: "Realtime Transcription Provider",
2275
- help: "Agent mode uses this provider to transcribe meeting audio before regular OpenClaw TTS answers."
2276
- },
2277
- "realtime.voiceProvider": {
2278
- label: "Bidi Voice Provider",
2279
- help: "Bidi mode uses this realtime voice provider. Falls back to realtime.provider when unset."
2280
- },
2281
- "realtime.model": {
2282
- label: "Bidi Realtime Model",
2283
- help: "Only used by mode=bidi. Agent mode answers with the configured OpenClaw agent and regular TTS.",
2284
- advanced: true
2285
- },
2286
- "realtime.instructions": {
2287
- label: "Realtime Instructions",
2288
- advanced: true
2289
- },
2290
- "realtime.introMessage": {
2291
- label: "Realtime Intro Message",
2292
- help: "Spoken once when the realtime bridge is ready. Set to an empty string to join silently."
2293
- },
2294
- "realtime.agentId": {
2295
- label: "Realtime Consult Agent",
2296
- help: "OpenClaw agent id used by openclaw_agent_consult. Defaults to \"main\".",
2297
- advanced: true
2298
- },
2299
- "realtime.toolPolicy": {
2300
- label: "Realtime Tool Policy",
2301
- help: "Safe read-only tools are available by default; owner requests can unlock broader tools.",
2302
- advanced: true
2303
- },
2304
- "oauth.clientId": { label: "OAuth Client ID" },
2305
- "oauth.clientSecret": {
2306
- label: "OAuth Client Secret",
2307
- sensitive: true
2308
- },
2309
- "oauth.refreshToken": {
2310
- label: "OAuth Refresh Token",
2311
- sensitive: true
2312
- },
2313
- "oauth.accessToken": {
2314
- label: "Cached Access Token",
2315
- sensitive: true,
2316
- advanced: true
2317
- },
2318
- "oauth.expiresAt": {
2319
- label: "Cached Access Token Expiry",
2320
- help: "Unix epoch milliseconds used only for the cached access-token fast path.",
2321
- advanced: true
2322
- }
2323
- }
2324
- };
11
+ const googleMeetConfigSchema = { parse(value) {
12
+ return resolveGoogleMeetConfig(value);
13
+ } };
2325
14
  const GoogleMeetToolSchema = Type.Object({
2326
15
  action: Type.String({
2327
16
  enum: [
@@ -2420,247 +109,186 @@ var google_meet_default = definePluginEntry({
2420
109
  api,
2421
110
  config
2422
111
  });
2423
- api.registerTranscriptSourceProvider(createMeetingTranscriptSourceProvider({
2424
- id: "google-meet",
2425
- aliases: ["googlemeet", "meet"],
2426
- name: "Google Meet",
2427
- runtime: async () => (await ensureRuntime()).transcriptSourceRuntime()
2428
- }));
2429
- api.registerGatewayMethod("googlemeet.join", async ({ params, client, respond }) => {
2430
- try {
2431
- const trustedParams = keepTrustedToolAgentId(asParamRecord(params), client);
2432
- respond(true, await (await ensureRuntime()).join({
2433
- url: resolveMeetingInput(config, trustedParams.url),
2434
- transport: normalizeTransport(trustedParams.transport),
2435
- mode: normalizeMode(trustedParams.mode),
2436
- dialInNumber: normalizeOptionalString(trustedParams.dialInNumber),
2437
- pin: normalizeOptionalString(trustedParams.pin),
2438
- dtmfSequence: normalizeOptionalString(trustedParams.dtmfSequence),
2439
- message: normalizeOptionalString(trustedParams.message),
2440
- requesterSessionKey: normalizeOptionalString(trustedParams.requesterSessionKey),
2441
- agentId: normalizeOptionalString(trustedParams.agentId)
2442
- }));
2443
- } catch (err) {
2444
- sendGoogleMeetGatewayError(respond, err);
2445
- }
2446
- });
2447
- api.registerGatewayMethod("googlemeet.create", async ({ params, client, respond }) => {
2448
- try {
2449
- const raw = keepTrustedToolAgentId(asParamRecord(params), client);
2450
- respond(true, shouldJoinCreatedMeet(raw) ? await createAndJoinMeetFromParams({
2451
- config,
2452
- runtime: api.runtime,
2453
- raw,
2454
- ensureRuntime
2455
- }) : await createMeetFromParams({
2456
- config,
2457
- runtime: api.runtime,
2458
- raw
2459
- }));
2460
- } catch (err) {
2461
- sendGoogleMeetGatewayError(respond, err);
2462
- }
2463
- });
2464
- api.registerGatewayMethod("googlemeet.status", async ({ params, respond }) => {
2465
- try {
2466
- respond(true, await (await ensureRuntime()).status(normalizeOptionalString(params?.sessionId)));
2467
- } catch (err) {
2468
- sendGoogleMeetGatewayError(respond, err);
2469
- }
2470
- });
2471
- api.registerGatewayMethod("googlemeet.transcript", async ({ params, respond }) => {
2472
- try {
2473
- const sessionId = normalizeOptionalString(params?.sessionId);
2474
- if (!sessionId) {
2475
- sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sessionId required"), ErrorCodes.INVALID_REQUEST);
2476
- return;
2477
- }
2478
- const sinceIndex = params?.sinceIndex;
2479
- if (sinceIndex !== void 0 && (typeof sinceIndex !== "number" || !Number.isSafeInteger(sinceIndex) || sinceIndex < 0)) {
2480
- sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sinceIndex must be a non-negative safe integer"), ErrorCodes.INVALID_REQUEST);
2481
- return;
112
+ const registerGatewayMethod = (method, handler) => {
113
+ api.registerGatewayMethod(method, async (options) => {
114
+ try {
115
+ await handler(options);
116
+ } catch (err) {
117
+ sendGoogleMeetGatewayError(options.respond, err);
2482
118
  }
2483
- respond(true, await (await ensureRuntime()).transcript(sessionId, sinceIndex === void 0 ? {} : { sinceIndex }));
2484
- } catch (err) {
2485
- sendGoogleMeetGatewayError(respond, err);
2486
- }
2487
- });
2488
- api.registerGatewayMethod("googlemeet.recoverCurrentTab", async ({ params, respond }) => {
2489
- try {
2490
- respond(true, await (await ensureRuntime()).recoverCurrentTab({
2491
- url: normalizeOptionalString(params?.url),
2492
- transport: normalizeTransport(params?.transport)
2493
- }));
2494
- } catch (err) {
2495
- sendGoogleMeetGatewayError(respond, err);
2496
- }
2497
- });
2498
- api.registerGatewayMethod("googlemeet.setup", async ({ params, respond }) => {
2499
- try {
2500
- respond(true, await (await ensureRuntime()).setupStatus({
2501
- transport: normalizeTransport(params?.transport),
2502
- mode: normalizeMode(params?.mode),
2503
- dialInNumber: normalizeOptionalString(params?.dialInNumber)
2504
- }));
2505
- } catch (err) {
2506
- sendGoogleMeetGatewayError(respond, err);
2507
- }
2508
- });
2509
- api.registerGatewayMethod("googlemeet.latest", async ({ params, respond }) => {
2510
- try {
2511
- const raw = asParamRecord(params);
2512
- const token = await resolveGoogleMeetTokenFromParams(config, raw);
2513
- const resolved = await resolveMeetingFromParams({
119
+ });
120
+ };
121
+ const resolveTrustedJoinParams = ({ params, client }) => {
122
+ const trustedParams = keepTrustedToolAgentId(asParamRecord(params), client);
123
+ return {
124
+ url: resolveMeetingInput(config, trustedParams.url),
125
+ transport: normalizeTransport(trustedParams.transport),
126
+ mode: normalizeMode(trustedParams.mode),
127
+ dialInNumber: normalizeOptionalString(trustedParams.dialInNumber),
128
+ pin: normalizeOptionalString(trustedParams.pin),
129
+ dtmfSequence: normalizeOptionalString(trustedParams.dtmfSequence),
130
+ message: normalizeOptionalString(trustedParams.message),
131
+ requesterSessionKey: normalizeOptionalString(trustedParams.requesterSessionKey),
132
+ agentId: normalizeOptionalString(trustedParams.agentId)
133
+ };
134
+ };
135
+ const queryActions = {
136
+ latest: async (raw) => {
137
+ const helpers = await loadGoogleMeetPluginHelpers();
138
+ const token = await helpers.resolveGoogleMeetTokenFromParams(config, raw);
139
+ const resolved = await helpers.resolveMeetingFromParams({
2514
140
  config,
2515
141
  raw,
2516
142
  accessToken: token.accessToken
2517
143
  });
2518
- respond(true, {
2519
- ...await fetchLatestGoogleMeetConferenceRecord({
144
+ return {
145
+ ...await helpers.fetchLatestGoogleMeetConferenceRecord({
2520
146
  accessToken: token.accessToken,
2521
147
  meeting: resolved.meeting
2522
148
  }),
2523
149
  ...resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {}
2524
- });
2525
- } catch (err) {
2526
- sendGoogleMeetGatewayError(respond, err);
2527
- }
2528
- });
2529
- api.registerGatewayMethod("googlemeet.calendarEvents", async ({ params, respond }) => {
2530
- try {
2531
- const raw = asParamRecord(params);
2532
- const token = await resolveGoogleMeetTokenFromParams(config, raw);
2533
- const window = raw.today === true ? buildGoogleMeetCalendarDayWindow() : {};
2534
- respond(true, await listGoogleMeetCalendarEvents({
150
+ };
151
+ },
152
+ calendar_events: async (raw) => {
153
+ const helpers = await loadGoogleMeetPluginHelpers();
154
+ const token = await helpers.resolveGoogleMeetTokenFromParams(config, raw);
155
+ const window = raw.today === true ? helpers.buildGoogleMeetCalendarDayWindow() : {};
156
+ return helpers.listGoogleMeetCalendarEvents({
2535
157
  accessToken: token.accessToken,
2536
158
  calendarId: normalizeOptionalString(raw.calendarId),
2537
159
  eventQuery: normalizeOptionalString(raw.event),
2538
160
  ...window
2539
- }));
2540
- } catch (err) {
2541
- sendGoogleMeetGatewayError(respond, err);
2542
- }
2543
- });
2544
- api.registerGatewayMethod("googlemeet.artifacts", async ({ params, respond }) => {
2545
- try {
2546
- const raw = asParamRecord(params);
2547
- const resolved = await resolveArtifactQueryFromParams(config, raw);
2548
- respond(true, await fetchGoogleMeetArtifacts({
2549
- accessToken: resolved.token.accessToken,
2550
- meeting: resolved.meeting,
2551
- conferenceRecord: resolved.conferenceRecord,
2552
- pageSize: resolved.pageSize,
2553
- includeTranscriptEntries: resolved.includeTranscriptEntries,
2554
- includeDocumentBodies: resolved.includeDocumentBodies,
2555
- allConferenceRecords: resolved.allConferenceRecords
2556
- }));
2557
- } catch (err) {
2558
- sendGoogleMeetGatewayError(respond, err);
2559
- }
2560
- });
2561
- api.registerGatewayMethod("googlemeet.attendance", async ({ params, respond }) => {
2562
- try {
2563
- const raw = asParamRecord(params);
2564
- const resolved = await resolveArtifactQueryFromParams(config, raw);
2565
- respond(true, await fetchGoogleMeetAttendance({
2566
- accessToken: resolved.token.accessToken,
2567
- meeting: resolved.meeting,
2568
- conferenceRecord: resolved.conferenceRecord,
2569
- pageSize: resolved.pageSize,
2570
- allConferenceRecords: resolved.allConferenceRecords,
2571
- mergeDuplicateParticipants: resolved.mergeDuplicateParticipants,
2572
- lateAfterMinutes: resolved.lateAfterMinutes,
2573
- earlyBeforeMinutes: resolved.earlyBeforeMinutes
2574
- }));
2575
- } catch (err) {
2576
- sendGoogleMeetGatewayError(respond, err);
2577
- }
2578
- });
2579
- api.registerGatewayMethod("googlemeet.export", async ({ params, respond }) => {
2580
- try {
2581
- respond(true, await exportGoogleMeetBundleFromParams(config, asParamRecord(params)));
2582
- } catch (err) {
2583
- sendGoogleMeetGatewayError(respond, err);
2584
- }
2585
- });
2586
- api.registerGatewayMethod("googlemeet.leave", async ({ params, respond }) => {
2587
- try {
2588
- const sessionId = normalizeOptionalString(params?.sessionId);
2589
- if (!sessionId) {
2590
- sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sessionId required"), ErrorCodes.INVALID_REQUEST);
2591
- return;
2592
- }
2593
- respond(true, await (await ensureRuntime()).leave(sessionId));
2594
- } catch (err) {
2595
- sendGoogleMeetGatewayError(respond, err);
161
+ });
162
+ },
163
+ artifacts: async (raw) => {
164
+ const helpers = await loadGoogleMeetPluginHelpers();
165
+ return helpers.fetchResolvedGoogleMeetArtifacts(await helpers.resolveArtifactQueryFromParams(config, raw));
166
+ },
167
+ attendance: async (raw) => {
168
+ const helpers = await loadGoogleMeetPluginHelpers();
169
+ return helpers.fetchResolvedGoogleMeetAttendance(await helpers.resolveArtifactQueryFromParams(config, raw));
2596
170
  }
2597
- });
2598
- api.registerGatewayMethod("googlemeet.endActiveConference", async ({ params, respond }) => {
2599
- try {
2600
- const raw = asParamRecord(params);
2601
- respond(true, await endGoogleMeetActiveConference({
2602
- accessToken: (await resolveGoogleMeetTokenFromParams(config, raw)).accessToken,
2603
- meeting: resolveMeetingInput(config, raw.meeting)
2604
- }));
2605
- } catch (err) {
2606
- sendGoogleMeetGatewayError(respond, err);
171
+ };
172
+ const transcriptSourceRuntime = async () => (await ensureRuntime()).transcriptSourceRuntime();
173
+ api.registerTranscriptSourceProvider({
174
+ id: "google-meet",
175
+ aliases: ["googlemeet", "meet"],
176
+ name: "Google Meet",
177
+ sourceKinds: ["live-caption"],
178
+ start: async (request) => await (await transcriptSourceRuntime()).startTranscriptSource(request),
179
+ stop: async (request) => await (await transcriptSourceRuntime()).stopTranscriptSource(request)
180
+ });
181
+ registerGatewayMethod("googlemeet.join", async (options) => {
182
+ const runtime = await ensureRuntime();
183
+ options.respond(true, await runtime.join(resolveTrustedJoinParams(options)));
184
+ });
185
+ registerGatewayMethod("googlemeet.create", async ({ params, client, respond }) => {
186
+ const raw = keepTrustedToolAgentId(asParamRecord(params), client);
187
+ const helpers = await loadGoogleMeetPluginHelpers();
188
+ respond(true, shouldJoinCreatedMeet(raw) ? await helpers.createAndJoinMeetFromParams({
189
+ config,
190
+ runtime: api.runtime,
191
+ raw,
192
+ ensureRuntime
193
+ }) : await helpers.createMeetFromParams({
194
+ config,
195
+ runtime: api.runtime,
196
+ raw
197
+ }));
198
+ });
199
+ registerGatewayMethod("googlemeet.status", async ({ params, respond }) => {
200
+ respond(true, await (await ensureRuntime()).status(normalizeOptionalString(params?.sessionId)));
201
+ });
202
+ registerGatewayMethod("googlemeet.transcript", async ({ params, respond }) => {
203
+ const sessionId = normalizeOptionalString(params?.sessionId);
204
+ if (!sessionId) {
205
+ sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sessionId required"), "INVALID_REQUEST");
206
+ return;
2607
207
  }
2608
- });
2609
- api.registerGatewayMethod("googlemeet.speak", async ({ params, respond }) => {
2610
- try {
2611
- const sessionId = normalizeOptionalString(params?.sessionId);
2612
- if (!sessionId) {
2613
- sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sessionId required"), ErrorCodes.INVALID_REQUEST);
2614
- return;
2615
- }
2616
- respond(true, await (await ensureRuntime()).speak(sessionId, normalizeOptionalString(params?.message)));
2617
- } catch (err) {
2618
- sendGoogleMeetGatewayError(respond, err);
208
+ const sinceIndex = params?.sinceIndex;
209
+ if (sinceIndex !== void 0 && (typeof sinceIndex !== "number" || !Number.isSafeInteger(sinceIndex) || sinceIndex < 0)) {
210
+ sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sinceIndex must be a non-negative safe integer"), "INVALID_REQUEST");
211
+ return;
2619
212
  }
2620
- });
2621
- api.registerGatewayMethod("googlemeet.testSpeech", async ({ params, client, respond }) => {
2622
- try {
2623
- const trustedParams = keepTrustedToolAgentId(asParamRecord(params), client);
2624
- respond(true, await (await ensureRuntime()).testSpeech({
2625
- url: resolveMeetingInput(config, trustedParams.url),
2626
- transport: normalizeTransport(trustedParams.transport),
2627
- mode: normalizeMode(trustedParams.mode),
2628
- dialInNumber: normalizeOptionalString(trustedParams.dialInNumber),
2629
- pin: normalizeOptionalString(trustedParams.pin),
2630
- dtmfSequence: normalizeOptionalString(trustedParams.dtmfSequence),
2631
- message: normalizeOptionalString(trustedParams.message),
2632
- requesterSessionKey: normalizeOptionalString(trustedParams.requesterSessionKey),
2633
- agentId: normalizeOptionalString(trustedParams.agentId)
2634
- }));
2635
- } catch (err) {
2636
- sendGoogleMeetGatewayError(respond, err);
213
+ respond(true, await (await ensureRuntime()).transcript(sessionId, sinceIndex === void 0 ? {} : { sinceIndex }));
214
+ });
215
+ registerGatewayMethod("googlemeet.recoverCurrentTab", async ({ params, respond }) => {
216
+ respond(true, await (await ensureRuntime()).recoverCurrentTab({
217
+ url: normalizeOptionalString(params?.url),
218
+ transport: normalizeTransport(params?.transport)
219
+ }));
220
+ });
221
+ registerGatewayMethod("googlemeet.setup", async ({ params, respond }) => {
222
+ respond(true, await (await ensureRuntime()).setupStatus({
223
+ transport: normalizeTransport(params?.transport),
224
+ mode: normalizeMode(params?.mode),
225
+ dialInNumber: normalizeOptionalString(params?.dialInNumber)
226
+ }));
227
+ });
228
+ for (const [method, action] of [
229
+ ["googlemeet.latest", "latest"],
230
+ ["googlemeet.calendarEvents", "calendar_events"],
231
+ ["googlemeet.artifacts", "artifacts"],
232
+ ["googlemeet.attendance", "attendance"]
233
+ ]) registerGatewayMethod(method, async ({ params, respond }) => {
234
+ respond(true, await queryActions[action](asParamRecord(params)));
235
+ });
236
+ registerGatewayMethod("googlemeet.export", async ({ params, respond }) => {
237
+ respond(true, await (await loadGoogleMeetPluginHelpers()).exportGoogleMeetBundleFromParams(config, asParamRecord(params)));
238
+ });
239
+ registerGatewayMethod("googlemeet.leave", async ({ params, respond }) => {
240
+ const sessionId = normalizeOptionalString(params?.sessionId);
241
+ if (!sessionId) {
242
+ sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sessionId required"), "INVALID_REQUEST");
243
+ return;
2637
244
  }
2638
- });
2639
- api.registerGatewayMethod("googlemeet.testListen", async ({ params, client, respond }) => {
2640
- try {
2641
- const trustedParams = keepTrustedToolAgentId(asParamRecord(params), client);
2642
- respond(true, await (await ensureRuntime()).testListen({
2643
- url: resolveMeetingInput(config, trustedParams.url),
2644
- transport: normalizeTransport(trustedParams.transport),
2645
- mode: normalizeMode(trustedParams.mode),
2646
- agentId: normalizeOptionalString(trustedParams.agentId),
2647
- timeoutMs: readPositiveIntegerParam(trustedParams, "timeoutMs")
2648
- }));
2649
- } catch (err) {
2650
- sendGoogleMeetGatewayError(respond, err);
245
+ respond(true, await (await ensureRuntime()).leave(sessionId));
246
+ });
247
+ registerGatewayMethod("googlemeet.endActiveConference", async ({ params, respond }) => {
248
+ const raw = asParamRecord(params);
249
+ const helpers = await loadGoogleMeetPluginHelpers();
250
+ const token = await helpers.resolveGoogleMeetTokenFromParams(config, raw);
251
+ respond(true, await helpers.endGoogleMeetActiveConference({
252
+ accessToken: token.accessToken,
253
+ meeting: resolveMeetingInput(config, raw.meeting)
254
+ }));
255
+ });
256
+ registerGatewayMethod("googlemeet.speak", async ({ params, respond }) => {
257
+ const sessionId = normalizeOptionalString(params?.sessionId);
258
+ if (!sessionId) {
259
+ sendGoogleMeetGatewayError(respond, /* @__PURE__ */ new Error("sessionId required"), "INVALID_REQUEST");
260
+ return;
2651
261
  }
262
+ respond(true, await (await ensureRuntime()).speak(sessionId, normalizeOptionalString(params?.message)));
263
+ });
264
+ registerGatewayMethod("googlemeet.testSpeech", async (options) => {
265
+ const runtime = await ensureRuntime();
266
+ options.respond(true, await runtime.testSpeech(resolveTrustedJoinParams(options)));
267
+ });
268
+ registerGatewayMethod("googlemeet.testListen", async ({ params, client, respond }) => {
269
+ const trustedParams = keepTrustedToolAgentId(asParamRecord(params), client);
270
+ const runtime = await ensureRuntime();
271
+ const { readPositiveIntegerParam } = await import("openclaw/plugin-sdk/param-readers");
272
+ respond(true, await runtime.testListen({
273
+ url: resolveMeetingInput(config, trustedParams.url),
274
+ transport: normalizeTransport(trustedParams.transport),
275
+ mode: normalizeMode(trustedParams.mode),
276
+ agentId: normalizeOptionalString(trustedParams.agentId),
277
+ timeoutMs: readPositiveIntegerParam(trustedParams, "timeoutMs")
278
+ }));
2652
279
  });
2653
280
  api.registerTool((toolContext) => ({
2654
281
  name: "google_meet",
2655
282
  label: "Google Meet",
2656
- description: "Join and track Google Meet sessions through Chrome or Twilio. Call setup_status before join/create/test_listen/test_speech; if it reports a Chrome node offline, local audio missing, or missing Twilio dial plan, surface that blocker instead of retrying or switching transports. Twilio cannot dial a Meet URL directly: provide dialInNumber plus optional pin/dtmfSequence, or configure twilio.defaultDialInNumber. Offline nodes are diagnostics only, not usable candidates. If local Chrome talk-back audio is unsupported on this OS, use mode=transcribe, transport=twilio, or a macOS chrome-node for agent/bidi Chrome. If a Meet tab is already open after a timeout, call recover_current_tab before retrying join to report login, permission, or admission blockers without opening another tab.",
283
+ description: "Join and track Google Meet sessions through Chrome or Twilio. Call setup_status before join/create/test_listen/test_speech; if it reports a Chrome node offline, local audio missing, or missing Twilio dial plan, surface that blocker instead of retrying or switching transports. Twilio cannot dial a Meet URL directly: provide dialInNumber plus optional pin/dtmfSequence, or configure twilio.defaultDialInNumber. Offline nodes are diagnostics only, not usable candidates. Local Chrome talk-back needs macOS with BlackHole 2ch or Linux with PipeWire-Pulse; otherwise use mode=transcribe, transport=twilio, or a supported chrome-node. If a Meet tab is already open after a timeout, call recover_current_tab before retrying join to report login, permission, or admission blockers without opening another tab.",
2657
284
  parameters: GoogleMeetToolSchema,
2658
285
  async execute(_toolCallId, params) {
2659
286
  const raw = asParamRecord(params);
2660
287
  const requesterSessionKey = normalizeOptionalString(toolContext.sessionKey);
2661
- const contextAgentId = toolContext.agentId ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
2662
- const agentId = contextAgentId ? normalizeAgentId(contextAgentId) : void 0;
2663
288
  try {
289
+ const { normalizeAgentId, parseAgentSessionKey } = await import("openclaw/plugin-sdk/routing");
290
+ const contextAgentId = toolContext.agentId ?? parseAgentSessionKey(requesterSessionKey)?.agentId;
291
+ const agentId = contextAgentId ? normalizeAgentId(contextAgentId) : void 0;
2664
292
  const needsTrustedAgentRouting = Boolean(agentId && agentId !== "main");
2665
293
  const useTrustedRuntime = needsTrustedAgentRouting ? await api.runtime.gateway.isAvailable() : false;
2666
294
  if (needsTrustedAgentRouting && !useTrustedRuntime) throw new Error("Per-agent Google Meet routing requires a Gateway-hosted agent run.");
@@ -2674,131 +302,49 @@ var google_meet_default = definePluginEntry({
2674
302
  raw
2675
303
  });
2676
304
  switch (raw.action) {
2677
- case "join": return jsonResult(await callGoogleMeetGatewayFromTool({
2678
- config,
2679
- action: "join",
2680
- raw: rawWithRequester,
2681
- runtime: useTrustedRuntime ? api.runtime : void 0
2682
- }));
2683
- case "create": return jsonResult(await callGoogleMeetGatewayFromTool({
2684
- config,
2685
- action: "create",
2686
- raw: rawWithRequester,
2687
- runtime: useTrustedRuntime ? api.runtime : void 0
2688
- }));
2689
- case "test_speech": return jsonResult(await callGoogleMeetGatewayFromTool({
2690
- config,
2691
- action: "test_speech",
2692
- raw: rawWithRequester,
2693
- runtime: useTrustedRuntime ? api.runtime : void 0
2694
- }));
305
+ case "join":
306
+ case "create":
307
+ case "test_speech":
2695
308
  case "test_listen": return jsonResult(await callGoogleMeetGatewayFromTool({
2696
309
  config,
2697
- action: "test_listen",
310
+ action: raw.action,
2698
311
  raw: rawWithRequester,
2699
312
  runtime: useTrustedRuntime ? api.runtime : void 0
2700
313
  }));
2701
- case "status": return jsonResult(await callGoogleMeetGatewayFromTool({
2702
- config,
2703
- action: "status",
2704
- raw
2705
- }));
2706
- case "transcript": return jsonResult(await callGoogleMeetGatewayFromTool({
2707
- config,
2708
- action: "transcript",
2709
- raw
2710
- }));
2711
- case "recover_current_tab": return jsonResult(await callGoogleMeetGatewayFromTool({
2712
- config,
2713
- action: "recover_current_tab",
2714
- raw
2715
- }));
2716
- case "setup_status": return jsonResult(await callGoogleMeetGatewayFromTool({
314
+ case "status":
315
+ case "transcript":
316
+ case "recover_current_tab":
317
+ case "setup_status":
318
+ case "end_active_conference": return jsonResult(await callGoogleMeetGatewayFromTool({
2717
319
  config,
2718
- action: "setup_status",
320
+ action: raw.action,
2719
321
  raw
2720
322
  }));
2721
323
  case "resolve_space": {
2722
- const { token: _token, ...result } = await resolveSpaceFromParams(config, raw);
324
+ const { token: _token, ...result } = await (await loadGoogleMeetPluginHelpers()).resolveSpaceFromParams(config, raw);
2723
325
  return jsonResult(result);
2724
326
  }
2725
327
  case "preflight": {
2726
- const { meeting, token, space } = await resolveSpaceFromParams(config, raw);
2727
- return jsonResult(buildGoogleMeetPreflightReport({
328
+ const helpers = await loadGoogleMeetPluginHelpers();
329
+ const { meeting, token, space } = await helpers.resolveSpaceFromParams(config, raw);
330
+ return jsonResult(helpers.buildGoogleMeetPreflightReport({
2728
331
  input: meeting,
2729
332
  space,
2730
333
  previewAcknowledged: config.preview.enrollmentAcknowledged,
2731
334
  tokenSource: token.refreshed ? "refresh-token" : "cached-access-token"
2732
335
  }));
2733
336
  }
2734
- case "latest": {
2735
- const token = await resolveGoogleMeetTokenFromParams(config, raw);
2736
- const resolved = await resolveMeetingFromParams({
2737
- config,
2738
- raw,
2739
- accessToken: token.accessToken
2740
- });
2741
- return jsonResult({
2742
- ...await fetchLatestGoogleMeetConferenceRecord({
2743
- accessToken: token.accessToken,
2744
- meeting: resolved.meeting
2745
- }),
2746
- ...resolved.calendarEvent ? { calendarEvent: resolved.calendarEvent } : {}
2747
- });
2748
- }
2749
- case "calendar_events": {
2750
- const token = await resolveGoogleMeetTokenFromParams(config, raw);
2751
- const window = raw.today === true ? buildGoogleMeetCalendarDayWindow() : {};
2752
- return jsonResult(await listGoogleMeetCalendarEvents({
2753
- accessToken: token.accessToken,
2754
- calendarId: normalizeOptionalString(raw.calendarId),
2755
- eventQuery: normalizeOptionalString(raw.event),
2756
- ...window
2757
- }));
2758
- }
2759
- case "artifacts": {
2760
- const resolved = await resolveArtifactQueryFromParams(config, raw);
2761
- return jsonResult(await fetchGoogleMeetArtifacts({
2762
- accessToken: resolved.token.accessToken,
2763
- meeting: resolved.meeting,
2764
- conferenceRecord: resolved.conferenceRecord,
2765
- pageSize: resolved.pageSize,
2766
- includeTranscriptEntries: resolved.includeTranscriptEntries,
2767
- includeDocumentBodies: resolved.includeDocumentBodies,
2768
- allConferenceRecords: resolved.allConferenceRecords
2769
- }));
2770
- }
2771
- case "attendance": {
2772
- const resolved = await resolveArtifactQueryFromParams(config, raw);
2773
- return jsonResult(await fetchGoogleMeetAttendance({
2774
- accessToken: resolved.token.accessToken,
2775
- meeting: resolved.meeting,
2776
- conferenceRecord: resolved.conferenceRecord,
2777
- pageSize: resolved.pageSize,
2778
- allConferenceRecords: resolved.allConferenceRecords,
2779
- mergeDuplicateParticipants: resolved.mergeDuplicateParticipants,
2780
- lateAfterMinutes: resolved.lateAfterMinutes,
2781
- earlyBeforeMinutes: resolved.earlyBeforeMinutes
2782
- }));
2783
- }
2784
- case "export": return jsonResult(await exportGoogleMeetBundleFromParams(config, raw));
337
+ case "latest":
338
+ case "calendar_events":
339
+ case "artifacts":
340
+ case "attendance": return jsonResult(await queryActions[raw.action](raw));
341
+ case "export": return jsonResult(await (await loadGoogleMeetPluginHelpers()).exportGoogleMeetBundleFromParams(config, raw));
2785
342
  case "leave":
2786
- if (!normalizeOptionalString(raw.sessionId)) throw new Error("sessionId required");
2787
- return jsonResult(await callGoogleMeetGatewayFromTool({
2788
- config,
2789
- action: "leave",
2790
- raw
2791
- }));
2792
- case "end_active_conference": return jsonResult(await callGoogleMeetGatewayFromTool({
2793
- config,
2794
- action: "end_active_conference",
2795
- raw
2796
- }));
2797
343
  case "speak":
2798
344
  if (!normalizeOptionalString(raw.sessionId)) throw new Error("sessionId required");
2799
345
  return jsonResult(await callGoogleMeetGatewayFromTool({
2800
346
  config,
2801
- action: "speak",
347
+ action: raw.action,
2802
348
  raw
2803
349
  }));
2804
350
  default: throw new Error("unknown google_meet action");
@@ -2809,12 +355,12 @@ var google_meet_default = definePluginEntry({
2809
355
  }
2810
356
  }), { name: "google_meet" });
2811
357
  api.registerNodeHostCommand({
2812
- command: GOOGLE_MEET_CHROME_NODE_COMMAND,
358
+ command: GOOGLE_MEET_NODE_COMMAND,
2813
359
  cap: "google-meet",
2814
360
  dangerous: true,
2815
- handle: handleGoogleMeetNodeHostCommand
361
+ handle: async (paramsJSON) => await (await loadGoogleMeetNodeHostModule()).handleGoogleMeetNodeHostCommand(paramsJSON)
2816
362
  });
2817
- api.registerNodeInvokePolicy(createGoogleMeetChromeNodeInvokePolicy(config));
363
+ api.registerNodeInvokePolicy(createLazyGoogleMeetNodeInvokePolicy(config));
2818
364
  api.registerCli(async ({ program }) => {
2819
365
  const { registerGoogleMeetCli } = await loadGoogleMeetCliModule();
2820
366
  registerGoogleMeetCli({
@@ -2829,4 +375,4 @@ var google_meet_default = definePluginEntry({
2829
375
  }
2830
376
  });
2831
377
  //#endregion
2832
- export { testing as __testing, testing, google_meet_default as default };
378
+ export { google_meet_default as default };