@kodelyth/google-meet 2026.5.39 → 2026.5.42

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 (46) hide show
  1. package/dist/calendar-CEiBGUl2.js +136 -0
  2. package/dist/chrome-create-DMyPCiEd.js +965 -0
  3. package/dist/cli-BMVhSIb8.js +1390 -0
  4. package/dist/create-IbyMXB3V.js +108 -0
  5. package/dist/doctor-contract-api.js +56 -0
  6. package/dist/index.js +4979 -0
  7. package/dist/oauth-7_sWAae1.js +141 -0
  8. package/doctor-contract-api.ts +1 -0
  9. package/google-meet.live.test.ts +82 -0
  10. package/index.create.test.ts +671 -0
  11. package/index.test.ts +5051 -0
  12. package/index.ts +1224 -0
  13. package/klaw.plugin.json +12 -46
  14. package/node-host.test.ts +241 -0
  15. package/package.json +3 -3
  16. package/src/agent-consult.ts +158 -0
  17. package/src/calendar.ts +252 -0
  18. package/src/cli.test.ts +1234 -0
  19. package/src/cli.ts +2350 -0
  20. package/src/config-compat.test.ts +98 -0
  21. package/src/config-compat.ts +84 -0
  22. package/src/config.ts +589 -0
  23. package/src/create.ts +157 -0
  24. package/src/drive.ts +72 -0
  25. package/src/google-api-errors.ts +20 -0
  26. package/src/meet.ts +1024 -0
  27. package/src/node-host.ts +520 -0
  28. package/src/oauth.test.ts +73 -0
  29. package/src/oauth.ts +229 -0
  30. package/src/realtime-node.ts +780 -0
  31. package/src/realtime.ts +1334 -0
  32. package/src/runtime.ts +1008 -0
  33. package/src/setup.ts +285 -0
  34. package/src/test-support/plugin-harness.ts +232 -0
  35. package/src/transports/chrome-browser-proxy.test.ts +39 -0
  36. package/src/transports/chrome-browser-proxy.ts +204 -0
  37. package/src/transports/chrome-create.ts +364 -0
  38. package/src/transports/chrome.test.ts +12 -0
  39. package/src/transports/chrome.ts +1065 -0
  40. package/src/transports/twilio.ts +57 -0
  41. package/src/transports/types.ts +147 -0
  42. package/src/voice-call-gateway.test.ts +152 -0
  43. package/src/voice-call-gateway.ts +241 -0
  44. package/tsconfig.json +16 -0
  45. package/doctor-contract-api.js +0 -7
  46. package/index.js +0 -7
package/klaw.plugin.json CHANGED
@@ -3,24 +3,14 @@
3
3
  "name": "Google Meet",
4
4
  "description": "Join Google Meet calls through Chrome or Twilio transports.",
5
5
  "enabledByDefault": false,
6
- "commandAliases": [
7
- {
8
- "name": "googlemeet"
9
- }
10
- ],
6
+ "commandAliases": [{ "name": "googlemeet" }],
11
7
  "activation": {
12
8
  "onStartup": true,
13
- "onCommands": [
14
- "googlemeet"
15
- ],
16
- "onCapabilities": [
17
- "tool"
18
- ]
9
+ "onCommands": ["googlemeet"],
10
+ "onCapabilities": ["tool"]
19
11
  },
20
12
  "contracts": {
21
- "tools": [
22
- "google_meet"
23
- ]
13
+ "tools": ["google_meet"]
24
14
  },
25
15
  "uiHints": {
26
16
  "defaults.meeting": {
@@ -251,20 +241,12 @@
251
241
  },
252
242
  "defaultTransport": {
253
243
  "type": "string",
254
- "enum": [
255
- "chrome",
256
- "chrome-node",
257
- "twilio"
258
- ],
244
+ "enum": ["chrome", "chrome-node", "twilio"],
259
245
  "default": "chrome"
260
246
  },
261
247
  "defaultMode": {
262
248
  "type": "string",
263
- "enum": [
264
- "agent",
265
- "bidi",
266
- "transcribe"
267
- ],
249
+ "enum": ["agent", "bidi", "transcribe"],
268
250
  "default": "agent"
269
251
  },
270
252
  "chrome": {
@@ -273,9 +255,7 @@
273
255
  "properties": {
274
256
  "audioBackend": {
275
257
  "type": "string",
276
- "enum": [
277
- "blackhole-2ch"
278
- ],
258
+ "enum": ["blackhole-2ch"],
279
259
  "default": "blackhole-2ch"
280
260
  },
281
261
  "launch": {
@@ -307,10 +287,7 @@
307
287
  },
308
288
  "audioFormat": {
309
289
  "type": "string",
310
- "enum": [
311
- "pcm16-24khz",
312
- "g711-ulaw-8khz"
313
- ],
290
+ "enum": ["pcm16-24khz", "g711-ulaw-8khz"],
314
291
  "default": "pcm16-24khz"
315
292
  },
316
293
  "audioBufferBytes": {
@@ -464,10 +441,7 @@
464
441
  "properties": {
465
442
  "strategy": {
466
443
  "type": "string",
467
- "enum": [
468
- "agent",
469
- "bidi"
470
- ],
444
+ "enum": ["agent", "bidi"],
471
445
  "default": "agent"
472
446
  },
473
447
  "provider": {
@@ -498,11 +472,7 @@
498
472
  },
499
473
  "toolPolicy": {
500
474
  "type": "string",
501
- "enum": [
502
- "safe-read-only",
503
- "owner",
504
- "none"
505
- ],
475
+ "enum": ["safe-read-only", "owner", "none"],
506
476
  "default": "safe-read-only"
507
477
  },
508
478
  "providers": {
@@ -541,9 +511,7 @@
541
511
  "properties": {
542
512
  "provider": {
543
513
  "type": "string",
544
- "enum": [
545
- "google-oauth"
546
- ]
514
+ "enum": ["google-oauth"]
547
515
  },
548
516
  "clientId": {
549
517
  "type": "string"
@@ -559,8 +527,6 @@
559
527
  }
560
528
  },
561
529
  "configContracts": {
562
- "compatibilityMigrationPaths": [
563
- "plugins.entries.google-meet.config.realtime.provider"
564
- ]
530
+ "compatibilityMigrationPaths": ["plugins.entries.google-meet.config.realtime.provider"]
565
531
  }
566
532
  }
@@ -0,0 +1,241 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { EventEmitter } from "node:events";
3
+ import { afterAll, afterEach, describe, expect, it, vi } from "vitest";
4
+
5
+ type MockChild = EventEmitter & {
6
+ exitCode: number | null;
7
+ signalCode: NodeJS.Signals | null;
8
+ kill: ReturnType<typeof vi.fn>;
9
+ stdout?: EventEmitter;
10
+ stderr?: EventEmitter;
11
+ stdin?: { write: ReturnType<typeof vi.fn> };
12
+ };
13
+
14
+ const children: MockChild[] = [];
15
+
16
+ vi.mock("node:child_process", async (importOriginal) => {
17
+ const actual = await importOriginal<typeof import("node:child_process")>();
18
+ return {
19
+ ...actual,
20
+ spawnSync: vi.fn(() => ({
21
+ status: 0,
22
+ stdout: "BlackHole 2ch",
23
+ stderr: "",
24
+ })),
25
+ spawn: vi.fn(() => {
26
+ const child = Object.assign(new EventEmitter(), {
27
+ exitCode: null,
28
+ signalCode: null,
29
+ kill: vi.fn((signal?: NodeJS.Signals) => {
30
+ child.signalCode = signal ?? "SIGTERM";
31
+ return true;
32
+ }),
33
+ stdout: new EventEmitter(),
34
+ stderr: new EventEmitter(),
35
+ stdin: { write: vi.fn() },
36
+ }) as MockChild;
37
+ children.push(child);
38
+ return child;
39
+ }),
40
+ };
41
+ });
42
+
43
+ describe("google-meet node host bridge sessions", () => {
44
+ afterEach(() => {
45
+ vi.useRealTimers();
46
+ children.length = 0;
47
+ });
48
+
49
+ afterAll(() => {
50
+ vi.doUnmock("node:child_process");
51
+ vi.resetModules();
52
+ });
53
+
54
+ it("reports malformed params JSON with an owned error", async () => {
55
+ const { handleGoogleMeetNodeHostCommand } = await import("./src/node-host.js");
56
+
57
+ await expect(handleGoogleMeetNodeHostCommand("{not json")).rejects.toThrow(
58
+ "Google Meet node host received malformed params JSON.",
59
+ );
60
+ });
61
+
62
+ it("starts observe-only Chrome without BlackHole or bridge processes", async () => {
63
+ const { handleGoogleMeetNodeHostCommand } = await import("./src/node-host.js");
64
+ const originalPlatform = process.platform;
65
+ children.length = 0;
66
+ vi.mocked(spawnSync).mockClear();
67
+
68
+ Object.defineProperty(process, "platform", { configurable: true, value: "darwin" });
69
+ try {
70
+ const start = JSON.parse(
71
+ await handleGoogleMeetNodeHostCommand(
72
+ JSON.stringify({
73
+ action: "start",
74
+ url: "https://meet.google.com/xyz-abcd-uvw",
75
+ mode: "transcribe",
76
+ launch: false,
77
+ audioInputCommand: ["mock-rec"],
78
+ audioOutputCommand: ["mock-play"],
79
+ }),
80
+ ),
81
+ );
82
+
83
+ expect(start).toEqual({ launched: false });
84
+ expect(spawnSync).not.toHaveBeenCalled();
85
+ expect(children).toHaveLength(0);
86
+ } finally {
87
+ Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
88
+ }
89
+ });
90
+
91
+ it("clears output playback without closing the active bridge when the old output exits", async () => {
92
+ const { handleGoogleMeetNodeHostCommand } = await import("./src/node-host.js");
93
+ const originalPlatform = process.platform;
94
+ children.length = 0;
95
+
96
+ Object.defineProperty(process, "platform", { configurable: true, value: "darwin" });
97
+ try {
98
+ const start = JSON.parse(
99
+ await handleGoogleMeetNodeHostCommand(
100
+ JSON.stringify({
101
+ action: "start",
102
+ url: "https://meet.google.com/xyz-abcd-uvw",
103
+ mode: "realtime",
104
+ launch: false,
105
+ audioInputCommand: ["mock-rec"],
106
+ audioOutputCommand: ["mock-play"],
107
+ }),
108
+ ),
109
+ );
110
+
111
+ expect(children).toHaveLength(2);
112
+ const firstOutput = children[0];
113
+
114
+ const cleared = JSON.parse(
115
+ await handleGoogleMeetNodeHostCommand(
116
+ JSON.stringify({
117
+ action: "clearAudio",
118
+ bridgeId: start.bridgeId,
119
+ }),
120
+ ),
121
+ );
122
+
123
+ expect(cleared).toEqual({ bridgeId: start.bridgeId, ok: true, clearCount: 1 });
124
+ expect(children).toHaveLength(3);
125
+ expect(firstOutput?.kill).toHaveBeenCalledWith("SIGTERM");
126
+
127
+ firstOutput?.emit("error", new Error("stale output failed after clear"));
128
+ firstOutput?.emit("exit", 0, "SIGTERM");
129
+
130
+ const status = JSON.parse(
131
+ await handleGoogleMeetNodeHostCommand(
132
+ JSON.stringify({
133
+ action: "status",
134
+ bridgeId: start.bridgeId,
135
+ }),
136
+ ),
137
+ );
138
+
139
+ expect(status.bridge.bridgeId).toBe(start.bridgeId);
140
+ expect(status.bridge.closed).toBe(false);
141
+ expect(status.bridge.clearCount).toBe(1);
142
+ expect(typeof status.bridge.createdAt).toBe("string");
143
+
144
+ const audio = Buffer.from([1, 2, 3]);
145
+ await handleGoogleMeetNodeHostCommand(
146
+ JSON.stringify({
147
+ action: "pushAudio",
148
+ bridgeId: start.bridgeId,
149
+ base64: audio.toString("base64"),
150
+ }),
151
+ );
152
+
153
+ expect(children[2]?.stdin?.write).toHaveBeenCalledWith(audio);
154
+ expect(firstOutput?.stdin?.write).not.toHaveBeenCalled();
155
+
156
+ await handleGoogleMeetNodeHostCommand(
157
+ JSON.stringify({
158
+ action: "stop",
159
+ bridgeId: start.bridgeId,
160
+ }),
161
+ );
162
+ } finally {
163
+ Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
164
+ }
165
+ });
166
+
167
+ it("lists active bridge sessions and hides closed sessions", async () => {
168
+ const { handleGoogleMeetNodeHostCommand } = await import("./src/node-host.js");
169
+ const originalPlatform = process.platform;
170
+ children.length = 0;
171
+
172
+ Object.defineProperty(process, "platform", { configurable: true, value: "darwin" });
173
+ try {
174
+ const start = JSON.parse(
175
+ await handleGoogleMeetNodeHostCommand(
176
+ JSON.stringify({
177
+ action: "start",
178
+ url: "https://meet.google.com/abc-defg-hij?authuser=1",
179
+ mode: "realtime",
180
+ launch: false,
181
+ audioInputCommand: ["mock-rec"],
182
+ audioOutputCommand: ["mock-play"],
183
+ }),
184
+ ),
185
+ );
186
+
187
+ expect(typeof start.bridgeId).toBe("string");
188
+ expect(start.bridgeId.length).toBeGreaterThan(0);
189
+ expect(start).toEqual({
190
+ audioBridge: { type: "node-command-pair" },
191
+ bridgeId: start.bridgeId,
192
+ launched: false,
193
+ });
194
+
195
+ const activeList = JSON.parse(
196
+ await handleGoogleMeetNodeHostCommand(
197
+ JSON.stringify({
198
+ action: "list",
199
+ url: "https://meet.google.com/abc-defg-hij",
200
+ mode: "realtime",
201
+ }),
202
+ ),
203
+ );
204
+
205
+ expect(activeList.bridges).toHaveLength(1);
206
+ expect(activeList.bridges[0]?.bridgeId).toBe(start.bridgeId);
207
+ expect(activeList.bridges[0]?.closed).toBe(false);
208
+ expect(activeList.bridges[0]?.mode).toBe("realtime");
209
+ expect(activeList.bridges[0]?.url).toBe("https://meet.google.com/abc-defg-hij?authuser=1");
210
+ expect(typeof activeList.bridges[0]?.createdAt).toBe("string");
211
+
212
+ children[1]?.emit("exit", 0, null);
213
+
214
+ const afterExitList = JSON.parse(
215
+ await handleGoogleMeetNodeHostCommand(
216
+ JSON.stringify({
217
+ action: "list",
218
+ url: "https://meet.google.com/abc-defg-hij",
219
+ mode: "realtime",
220
+ }),
221
+ ),
222
+ );
223
+
224
+ expect(afterExitList).toEqual({ bridges: [] });
225
+
226
+ const stopped = JSON.parse(
227
+ await handleGoogleMeetNodeHostCommand(
228
+ JSON.stringify({
229
+ action: "stopByUrl",
230
+ url: "https://meet.google.com/abc-defg-hij",
231
+ mode: "realtime",
232
+ }),
233
+ ),
234
+ );
235
+
236
+ expect(stopped).toEqual({ ok: true, stopped: 0 });
237
+ } finally {
238
+ Object.defineProperty(process, "platform", { configurable: true, value: originalPlatform });
239
+ }
240
+ });
241
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodelyth/google-meet",
3
- "version": "2026.5.39",
3
+ "version": "2026.5.42",
4
4
  "description": "Klaw Google Meet participant plugin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,7 +13,7 @@
13
13
  },
14
14
  "devDependencies": {
15
15
  "@kodelyth/plugin-sdk": "1.0.1",
16
- "@kodelyth/klaw": "2026.5.41"
16
+ "@kodelyth/klaw": "2026.5.42"
17
17
  },
18
18
  "peerDependencies": {
19
19
  "@kodelyth/klaw": ">=2026.5.19"
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "klaw": {
27
27
  "extensions": [
28
- "./index.js"
28
+ "./index.ts"
29
29
  ],
30
30
  "install": {
31
31
  "npmSpec": "@kodelyth/google-meet",
@@ -0,0 +1,158 @@
1
+ import type { KlawConfig } from "klaw/plugin-sdk/config-contracts";
2
+ import { formatErrorMessage } from "klaw/plugin-sdk/error-runtime";
3
+ import type { PluginRuntime, RuntimeLogger } from "klaw/plugin-sdk/plugin-runtime";
4
+ import {
5
+ buildRealtimeVoiceAgentConsultWorkingResponse,
6
+ consultRealtimeVoiceAgent,
7
+ REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME,
8
+ resolveRealtimeVoiceAgentConsultTools,
9
+ resolveRealtimeVoiceAgentConsultToolsAllow,
10
+ type RealtimeVoiceBridgeSession,
11
+ type RealtimeVoiceToolCallEvent,
12
+ type RealtimeVoiceTool,
13
+ type TalkEventInput,
14
+ } from "klaw/plugin-sdk/realtime-voice";
15
+ import { normalizeAgentId } from "klaw/plugin-sdk/routing";
16
+ import { normalizeOptionalString } from "klaw/plugin-sdk/string-coerce-runtime";
17
+ import type { GoogleMeetConfig, GoogleMeetToolPolicy } from "./config.js";
18
+
19
+ export const GOOGLE_MEET_AGENT_CONSULT_TOOL_NAME = REALTIME_VOICE_AGENT_CONSULT_TOOL_NAME;
20
+
21
+ const GOOGLE_MEET_CONSULT_SYSTEM_PROMPT = [
22
+ "You are a behind-the-scenes consultant for a live meeting voice agent.",
23
+ "Prioritize a fast, speakable answer over exhaustive investigation.",
24
+ "For tool-backed status checks, prefer one or two bounded read-only queries before answering.",
25
+ "Do not print secret values or dump environment variables; only check whether required configuration is present.",
26
+ "Be accurate, brief, and speakable.",
27
+ ].join(" ");
28
+
29
+ export function resolveGoogleMeetRealtimeTools(policy: GoogleMeetToolPolicy): RealtimeVoiceTool[] {
30
+ return resolveRealtimeVoiceAgentConsultTools(policy);
31
+ }
32
+
33
+ export function submitGoogleMeetConsultWorkingResponse(
34
+ session: RealtimeVoiceBridgeSession,
35
+ callId: string,
36
+ ): void {
37
+ if (!session.bridge.supportsToolResultContinuation) {
38
+ return;
39
+ }
40
+ session.submitToolResult(callId, buildRealtimeVoiceAgentConsultWorkingResponse("participant"), {
41
+ willContinue: true,
42
+ });
43
+ }
44
+
45
+ export async function consultKlawAgentForGoogleMeet(params: {
46
+ config: GoogleMeetConfig;
47
+ fullConfig: KlawConfig;
48
+ runtime: PluginRuntime;
49
+ logger: RuntimeLogger;
50
+ meetingSessionId: string;
51
+ requesterSessionKey?: string;
52
+ args: unknown;
53
+ transcript: Array<{ role: "user" | "assistant"; text: string }>;
54
+ }): Promise<{ text: string }> {
55
+ const agentId = normalizeAgentId(params.config.realtime.agentId);
56
+ const requesterSessionKey =
57
+ normalizeOptionalString(params.requesterSessionKey) ?? `agent:${agentId}:main`;
58
+ const sessionKey = `agent:${agentId}:subagent:google-meet:${params.meetingSessionId}`;
59
+ return await consultRealtimeVoiceAgent({
60
+ cfg: params.fullConfig,
61
+ agentRuntime: params.runtime.agent,
62
+ logger: params.logger,
63
+ agentId,
64
+ sessionKey,
65
+ messageProvider: "google-meet",
66
+ lane: "google-meet",
67
+ runIdPrefix: `google-meet:${params.meetingSessionId}`,
68
+ spawnedBy: requesterSessionKey,
69
+ contextMode: "fork",
70
+ args: params.args,
71
+ transcript: params.transcript,
72
+ surface: "a private Google Meet",
73
+ userLabel: "Participant",
74
+ assistantLabel: "Agent",
75
+ questionSourceLabel: "participant",
76
+ toolsAllow: resolveRealtimeVoiceAgentConsultToolsAllow(params.config.realtime.toolPolicy),
77
+ extraSystemPrompt: GOOGLE_MEET_CONSULT_SYSTEM_PROMPT,
78
+ });
79
+ }
80
+
81
+ export function handleGoogleMeetRealtimeConsultToolCall(params: {
82
+ strategy: string;
83
+ session: RealtimeVoiceBridgeSession;
84
+ event: RealtimeVoiceToolCallEvent;
85
+ config: GoogleMeetConfig;
86
+ fullConfig: KlawConfig;
87
+ runtime: PluginRuntime;
88
+ logger: RuntimeLogger;
89
+ meetingSessionId: string;
90
+ requesterSessionKey?: string;
91
+ transcript: Array<{ role: "user" | "assistant"; text: string }>;
92
+ onTalkEvent?: (event: TalkEventInput) => void;
93
+ }): void {
94
+ const callId = params.event.callId || params.event.itemId;
95
+ if (params.strategy !== "bidi") {
96
+ params.onTalkEvent?.({
97
+ type: "tool.error",
98
+ callId,
99
+ payload: {
100
+ name: params.event.name,
101
+ error: `Tool "${params.event.name}" is only available in bidi realtime strategy`,
102
+ },
103
+ final: true,
104
+ });
105
+ params.session.submitToolResult(callId, {
106
+ error: `Tool "${params.event.name}" is only available in bidi realtime strategy`,
107
+ });
108
+ return;
109
+ }
110
+ if (params.event.name !== GOOGLE_MEET_AGENT_CONSULT_TOOL_NAME) {
111
+ params.onTalkEvent?.({
112
+ type: "tool.error",
113
+ callId,
114
+ payload: { name: params.event.name, error: `Tool "${params.event.name}" not available` },
115
+ final: true,
116
+ });
117
+ params.session.submitToolResult(callId, {
118
+ error: `Tool "${params.event.name}" not available`,
119
+ });
120
+ return;
121
+ }
122
+ params.onTalkEvent?.({
123
+ type: "tool.progress",
124
+ callId,
125
+ payload: { name: params.event.name, status: "working" },
126
+ });
127
+ submitGoogleMeetConsultWorkingResponse(params.session, callId);
128
+ void consultKlawAgentForGoogleMeet({
129
+ config: params.config,
130
+ fullConfig: params.fullConfig,
131
+ runtime: params.runtime,
132
+ logger: params.logger,
133
+ meetingSessionId: params.meetingSessionId,
134
+ requesterSessionKey: params.requesterSessionKey,
135
+ args: params.event.args,
136
+ transcript: params.transcript,
137
+ })
138
+ .then((result) => {
139
+ params.onTalkEvent?.({
140
+ type: "tool.result",
141
+ callId,
142
+ payload: { name: params.event.name, result },
143
+ final: true,
144
+ });
145
+ params.session.submitToolResult(callId, result);
146
+ })
147
+ .catch((error: Error) => {
148
+ params.onTalkEvent?.({
149
+ type: "tool.error",
150
+ callId,
151
+ payload: { name: params.event.name, error: formatErrorMessage(error) },
152
+ final: true,
153
+ });
154
+ params.session.submitToolResult(callId, {
155
+ error: formatErrorMessage(error),
156
+ });
157
+ });
158
+ }