@openclaw/google-meet 2026.7.2-beta.5 → 2026.8.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/chrome-create-PKAyxCiL.js +325 -0
  2. package/dist/{cli-GeuAaEIy.js → cli-o7RWVqDb.js} +166 -234
  3. package/dist/{config-BcaS1BDr.js → config-wBK8loBb.js} +97 -221
  4. package/dist/{create-af1MDlus.js → create-UHeaRzsH.js} +3 -14
  5. package/dist/google-meet-platform-adapter-C3J-iFRE.js +791 -0
  6. package/dist/google-meet-platform-constants-Bs5iAg3E.js +25 -0
  7. package/dist/index.js +185 -2639
  8. package/dist/meet-url-BP2Jlvjs.js +17 -0
  9. package/dist/node-host-BXtXbsMa.js +49 -0
  10. package/dist/node-invoke-policy-B0gOlLJR.js +24 -0
  11. package/dist/{oauth-DOfMKlGp.js → oauth-9sUJ68Mw.js} +3 -14
  12. package/dist/{chrome-create-DMKruRFW.js → plugin-helpers-CfeT1SYY.js} +335 -355
  13. package/dist/plugin-registration-rSOssQmm.js +141 -0
  14. package/dist/runtime-B8d1beHo.js +1308 -0
  15. package/openclaw.plugin.json +8 -47
  16. package/package.json +4 -4
  17. /package/node_modules/{core-util-is → jszip/node_modules/core-util-is}/LICENSE +0 -0
  18. /package/node_modules/{core-util-is → jszip/node_modules/core-util-is}/README.md +0 -0
  19. /package/node_modules/{core-util-is → jszip/node_modules/core-util-is}/lib/util.js +0 -0
  20. /package/node_modules/{core-util-is → jszip/node_modules/core-util-is}/package.json +0 -0
  21. /package/node_modules/{immediate → jszip/node_modules/immediate}/LICENSE.txt +0 -0
  22. /package/node_modules/{immediate → jszip/node_modules/immediate}/README.md +0 -0
  23. /package/node_modules/{immediate → jszip/node_modules/immediate}/dist/immediate.js +0 -0
  24. /package/node_modules/{immediate → jszip/node_modules/immediate}/dist/immediate.min.js +0 -0
  25. /package/node_modules/{immediate → jszip/node_modules/immediate}/lib/browser.js +0 -0
  26. /package/node_modules/{immediate → jszip/node_modules/immediate}/lib/index.js +0 -0
  27. /package/node_modules/{immediate → jszip/node_modules/immediate}/package.json +0 -0
  28. /package/node_modules/{inherits → jszip/node_modules/inherits}/LICENSE +0 -0
  29. /package/node_modules/{inherits → jszip/node_modules/inherits}/README.md +0 -0
  30. /package/node_modules/{inherits → jszip/node_modules/inherits}/inherits.js +0 -0
  31. /package/node_modules/{inherits → jszip/node_modules/inherits}/inherits_browser.js +0 -0
  32. /package/node_modules/{inherits → jszip/node_modules/inherits}/package.json +0 -0
  33. /package/node_modules/{isarray → jszip/node_modules/isarray}/.npmignore +0 -0
  34. /package/node_modules/{isarray → jszip/node_modules/isarray}/.travis.yml +0 -0
  35. /package/node_modules/{isarray → jszip/node_modules/isarray}/Makefile +0 -0
  36. /package/node_modules/{isarray → jszip/node_modules/isarray}/README.md +0 -0
  37. /package/node_modules/{isarray → jszip/node_modules/isarray}/component.json +0 -0
  38. /package/node_modules/{isarray → jszip/node_modules/isarray}/index.js +0 -0
  39. /package/node_modules/{isarray → jszip/node_modules/isarray}/package.json +0 -0
  40. /package/node_modules/{isarray → jszip/node_modules/isarray}/test.js +0 -0
  41. /package/node_modules/{lie → jszip/node_modules/lie}/README.md +0 -0
  42. /package/node_modules/{lie → jszip/node_modules/lie}/dist/lie.js +0 -0
  43. /package/node_modules/{lie → jszip/node_modules/lie}/dist/lie.min.js +0 -0
  44. /package/node_modules/{lie → jszip/node_modules/lie}/dist/lie.polyfill.js +0 -0
  45. /package/node_modules/{lie → jszip/node_modules/lie}/dist/lie.polyfill.min.js +0 -0
  46. /package/node_modules/{lie → jszip/node_modules/lie}/lib/browser.js +0 -0
  47. /package/node_modules/{lie → jszip/node_modules/lie}/lib/index.js +0 -0
  48. /package/node_modules/{lie → jszip/node_modules/lie}/license.md +0 -0
  49. /package/node_modules/{lie → jszip/node_modules/lie}/lie.d.ts +0 -0
  50. /package/node_modules/{lie → jszip/node_modules/lie}/package.json +0 -0
  51. /package/node_modules/{lie → jszip/node_modules/lie}/polyfill.js +0 -0
  52. /package/node_modules/{pako → jszip/node_modules/pako}/CHANGELOG.md +0 -0
  53. /package/node_modules/{pako → jszip/node_modules/pako}/LICENSE +0 -0
  54. /package/node_modules/{pako → jszip/node_modules/pako}/README.md +0 -0
  55. /package/node_modules/{pako → jszip/node_modules/pako}/dist/pako.js +0 -0
  56. /package/node_modules/{pako → jszip/node_modules/pako}/dist/pako.min.js +0 -0
  57. /package/node_modules/{pako → jszip/node_modules/pako}/dist/pako_deflate.js +0 -0
  58. /package/node_modules/{pako → jszip/node_modules/pako}/dist/pako_deflate.min.js +0 -0
  59. /package/node_modules/{pako → jszip/node_modules/pako}/dist/pako_inflate.js +0 -0
  60. /package/node_modules/{pako → jszip/node_modules/pako}/dist/pako_inflate.min.js +0 -0
  61. /package/node_modules/{pako → jszip/node_modules/pako}/index.js +0 -0
  62. /package/node_modules/{pako → jszip/node_modules/pako}/lib/deflate.js +0 -0
  63. /package/node_modules/{pako → jszip/node_modules/pako}/lib/inflate.js +0 -0
  64. /package/node_modules/{pako → jszip/node_modules/pako}/lib/utils/common.js +0 -0
  65. /package/node_modules/{pako → jszip/node_modules/pako}/lib/utils/strings.js +0 -0
  66. /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/README +0 -0
  67. /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/adler32.js +0 -0
  68. /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/constants.js +0 -0
  69. /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/crc32.js +0 -0
  70. /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/deflate.js +0 -0
  71. /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/gzheader.js +0 -0
  72. /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/inffast.js +0 -0
  73. /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/inflate.js +0 -0
  74. /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/inftrees.js +0 -0
  75. /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/messages.js +0 -0
  76. /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/trees.js +0 -0
  77. /package/node_modules/{pako → jszip/node_modules/pako}/lib/zlib/zstream.js +0 -0
  78. /package/node_modules/{pako → jszip/node_modules/pako}/package.json +0 -0
  79. /package/node_modules/{process-nextick-args → jszip/node_modules/process-nextick-args}/index.js +0 -0
  80. /package/node_modules/{process-nextick-args → jszip/node_modules/process-nextick-args}/license.md +0 -0
  81. /package/node_modules/{process-nextick-args → jszip/node_modules/process-nextick-args}/package.json +0 -0
  82. /package/node_modules/{process-nextick-args → jszip/node_modules/process-nextick-args}/readme.md +0 -0
  83. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/.travis.yml +0 -0
  84. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/CONTRIBUTING.md +0 -0
  85. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/GOVERNANCE.md +0 -0
  86. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/LICENSE +0 -0
  87. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/README.md +0 -0
  88. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/doc/wg-meetings/2015-01-30.md +0 -0
  89. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/duplex-browser.js +0 -0
  90. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/duplex.js +0 -0
  91. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/_stream_duplex.js +0 -0
  92. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/_stream_passthrough.js +0 -0
  93. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/_stream_readable.js +0 -0
  94. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/_stream_transform.js +0 -0
  95. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/_stream_writable.js +0 -0
  96. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/internal/streams/BufferList.js +0 -0
  97. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/internal/streams/destroy.js +0 -0
  98. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/internal/streams/stream-browser.js +0 -0
  99. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/lib/internal/streams/stream.js +0 -0
  100. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/package.json +0 -0
  101. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/passthrough.js +0 -0
  102. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/readable-browser.js +0 -0
  103. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/readable.js +0 -0
  104. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/transform.js +0 -0
  105. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/writable-browser.js +0 -0
  106. /package/node_modules/{readable-stream → jszip/node_modules/readable-stream}/writable.js +0 -0
  107. /package/node_modules/{safe-buffer → jszip/node_modules/safe-buffer}/LICENSE +0 -0
  108. /package/node_modules/{safe-buffer → jszip/node_modules/safe-buffer}/README.md +0 -0
  109. /package/node_modules/{safe-buffer → jszip/node_modules/safe-buffer}/index.d.ts +0 -0
  110. /package/node_modules/{safe-buffer → jszip/node_modules/safe-buffer}/index.js +0 -0
  111. /package/node_modules/{safe-buffer → jszip/node_modules/safe-buffer}/package.json +0 -0
  112. /package/node_modules/{setimmediate → jszip/node_modules/setimmediate}/LICENSE.txt +0 -0
  113. /package/node_modules/{setimmediate → jszip/node_modules/setimmediate}/package.json +0 -0
  114. /package/node_modules/{setimmediate → jszip/node_modules/setimmediate}/setImmediate.js +0 -0
  115. /package/node_modules/{string_decoder → jszip/node_modules/string_decoder}/.travis.yml +0 -0
  116. /package/node_modules/{string_decoder → jszip/node_modules/string_decoder}/LICENSE +0 -0
  117. /package/node_modules/{string_decoder → jszip/node_modules/string_decoder}/README.md +0 -0
  118. /package/node_modules/{string_decoder → jszip/node_modules/string_decoder}/lib/string_decoder.js +0 -0
  119. /package/node_modules/{string_decoder → jszip/node_modules/string_decoder}/package.json +0 -0
  120. /package/node_modules/{util-deprecate → jszip/node_modules/util-deprecate}/History.md +0 -0
  121. /package/node_modules/{util-deprecate → jszip/node_modules/util-deprecate}/LICENSE +0 -0
  122. /package/node_modules/{util-deprecate → jszip/node_modules/util-deprecate}/README.md +0 -0
  123. /package/node_modules/{util-deprecate → jszip/node_modules/util-deprecate}/browser.js +0 -0
  124. /package/node_modules/{util-deprecate → jszip/node_modules/util-deprecate}/node.js +0 -0
  125. /package/node_modules/{util-deprecate → jszip/node_modules/util-deprecate}/package.json +0 -0
  126. /package/node_modules/{parse-ms → pretty-ms/node_modules/parse-ms}/index.d.ts +0 -0
  127. /package/node_modules/{parse-ms → pretty-ms/node_modules/parse-ms}/index.js +0 -0
  128. /package/node_modules/{parse-ms → pretty-ms/node_modules/parse-ms}/license +0 -0
  129. /package/node_modules/{parse-ms → pretty-ms/node_modules/parse-ms}/package.json +0 -0
  130. /package/node_modules/{parse-ms → pretty-ms/node_modules/parse-ms}/readme.md +0 -0
@@ -0,0 +1,1308 @@
1
+ import { n as GOOGLE_MEET_NODE_COMMAND } from "./google-meet-platform-constants-Bs5iAg3E.js";
2
+ import { t as normalizeMeetUrl } from "./meet-url-BP2Jlvjs.js";
3
+ import { c as callBrowserProxyOnNode, l as resolveChromeNode, u as resolveChromeNodeInfo } from "./chrome-create-PKAyxCiL.js";
4
+ import { n as normalizeDialInNumber, t as GOOGLE_MEET_PLATFORM_ADAPTER } from "./google-meet-platform-adapter-C3J-iFRE.js";
5
+ import { asOptionalRecord, asRecord, normalizeOptionalString, uniqueStrings } from "openclaw/plugin-sdk/string-coerce-runtime";
6
+ import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
7
+ import { addTimerTimeoutGraceMs } from "openclaw/plugin-sdk/number-runtime";
8
+ import { GatewayClient, startGatewayClientWhenEventLoopReady } from "openclaw/plugin-sdk/gateway-runtime";
9
+ import path from "node:path";
10
+ import { isBlockedHostnameOrIp } from "openclaw/plugin-sdk/ssrf-runtime";
11
+ import { MeetingPlatformAdapter, MeetingSessionRuntime, addMeetingSetupCheck, createLocalMeetingRealtimeAudioTransport, createMeetingRealtimeEngineBindings, createMeetingSession, createMeetingSetupStatus, createMeetingVoiceCallGateway, createNodeMeetingRealtimeAudioTransport, endMeetingVoiceCallGatewayCall, getMeetingVoiceCallGatewayCall, isMeetingVoiceCallMissingError, joinMeetingViaVoiceCallGateway, leaveMeetingWithBrowser, openMeetingWithBrowser, readMeetingTranscriptWithBrowser, recoverMeetingBrowserTab, resolveLocalMeetingBrowserRequest, speakMeetingViaVoiceCallGateway, startMeetingAgentRealtimeEngine, startMeetingRealtimeEngine } from "openclaw/plugin-sdk/meeting-runtime";
12
+ import { resolveDefaultAgentId } from "openclaw/plugin-sdk/agent-runtime";
13
+ import { normalizeAgentId } from "openclaw/plugin-sdk/routing";
14
+ import fs from "node:fs";
15
+ import os from "node:os";
16
+ import { resolveTranscriptsConfig } from "openclaw/plugin-sdk/transcripts";
17
+ //#region extensions/google-meet/src/runtime-probes.ts
18
+ function resolveProbeTimeoutMs(input, fallback) {
19
+ if (input === void 0) return Math.min(Math.max(fallback, 1), 12e4);
20
+ if (!Number.isFinite(input) || input <= 0) throw new Error("timeoutMs must be a positive number");
21
+ return Math.min(Math.trunc(input), 12e4);
22
+ }
23
+ const probes = MeetingPlatformAdapter.createRuntimeProbes({
24
+ defaultSpeechMessage: "Say exactly: Google Meet speech test complete.",
25
+ invalidRequest: (message) => new Error(message),
26
+ resolveTimeoutMs: resolveProbeTimeoutMs,
27
+ shouldWaitForListening: (session) => Boolean((session.transport === "chrome" || session.transport === "chrome-node") && session.chrome?.launched),
28
+ talkBackMode: MeetingPlatformAdapter.isTalkBackMode,
29
+ normalizeUrl: normalizeMeetUrl,
30
+ resolveRequestMode: (mode) => mode === "realtime" ? "agent" : mode,
31
+ defaultTransport: (config) => config.defaultTransport,
32
+ validateListeningTransport: (transport) => {
33
+ if (transport === "twilio") throw new Error("test_listen supports chrome or chrome-node transports");
34
+ },
35
+ resolveSpeechTimeoutMs: (_request, config) => Math.min(config.chrome.joinTimeoutMs, 5e3),
36
+ refreshCaptionHealth: async (context, session) => await context.refreshCaptionHealth(session),
37
+ speechModeError: "test_speech requires mode: agent or bidi; use join mode: transcribe for observe-only sessions.",
38
+ listeningModeError: "test_listen requires mode: transcribe; use test_speech for talk-back sessions."
39
+ });
40
+ const testGoogleMeetListening = probes.testListening;
41
+ const testGoogleMeetSpeech = probes.testSpeech;
42
+ //#endregion
43
+ //#region extensions/google-meet/src/runtime-session.ts
44
+ function resolveTransport(input, config) {
45
+ return input ?? config.defaultTransport;
46
+ }
47
+ function resolveMode(input, config) {
48
+ return input === "realtime" ? "agent" : input ?? config.defaultMode;
49
+ }
50
+ function withSessionAgentConfig(config, agentId) {
51
+ return config.realtime.agentId === agentId ? config : {
52
+ ...config,
53
+ realtime: {
54
+ ...config.realtime,
55
+ agentId
56
+ }
57
+ };
58
+ }
59
+ function isBrowserTransport(transport) {
60
+ return transport === "chrome" || transport === "chrome-node";
61
+ }
62
+ function noteSession(session, note) {
63
+ session.notes = [...session.notes.filter((item) => item !== note), note];
64
+ }
65
+ //#endregion
66
+ //#region extensions/google-meet/src/setup.ts
67
+ function resolveUserPath(input) {
68
+ if (input === "~") return os.homedir();
69
+ if (input.startsWith("~/")) return path.join(os.homedir(), input.slice(2));
70
+ return input;
71
+ }
72
+ function isProviderUnreachableWebhookUrl(webhookUrl) {
73
+ try {
74
+ return isBlockedHostnameOrIp(new URL(webhookUrl).hostname);
75
+ } catch {
76
+ return false;
77
+ }
78
+ }
79
+ function resolveVoiceCallSetupValue(configured, fallback) {
80
+ return normalizeOptionalString(configured) ?? normalizeOptionalString(fallback);
81
+ }
82
+ function getVoiceCallWebhookExposureCheck(voiceCallConfig) {
83
+ const publicUrl = normalizeOptionalString(voiceCallConfig.publicUrl);
84
+ const tunnel = asRecord(voiceCallConfig.tunnel);
85
+ const tailscale = asRecord(voiceCallConfig.tailscale);
86
+ const tunnelProvider = normalizeOptionalString(tunnel.provider);
87
+ const tailscaleMode = normalizeOptionalString(tailscale.mode);
88
+ if (publicUrl) {
89
+ const ok = !isProviderUnreachableWebhookUrl(publicUrl);
90
+ return {
91
+ id: "twilio-voice-call-webhook",
92
+ ok,
93
+ message: ok ? `Voice-call public webhook URL configured: ${publicUrl}` : `Voice-call publicUrl is local/private and cannot be reached by Twilio: ${publicUrl}`
94
+ };
95
+ }
96
+ if (tunnelProvider && tunnelProvider !== "none") return {
97
+ id: "twilio-voice-call-webhook",
98
+ ok: true,
99
+ message: "Voice-call webhook exposure configured through tunnel"
100
+ };
101
+ if (tailscaleMode && tailscaleMode !== "off") return {
102
+ id: "twilio-voice-call-webhook",
103
+ ok: true,
104
+ message: "Voice-call webhook exposure configured through Tailscale"
105
+ };
106
+ return {
107
+ id: "twilio-voice-call-webhook",
108
+ ok: false,
109
+ message: "Set plugins.entries.voice-call.config.publicUrl or configure voice-call tunnel/tailscale exposure for Twilio dialing"
110
+ };
111
+ }
112
+ function getGoogleMeetSetupStatus(config, options) {
113
+ const checks = [];
114
+ const env = options?.env ?? process.env;
115
+ const fullConfig = asRecord(options?.fullConfig);
116
+ const mode = options?.mode ?? config.defaultMode;
117
+ const transport = options?.transport ?? config.defaultTransport;
118
+ const needsChromeRealtimeAudio = MeetingPlatformAdapter.isTalkBackMode(mode) && (transport === "chrome" || transport === "chrome-node");
119
+ const pluginEntries = asRecord(asRecord(fullConfig.plugins).entries);
120
+ const pluginAllow = asRecord(fullConfig.plugins).allow;
121
+ const voiceCallEntry = asRecord(pluginEntries["voice-call"]);
122
+ const voiceCallConfig = asRecord(voiceCallEntry.config);
123
+ const voiceCallTwilioConfig = asRecord(voiceCallConfig.twilio);
124
+ if (config.auth.tokenPath) {
125
+ const tokenPath = resolveUserPath(config.auth.tokenPath);
126
+ checks.push({
127
+ id: "google-oauth-token",
128
+ ok: fs.existsSync(tokenPath),
129
+ message: fs.existsSync(tokenPath) ? "Google OAuth token file found" : `Google OAuth token file missing at ${config.auth.tokenPath}`
130
+ });
131
+ } else checks.push({
132
+ id: "google-oauth-token",
133
+ ok: true,
134
+ message: "Google OAuth token path not configured; Chrome profile auth will be used"
135
+ });
136
+ checks.push({
137
+ id: "chrome-profile",
138
+ ok: true,
139
+ 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"
140
+ });
141
+ if (needsChromeRealtimeAudio) {
142
+ const hasCommandPair = Boolean(config.chrome.audioInputCommand && config.chrome.audioOutputCommand);
143
+ const hasExternalBridge = Boolean(config.chrome.audioBridgeCommand);
144
+ const agentModeExternalBridgeInvalid = mode === "agent" && hasExternalBridge;
145
+ checks.push({
146
+ id: "audio-bridge",
147
+ ok: mode === "agent" ? hasCommandPair && !agentModeExternalBridgeInvalid : hasExternalBridge || hasCommandPair,
148
+ 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"
149
+ });
150
+ } else if (transport === "chrome" || transport === "chrome-node") checks.push({
151
+ id: "audio-bridge",
152
+ ok: true,
153
+ message: "Chrome observe-only mode does not require a realtime audio bridge"
154
+ });
155
+ checks.push({
156
+ id: "guest-join-defaults",
157
+ ok: Boolean(config.chrome.guestName && config.chrome.autoJoin && config.chrome.reuseExistingTab),
158
+ 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"
159
+ });
160
+ checks.push({
161
+ id: "chrome-node-target",
162
+ ok: config.defaultTransport !== "chrome-node" || Boolean(config.chromeNode.node),
163
+ 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"
164
+ });
165
+ if (needsChromeRealtimeAudio) checks.push({
166
+ id: "intro-after-in-call",
167
+ ok: config.chrome.waitForInCallMs > 0,
168
+ 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"
169
+ });
170
+ if (transport === "twilio") {
171
+ const hasRequestDialPlan = Boolean(options?.twilioDialInNumber);
172
+ const hasDefaultDialPlan = Boolean(config.twilio.defaultDialInNumber);
173
+ const hasDialPlan = hasRequestDialPlan || hasDefaultDialPlan;
174
+ checks.push({
175
+ id: "twilio-dial-plan",
176
+ ok: hasDialPlan,
177
+ 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"
178
+ });
179
+ }
180
+ if (config.voiceCall.enabled && (transport === "twilio" || Boolean(config.twilio.defaultDialInNumber) || Object.hasOwn(pluginEntries, "voice-call"))) {
181
+ const voiceCallAllowed = !Array.isArray(pluginAllow) || pluginAllow.includes("voice-call");
182
+ const voiceCallEnabled = Object.hasOwn(pluginEntries, "voice-call") && voiceCallEntry.enabled !== false;
183
+ checks.push({
184
+ id: "twilio-voice-call-plugin",
185
+ ok: voiceCallAllowed && voiceCallEnabled,
186
+ 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"
187
+ });
188
+ if ((normalizeOptionalString(voiceCallConfig.provider) ?? "twilio") === "twilio") {
189
+ const accountSid = resolveVoiceCallSetupValue(voiceCallTwilioConfig.accountSid, env.TWILIO_ACCOUNT_SID);
190
+ const authToken = resolveVoiceCallSetupValue(voiceCallTwilioConfig.authToken, env.TWILIO_AUTH_TOKEN);
191
+ const fromNumber = resolveVoiceCallSetupValue(voiceCallConfig.fromNumber, env.TWILIO_FROM_NUMBER);
192
+ const twilioReady = Boolean(accountSid && authToken && fromNumber);
193
+ checks.push({
194
+ id: "twilio-voice-call-credentials",
195
+ ok: twilioReady,
196
+ 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"
197
+ });
198
+ checks.push(getVoiceCallWebhookExposureCheck(voiceCallConfig));
199
+ }
200
+ }
201
+ return createMeetingSetupStatus(checks);
202
+ }
203
+ function addGoogleMeetSetupCheck(status, check) {
204
+ return addMeetingSetupCheck(status, check);
205
+ }
206
+ //#endregion
207
+ //#region extensions/google-meet/src/transports/chrome.ts
208
+ function shouldCaptureCaptions(mode, fullConfig) {
209
+ return mode === "transcribe" || !fullConfig || resolveTranscriptsConfig(fullConfig.transcripts).enabled;
210
+ }
211
+ async function prepareGoogleMeetAudioRuntime(params) {
212
+ const audio = MeetingPlatformAdapter.resolveAudioRuntimeForFormat({
213
+ backend: params.config.chrome.audioBackend,
214
+ bufferBytes: params.config.chrome.audioBufferBytes,
215
+ format: params.config.chrome.audioFormat,
216
+ inputCommand: params.config.chrome.audioInputCommandOverride,
217
+ outputCommand: params.config.chrome.audioOutputCommandOverride
218
+ });
219
+ await MeetingPlatformAdapter.ensureAudioBackend({
220
+ backend: audio.backend,
221
+ timeoutMs: params.timeoutMs,
222
+ run: async (argv, timeoutMs) => {
223
+ const result = await params.runtime.system.runCommandWithTimeout(argv, { timeoutMs });
224
+ return {
225
+ ...result,
226
+ code: result.code ?? 1
227
+ };
228
+ }
229
+ });
230
+ return audio;
231
+ }
232
+ async function assertGoogleMeetAudioAvailable(params) {
233
+ await prepareGoogleMeetAudioRuntime(params);
234
+ }
235
+ async function launchChromeMeet(params) {
236
+ let audio;
237
+ const checkRealtimeAudioPrerequisites = async () => {
238
+ if (!MeetingPlatformAdapter.isTalkBackMode(params.mode)) return;
239
+ audio = await prepareGoogleMeetAudioRuntime({
240
+ runtime: params.runtime,
241
+ config: params.config,
242
+ timeoutMs: Math.min(params.config.chrome.joinTimeoutMs, 1e4)
243
+ });
244
+ if (params.config.chrome.audioBridgeHealthCommand) {
245
+ const health = await params.runtime.system.runCommandWithTimeout(params.config.chrome.audioBridgeHealthCommand, { timeoutMs: params.config.chrome.joinTimeoutMs });
246
+ if (health.code !== 0) throw new Error(`Chrome audio bridge health check failed: ${health.stderr || health.stdout || health.code}`);
247
+ }
248
+ };
249
+ const startRealtimeAudioBridge = async () => {
250
+ if (!MeetingPlatformAdapter.isTalkBackMode(params.mode)) return;
251
+ if (params.config.chrome.audioBridgeCommand) {
252
+ 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.");
253
+ const bridge = await params.runtime.system.runCommandWithTimeout(params.config.chrome.audioBridgeCommand, { timeoutMs: params.config.chrome.joinTimeoutMs });
254
+ if (bridge.code !== 0) throw new Error(`failed to start Chrome audio bridge: ${bridge.stderr || bridge.stdout || bridge.code}`);
255
+ return { type: "external-command" };
256
+ }
257
+ 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.");
258
+ if (!audio) throw new Error("Google Meet audio backend was not prepared.");
259
+ const transport = createLocalMeetingRealtimeAudioTransport({
260
+ inputCommand: audio.inputCommand,
261
+ outputCommand: audio.outputCommand,
262
+ audioFormat: params.config.chrome.audioFormat,
263
+ bargeInInputCommand: params.config.chrome.bargeInInputCommand,
264
+ bargeInRmsThreshold: params.config.chrome.bargeInRmsThreshold,
265
+ bargeInPeakThreshold: params.config.chrome.bargeInPeakThreshold,
266
+ bargeInCooldownMs: params.config.chrome.bargeInCooldownMs,
267
+ logger: params.logger,
268
+ logScope: GOOGLE_MEET_PLATFORM_ADAPTER.logScope
269
+ });
270
+ const bindings = createMeetingRealtimeEngineBindings({
271
+ platform: GOOGLE_MEET_PLATFORM_ADAPTER,
272
+ ...params
273
+ });
274
+ const engine = params.mode === "agent" ? await startMeetingAgentRealtimeEngine({
275
+ config: params.config,
276
+ fullConfig: params.fullConfig,
277
+ runtime: params.runtime,
278
+ platform: bindings.platform,
279
+ meetingSessionId: params.meetingSessionId,
280
+ requesterSessionKey: params.requesterSessionKey,
281
+ transport,
282
+ logger: params.logger,
283
+ consultAgent: bindings.consultAgent
284
+ }) : await startMeetingRealtimeEngine({
285
+ config: {
286
+ ...params.config,
287
+ realtime: {
288
+ ...params.config.realtime,
289
+ strategy: "bidi"
290
+ }
291
+ },
292
+ fullConfig: params.fullConfig,
293
+ runtime: params.runtime,
294
+ ...bindings,
295
+ meetingSessionId: params.meetingSessionId,
296
+ requesterSessionKey: params.requesterSessionKey,
297
+ transport,
298
+ logger: params.logger
299
+ });
300
+ return {
301
+ type: "command-pair",
302
+ inputCommand: audio.inputCommand,
303
+ outputCommand: audio.outputCommand,
304
+ ...engine
305
+ };
306
+ };
307
+ await checkRealtimeAudioPrerequisites();
308
+ if (!params.config.chrome.launch) {
309
+ const recovered = await recoverMeetingBrowserTab({
310
+ adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
311
+ allowSessionAdoption: true,
312
+ autoJoin: params.config.chrome.autoJoin,
313
+ callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
314
+ captureCaptions: shouldCaptureCaptions(params.mode, params.fullConfig),
315
+ config: params.config.chrome,
316
+ locationLabel: "in local Chrome",
317
+ meetingSessionId: params.meetingSessionId,
318
+ mode: params.mode,
319
+ requestedMeetingUrl: params.url,
320
+ trackedMeetingUrl: params.url,
321
+ trackedTargetId: void 0
322
+ });
323
+ const audioBridge = MeetingPlatformAdapter.isRealtimeRouteReady(params.mode, recovered.browser) ? await startRealtimeAudioBridge() : void 0;
324
+ return {
325
+ launched: false,
326
+ audioBackend: audio?.backend,
327
+ audioBridge,
328
+ browser: recovered.browser,
329
+ tab: recovered.targetId ? {
330
+ targetId: recovered.targetId,
331
+ openedByPlugin: false
332
+ } : void 0
333
+ };
334
+ }
335
+ const result = await openMeetingWithBrowser({
336
+ adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
337
+ callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
338
+ config: params.config.chrome,
339
+ session: {
340
+ captureCaptions: shouldCaptureCaptions(params.mode, params.fullConfig),
341
+ meetingSessionId: params.meetingSessionId,
342
+ mode: params.mode,
343
+ url: params.url
344
+ }
345
+ });
346
+ const audioBridge = MeetingPlatformAdapter.isRealtimeRouteReady(params.mode, result.browser) ? await startRealtimeAudioBridge() : void 0;
347
+ return {
348
+ ...result,
349
+ audioBackend: audio?.backend,
350
+ audioBridge
351
+ };
352
+ }
353
+ function parseNodeStartResult(raw) {
354
+ const value = raw && typeof raw === "object" && "payload" in raw ? raw.payload : raw;
355
+ if (!value || typeof value !== "object") throw new Error("Google Meet node returned an invalid start result.");
356
+ return value;
357
+ }
358
+ async function leaveChromeMeet(params) {
359
+ return await leaveMeetingWithBrowser({
360
+ adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
361
+ callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
362
+ launch: params.config.chrome.launch,
363
+ meetingSessionId: params.meetingSessionId,
364
+ meetingUrl: params.meetingUrl,
365
+ tab: params.tab,
366
+ timeoutMs: params.config.chrome.joinTimeoutMs
367
+ });
368
+ }
369
+ async function readChromeMeetTranscript(params) {
370
+ return await readMeetingTranscriptWithBrowser({
371
+ adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
372
+ callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
373
+ finalize: params.finalize === true,
374
+ meetingUrl: params.meetingUrl,
375
+ meetingSessionId: params.meetingSessionId,
376
+ tab: params.tab,
377
+ timeoutMs: Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4)
378
+ });
379
+ }
380
+ async function readChromeMeetTranscriptOnNode(params) {
381
+ const nodeId = params.nodeId ?? await resolveChromeNode({
382
+ runtime: params.runtime,
383
+ requestedNode: params.config.chromeNode.node
384
+ });
385
+ const timeoutMs = Math.min(Math.max(1e3, params.config.chrome.joinTimeoutMs), 1e4);
386
+ return await readMeetingTranscriptWithBrowser({
387
+ adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
388
+ callBrowser: async (request) => await callBrowserProxyOnNode({
389
+ runtime: params.runtime,
390
+ nodeId,
391
+ method: request.method,
392
+ path: request.path,
393
+ body: request.body,
394
+ timeoutMs: request.timeoutMs
395
+ }),
396
+ finalize: params.finalize === true,
397
+ meetingUrl: params.meetingUrl,
398
+ meetingSessionId: params.meetingSessionId,
399
+ tab: params.tab,
400
+ timeoutMs
401
+ });
402
+ }
403
+ async function leaveChromeMeetOnNode(params) {
404
+ const nodeId = params.nodeId ?? await resolveChromeNode({
405
+ runtime: params.runtime,
406
+ requestedNode: params.config.chromeNode.node
407
+ });
408
+ return await leaveMeetingWithBrowser({
409
+ adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
410
+ callBrowser: async (request) => await callBrowserProxyOnNode({
411
+ runtime: params.runtime,
412
+ nodeId,
413
+ method: request.method,
414
+ path: request.path,
415
+ body: request.body,
416
+ timeoutMs: request.timeoutMs
417
+ }),
418
+ launch: params.config.chrome.launch,
419
+ meetingSessionId: params.meetingSessionId,
420
+ meetingUrl: params.meetingUrl,
421
+ tab: params.tab,
422
+ timeoutMs: params.config.chrome.joinTimeoutMs
423
+ });
424
+ }
425
+ async function openMeetWithBrowserProxy(params) {
426
+ const callBrowser = async (request) => await callBrowserProxyOnNode({
427
+ runtime: params.runtime,
428
+ nodeId: params.nodeId,
429
+ method: request.method,
430
+ path: request.path,
431
+ body: request.body,
432
+ timeoutMs: request.timeoutMs
433
+ });
434
+ if (!params.config.chrome.launch) {
435
+ const recovered = await recoverMeetingBrowserTab({
436
+ adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
437
+ allowSessionAdoption: true,
438
+ autoJoin: params.config.chrome.autoJoin,
439
+ callBrowser,
440
+ captureCaptions: params.captureCaptions,
441
+ config: params.config.chrome,
442
+ locationLabel: "on the selected Chrome node",
443
+ meetingSessionId: params.meetingSessionId,
444
+ mode: params.mode,
445
+ requestedMeetingUrl: params.url,
446
+ trackedMeetingUrl: params.url,
447
+ trackedTargetId: void 0
448
+ });
449
+ return {
450
+ launched: false,
451
+ browser: recovered.browser,
452
+ tab: recovered.targetId ? {
453
+ targetId: recovered.targetId,
454
+ openedByPlugin: false
455
+ } : void 0
456
+ };
457
+ }
458
+ return await openMeetingWithBrowser({
459
+ adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
460
+ callBrowser,
461
+ config: params.config.chrome,
462
+ session: {
463
+ captureCaptions: params.captureCaptions,
464
+ mode: params.mode,
465
+ meetingSessionId: params.meetingSessionId,
466
+ url: params.url
467
+ }
468
+ });
469
+ }
470
+ async function recoverCurrentMeetTab(params) {
471
+ return {
472
+ transport: "chrome",
473
+ ...await recoverMeetingBrowserTab({
474
+ adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
475
+ callBrowser: await resolveLocalMeetingBrowserRequest(params.runtime),
476
+ captureCaptions: shouldCaptureCaptions(params.mode ?? "bidi", params.fullConfig),
477
+ config: params.config.chrome,
478
+ locationLabel: "in local Chrome",
479
+ mode: params.mode ?? "bidi",
480
+ readOnly: params.readOnly,
481
+ requestedMeetingUrl: params.url,
482
+ trackedMeetingUrl: params.trackedMeetingUrl,
483
+ trackedTargetId: params.trackedTargetId
484
+ })
485
+ };
486
+ }
487
+ async function recoverCurrentMeetTabOnNode(params) {
488
+ const nodeId = await resolveChromeNode({
489
+ runtime: params.runtime,
490
+ requestedNode: params.config.chromeNode.node
491
+ });
492
+ return {
493
+ transport: "chrome-node",
494
+ nodeId,
495
+ ...await recoverMeetingBrowserTab({
496
+ adapter: GOOGLE_MEET_PLATFORM_ADAPTER,
497
+ callBrowser: async (request) => await callBrowserProxyOnNode({
498
+ runtime: params.runtime,
499
+ nodeId,
500
+ method: request.method,
501
+ path: request.path,
502
+ body: request.body,
503
+ timeoutMs: request.timeoutMs
504
+ }),
505
+ captureCaptions: shouldCaptureCaptions(params.mode ?? "bidi", params.fullConfig),
506
+ config: params.config.chrome,
507
+ locationLabel: "on the selected Chrome node",
508
+ mode: params.mode ?? "bidi",
509
+ readOnly: params.readOnly,
510
+ requestedMeetingUrl: params.url,
511
+ trackedMeetingUrl: params.trackedMeetingUrl,
512
+ trackedTargetId: params.trackedTargetId
513
+ })
514
+ };
515
+ }
516
+ async function launchChromeMeetOnNode(params) {
517
+ const nodeId = await resolveChromeNode({
518
+ runtime: params.runtime,
519
+ requestedNode: params.config.chromeNode.node
520
+ });
521
+ try {
522
+ await params.runtime.nodes.invoke({
523
+ nodeId,
524
+ command: GOOGLE_MEET_NODE_COMMAND,
525
+ params: {
526
+ action: "stopByUrl",
527
+ url: params.url,
528
+ mode: params.mode
529
+ },
530
+ timeoutMs: 5e3
531
+ });
532
+ } catch (error) {
533
+ params.logger.debug?.(`[google-meet] node bridge cleanup before join ignored: ${error instanceof Error ? error.message : String(error)}`);
534
+ }
535
+ const setup = MeetingPlatformAdapter.isTalkBackMode(params.mode) ? parseNodeStartResult(await params.runtime.nodes.invoke({
536
+ nodeId,
537
+ command: GOOGLE_MEET_NODE_COMMAND,
538
+ params: {
539
+ action: "setup",
540
+ audioBackend: params.config.chrome.audioBackend,
541
+ audioFormat: params.config.chrome.audioFormat,
542
+ audioBufferBytes: params.config.chrome.audioBufferBytes,
543
+ ...params.config.chrome.audioInputCommandOverride ? { audioInputCommand: params.config.chrome.audioInputCommandOverride } : {},
544
+ ...params.config.chrome.audioOutputCommandOverride ? { audioOutputCommand: params.config.chrome.audioOutputCommandOverride } : {}
545
+ },
546
+ timeoutMs: 12e3
547
+ })) : void 0;
548
+ const browserControl = await openMeetWithBrowserProxy({
549
+ runtime: params.runtime,
550
+ nodeId,
551
+ config: params.config,
552
+ captureCaptions: shouldCaptureCaptions(params.mode, params.fullConfig),
553
+ mode: params.mode,
554
+ meetingSessionId: params.meetingSessionId,
555
+ url: params.url
556
+ });
557
+ if (MeetingPlatformAdapter.isTalkBackMode(params.mode) && !MeetingPlatformAdapter.isRealtimeRouteReady(params.mode, browserControl.browser)) return {
558
+ nodeId,
559
+ launched: browserControl.launched,
560
+ audioBackend: setup?.audioBackend,
561
+ browser: browserControl.browser,
562
+ tab: browserControl.tab
563
+ };
564
+ const result = parseNodeStartResult(await params.runtime.nodes.invoke({
565
+ nodeId,
566
+ command: GOOGLE_MEET_NODE_COMMAND,
567
+ params: {
568
+ action: "start",
569
+ url: params.url,
570
+ mode: params.mode,
571
+ launch: false,
572
+ browserProfile: params.config.chrome.browserProfile,
573
+ joinTimeoutMs: params.config.chrome.joinTimeoutMs,
574
+ audioBackend: params.config.chrome.audioBackend,
575
+ audioFormat: params.config.chrome.audioFormat,
576
+ audioBufferBytes: params.config.chrome.audioBufferBytes,
577
+ ...params.config.chrome.audioInputCommandOverride ? { audioInputCommand: params.config.chrome.audioInputCommandOverride } : {},
578
+ ...params.config.chrome.audioOutputCommandOverride ? { audioOutputCommand: params.config.chrome.audioOutputCommandOverride } : {},
579
+ audioBridgeCommand: params.config.chrome.audioBridgeCommand,
580
+ audioBridgeHealthCommand: params.config.chrome.audioBridgeHealthCommand
581
+ },
582
+ timeoutMs: addTimerTimeoutGraceMs(params.config.chrome.joinTimeoutMs) ?? 1
583
+ }));
584
+ if (result.audioBridge?.type === "node-command-pair") {
585
+ if (!result.bridgeId) throw new Error("Google Meet node did not return an audio bridge id.");
586
+ const transport = createNodeMeetingRealtimeAudioTransport({
587
+ runtime: params.runtime,
588
+ nodeId,
589
+ bridgeId: result.bridgeId,
590
+ audioFormat: params.config.chrome.audioFormat,
591
+ logger: params.logger,
592
+ commandName: GOOGLE_MEET_NODE_COMMAND,
593
+ logScope: GOOGLE_MEET_PLATFORM_ADAPTER.logScope,
594
+ logPrefix: params.mode === "agent" ? "node agent" : "node"
595
+ });
596
+ Reflect.set(transport, Symbol.for("openclaw.internal.meeting-node-output-generation.v1"), result.audioBridge.outputGeneration === true);
597
+ const bindings = createMeetingRealtimeEngineBindings({
598
+ platform: GOOGLE_MEET_PLATFORM_ADAPTER,
599
+ ...params
600
+ });
601
+ const engine = params.mode === "agent" ? await startMeetingAgentRealtimeEngine({
602
+ config: params.config,
603
+ fullConfig: params.fullConfig,
604
+ runtime: params.runtime,
605
+ platform: bindings.platform,
606
+ meetingSessionId: params.meetingSessionId,
607
+ requesterSessionKey: params.requesterSessionKey,
608
+ logPrefix: "node",
609
+ transport,
610
+ logger: params.logger,
611
+ consultAgent: bindings.consultAgent
612
+ }) : await startMeetingRealtimeEngine({
613
+ config: {
614
+ ...params.config,
615
+ realtime: {
616
+ ...params.config.realtime,
617
+ strategy: "bidi"
618
+ }
619
+ },
620
+ fullConfig: params.fullConfig,
621
+ runtime: params.runtime,
622
+ ...bindings,
623
+ meetingSessionId: params.meetingSessionId,
624
+ requesterSessionKey: params.requesterSessionKey,
625
+ logPrefix: "node",
626
+ talkSessionId: `google-meet:${params.meetingSessionId}:${result.bridgeId}:node-realtime`,
627
+ talkContext: {
628
+ nodeId,
629
+ bridgeId: result.bridgeId
630
+ },
631
+ transport,
632
+ logger: params.logger
633
+ });
634
+ const bridge = {
635
+ type: "node-command-pair",
636
+ nodeId,
637
+ bridgeId: result.bridgeId,
638
+ ...engine
639
+ };
640
+ return {
641
+ nodeId,
642
+ launched: browserControl.launched || result.launched === true,
643
+ audioBackend: result.audioBackend ?? setup?.audioBackend,
644
+ audioBridge: bridge,
645
+ browser: browserControl.browser ?? result.browser,
646
+ tab: browserControl.tab
647
+ };
648
+ }
649
+ if (result.audioBridge?.type === "external-command") return {
650
+ nodeId,
651
+ launched: browserControl.launched || result.launched === true,
652
+ audioBackend: result.audioBackend ?? setup?.audioBackend,
653
+ audioBridge: { type: "external-command" },
654
+ browser: browserControl.browser ?? result.browser,
655
+ tab: browserControl.tab
656
+ };
657
+ return {
658
+ nodeId,
659
+ launched: browserControl.launched || result.launched === true,
660
+ audioBackend: result.audioBackend ?? setup?.audioBackend,
661
+ browser: browserControl.browser ?? result.browser,
662
+ tab: browserControl.tab
663
+ };
664
+ }
665
+ //#endregion
666
+ //#region extensions/google-meet/src/runtime-setup.ts
667
+ function collectChromeAudioCommands(config) {
668
+ return uniqueStrings((config.chrome.audioBridgeCommand ? [config.chrome.audioBridgeCommand[0]] : [
669
+ config.chrome.audioInputCommand?.[0],
670
+ config.chrome.audioOutputCommand?.[0],
671
+ config.chrome.bargeInInputCommand?.[0]
672
+ ]).filter((value) => Boolean(value?.trim())));
673
+ }
674
+ async function commandExists(runtime, command) {
675
+ return (await runtime.system.runCommandWithTimeout([
676
+ "/bin/sh",
677
+ "-lc",
678
+ "command -v \"$1\" >/dev/null 2>&1",
679
+ "sh",
680
+ command
681
+ ], { timeoutMs: 5e3 })).code === 0;
682
+ }
683
+ async function getGoogleMeetRuntimeSetupStatus(params) {
684
+ const options = params.options ?? {};
685
+ const transport = options.transport ?? params.config.defaultTransport;
686
+ const mode = options.mode === "realtime" ? "agent" : options.mode ?? params.config.defaultMode;
687
+ const twilioDialInNumber = transport === "twilio" ? normalizeDialInNumber(options.dialInNumber) : void 0;
688
+ const shouldCheckChromeNode = transport === "chrome-node" || !options.transport && Boolean(params.config.chromeNode.node);
689
+ let status = getGoogleMeetSetupStatus(params.config, {
690
+ fullConfig: params.fullConfig,
691
+ mode,
692
+ transport,
693
+ twilioDialInNumber
694
+ });
695
+ if (shouldCheckChromeNode) try {
696
+ const node = await resolveChromeNodeInfo({
697
+ runtime: params.runtime,
698
+ requestedNode: params.config.chromeNode.node
699
+ });
700
+ const label = node.displayName ?? node.remoteIp ?? node.nodeId ?? "connected node";
701
+ status = addGoogleMeetSetupCheck(status, {
702
+ id: "chrome-node-connected",
703
+ ok: true,
704
+ message: `Connected Google Meet node ready: ${label}`
705
+ });
706
+ if ((mode === "agent" || mode === "bidi") && node.nodeId) {
707
+ const setup = await params.runtime.nodes.invoke({
708
+ nodeId: node.nodeId,
709
+ command: GOOGLE_MEET_NODE_COMMAND,
710
+ params: {
711
+ action: "setup",
712
+ audioBackend: params.config.chrome.audioBackend,
713
+ audioFormat: params.config.chrome.audioFormat,
714
+ audioBufferBytes: params.config.chrome.audioBufferBytes,
715
+ ...params.config.chrome.audioInputCommandOverride ? { audioInputCommand: params.config.chrome.audioInputCommandOverride } : {},
716
+ ...params.config.chrome.audioOutputCommandOverride ? { audioOutputCommand: params.config.chrome.audioOutputCommandOverride } : {}
717
+ },
718
+ timeoutMs: 12e3
719
+ });
720
+ status = addGoogleMeetSetupCheck(status, {
721
+ id: "chrome-node-audio-prerequisites",
722
+ ok: true,
723
+ message: setup ? "Remote virtual audio backend and command-pair prerequisites are ready" : "Remote audio setup completed"
724
+ });
725
+ }
726
+ } catch (error) {
727
+ status = addGoogleMeetSetupCheck(status, {
728
+ id: "chrome-node-connected",
729
+ ok: false,
730
+ message: formatErrorMessage(error)
731
+ });
732
+ }
733
+ if (transport !== "chrome" || mode !== "agent" && mode !== "bidi") return status;
734
+ try {
735
+ await assertGoogleMeetAudioAvailable({
736
+ runtime: params.runtime,
737
+ config: params.config,
738
+ timeoutMs: Math.min(params.config.chrome.joinTimeoutMs, 1e4)
739
+ });
740
+ status = addGoogleMeetSetupCheck(status, {
741
+ id: "chrome-local-audio-device",
742
+ ok: true,
743
+ message: "Virtual meeting audio backend is ready"
744
+ });
745
+ } catch (error) {
746
+ status = addGoogleMeetSetupCheck(status, {
747
+ id: "chrome-local-audio-device",
748
+ ok: false,
749
+ message: formatErrorMessage(error)
750
+ });
751
+ }
752
+ const commands = collectChromeAudioCommands(params.config);
753
+ const missingCommands = [];
754
+ for (const command of commands) try {
755
+ if (!await commandExists(params.runtime, command)) missingCommands.push(command);
756
+ } catch {
757
+ missingCommands.push(command);
758
+ }
759
+ return addGoogleMeetSetupCheck(status, {
760
+ id: "chrome-local-audio-commands",
761
+ ok: commands.length > 0 && missingCommands.length === 0,
762
+ 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(", ")}`
763
+ });
764
+ }
765
+ //#endregion
766
+ //#region extensions/google-meet/src/voice-call-gateway.ts
767
+ const GOOGLE_MEET_VOICE_CALL_SURFACE = {
768
+ clientDisplayName: "Google Meet plugin",
769
+ configPath: "google-meet voiceCall.gatewayUrl",
770
+ logScope: "[google-meet]",
771
+ meetingLabel: "Meet",
772
+ providerLabel: "Twilio"
773
+ };
774
+ async function createConnectedGatewayClient(params) {
775
+ let client;
776
+ const abortStart = new AbortController();
777
+ let timer;
778
+ try {
779
+ await new Promise((resolve, reject) => {
780
+ timer = setTimeout(() => {
781
+ abortStart.abort();
782
+ reject(/* @__PURE__ */ new Error("gateway connect timeout"));
783
+ }, params.config.requestTimeoutMs);
784
+ client = new GatewayClient({
785
+ url: params.config.gatewayUrl,
786
+ token: params.config.token,
787
+ requestTimeoutMs: params.config.requestTimeoutMs,
788
+ clientName: "cli",
789
+ clientDisplayName: params.surface.clientDisplayName,
790
+ scopes: ["operator.write"],
791
+ onHelloOk: () => {
792
+ clearTimeout(timer);
793
+ resolve();
794
+ },
795
+ onConnectError: (error) => {
796
+ clearTimeout(timer);
797
+ abortStart.abort();
798
+ reject(error);
799
+ }
800
+ });
801
+ startGatewayClientWhenEventLoopReady(client, {
802
+ timeoutMs: params.config.requestTimeoutMs,
803
+ signal: abortStart.signal
804
+ }).then((readiness) => {
805
+ if (!readiness.ready && !readiness.aborted) {
806
+ clearTimeout(timer);
807
+ reject(/* @__PURE__ */ new Error("gateway event loop readiness timeout"));
808
+ }
809
+ }).catch((error) => {
810
+ clearTimeout(timer);
811
+ reject(error instanceof Error ? error : new Error(String(error)));
812
+ });
813
+ });
814
+ return client;
815
+ } catch (error) {
816
+ clearTimeout(timer);
817
+ abortStart.abort();
818
+ await client?.stopAndWait().catch(() => {});
819
+ throw error;
820
+ }
821
+ }
822
+ function createVoiceCallGateway(params) {
823
+ return createMeetingVoiceCallGateway({
824
+ config: params.config.voiceCall,
825
+ runtime: params.runtime,
826
+ surface: GOOGLE_MEET_VOICE_CALL_SURFACE,
827
+ connectClient: createConnectedGatewayClient
828
+ });
829
+ }
830
+ const isVoiceCallMissingError = isMeetingVoiceCallMissingError;
831
+ async function joinMeetViaVoiceCallGateway(params) {
832
+ return await joinMeetingViaVoiceCallGateway({
833
+ ...params,
834
+ config: params.config.voiceCall,
835
+ surface: GOOGLE_MEET_VOICE_CALL_SURFACE
836
+ });
837
+ }
838
+ async function endMeetVoiceCallGatewayCall(params) {
839
+ await endMeetingVoiceCallGatewayCall(params);
840
+ }
841
+ async function getMeetVoiceCallGatewayCall(params) {
842
+ return await getMeetingVoiceCallGatewayCall(params);
843
+ }
844
+ async function speakMeetViaVoiceCallGateway(params) {
845
+ await speakMeetingViaVoiceCallGateway(params);
846
+ }
847
+ //#endregion
848
+ //#region extensions/google-meet/src/runtime.ts
849
+ const nowIso = () => (/* @__PURE__ */ new Date()).toISOString();
850
+ var GoogleMeetRuntime = class {
851
+ #createdBrowserTabs;
852
+ #voiceCallGateway;
853
+ #sessions;
854
+ constructor(params) {
855
+ this.params = params;
856
+ this.#createdBrowserTabs = /* @__PURE__ */ new Map();
857
+ this.transcriptSourceRuntime = () => this.#sessions;
858
+ const adapter = GOOGLE_MEET_PLATFORM_ADAPTER;
859
+ this.#voiceCallGateway = createVoiceCallGateway(params);
860
+ this.#sessions = new MeetingSessionRuntime({
861
+ logger: params.logger,
862
+ logScope: "[google-meet]",
863
+ formatError: formatErrorMessage,
864
+ reuseExistingBrowserTab: params.config.chrome.reuseExistingTab,
865
+ waitForInCallMs: params.config.chrome.waitForInCallMs,
866
+ joinTimeoutMs: params.config.chrome.joinTimeoutMs,
867
+ defaultSpeechInstructions: params.config.realtime.introMessage,
868
+ transientSpeechBlockedReasons: /* @__PURE__ */ new Set([
869
+ "not-in-call",
870
+ "browser-unverified",
871
+ "meet-microphone-muted"
872
+ ]),
873
+ messages: {
874
+ previousBrowserLeaveFailed: "Could not leave the previous Meet browser tab before reassignment.",
875
+ reassignedSessionNote: "Ended before the same Meet tab was reassigned to another agent.",
876
+ reusedSessionNote: "Reused existing active Meet session.",
877
+ replacementBrowserLeaveFailed: "Could not leave the previous Meet browser tab before reassignment.",
878
+ speechBlockedFallback: "Realtime speech blocked until Google Meet is ready.",
879
+ speech: {
880
+ audioBridgeUnavailable: "Realtime speech requires an active Chrome audio bridge.",
881
+ browserUnverified: "Google Meet browser state has not been verified yet.",
882
+ microphoneMuted: "Turn on the OpenClaw Google Meet microphone before asking OpenClaw to speak.",
883
+ microphoneMutedReason: "meet-microphone-muted",
884
+ notInCall: "Google Meet has not reported that the browser participant is in the call.",
885
+ notInCallReason: "not-in-call",
886
+ browserUnverifiedReason: "browser-unverified",
887
+ audioBridgeUnavailableReason: "audio-bridge-unavailable"
888
+ }
889
+ },
890
+ resolveJoin: (request) => ({
891
+ url: adapter.urls.validateAndNormalize(request.url),
892
+ transport: resolveTransport(request.transport, params.config),
893
+ mode: resolveMode(request.mode, params.config),
894
+ agentId: this.#resolveAgentId(request.agentId)
895
+ }),
896
+ createSession: ({ resolved, createdAt }) => createMeetingSession({
897
+ platform: adapter,
898
+ config: params.config,
899
+ resolved,
900
+ createdAt
901
+ }),
902
+ resolveSpeechInstructions: (request) => request.message ?? params.config.realtime.introMessage,
903
+ isBrowserTransport,
904
+ isTalkBackMode: (mode) => MeetingPlatformAdapter.isTalkBackMode(mode),
905
+ isTranscribeMode: (mode) => mode === "transcribe",
906
+ sameMeetingUrl: (left, right) => adapter.urls.isSameMeeting(left, right),
907
+ normalizeMeetingUrlForReuse: (url) => adapter.urls.normalizeForReuse(url),
908
+ getBrowser: (session) => session.chrome ? {
909
+ launched: session.chrome.launched,
910
+ nodeId: session.chrome.nodeId,
911
+ tab: session.chrome.browserTab,
912
+ health: session.chrome.health,
913
+ hasAudioBridge: Boolean(session.chrome.audioBridge)
914
+ } : void 0,
915
+ setBrowserTab: (session, tab) => {
916
+ if (session.chrome) session.chrome.browserTab = tab;
917
+ },
918
+ setBrowserHealth: (session, health) => {
919
+ if (session.chrome) session.chrome.health = health;
920
+ },
921
+ joinTransport: async ({ request, session, context }) => await this.#joinTransport(request, session, context),
922
+ releaseBrowserTab: async (session) => await this.#releaseBrowserTab(session),
923
+ refreshBrowserHealth: async (session, options) => await this.#refreshBrowserHealth(session, options),
924
+ refreshStatus: async (session) => await this.#refreshStatus(session),
925
+ refreshReusableSession: async (session, _request, _resolved) => {
926
+ if (session.transport === "twilio") await this.#refreshTwilioVoiceCallStatus(session);
927
+ },
928
+ ensureRealtimeBridge: async (session) => await this.#ensureChromeRealtimeBridge(session),
929
+ captureTranscript: async (session, options) => await this.#captureTranscript(session, options),
930
+ speakViaTransport: async (session, instructions) => await this.#speakViaTransport(session, instructions),
931
+ durableTranscripts: {
932
+ config: params.fullConfig.transcripts,
933
+ providerId: "google-meet",
934
+ providerName: "Google Meet"
935
+ }
936
+ });
937
+ }
938
+ list() {
939
+ return this.#sessions.list();
940
+ }
941
+ async status(sessionId) {
942
+ return await this.#sessions.status(sessionId);
943
+ }
944
+ async transcript(sessionId, options = {}) {
945
+ return await this.#sessions.transcript(sessionId, options);
946
+ }
947
+ async setupStatus(options = {}) {
948
+ return await getGoogleMeetRuntimeSetupStatus({
949
+ config: this.params.config,
950
+ fullConfig: this.params.fullConfig,
951
+ runtime: this.params.runtime,
952
+ options
953
+ });
954
+ }
955
+ async createViaBrowser() {
956
+ const result = await GOOGLE_MEET_PLATFORM_ADAPTER.create.browser({
957
+ runtime: this.params.runtime,
958
+ config: this.params.config
959
+ });
960
+ if (result.openedByPlugin && result.targetId) this.#createdBrowserTabs.set(`${result.nodeId}:${result.targetId}`, result.meetingUri);
961
+ return result;
962
+ }
963
+ async recoverCurrentTab(request = {}) {
964
+ const transport = resolveTransport(request.transport, this.params.config);
965
+ if (transport === "twilio") throw new Error("recover_current_tab only supports chrome or chrome-node transports");
966
+ const url = request.url ? GOOGLE_MEET_PLATFORM_ADAPTER.urls.validateAndNormalize(request.url) : void 0;
967
+ return transport === "chrome-node" ? await recoverCurrentMeetTabOnNode({
968
+ runtime: this.params.runtime,
969
+ config: this.params.config,
970
+ fullConfig: this.params.fullConfig,
971
+ url
972
+ }) : await recoverCurrentMeetTab({
973
+ runtime: this.params.runtime,
974
+ config: this.params.config,
975
+ fullConfig: this.params.fullConfig,
976
+ url
977
+ });
978
+ }
979
+ async join(request) {
980
+ return await this.#sessions.join(request);
981
+ }
982
+ async leave(sessionId, options) {
983
+ return await this.#sessions.leave(sessionId, options);
984
+ }
985
+ async speak(sessionId, instructions) {
986
+ return await this.#sessions.speak(sessionId, instructions);
987
+ }
988
+ async testSpeech(request) {
989
+ return await testGoogleMeetSpeech(this.#probeContext(), request);
990
+ }
991
+ async testListen(request) {
992
+ return await testGoogleMeetListening(this.#probeContext(), request);
993
+ }
994
+ #probeContext() {
995
+ return {
996
+ config: this.params.config,
997
+ resolveAgentId: (request) => this.#resolveAgentId(request.agentId),
998
+ list: () => this.list(),
999
+ join: async (request) => await this.join(request),
1000
+ isReusable: (session, resolved) => this.#sessions.isReusableSession(session, resolved),
1001
+ hasHealthHandle: (sessionId) => this.#sessions.hasHealthHandle(sessionId),
1002
+ refreshHealth: (sessionId) => this.#sessions.refreshHealth(sessionId),
1003
+ refreshCaptionHealth: async (session) => await this.#sessions.refreshCaptionHealth(session)
1004
+ };
1005
+ }
1006
+ #resolveAgentId(requestedAgentId) {
1007
+ return normalizeAgentId(requestedAgentId ?? this.params.config.realtime.agentId ?? resolveDefaultAgentId(this.params.fullConfig));
1008
+ }
1009
+ async #joinTransport(request, session, context) {
1010
+ if (isBrowserTransport(session.transport)) {
1011
+ const chromeConfig = withSessionAgentConfig(this.params.config, session.agentId);
1012
+ const result = session.transport === "chrome-node" ? await launchChromeMeetOnNode({
1013
+ runtime: this.params.runtime,
1014
+ config: chromeConfig,
1015
+ fullConfig: this.params.fullConfig,
1016
+ meetingSessionId: session.id,
1017
+ requesterSessionKey: request.requesterSessionKey,
1018
+ mode: session.mode,
1019
+ url: session.url,
1020
+ logger: this.params.logger
1021
+ }) : await launchChromeMeet({
1022
+ runtime: this.params.runtime,
1023
+ config: chromeConfig,
1024
+ fullConfig: this.params.fullConfig,
1025
+ meetingSessionId: session.id,
1026
+ requesterSessionKey: request.requesterSessionKey,
1027
+ mode: session.mode,
1028
+ url: session.url,
1029
+ logger: this.params.logger
1030
+ });
1031
+ const nodeId = "nodeId" in result ? result.nodeId : void 0;
1032
+ let tab = result.tab;
1033
+ const createdKey = session.transport === "chrome-node" && nodeId && tab ? `${nodeId}:${tab.targetId}` : void 0;
1034
+ const createdUrl = createdKey ? this.#createdBrowserTabs.get(createdKey) : void 0;
1035
+ if (createdKey) this.#createdBrowserTabs.delete(createdKey);
1036
+ if (tab && GOOGLE_MEET_PLATFORM_ADAPTER.urls.isSameMeeting(createdUrl, session.url)) tab = {
1037
+ ...tab,
1038
+ openedByPlugin: true
1039
+ };
1040
+ tab = context.inheritedBrowserTab({
1041
+ session,
1042
+ transport: session.transport,
1043
+ nodeId,
1044
+ meetingUrl: session.url,
1045
+ tab
1046
+ });
1047
+ session.chrome = {
1048
+ audioBackend: result.audioBackend,
1049
+ launched: result.launched,
1050
+ nodeId,
1051
+ browserProfile: this.params.config.chrome.browserProfile,
1052
+ browserTab: tab,
1053
+ health: result.browser
1054
+ };
1055
+ const handles = this.#attachChromeAudioBridge(session, result.audioBridge);
1056
+ if (handles) context.attachRuntimeHandles(session, handles);
1057
+ 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 is waiting for verified virtual input/output audio routing." : "Chrome transport joins as the signed-in Google profile without starting the realtime audio bridge.");
1058
+ this.#sessions.refreshSpeechReadiness(session);
1059
+ return {};
1060
+ }
1061
+ const dialPlan = GOOGLE_MEET_PLATFORM_ADAPTER.dialIn.buildPlan({
1062
+ dialInNumber: request.dialInNumber,
1063
+ defaultDialInNumber: this.params.config.twilio.defaultDialInNumber,
1064
+ pin: request.pin,
1065
+ defaultPin: this.params.config.twilio.defaultPin,
1066
+ dtmfSequence: request.dtmfSequence,
1067
+ defaultDtmfSequence: this.params.config.twilio.defaultDtmfSequence,
1068
+ dtmfDelayMs: this.params.config.voiceCall.dtmfDelayMs
1069
+ });
1070
+ const dialInNumber = dialPlan.number;
1071
+ 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.");
1072
+ const dtmfSequence = dialPlan.dtmfSequence;
1073
+ const delegatedAgentId = Boolean(normalizeOptionalString(request.agentId) || normalizeOptionalString(this.params.config.realtime.agentId)) ? session.agentId : void 0;
1074
+ const voiceCallResult = this.params.config.voiceCall.enabled ? await joinMeetViaVoiceCallGateway({
1075
+ config: this.params.config,
1076
+ gateway: this.#voiceCallGateway,
1077
+ dialInNumber,
1078
+ dtmfSequence,
1079
+ logger: this.params.logger,
1080
+ ...request.requesterSessionKey ? { requesterSessionKey: request.requesterSessionKey } : {},
1081
+ agentId: delegatedAgentId,
1082
+ sessionKey: delegatedAgentId ? `agent:${delegatedAgentId}:google-meet:${session.id}` : `voice:google-meet:${session.id}`,
1083
+ message: MeetingPlatformAdapter.isTalkBackMode(session.mode) ? request.message ?? this.params.config.voiceCall.introMessage ?? this.params.config.realtime.introMessage : void 0
1084
+ }) : void 0;
1085
+ session.twilio = {
1086
+ dialInNumber,
1087
+ pinProvided: Boolean(dialPlan.pin),
1088
+ dtmfSequence,
1089
+ voiceCallId: voiceCallResult?.callId,
1090
+ dtmfSent: voiceCallResult?.dtmfSent,
1091
+ introSent: voiceCallResult?.introSent
1092
+ };
1093
+ if (voiceCallResult?.callId) context.attachRuntimeHandles(session, { stop: async () => {
1094
+ await endMeetVoiceCallGatewayCall({
1095
+ gateway: this.#voiceCallGateway,
1096
+ callId: voiceCallResult.callId
1097
+ });
1098
+ } });
1099
+ 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.");
1100
+ return { delegatedSpoken: Boolean(voiceCallResult?.introSent) };
1101
+ }
1102
+ #attachChromeAudioBridge(session, audioBridge) {
1103
+ if (!session.chrome || !audioBridge) return;
1104
+ session.chrome.audioBridge = {
1105
+ type: audioBridge.type,
1106
+ provider: audioBridge.type === "command-pair" || audioBridge.type === "node-command-pair" ? audioBridge.providerId : void 0
1107
+ };
1108
+ return audioBridge.type === "command-pair" || audioBridge.type === "node-command-pair" ? {
1109
+ stop: audioBridge.stop,
1110
+ speak: audioBridge.speak,
1111
+ getHealth: audioBridge.getHealth
1112
+ } : void 0;
1113
+ }
1114
+ async #ensureChromeRealtimeBridge(session) {
1115
+ if (!MeetingPlatformAdapter.isTalkBackMode(session.mode) || !isBrowserTransport(session.transport) || session.state !== "active" || !session.chrome || session.chrome.audioBridge || !MeetingPlatformAdapter.isRealtimeRouteReady(session.mode, session.chrome.health)) return;
1116
+ const config = withSessionAgentConfig(this.params.config, session.agentId);
1117
+ const recoveryConfig = {
1118
+ ...config,
1119
+ chrome: {
1120
+ ...config.chrome,
1121
+ launch: false
1122
+ },
1123
+ ...session.chrome.nodeId ? { chromeNode: {
1124
+ ...config.chromeNode,
1125
+ node: session.chrome.nodeId
1126
+ } } : {}
1127
+ };
1128
+ const result = session.transport === "chrome-node" ? await launchChromeMeetOnNode({
1129
+ runtime: this.params.runtime,
1130
+ config: recoveryConfig,
1131
+ fullConfig: this.params.fullConfig,
1132
+ meetingSessionId: session.id,
1133
+ mode: session.mode,
1134
+ url: session.url,
1135
+ logger: this.params.logger
1136
+ }) : await launchChromeMeet({
1137
+ runtime: this.params.runtime,
1138
+ config: recoveryConfig,
1139
+ fullConfig: this.params.fullConfig,
1140
+ meetingSessionId: session.id,
1141
+ mode: session.mode,
1142
+ url: session.url,
1143
+ logger: this.params.logger
1144
+ });
1145
+ session.updatedAt = nowIso();
1146
+ return this.#attachChromeAudioBridge(session, result.audioBridge);
1147
+ }
1148
+ async #refreshBrowserHealth(session, options = {}) {
1149
+ try {
1150
+ const result = session.transport === "chrome-node" ? await recoverCurrentMeetTabOnNode({
1151
+ runtime: this.params.runtime,
1152
+ config: this.params.config,
1153
+ fullConfig: this.params.fullConfig,
1154
+ mode: session.mode,
1155
+ readOnly: options.readOnly,
1156
+ trackedMeetingUrl: session.url,
1157
+ trackedTargetId: session.chrome?.browserTab?.targetId,
1158
+ url: session.url
1159
+ }) : await recoverCurrentMeetTab({
1160
+ runtime: this.params.runtime,
1161
+ config: this.params.config,
1162
+ fullConfig: this.params.fullConfig,
1163
+ mode: session.mode,
1164
+ readOnly: options.readOnly,
1165
+ trackedMeetingUrl: session.url,
1166
+ trackedTargetId: session.chrome?.browserTab?.targetId,
1167
+ url: session.url
1168
+ });
1169
+ if (result.found && session.chrome) {
1170
+ if (result.targetId) {
1171
+ const currentTab = session.chrome.browserTab;
1172
+ session.chrome.browserTab = {
1173
+ targetId: result.targetId,
1174
+ openedByPlugin: result.targetId === currentTab?.targetId ? currentTab.openedByPlugin : false
1175
+ };
1176
+ }
1177
+ if (result.browser) session.chrome.health = {
1178
+ ...session.chrome.health,
1179
+ ...result.browser
1180
+ };
1181
+ session.updatedAt = nowIso();
1182
+ }
1183
+ } catch (error) {
1184
+ this.params.logger.debug?.(`[google-meet] browser readiness refresh ignored: ${formatErrorMessage(error)}`);
1185
+ }
1186
+ }
1187
+ async #refreshStatus(session) {
1188
+ if (isBrowserTransport(session.transport)) await this.#sessions.refreshBrowserHealth(session, {
1189
+ force: true,
1190
+ readOnly: true
1191
+ });
1192
+ else if (session.transport === "twilio") await this.#refreshTwilioVoiceCallStatus(session);
1193
+ else this.#sessions.refreshSpeechReadiness(session);
1194
+ }
1195
+ async #refreshTwilioVoiceCallStatus(session) {
1196
+ const callId = session.twilio?.voiceCallId;
1197
+ if (!callId || session.state !== "active") {
1198
+ this.#sessions.refreshSpeechReadiness(session);
1199
+ return;
1200
+ }
1201
+ try {
1202
+ const status = await getMeetVoiceCallGatewayCall({
1203
+ gateway: this.#voiceCallGateway,
1204
+ callId
1205
+ });
1206
+ const call = asOptionalRecord(status.call);
1207
+ if (status.found === false || call?.endedAt !== void 0 || call?.endReason !== void 0) this.#sessions.markSessionEnded(session, "Voice Call is no longer active.");
1208
+ } catch (error) {
1209
+ this.params.logger.debug?.(`[google-meet] voice-call status refresh ignored: ${formatErrorMessage(error)}`);
1210
+ }
1211
+ this.#sessions.refreshSpeechReadiness(session);
1212
+ }
1213
+ async #speakViaTransport(session, instructions) {
1214
+ if (session.transport !== "twilio" || !session.twilio?.voiceCallId) return;
1215
+ try {
1216
+ await speakMeetViaVoiceCallGateway({
1217
+ gateway: this.#voiceCallGateway,
1218
+ callId: session.twilio.voiceCallId,
1219
+ message: instructions || this.params.config.voiceCall.introMessage || this.params.config.realtime.introMessage || ""
1220
+ });
1221
+ } catch (error) {
1222
+ if (!isVoiceCallMissingError(error)) throw error;
1223
+ this.#sessions.markSessionEnded(session, "Voice Call is no longer active.");
1224
+ return {
1225
+ handled: true,
1226
+ spoken: false
1227
+ };
1228
+ }
1229
+ session.twilio.introSent = true;
1230
+ session.updatedAt = nowIso();
1231
+ return {
1232
+ handled: true,
1233
+ spoken: true
1234
+ };
1235
+ }
1236
+ async #captureTranscript(session, options = {}) {
1237
+ const tab = session.chrome?.browserTab;
1238
+ if (!tab) return;
1239
+ return session.transport === "chrome-node" ? await readChromeMeetTranscriptOnNode({
1240
+ runtime: this.params.runtime,
1241
+ nodeId: session.chrome?.nodeId,
1242
+ config: this.params.config,
1243
+ ...options.finalize === void 0 ? {} : { finalize: options.finalize },
1244
+ meetingUrl: session.url,
1245
+ meetingSessionId: session.id,
1246
+ tab
1247
+ }) : await readChromeMeetTranscript({
1248
+ runtime: this.params.runtime,
1249
+ config: this.params.config,
1250
+ ...options.finalize === void 0 ? {} : { finalize: options.finalize },
1251
+ meetingUrl: session.url,
1252
+ meetingSessionId: session.id,
1253
+ tab
1254
+ });
1255
+ }
1256
+ async #releaseBrowserTab(session) {
1257
+ if (!isBrowserTransport(session.transport)) return;
1258
+ const tab = session.chrome?.browserTab;
1259
+ if (!tab) {
1260
+ noteSession(session, "No tracked Meet browser tab for this session; close the Meet tab manually if it is still in the call.");
1261
+ session.browserLeft = false;
1262
+ return false;
1263
+ }
1264
+ 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)) {
1265
+ noteSession(session, "Kept the shared Meet tab open because another active session uses it.");
1266
+ session.browserLeft = void 0;
1267
+ return;
1268
+ }
1269
+ let left;
1270
+ try {
1271
+ const result = session.transport === "chrome-node" ? await leaveChromeMeetOnNode({
1272
+ runtime: this.params.runtime,
1273
+ nodeId: session.chrome?.nodeId,
1274
+ config: this.params.config,
1275
+ meetingSessionId: session.id,
1276
+ meetingUrl: session.url,
1277
+ tab
1278
+ }) : await leaveChromeMeet({
1279
+ runtime: this.params.runtime,
1280
+ config: this.params.config,
1281
+ meetingSessionId: session.id,
1282
+ meetingUrl: session.url,
1283
+ tab
1284
+ });
1285
+ noteSession(session, result.note);
1286
+ left = result.left;
1287
+ } catch (error) {
1288
+ noteSession(session, `Browser control could not leave the Meet tab: ${formatErrorMessage(error)}`);
1289
+ left = false;
1290
+ }
1291
+ if (session.chrome && left) {
1292
+ session.chrome.browserTab = void 0;
1293
+ if (session.chrome.health) session.chrome.health = {
1294
+ ...session.chrome.health,
1295
+ captioning: false,
1296
+ audioOutputRouted: false,
1297
+ providerConnected: false,
1298
+ realtimeReady: false,
1299
+ audioInputActive: false,
1300
+ audioOutputActive: false
1301
+ };
1302
+ }
1303
+ session.browserLeft = left;
1304
+ return left;
1305
+ }
1306
+ };
1307
+ //#endregion
1308
+ export { GoogleMeetRuntime };