@openclaw/google-meet 2026.5.1-beta.2 → 2026.5.2-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.
@@ -62,6 +62,7 @@ function setup(
62
62
  unknown
63
63
  >,
64
64
  );
65
+ googleMeetPluginTesting.setPlatformForTests(() => options?.registerPlatform ?? "darwin");
65
66
  return harness;
66
67
  }
67
68
 
@@ -106,9 +107,10 @@ describe("google-meet create flow", () => {
106
107
  afterEach(() => {
107
108
  vi.unstubAllGlobals();
108
109
  googleMeetPluginTesting.setCallGatewayFromCliForTests();
110
+ googleMeetPluginTesting.setPlatformForTests();
109
111
  });
110
112
 
111
- it("CLI create prints the new meeting URL", async () => {
113
+ it("CLI create can configure API-created space access", async () => {
112
114
  const fetchMock = vi.fn(async (input: RequestInfo | URL, _init?: RequestInit) => {
113
115
  const url = input instanceof Request ? input.url : input.toString();
114
116
  if (url.includes("oauth2.googleapis.com")) {
@@ -142,9 +144,27 @@ describe("google-meet create flow", () => {
142
144
  });
143
145
 
144
146
  try {
145
- await program.parseAsync(["googlemeet", "create", "--no-join"], { from: "user" });
147
+ await program.parseAsync(
148
+ [
149
+ "googlemeet",
150
+ "create",
151
+ "--no-join",
152
+ "--access-type",
153
+ "OPEN",
154
+ "--entry-point-access",
155
+ "ALL",
156
+ ],
157
+ { from: "user" },
158
+ );
146
159
  expect(stdout.output()).toContain("meeting uri: https://meet.google.com/new-abcd-xyz");
147
160
  expect(stdout.output()).toContain("space: spaces/new-space");
161
+ expect(fetchMock).toHaveBeenCalledWith(
162
+ "https://meet.googleapis.com/v2/spaces",
163
+ expect.objectContaining({
164
+ method: "POST",
165
+ body: JSON.stringify({ config: { accessType: "OPEN", entryPointAccess: "ALL" } }),
166
+ }),
167
+ );
148
168
  } finally {
149
169
  stdout.restore();
150
170
  }
@@ -220,6 +240,27 @@ describe("google-meet create flow", () => {
220
240
  );
221
241
  });
222
242
 
243
+ it("rejects access policy flags when tool create would use browser fallback", async () => {
244
+ const { methods } = setup(
245
+ {
246
+ defaultTransport: "chrome-node",
247
+ chromeNode: { node: "parallels-macos" },
248
+ },
249
+ {
250
+ nodesInvokeHandler: async () => {
251
+ throw new Error("browser fallback should not run");
252
+ },
253
+ },
254
+ );
255
+
256
+ await expect(
257
+ invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.create", {
258
+ join: false,
259
+ accessType: "OPEN",
260
+ }),
261
+ ).rejects.toThrow("access policy options require OAuth/API room creation");
262
+ });
263
+
223
264
  it("reports structured manual action when browser creation needs Google login", async () => {
224
265
  const { methods } = setup(
225
266
  {
package/index.test.ts CHANGED
@@ -86,6 +86,7 @@ function setup(
86
86
  unknown
87
87
  >,
88
88
  );
89
+ googleMeetPluginTesting.setPlatformForTests(() => options?.registerPlatform ?? "darwin");
89
90
  return harness;
90
91
  }
91
92
 
@@ -303,6 +304,7 @@ describe("google-meet plugin", () => {
303
304
  vi.unstubAllGlobals();
304
305
  chromeTransportTesting.setDepsForTest(null);
305
306
  googleMeetPluginTesting.setCallGatewayFromCliForTests();
307
+ googleMeetPluginTesting.setPlatformForTests();
306
308
  });
307
309
 
308
310
  it("defaults to chrome realtime with safe read-only tools", () => {
@@ -507,6 +509,42 @@ describe("google-meet plugin", () => {
507
509
  );
508
510
  });
509
511
 
512
+ it("keeps the agent tool visible on non-macOS hosts but blocks local Chrome realtime joins", async () => {
513
+ const { cliRegistrations, methods, tools } = setup(undefined, { registerPlatform: "linux" });
514
+ const tool = tools[0] as {
515
+ execute: (id: string, params: unknown) => Promise<{ isError?: boolean; content: unknown }>;
516
+ };
517
+
518
+ expect(tools).toHaveLength(1);
519
+ expect(cliRegistrations).toHaveLength(1);
520
+ expect(methods.has("googlemeet.setup")).toBe(true);
521
+ expect(
522
+ googleMeetPluginTesting.isGoogleMeetAgentToolActionUnsupportedOnHost({
523
+ config: resolveGoogleMeetConfig({}),
524
+ raw: { action: "join" },
525
+ platform: "linux",
526
+ }),
527
+ ).toBe(true);
528
+
529
+ const blocked = await tool.execute("id", { action: "join" });
530
+ expect(JSON.stringify(blocked)).toContain("local Chrome realtime audio is macOS-only");
531
+
532
+ expect(
533
+ googleMeetPluginTesting.isGoogleMeetAgentToolActionUnsupportedOnHost({
534
+ config: resolveGoogleMeetConfig({}),
535
+ raw: { action: "join", mode: "transcribe" },
536
+ platform: "linux",
537
+ }),
538
+ ).toBe(false);
539
+ expect(
540
+ googleMeetPluginTesting.isGoogleMeetAgentToolActionUnsupportedOnHost({
541
+ config: resolveGoogleMeetConfig({}),
542
+ raw: { action: "join", transport: "chrome-node" },
543
+ platform: "linux",
544
+ }),
545
+ ).toBe(false);
546
+ });
547
+
510
548
  it("returns structured gateway errors for missing session ids", async () => {
511
549
  const { methods } = setup();
512
550
  for (const method of ["googlemeet.leave", "googlemeet.speak"]) {
@@ -557,8 +595,10 @@ describe("google-meet plugin", () => {
557
595
  "export",
558
596
  "recover_current_tab",
559
597
  "leave",
598
+ "end_active_conference",
560
599
  "speak",
561
600
  "test_speech",
601
+ "test_listen",
562
602
  ],
563
603
  description: expect.stringContaining("recover_current_tab"),
564
604
  },
@@ -1028,6 +1068,21 @@ describe("google-meet plugin", () => {
1028
1068
  });
1029
1069
  });
1030
1070
 
1071
+ it("explains that Twilio joins need dial-in details", async () => {
1072
+ const { tools } = setup({ defaultTransport: "twilio" });
1073
+ const tool = tools[0] as {
1074
+ execute: (id: string, params: unknown) => Promise<{ details: { error?: string } }>;
1075
+ };
1076
+
1077
+ const result = await tool.execute("id", {
1078
+ action: "join",
1079
+ url: "https://meet.google.com/abc-defg-hij",
1080
+ });
1081
+
1082
+ expect(result.details.error).toContain("Twilio transport requires a Meet dial-in phone number");
1083
+ expect(result.details.error).toContain("Google Meet URLs do not include dial-in details");
1084
+ });
1085
+
1031
1086
  it("hangs up delegated Twilio calls on leave", async () => {
1032
1087
  const { tools } = setup({ defaultTransport: "twilio" });
1033
1088
  const tool = tools[0] as {
@@ -1579,6 +1634,98 @@ describe("google-meet plugin", () => {
1579
1634
  );
1580
1635
  });
1581
1636
 
1637
+ it("reports missing Twilio dial plan for explicit Twilio setup", async () => {
1638
+ vi.stubEnv("TWILIO_ACCOUNT_SID", "AC123");
1639
+ vi.stubEnv("TWILIO_AUTH_TOKEN", "secret");
1640
+ vi.stubEnv("TWILIO_FROM_NUMBER", "+15550001234");
1641
+ const { tools } = setup(
1642
+ { defaultTransport: "chrome" },
1643
+ {
1644
+ fullConfig: {
1645
+ plugins: {
1646
+ allow: ["google-meet", "voice-call"],
1647
+ entries: {
1648
+ "voice-call": {
1649
+ enabled: true,
1650
+ config: {
1651
+ provider: "twilio",
1652
+ publicUrl: "https://voice.example.com/voice/webhook",
1653
+ },
1654
+ },
1655
+ },
1656
+ },
1657
+ },
1658
+ },
1659
+ );
1660
+ const tool = tools[0] as {
1661
+ execute: (
1662
+ id: string,
1663
+ params: unknown,
1664
+ ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>;
1665
+ };
1666
+
1667
+ const result = await tool.execute("id", { action: "setup_status", transport: "twilio" });
1668
+
1669
+ expect(result.details.ok).toBe(false);
1670
+ expect(result.details.checks).toEqual(
1671
+ expect.arrayContaining([
1672
+ expect.objectContaining({
1673
+ id: "twilio-dial-plan",
1674
+ ok: false,
1675
+ message: expect.stringContaining("dial-in phone number"),
1676
+ }),
1677
+ ]),
1678
+ );
1679
+ });
1680
+
1681
+ it("accepts request-provided Twilio dial-in details during setup", async () => {
1682
+ vi.stubEnv("TWILIO_ACCOUNT_SID", "AC123");
1683
+ vi.stubEnv("TWILIO_AUTH_TOKEN", "secret");
1684
+ vi.stubEnv("TWILIO_FROM_NUMBER", "+15550001234");
1685
+ const { tools } = setup(
1686
+ { defaultTransport: "chrome" },
1687
+ {
1688
+ fullConfig: {
1689
+ plugins: {
1690
+ allow: ["google-meet", "voice-call"],
1691
+ entries: {
1692
+ "voice-call": {
1693
+ enabled: true,
1694
+ config: {
1695
+ provider: "twilio",
1696
+ publicUrl: "https://voice.example.com/voice/webhook",
1697
+ },
1698
+ },
1699
+ },
1700
+ },
1701
+ },
1702
+ },
1703
+ );
1704
+ const tool = tools[0] as {
1705
+ execute: (
1706
+ id: string,
1707
+ params: unknown,
1708
+ ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>;
1709
+ };
1710
+
1711
+ const result = await tool.execute("id", {
1712
+ action: "setup_status",
1713
+ transport: "twilio",
1714
+ dialInNumber: "+15551234567",
1715
+ });
1716
+
1717
+ expect(result.details.ok).toBe(true);
1718
+ expect(result.details.checks).toEqual(
1719
+ expect.arrayContaining([
1720
+ expect.objectContaining({
1721
+ id: "twilio-dial-plan",
1722
+ ok: true,
1723
+ message: expect.stringContaining("request includes"),
1724
+ }),
1725
+ ]),
1726
+ );
1727
+ });
1728
+
1582
1729
  it.each([
1583
1730
  "http://127.0.0.1:3334/voice/webhook",
1584
1731
  "http://[::1]:3334/voice/webhook",
@@ -2394,6 +2541,52 @@ describe("google-meet plugin", () => {
2394
2541
  expect(result.details).toMatchObject({ createdSession: true });
2395
2542
  });
2396
2543
 
2544
+ it("exposes a test-listen action that proves transcript movement", async () => {
2545
+ const { tools, nodesInvoke } = setup(
2546
+ {
2547
+ defaultTransport: "chrome-node",
2548
+ },
2549
+ {
2550
+ browserActResult: {
2551
+ inCall: true,
2552
+ captioning: true,
2553
+ transcriptLines: 1,
2554
+ lastCaptionText: "hello from the meeting",
2555
+ title: "Meet call",
2556
+ url: "https://meet.google.com/abc-defg-hij",
2557
+ },
2558
+ nodesInvokeResult: {
2559
+ payload: {
2560
+ launched: true,
2561
+ },
2562
+ },
2563
+ },
2564
+ );
2565
+ const tool = tools[0] as {
2566
+ execute: (
2567
+ id: string,
2568
+ params: unknown,
2569
+ ) => Promise<{ details: { listenVerified?: boolean; transcriptLines?: number } }>;
2570
+ };
2571
+
2572
+ const result = await tool.execute("id", {
2573
+ action: "test_listen",
2574
+ url: "https://meet.google.com/abc-defg-hij",
2575
+ timeoutMs: 100,
2576
+ });
2577
+
2578
+ expect(nodesInvoke).toHaveBeenCalledWith(
2579
+ expect.objectContaining({
2580
+ command: "googlemeet.chrome",
2581
+ params: expect.objectContaining({
2582
+ action: "start",
2583
+ mode: "transcribe",
2584
+ }),
2585
+ }),
2586
+ );
2587
+ expect(result.details).toMatchObject({ listenVerified: true, transcriptLines: 1 });
2588
+ });
2589
+
2397
2590
  it("does not start a second realtime response for test speech", async () => {
2398
2591
  const runtime = new GoogleMeetRuntime({
2399
2592
  config: resolveGoogleMeetConfig({}),
@@ -2455,6 +2648,29 @@ describe("google-meet plugin", () => {
2455
2648
  ).rejects.toThrow("test_speech requires mode: realtime");
2456
2649
  });
2457
2650
 
2651
+ it("rejects realtime and Twilio modes for test listen", async () => {
2652
+ const runtime = new GoogleMeetRuntime({
2653
+ config: resolveGoogleMeetConfig({}),
2654
+ fullConfig: {} as never,
2655
+ runtime: {} as never,
2656
+ logger: noopLogger,
2657
+ });
2658
+
2659
+ await expect(
2660
+ runtime.testListen({
2661
+ url: "https://meet.google.com/abc-defg-hij",
2662
+ mode: "realtime",
2663
+ }),
2664
+ ).rejects.toThrow("test_listen requires mode: transcribe");
2665
+
2666
+ await expect(
2667
+ runtime.testListen({
2668
+ url: "https://meet.google.com/abc-defg-hij",
2669
+ transport: "twilio",
2670
+ }),
2671
+ ).rejects.toThrow("test_listen supports chrome or chrome-node");
2672
+ });
2673
+
2458
2674
  it("reports manual action when the browser profile needs Google login", async () => {
2459
2675
  const { tools } = setup(
2460
2676
  {
@@ -2850,6 +3066,7 @@ describe("google-meet plugin", () => {
2850
3066
  resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
2851
3067
  loadSessionStore: vi.fn(() => sessionStore),
2852
3068
  saveSessionStore: vi.fn(async () => {}),
3069
+ updateSessionStore: vi.fn(async (_storePath, mutator) => mutator(sessionStore as never)),
2853
3070
  resolveSessionFilePath: vi.fn(() => "/tmp/session.json"),
2854
3071
  },
2855
3072
  runEmbeddedPiAgent: vi.fn(async () => ({
@@ -3106,6 +3323,7 @@ describe("google-meet plugin", () => {
3106
3323
  },
3107
3324
  };
3108
3325
  let pullCount = 0;
3326
+ const sessionStore: Record<string, unknown> = {};
3109
3327
  const runtime = {
3110
3328
  nodes: {
3111
3329
  invoke: vi.fn(async ({ params }: { params?: { action?: string; base64?: string } }) => {
@@ -3126,8 +3344,9 @@ describe("google-meet plugin", () => {
3126
3344
  ensureAgentWorkspace: vi.fn(async () => {}),
3127
3345
  session: {
3128
3346
  resolveStorePath: vi.fn(() => "/tmp/sessions.json"),
3129
- loadSessionStore: vi.fn(() => ({})),
3347
+ loadSessionStore: vi.fn(() => sessionStore),
3130
3348
  saveSessionStore: vi.fn(async () => {}),
3349
+ updateSessionStore: vi.fn(async (_storePath, mutator) => mutator(sessionStore as never)),
3131
3350
  resolveSessionFilePath: vi.fn(() => "/tmp/session.json"),
3132
3351
  },
3133
3352
  runEmbeddedPiAgent: vi.fn(async () => ({
package/index.ts CHANGED
@@ -22,6 +22,7 @@ import {
22
22
  } from "./src/config.js";
23
23
  import {
24
24
  buildGoogleMeetPreflightReport,
25
+ endGoogleMeetActiveConference,
25
26
  fetchGoogleMeetArtifacts,
26
27
  fetchGoogleMeetAttendance,
27
28
  fetchLatestGoogleMeetConferenceRecord,
@@ -201,8 +202,10 @@ const GoogleMeetToolSchema = Type.Object({
201
202
  "export",
202
203
  "recover_current_tab",
203
204
  "leave",
205
+ "end_active_conference",
204
206
  "speak",
205
207
  "test_speech",
208
+ "test_listen",
206
209
  ],
207
210
  description:
208
211
  "Google Meet action to run. create creates and joins by default; pass join=false to only mint a URL. After a timeout or unclear browser state, call recover_current_tab before retrying join.",
@@ -212,6 +215,19 @@ const GoogleMeetToolSchema = Type.Object({
212
215
  description: "For action=create, set false to create the URL without joining.",
213
216
  }),
214
217
  ),
218
+ accessType: Type.Optional(
219
+ Type.String({
220
+ enum: ["OPEN", "TRUSTED", "RESTRICTED"],
221
+ description:
222
+ "For action=create with Google Meet OAuth, configure who can join without knocking.",
223
+ }),
224
+ ),
225
+ entryPointAccess: Type.Optional(
226
+ Type.String({
227
+ enum: ["ALL", "CREATOR_APP_ONLY"],
228
+ description: "For action=create with Google Meet OAuth, configure allowed join entry points.",
229
+ }),
230
+ ),
215
231
  url: Type.Optional(Type.String({ description: "Explicit https://meet.google.com/... URL" })),
216
232
  transport: Type.Optional(
217
233
  Type.String({ enum: ["chrome", "chrome-node", "twilio"], description: "Join transport" }),
@@ -223,11 +239,19 @@ const GoogleMeetToolSchema = Type.Object({
223
239
  "Join mode. realtime starts live listen/talk-back through the realtime voice model; transcribe joins without the realtime talk-back bridge.",
224
240
  }),
225
241
  ),
226
- dialInNumber: Type.Optional(Type.String({ description: "Meet dial-in number for Twilio" })),
227
- pin: Type.Optional(Type.String({ description: "Meet phone PIN for Twilio" })),
242
+ dialInNumber: Type.Optional(
243
+ Type.String({
244
+ description:
245
+ "Meet dial-in phone number for Twilio. Required for Twilio unless twilio.defaultDialInNumber is configured; Meet URLs cannot be dialed directly.",
246
+ }),
247
+ ),
248
+ pin: Type.Optional(
249
+ Type.String({ description: "Meet phone PIN for Twilio; # is appended if omitted" }),
250
+ ),
228
251
  dtmfSequence: Type.Optional(Type.String({ description: "Explicit DTMF sequence for Twilio" })),
229
252
  sessionId: Type.Optional(Type.String({ description: "Meet session ID" })),
230
253
  message: Type.Optional(Type.String({ description: "Realtime instructions to speak now" })),
254
+ timeoutMs: Type.Optional(Type.Number({ description: "Probe timeout in milliseconds" })),
231
255
  meeting: Type.Optional(Type.String({ description: "Meet URL, meeting code, or spaces/{id}" })),
232
256
  today: Type.Optional(
233
257
  Type.Boolean({
@@ -328,12 +352,17 @@ function shouldJoinCreatedMeet(raw: Record<string, unknown>): boolean {
328
352
 
329
353
  const googleMeetToolDeps = {
330
354
  callGatewayFromCli,
355
+ platform: () => process.platform,
331
356
  };
332
357
 
333
358
  export const __testing = {
334
359
  setCallGatewayFromCliForTests(next?: typeof callGatewayFromCli): void {
335
360
  googleMeetToolDeps.callGatewayFromCli = next ?? callGatewayFromCli;
336
361
  },
362
+ setPlatformForTests(next?: () => NodeJS.Platform): void {
363
+ googleMeetToolDeps.platform = next ?? (() => process.platform);
364
+ },
365
+ isGoogleMeetAgentToolActionUnsupportedOnHost,
337
366
  };
338
367
 
339
368
  type GoogleMeetGatewayToolAction =
@@ -343,8 +372,10 @@ type GoogleMeetGatewayToolAction =
343
372
  | "recover_current_tab"
344
373
  | "setup_status"
345
374
  | "leave"
375
+ | "end_active_conference"
346
376
  | "speak"
347
- | "test_speech";
377
+ | "test_speech"
378
+ | "test_listen";
348
379
 
349
380
  function googleMeetGatewayMethodForToolAction(action: GoogleMeetGatewayToolAction): string {
350
381
  switch (action) {
@@ -354,11 +385,52 @@ function googleMeetGatewayMethodForToolAction(action: GoogleMeetGatewayToolActio
354
385
  return "googlemeet.setup";
355
386
  case "test_speech":
356
387
  return "googlemeet.testSpeech";
388
+ case "test_listen":
389
+ return "googlemeet.testListen";
390
+ case "end_active_conference":
391
+ return "googlemeet.endActiveConference";
357
392
  default:
358
393
  return `googlemeet.${action}`;
359
394
  }
360
395
  }
361
396
 
397
+ function isGoogleMeetAgentToolActionUnsupportedOnHost(params: {
398
+ config: GoogleMeetConfig;
399
+ raw: Record<string, unknown>;
400
+ platform?: NodeJS.Platform;
401
+ }): boolean {
402
+ const platform = params.platform ?? googleMeetToolDeps.platform();
403
+ if (platform === "darwin") {
404
+ return false;
405
+ }
406
+ const action = params.raw.action;
407
+ if (
408
+ action !== "join" &&
409
+ action !== "test_speech" &&
410
+ !(action === "create" && shouldJoinCreatedMeet(params.raw))
411
+ ) {
412
+ return false;
413
+ }
414
+ const transport = normalizeTransport(params.raw.transport) ?? params.config.defaultTransport;
415
+ const mode =
416
+ action === "test_speech"
417
+ ? "realtime"
418
+ : (normalizeMode(params.raw.mode) ?? params.config.defaultMode);
419
+ return transport === "chrome" && mode === "realtime";
420
+ }
421
+
422
+ function assertGoogleMeetAgentToolActionSupported(params: {
423
+ config: GoogleMeetConfig;
424
+ raw: Record<string, unknown>;
425
+ }): void {
426
+ if (!isGoogleMeetAgentToolActionUnsupportedOnHost(params)) {
427
+ return;
428
+ }
429
+ throw new Error(
430
+ "Google Meet local Chrome realtime audio is macOS-only. On this host, use mode: transcribe, transport: twilio, or transport: chrome-node backed by a macOS node.",
431
+ );
432
+ }
433
+
362
434
  function resolveGoogleMeetToolGatewayTimeoutMs(config: GoogleMeetConfig): number {
363
435
  return Math.max(
364
436
  60_000,
@@ -711,6 +783,7 @@ export default definePluginEntry({
711
783
  await rt.setupStatus({
712
784
  transport: normalizeTransport(params?.transport),
713
785
  mode: normalizeMode(params?.mode),
786
+ dialInNumber: normalizeOptionalString(params?.dialInNumber),
714
787
  }),
715
788
  );
716
789
  } catch (err) {
@@ -842,6 +915,25 @@ export default definePluginEntry({
842
915
  },
843
916
  );
844
917
 
918
+ api.registerGatewayMethod(
919
+ "googlemeet.endActiveConference",
920
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
921
+ try {
922
+ const raw = asParamRecord(params);
923
+ const token = await resolveGoogleMeetTokenFromParams(config, raw);
924
+ respond(
925
+ true,
926
+ await endGoogleMeetActiveConference({
927
+ accessToken: token.accessToken,
928
+ meeting: resolveMeetingInput(config, raw.meeting),
929
+ }),
930
+ );
931
+ } catch (err) {
932
+ sendError(respond, err);
933
+ }
934
+ },
935
+ );
936
+
845
937
  api.registerGatewayMethod(
846
938
  "googlemeet.speak",
847
939
  async ({ params, respond }: GatewayRequestHandlerOptions) => {
@@ -880,15 +972,34 @@ export default definePluginEntry({
880
972
  },
881
973
  );
882
974
 
975
+ api.registerGatewayMethod(
976
+ "googlemeet.testListen",
977
+ async ({ params, respond }: GatewayRequestHandlerOptions) => {
978
+ try {
979
+ const rt = await ensureRuntime();
980
+ const result = await rt.testListen({
981
+ url: resolveMeetingInput(config, params?.url),
982
+ transport: normalizeTransport(params?.transport),
983
+ mode: normalizeMode(params?.mode),
984
+ timeoutMs: typeof params?.timeoutMs === "number" ? params.timeoutMs : undefined,
985
+ });
986
+ respond(true, result);
987
+ } catch (err) {
988
+ sendError(respond, err);
989
+ }
990
+ },
991
+ );
992
+
883
993
  api.registerTool({
884
994
  name: "google_meet",
885
995
  label: "Google Meet",
886
996
  description:
887
- "Join and track Google Meet sessions through Chrome or Twilio. Call setup_status before join/create/test_speech; if it reports a Chrome node offline or local audio missing, surface that blocker instead of retrying or switching transports. Offline nodes are diagnostics only, not usable candidates. 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.",
997
+ "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 realtime audio is unsupported on this OS, use mode=transcribe, transport=twilio, or a macOS chrome-node for realtime 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.",
888
998
  parameters: GoogleMeetToolSchema,
889
999
  async execute(_toolCallId, params) {
890
1000
  const raw = asParamRecord(params);
891
1001
  try {
1002
+ assertGoogleMeetAgentToolActionSupported({ config, raw });
892
1003
  switch (raw.action) {
893
1004
  case "join": {
894
1005
  return json(await callGoogleMeetGatewayFromTool({ config, action: "join", raw }));
@@ -901,6 +1012,11 @@ export default definePluginEntry({
901
1012
  await callGoogleMeetGatewayFromTool({ config, action: "test_speech", raw }),
902
1013
  );
903
1014
  }
1015
+ case "test_listen": {
1016
+ return json(
1017
+ await callGoogleMeetGatewayFromTool({ config, action: "test_listen", raw }),
1018
+ );
1019
+ }
904
1020
  case "status": {
905
1021
  return json(await callGoogleMeetGatewayFromTool({ config, action: "status", raw }));
906
1022
  }
@@ -999,6 +1115,15 @@ export default definePluginEntry({
999
1115
  }
1000
1116
  return json(await callGoogleMeetGatewayFromTool({ config, action: "leave", raw }));
1001
1117
  }
1118
+ case "end_active_conference": {
1119
+ return json(
1120
+ await callGoogleMeetGatewayFromTool({
1121
+ config,
1122
+ action: "end_active_conference",
1123
+ raw,
1124
+ }),
1125
+ );
1126
+ }
1002
1127
  case "speak": {
1003
1128
  const sessionId = normalizeOptionalString(raw.sessionId);
1004
1129
  if (!sessionId) {
@@ -9,6 +9,9 @@
9
9
  "onCommands": ["googlemeet"],
10
10
  "onCapabilities": ["tool"]
11
11
  },
12
+ "contracts": {
13
+ "tools": ["google_meet"]
14
+ },
12
15
  "uiHints": {
13
16
  "defaults.meeting": {
14
17
  "label": "Default Meeting",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/google-meet",
3
- "version": "2026.5.1-beta.2",
3
+ "version": "2026.5.2-beta.2",
4
4
  "description": "OpenClaw Google Meet participant plugin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -16,7 +16,7 @@
16
16
  "openclaw": "workspace:*"
17
17
  },
18
18
  "peerDependencies": {
19
- "openclaw": ">=2026.4.25"
19
+ "openclaw": ">=2026.5.2-beta.2"
20
20
  },
21
21
  "peerDependenciesMeta": {
22
22
  "openclaw": {
@@ -33,10 +33,10 @@
33
33
  "minHostVersion": ">=2026.4.20"
34
34
  },
35
35
  "compat": {
36
- "pluginApi": ">=2026.4.25"
36
+ "pluginApi": ">=2026.5.2-beta.2"
37
37
  },
38
38
  "build": {
39
- "openclawVersion": "2026.5.1-beta.2"
39
+ "openclawVersion": "2026.5.2-beta.2"
40
40
  },
41
41
  "release": {
42
42
  "publishToClawHub": true,