@openclaw/google-meet 2026.5.1-beta.2 → 2026.5.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.create.test.ts +43 -2
- package/index.test.ts +220 -1
- package/index.ts +129 -4
- package/openclaw.plugin.json +3 -0
- package/package.json +4 -4
- package/src/cli.test.ts +107 -0
- package/src/cli.ts +83 -2
- package/src/create.ts +56 -6
- package/src/meet.ts +69 -7
- package/src/oauth.ts +6 -5
- package/src/runtime.ts +133 -3
- package/src/setup.ts +17 -0
- package/src/test-support/plugin-harness.ts +11 -1
- package/src/transports/types.ts +1 -0
- package/src/voice-call-gateway.test.ts +24 -4
- package/src/voice-call-gateway.ts +64 -6
- package/dist/.boundary-tsc.stamp +0 -1
- package/dist/.boundary-tsc.tsbuildinfo +0 -1
package/index.create.test.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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(
|
|
227
|
-
|
|
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
|
|
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) {
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openclaw/google-meet",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.2-beta.1",
|
|
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.
|
|
19
|
+
"openclaw": ">=2026.5.2-beta.1"
|
|
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.
|
|
36
|
+
"pluginApi": ">=2026.5.2-beta.1"
|
|
37
37
|
},
|
|
38
38
|
"build": {
|
|
39
|
-
"openclawVersion": "2026.5.
|
|
39
|
+
"openclawVersion": "2026.5.2-beta.1"
|
|
40
40
|
},
|
|
41
41
|
"release": {
|
|
42
42
|
"publishToClawHub": true,
|