@kodelyth/google-meet 2026.5.42 → 2026.6.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.
@@ -1,204 +0,0 @@
1
- import type { PluginRuntime } from "klaw/plugin-sdk/plugin-runtime";
2
-
3
- type BrowserProxyResult = {
4
- result?: unknown;
5
- };
6
-
7
- export type BrowserTab = {
8
- targetId?: string;
9
- title?: string;
10
- url?: string;
11
- };
12
-
13
- export function normalizeMeetUrlForReuse(url: string | undefined): string | undefined {
14
- if (!url) {
15
- return undefined;
16
- }
17
- try {
18
- const parsed = new URL(url);
19
- if (parsed.protocol !== "https:" || parsed.hostname.toLowerCase() !== "meet.google.com") {
20
- return undefined;
21
- }
22
- const match = parsed.pathname.match(/^\/(new|[a-z]{3}-[a-z]{4}-[a-z]{3})(?:\/)?$/i);
23
- if (!match?.[1]) {
24
- return undefined;
25
- }
26
- return `https://meet.google.com/${match[1].toLowerCase()}`;
27
- } catch {
28
- return undefined;
29
- }
30
- }
31
-
32
- export function isSameMeetUrlForReuse(a: string | undefined, b: string | undefined): boolean {
33
- const normalizedA = normalizeMeetUrlForReuse(a);
34
- const normalizedB = normalizeMeetUrlForReuse(b);
35
- return Boolean(normalizedA && normalizedB && normalizedA === normalizedB);
36
- }
37
-
38
- type GoogleMeetNodeInfo = {
39
- caps?: string[];
40
- commands?: string[];
41
- connected?: boolean;
42
- nodeId?: string;
43
- displayName?: string;
44
- remoteIp?: string;
45
- };
46
-
47
- function isGoogleMeetNode(node: GoogleMeetNodeInfo) {
48
- const commands = Array.isArray(node.commands) ? node.commands : [];
49
- const caps = Array.isArray(node.caps) ? node.caps : [];
50
- return (
51
- node.connected === true &&
52
- commands.includes("googlemeet.chrome") &&
53
- (commands.includes("browser.proxy") || caps.includes("browser"))
54
- );
55
- }
56
-
57
- function matchesRequestedNode(node: GoogleMeetNodeInfo, requested: string): boolean {
58
- return [node.nodeId, node.displayName, node.remoteIp].some((value) => value === requested);
59
- }
60
-
61
- function formatNodeLabel(node: GoogleMeetNodeInfo): string {
62
- const parts = [node.displayName, node.nodeId, node.remoteIp].filter(Boolean);
63
- return parts.length > 0 ? parts.join(" / ") : "unknown node";
64
- }
65
-
66
- function describeNodeUsabilityIssues(node: GoogleMeetNodeInfo): string[] {
67
- const commands = Array.isArray(node.commands) ? node.commands : [];
68
- const caps = Array.isArray(node.caps) ? node.caps : [];
69
- const issues: string[] = [];
70
- if (node.connected !== true) {
71
- issues.push("offline");
72
- }
73
- if (!commands.includes("googlemeet.chrome")) {
74
- issues.push("missing googlemeet.chrome");
75
- }
76
- if (!commands.includes("browser.proxy") && !caps.includes("browser")) {
77
- issues.push("missing browser.proxy/browser capability");
78
- }
79
- return issues;
80
- }
81
-
82
- async function listGoogleMeetNodes(
83
- runtime: PluginRuntime,
84
- params?: { connected?: boolean },
85
- ): Promise<{ nodes: GoogleMeetNodeInfo[] }> {
86
- try {
87
- return params ? await runtime.nodes.list(params) : await runtime.nodes.list();
88
- } catch (error) {
89
- throw new Error("Google Meet node inventory unavailable", {
90
- cause: error,
91
- });
92
- }
93
- }
94
-
95
- export async function resolveChromeNodeInfo(params: {
96
- runtime: PluginRuntime;
97
- requestedNode?: string;
98
- }): Promise<GoogleMeetNodeInfo> {
99
- const requested = params.requestedNode?.trim();
100
- if (requested) {
101
- const list = await listGoogleMeetNodes(params.runtime);
102
- const matches = list.nodes.filter((node) => matchesRequestedNode(node, requested));
103
- if (matches.length === 1) {
104
- const [node] = matches;
105
- if (isGoogleMeetNode(node)) {
106
- return node;
107
- }
108
- throw new Error(
109
- `Configured Google Meet node ${requested} is not usable (${formatNodeLabel(node)}): ${describeNodeUsabilityIssues(node).join("; ")}. Start or reinstall \`klaw node run\` on that Chrome host, approve pairing, and allow googlemeet.chrome plus browser.proxy.`,
110
- );
111
- }
112
- if (matches.length > 1) {
113
- throw new Error(
114
- `Configured Google Meet node ${requested} is ambiguous (${matches.length} matches). Pin chromeNode.node to a unique node id, display name, or remote IP.`,
115
- );
116
- }
117
- throw new Error(
118
- `Configured Google Meet node ${requested} was not found. Run \`klaw nodes status\` and start or approve the Chrome node.`,
119
- );
120
- }
121
-
122
- const list = await listGoogleMeetNodes(params.runtime, { connected: true });
123
- const nodes = list.nodes.filter(isGoogleMeetNode);
124
- if (nodes.length === 0) {
125
- throw new Error(
126
- "No connected Google Meet-capable node with browser proxy. Run `klaw node run` on the Chrome host with browser proxy enabled, approve pairing, and allow googlemeet.chrome plus browser.proxy.",
127
- );
128
- }
129
- if (nodes.length === 1) {
130
- return nodes[0];
131
- }
132
- throw new Error(
133
- "Multiple Google Meet-capable nodes connected. Set plugins.entries.google-meet.config.chromeNode.node.",
134
- );
135
- }
136
-
137
- export async function resolveChromeNode(params: {
138
- runtime: PluginRuntime;
139
- requestedNode?: string;
140
- }): Promise<string> {
141
- const node = await resolveChromeNodeInfo(params);
142
- if (!node.nodeId) {
143
- throw new Error("Google Meet node did not include a node id.");
144
- }
145
- return node.nodeId;
146
- }
147
-
148
- function unwrapNodeInvokePayload(raw: unknown): unknown {
149
- const record = raw && typeof raw === "object" ? (raw as Record<string, unknown>) : {};
150
- if (typeof record.payloadJSON === "string" && record.payloadJSON.trim()) {
151
- try {
152
- return JSON.parse(record.payloadJSON);
153
- } catch (error) {
154
- throw new Error("Google Meet browser proxy returned malformed payloadJSON.", {
155
- cause: error,
156
- });
157
- }
158
- }
159
- if ("payload" in record) {
160
- return record.payload;
161
- }
162
- return raw;
163
- }
164
-
165
- function parseBrowserProxyResult(raw: unknown): unknown {
166
- const payload = unwrapNodeInvokePayload(raw);
167
- const proxy =
168
- payload && typeof payload === "object" ? (payload as BrowserProxyResult) : undefined;
169
- if (!proxy || !("result" in proxy)) {
170
- throw new Error("Google Meet browser proxy returned an invalid result.");
171
- }
172
- return proxy.result;
173
- }
174
-
175
- export async function callBrowserProxyOnNode(params: {
176
- runtime: PluginRuntime;
177
- nodeId: string;
178
- method: "GET" | "POST" | "DELETE";
179
- path: string;
180
- body?: unknown;
181
- timeoutMs: number;
182
- }) {
183
- const raw = await params.runtime.nodes.invoke({
184
- nodeId: params.nodeId,
185
- command: "browser.proxy",
186
- params: {
187
- method: params.method,
188
- path: params.path,
189
- body: params.body,
190
- timeoutMs: params.timeoutMs,
191
- },
192
- timeoutMs: params.timeoutMs + 5_000,
193
- });
194
- return parseBrowserProxyResult(raw);
195
- }
196
-
197
- export function asBrowserTabs(result: unknown): BrowserTab[] {
198
- const record = result && typeof result === "object" ? (result as Record<string, unknown>) : {};
199
- return Array.isArray(record.tabs) ? (record.tabs as BrowserTab[]) : [];
200
- }
201
-
202
- export function readBrowserTab(result: unknown): BrowserTab | undefined {
203
- return result && typeof result === "object" ? (result as BrowserTab) : undefined;
204
- }
@@ -1,364 +0,0 @@
1
- import type { PluginRuntime } from "klaw/plugin-sdk/plugin-runtime";
2
- import { sleep } from "klaw/plugin-sdk/runtime-env";
3
- import type { GoogleMeetConfig } from "../config.js";
4
- import {
5
- asBrowserTabs,
6
- callBrowserProxyOnNode,
7
- readBrowserTab,
8
- resolveChromeNode,
9
- type BrowserTab,
10
- } from "./chrome-browser-proxy.js";
11
- import type { GoogleMeetChromeHealth } from "./types.js";
12
-
13
- const GOOGLE_MEET_NEW_URL = "https://meet.google.com/new";
14
- const GOOGLE_MEET_BROWSER_CREATE_TIMEOUT_MS = 60_000;
15
- const GOOGLE_MEET_BROWSER_STEP_TIMEOUT_MS = 10_000;
16
- const GOOGLE_MEET_BROWSER_NAVIGATION_RETRY_MS = 1_000;
17
- const GOOGLE_MEET_BROWSER_POLL_MS = 500;
18
-
19
- type BrowserCreateStepResult = {
20
- meetingUri?: string;
21
- browserUrl?: string;
22
- browserTitle?: string;
23
- manualAction?: string;
24
- manualActionReason?: GoogleMeetChromeHealth["manualActionReason"];
25
- notes?: string[];
26
- retryAfterMs?: number;
27
- };
28
-
29
- type GoogleMeetBrowserCreateResult = {
30
- meetingUri: string;
31
- nodeId: string;
32
- targetId?: string;
33
- browserUrl?: string;
34
- browserTitle?: string;
35
- notes?: string[];
36
- source: "browser";
37
- };
38
-
39
- type GoogleMeetBrowserManualAction = {
40
- source: "browser";
41
- error: string;
42
- manualActionRequired: true;
43
- manualActionReason?: GoogleMeetChromeHealth["manualActionReason"];
44
- manualActionMessage: string;
45
- browser: {
46
- nodeId: string;
47
- targetId?: string;
48
- browserUrl?: string;
49
- browserTitle?: string;
50
- notes?: string[];
51
- };
52
- };
53
-
54
- class GoogleMeetBrowserManualActionError extends Error {
55
- readonly payload: GoogleMeetBrowserManualAction;
56
-
57
- constructor(payload: Omit<GoogleMeetBrowserManualAction, "source" | "error">) {
58
- const prefix = payload.manualActionReason ? `${payload.manualActionReason}: ` : "";
59
- super(`${prefix}${payload.manualActionMessage}`);
60
- this.name = "GoogleMeetBrowserManualActionError";
61
- this.payload = {
62
- source: "browser",
63
- error: this.message,
64
- ...payload,
65
- };
66
- }
67
- }
68
-
69
- export function isGoogleMeetBrowserManualActionError(
70
- error: unknown,
71
- ): error is GoogleMeetBrowserManualActionError {
72
- return error instanceof GoogleMeetBrowserManualActionError;
73
- }
74
-
75
- function formatBrowserAutomationError(error: unknown): string {
76
- if (error instanceof Error) {
77
- return error.message;
78
- }
79
- try {
80
- return JSON.stringify(error);
81
- } catch {
82
- return "unknown error";
83
- }
84
- }
85
-
86
- function isBrowserNavigationInterruption(error: unknown): boolean {
87
- return /execution context was destroyed|navigation|target closed/i.test(
88
- formatBrowserAutomationError(error),
89
- );
90
- }
91
-
92
- function isGoogleMeetCreateTab(tab: BrowserTab): boolean {
93
- const url = tab.url ?? "";
94
- if (/^https:\/\/meet\.google\.com\/(?:new|[a-z]{3}-[a-z]{4}-[a-z]{3})(?:$|[/?#])/i.test(url)) {
95
- return true;
96
- }
97
- return (
98
- url.startsWith("https://accounts.google.com/") &&
99
- /sign in|google accounts|meet/i.test(tab.title ?? "")
100
- );
101
- }
102
-
103
- async function findGoogleMeetCreateTab(params: {
104
- runtime: PluginRuntime;
105
- nodeId: string;
106
- timeoutMs: number;
107
- }): Promise<BrowserTab | undefined> {
108
- const tabs = asBrowserTabs(
109
- await callBrowserProxyOnNode({
110
- runtime: params.runtime,
111
- nodeId: params.nodeId,
112
- method: "GET",
113
- path: "/tabs",
114
- timeoutMs: params.timeoutMs,
115
- }),
116
- );
117
- return tabs.find(isGoogleMeetCreateTab);
118
- }
119
-
120
- async function focusBrowserTab(params: {
121
- runtime: PluginRuntime;
122
- nodeId: string;
123
- targetId: string;
124
- timeoutMs: number;
125
- }): Promise<void> {
126
- await callBrowserProxyOnNode({
127
- runtime: params.runtime,
128
- nodeId: params.nodeId,
129
- method: "POST",
130
- path: "/tabs/focus",
131
- body: { targetId: params.targetId },
132
- timeoutMs: params.timeoutMs,
133
- });
134
- }
135
-
136
- function readStringArray(value: unknown): string[] | undefined {
137
- return Array.isArray(value)
138
- ? value.filter((entry): entry is string => typeof entry === "string")
139
- : undefined;
140
- }
141
-
142
- function readBrowserCreateResult(result: unknown): BrowserCreateStepResult {
143
- const record = result && typeof result === "object" ? (result as Record<string, unknown>) : {};
144
- const nested =
145
- record.result && typeof record.result === "object"
146
- ? (record.result as Record<string, unknown>)
147
- : record;
148
- return {
149
- meetingUri: typeof nested.meetingUri === "string" ? nested.meetingUri : undefined,
150
- browserUrl: typeof nested.browserUrl === "string" ? nested.browserUrl : undefined,
151
- browserTitle: typeof nested.browserTitle === "string" ? nested.browserTitle : undefined,
152
- manualAction: typeof nested.manualAction === "string" ? nested.manualAction : undefined,
153
- manualActionReason:
154
- typeof nested.manualActionReason === "string"
155
- ? (nested.manualActionReason as GoogleMeetChromeHealth["manualActionReason"])
156
- : undefined,
157
- notes: readStringArray(nested.notes),
158
- retryAfterMs:
159
- typeof nested.retryAfterMs === "number" && Number.isFinite(nested.retryAfterMs)
160
- ? nested.retryAfterMs
161
- : undefined,
162
- };
163
- }
164
-
165
- export const CREATE_MEET_FROM_BROWSER_SCRIPT = `async () => {
166
- const meetUrlPattern = /^https:\\/\\/meet\\.google\\.com\\/[a-z]{3}-[a-z]{4}-[a-z]{3}(?:$|[/?#])/i;
167
- const text = (node) => (node?.innerText || node?.textContent || "").trim();
168
- const current = () => location.href;
169
- const notes = [];
170
- const findButton = (pattern) =>
171
- [...document.querySelectorAll("button")].find((button) => {
172
- const label = [
173
- button.getAttribute("aria-label"),
174
- button.getAttribute("data-tooltip"),
175
- text(button),
176
- ]
177
- .filter(Boolean)
178
- .join(" ");
179
- return pattern.test(label) && !button.disabled;
180
- });
181
- const clickButton = (pattern, note) => {
182
- const button = findButton(pattern);
183
- if (!button) {
184
- return false;
185
- }
186
- button.click();
187
- notes.push(note);
188
- return true;
189
- };
190
- if (!current().startsWith("https://meet.google.com/")) {
191
- return {
192
- manualActionReason: "google-login-required",
193
- manualAction: "Sign in to Google in the Klaw browser profile, then retry meeting creation.",
194
- browserUrl: current(),
195
- browserTitle: document.title,
196
- notes,
197
- };
198
- }
199
- const href = current();
200
- if (meetUrlPattern.test(href)) {
201
- return { meetingUri: href, browserUrl: href, browserTitle: document.title, notes };
202
- }
203
- const pageText = text(document.body);
204
- if (clickButton(/\\buse microphone\\b/i, "Accepted Meet microphone prompt with browser automation.")) {
205
- return { browserUrl: href, browserTitle: document.title, notes, retryAfterMs: 1000 };
206
- }
207
- if (
208
- clickButton(
209
- /continue without microphone/i,
210
- "Continued through Meet microphone prompt with browser automation.",
211
- )
212
- ) {
213
- return { browserUrl: href, browserTitle: document.title, notes, retryAfterMs: 1000 };
214
- }
215
- if (/do you want people to hear you in the meeting/i.test(pageText)) {
216
- return {
217
- manualActionReason: "meet-audio-choice-required",
218
- manualAction: "Meet is showing the microphone choice. Click Use microphone in the Klaw browser profile, then retry meeting creation.",
219
- browserUrl: href,
220
- browserTitle: document.title,
221
- notes,
222
- };
223
- }
224
- if (/allow.*(microphone|camera)|blocked.*(microphone|camera)|permission.*(microphone|camera)/i.test(pageText)) {
225
- return {
226
- manualActionReason: "meet-permission-required",
227
- manualAction: "Allow microphone/camera permissions for Meet in the Klaw browser profile, then retry meeting creation.",
228
- browserUrl: href,
229
- browserTitle: document.title,
230
- notes,
231
- };
232
- }
233
- if (/couldn't create|unable to create/i.test(pageText)) {
234
- return {
235
- manualAction: "Resolve the Google Meet page prompt in the Klaw browser profile, then retry meeting creation.",
236
- browserUrl: href,
237
- browserTitle: document.title,
238
- notes,
239
- };
240
- }
241
- if (location.hostname.toLowerCase() === "accounts.google.com" || /use your google account|to continue to google meet|choose an account|sign in to (join|continue)/i.test(pageText)) {
242
- return {
243
- manualActionReason: "google-login-required",
244
- manualAction: "Sign in to Google in the Klaw browser profile, then retry meeting creation.",
245
- browserUrl: href,
246
- browserTitle: document.title,
247
- notes,
248
- };
249
- }
250
- return {
251
- retryAfterMs: 500,
252
- browserUrl: current(),
253
- browserTitle: document.title,
254
- notes,
255
- };
256
- }`;
257
-
258
- export async function createMeetWithBrowserProxyOnNode(params: {
259
- runtime: PluginRuntime;
260
- config: GoogleMeetConfig;
261
- }): Promise<GoogleMeetBrowserCreateResult> {
262
- const nodeId = await resolveChromeNode({
263
- runtime: params.runtime,
264
- requestedNode: params.config.chromeNode.node,
265
- });
266
- const timeoutMs = Math.max(
267
- GOOGLE_MEET_BROWSER_CREATE_TIMEOUT_MS,
268
- params.config.chrome.joinTimeoutMs,
269
- );
270
- const stepTimeoutMs = Math.min(timeoutMs, GOOGLE_MEET_BROWSER_STEP_TIMEOUT_MS);
271
- let tab = await findGoogleMeetCreateTab({
272
- runtime: params.runtime,
273
- nodeId,
274
- timeoutMs: stepTimeoutMs,
275
- });
276
- if (tab?.targetId) {
277
- await focusBrowserTab({
278
- runtime: params.runtime,
279
- nodeId,
280
- targetId: tab.targetId,
281
- timeoutMs: stepTimeoutMs,
282
- });
283
- } else {
284
- tab = readBrowserTab(
285
- await callBrowserProxyOnNode({
286
- runtime: params.runtime,
287
- nodeId,
288
- method: "POST",
289
- path: "/tabs/open",
290
- body: { url: GOOGLE_MEET_NEW_URL },
291
- timeoutMs: stepTimeoutMs,
292
- }),
293
- );
294
- }
295
- const targetId = tab?.targetId;
296
- if (!targetId) {
297
- throw new Error("Browser fallback opened Google Meet but did not return a targetId.");
298
- }
299
- const notes = new Set<string>();
300
- let lastResult: BrowserCreateStepResult | undefined;
301
- let lastError: unknown;
302
- const deadline = Date.now() + timeoutMs;
303
- while (Date.now() <= deadline) {
304
- try {
305
- const evaluated = await callBrowserProxyOnNode({
306
- runtime: params.runtime,
307
- nodeId,
308
- method: "POST",
309
- path: "/act",
310
- body: {
311
- kind: "evaluate",
312
- targetId,
313
- fn: CREATE_MEET_FROM_BROWSER_SCRIPT,
314
- },
315
- timeoutMs: stepTimeoutMs,
316
- });
317
- const result = readBrowserCreateResult(evaluated);
318
- lastResult = result;
319
- for (const note of result.notes ?? []) {
320
- notes.add(note);
321
- }
322
- if (result.meetingUri) {
323
- return {
324
- source: "browser",
325
- nodeId,
326
- targetId,
327
- meetingUri: result.meetingUri,
328
- browserUrl: result.browserUrl,
329
- browserTitle: result.browserTitle,
330
- notes: [...notes],
331
- };
332
- }
333
- if (result.manualAction) {
334
- throw new GoogleMeetBrowserManualActionError({
335
- manualActionRequired: true,
336
- manualActionReason: result.manualActionReason,
337
- manualActionMessage: result.manualAction,
338
- browser: {
339
- nodeId,
340
- targetId,
341
- browserUrl: result.browserUrl,
342
- browserTitle: result.browserTitle,
343
- notes: [...notes],
344
- },
345
- });
346
- }
347
- await sleep(result.retryAfterMs ?? GOOGLE_MEET_BROWSER_POLL_MS);
348
- } catch (error) {
349
- lastError = error;
350
- if (!isBrowserNavigationInterruption(error)) {
351
- throw error;
352
- }
353
- await sleep(GOOGLE_MEET_BROWSER_NAVIGATION_RETRY_MS);
354
- }
355
- }
356
- throw new Error(
357
- lastResult?.manualAction ??
358
- `Google Meet did not return a meeting URL from the browser create flow before timeout.${
359
- lastError
360
- ? ` Last browser automation error: ${formatBrowserAutomationError(lastError)}`
361
- : ""
362
- }`,
363
- );
364
- }
@@ -1,12 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { testing } from "./chrome.js";
3
-
4
- describe("google meet chrome transport", () => {
5
- it("wraps malformed browser status JSON", () => {
6
- expect(() =>
7
- testing.parseMeetBrowserStatusForTest({
8
- result: "{not json",
9
- }),
10
- ).toThrow("Google Meet browser status JSON is malformed.");
11
- });
12
- });