@gakr-gakr/google-meet 0.1.0

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/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@gakr-gakr/google-meet",
3
+ "version": "0.1.0",
4
+ "description": "AutoBot Google Meet participant plugin",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "https://github.com/autobot/autobot"
8
+ },
9
+ "type": "module",
10
+ "dependencies": {
11
+ "commander": "14.0.3",
12
+ "typebox": "1.1.38"
13
+ },
14
+ "devDependencies": {
15
+ "@gakr-gakr/plugin-sdk": "workspace:*",
16
+ "@gakr-gakr/autobot": "workspace:*",
17
+ "autobot": "workspace:@gakr-gakr/autobot@*"
18
+ },
19
+ "peerDependencies": {
20
+ "@gakr-gakr/autobot": ">=0.1.0"
21
+ },
22
+ "peerDependenciesMeta": {
23
+ "@gakr-gakr/autobot": {
24
+ "optional": true
25
+ }
26
+ },
27
+ "autobot": {
28
+ "extensions": [
29
+ "./index.ts"
30
+ ],
31
+ "install": {
32
+ "npmSpec": "@gakr-gakr/google-meet",
33
+ "defaultChoice": "npm",
34
+ "minHostVersion": ">=2026.4.20"
35
+ },
36
+ "compat": {
37
+ "pluginApi": ">=2026.5.19"
38
+ },
39
+ "build": {
40
+ "autobotVersion": "2026.5.19"
41
+ },
42
+ "release": {
43
+ "publishToClawHub": true,
44
+ "publishToNpm": true
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,158 @@
1
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
2
+ import { formatErrorMessage } from "autobot/plugin-sdk/error-runtime";
3
+ import type { PluginRuntime, RuntimeLogger } from "autobot/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 "autobot/plugin-sdk/realtime-voice";
15
+ import { normalizeAgentId } from "autobot/plugin-sdk/routing";
16
+ import { normalizeOptionalString } from "autobot/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 consultAutoBotAgentForGoogleMeet(params: {
46
+ config: GoogleMeetConfig;
47
+ fullConfig: AutoBotConfig;
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: AutoBotConfig;
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 consultAutoBotAgentForGoogleMeet({
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
+ }
@@ -0,0 +1,252 @@
1
+ import { fetchWithSsrFGuard } from "autobot/plugin-sdk/ssrf-runtime";
2
+ import { googleApiError } from "./google-api-errors.js";
3
+
4
+ const GOOGLE_CALENDAR_API_BASE_URL = "https://www.googleapis.com/calendar/v3";
5
+ const GOOGLE_CALENDAR_API_HOST = "www.googleapis.com";
6
+ const GOOGLE_MEET_URL_HOST = "meet.google.com";
7
+ const GOOGLE_CALENDAR_EVENTS_SCOPE = "https://www.googleapis.com/auth/calendar.events.readonly";
8
+
9
+ type GoogleCalendarEventDate = {
10
+ date?: string;
11
+ dateTime?: string;
12
+ timeZone?: string;
13
+ };
14
+
15
+ type GoogleCalendarConferenceEntryPoint = {
16
+ entryPointType?: string;
17
+ uri?: string;
18
+ label?: string;
19
+ };
20
+
21
+ type GoogleMeetCalendarEvent = {
22
+ id?: string;
23
+ summary?: string;
24
+ description?: string;
25
+ location?: string;
26
+ status?: string;
27
+ htmlLink?: string;
28
+ hangoutLink?: string;
29
+ start?: GoogleCalendarEventDate;
30
+ end?: GoogleCalendarEventDate;
31
+ conferenceData?: {
32
+ conferenceId?: string;
33
+ conferenceSolution?: {
34
+ key?: { type?: string };
35
+ name?: string;
36
+ };
37
+ entryPoints?: GoogleCalendarConferenceEntryPoint[];
38
+ };
39
+ };
40
+
41
+ export type GoogleMeetCalendarLookupResult = {
42
+ calendarId: string;
43
+ event: GoogleMeetCalendarEvent;
44
+ meetingUri: string;
45
+ };
46
+
47
+ type GoogleMeetCalendarEventsResult = {
48
+ calendarId: string;
49
+ events: Array<{
50
+ event: GoogleMeetCalendarEvent;
51
+ meetingUri: string;
52
+ selected: boolean;
53
+ }>;
54
+ };
55
+
56
+ function appendQuery(url: string, query: Record<string, string | number | boolean | undefined>) {
57
+ const parsed = new URL(url);
58
+ for (const [key, value] of Object.entries(query)) {
59
+ if (value !== undefined) {
60
+ parsed.searchParams.set(key, String(value));
61
+ }
62
+ }
63
+ return parsed.toString();
64
+ }
65
+
66
+ function isGoogleMeetUri(value: string | undefined): value is string {
67
+ if (!value?.trim()) {
68
+ return false;
69
+ }
70
+ try {
71
+ return new URL(value).hostname === GOOGLE_MEET_URL_HOST;
72
+ } catch {
73
+ return false;
74
+ }
75
+ }
76
+
77
+ function extractGoogleMeetUriFromText(value: string | undefined): string | undefined {
78
+ const match = value?.match(/https:\/\/meet\.google\.com\/[a-z0-9-]+/i);
79
+ return match?.[0];
80
+ }
81
+
82
+ export function extractGoogleMeetUriFromCalendarEvent(
83
+ event: GoogleMeetCalendarEvent,
84
+ ): string | undefined {
85
+ if (isGoogleMeetUri(event.hangoutLink)) {
86
+ return event.hangoutLink;
87
+ }
88
+ const entryPoints = event.conferenceData?.entryPoints ?? [];
89
+ const videoEntry = entryPoints.find(
90
+ (entry) => entry.entryPointType === "video" && isGoogleMeetUri(entry.uri),
91
+ );
92
+ if (videoEntry?.uri) {
93
+ return videoEntry.uri;
94
+ }
95
+ const meetEntry = entryPoints.find((entry) => isGoogleMeetUri(entry.uri));
96
+ if (meetEntry?.uri) {
97
+ return meetEntry.uri;
98
+ }
99
+ return (
100
+ extractGoogleMeetUriFromText(event.location) ?? extractGoogleMeetUriFromText(event.description)
101
+ );
102
+ }
103
+
104
+ export function buildGoogleMeetCalendarDayWindow(now = new Date()): {
105
+ timeMin: string;
106
+ timeMax: string;
107
+ } {
108
+ const start = new Date(now);
109
+ start.setHours(0, 0, 0, 0);
110
+ const end = new Date(start);
111
+ end.setDate(start.getDate() + 1);
112
+ return { timeMin: start.toISOString(), timeMax: end.toISOString() };
113
+ }
114
+
115
+ function parseCalendarEventTime(value: GoogleCalendarEventDate | undefined): number | undefined {
116
+ const raw = value?.dateTime ?? value?.date;
117
+ if (!raw) {
118
+ return undefined;
119
+ }
120
+ const parsed = Date.parse(raw);
121
+ return Number.isFinite(parsed) ? parsed : undefined;
122
+ }
123
+
124
+ function rankCalendarEvent(event: GoogleMeetCalendarEvent, nowMs: number): number {
125
+ const startMs = parseCalendarEventTime(event.start) ?? Number.POSITIVE_INFINITY;
126
+ const endMs = parseCalendarEventTime(event.end) ?? startMs;
127
+ if (startMs <= nowMs && endMs >= nowMs) {
128
+ return 0;
129
+ }
130
+ if (startMs > nowMs) {
131
+ return startMs - nowMs;
132
+ }
133
+ return nowMs - startMs + 30 * 24 * 60 * 60 * 1000;
134
+ }
135
+
136
+ function chooseBestMeetCalendarEvent(
137
+ events: GoogleMeetCalendarEvent[],
138
+ now: Date,
139
+ ): GoogleMeetCalendarLookupResult["event"] | undefined {
140
+ const nowMs = now.getTime();
141
+ let selected: GoogleMeetCalendarEvent | undefined;
142
+ let selectedRank = Number.POSITIVE_INFINITY;
143
+ for (const event of events) {
144
+ if (event.status === "cancelled" || !extractGoogleMeetUriFromCalendarEvent(event)) {
145
+ continue;
146
+ }
147
+ const rank = rankCalendarEvent(event, nowMs);
148
+ if (!selected || rank < selectedRank) {
149
+ selected = event;
150
+ selectedRank = rank;
151
+ }
152
+ }
153
+ return selected;
154
+ }
155
+
156
+ async function fetchGoogleCalendarEvents(params: {
157
+ accessToken: string;
158
+ calendarId?: string;
159
+ eventQuery?: string;
160
+ timeMin?: string;
161
+ timeMax?: string;
162
+ maxResults?: number;
163
+ now?: Date;
164
+ }): Promise<{ calendarId: string; events: GoogleMeetCalendarEvent[]; now: Date }> {
165
+ const calendarId = params.calendarId?.trim() || "primary";
166
+ const now = params.now ?? new Date();
167
+ const defaultTimeMax = new Date(now);
168
+ defaultTimeMax.setDate(defaultTimeMax.getDate() + 7);
169
+ const { response, release } = await fetchWithSsrFGuard({
170
+ url: appendQuery(
171
+ `${GOOGLE_CALENDAR_API_BASE_URL}/calendars/${encodeURIComponent(calendarId)}/events`,
172
+ {
173
+ maxResults: params.maxResults ?? 50,
174
+ orderBy: "startTime",
175
+ q: params.eventQuery?.trim() || undefined,
176
+ showDeleted: false,
177
+ singleEvents: true,
178
+ timeMin: params.timeMin ?? now.toISOString(),
179
+ timeMax: params.timeMax ?? defaultTimeMax.toISOString(),
180
+ },
181
+ ),
182
+ init: {
183
+ headers: {
184
+ Authorization: `Bearer ${params.accessToken}`,
185
+ Accept: "application/json",
186
+ },
187
+ },
188
+ policy: { allowedHostnames: [GOOGLE_CALENDAR_API_HOST] },
189
+ auditContext: "google-meet.calendar.events.list",
190
+ });
191
+ try {
192
+ if (!response.ok) {
193
+ const detail = await response.text();
194
+ throw await googleApiError({
195
+ response,
196
+ detail,
197
+ prefix: "Google Calendar events.list",
198
+ scopes: [GOOGLE_CALENDAR_EVENTS_SCOPE],
199
+ });
200
+ }
201
+ const payload = (await response.json()) as { items?: unknown };
202
+ if (payload.items !== undefined && !Array.isArray(payload.items)) {
203
+ throw new Error("Google Calendar events.list response had non-array items");
204
+ }
205
+ return { calendarId, events: (payload.items ?? []) as GoogleMeetCalendarEvent[], now };
206
+ } finally {
207
+ await release();
208
+ }
209
+ }
210
+
211
+ export async function listGoogleMeetCalendarEvents(params: {
212
+ accessToken: string;
213
+ calendarId?: string;
214
+ eventQuery?: string;
215
+ timeMin?: string;
216
+ timeMax?: string;
217
+ maxResults?: number;
218
+ now?: Date;
219
+ }): Promise<GoogleMeetCalendarEventsResult> {
220
+ const { calendarId, events, now } = await fetchGoogleCalendarEvents(params);
221
+ const best = chooseBestMeetCalendarEvent(events, now);
222
+ return {
223
+ calendarId,
224
+ events: events
225
+ .map((event) => {
226
+ const meetingUri = extractGoogleMeetUriFromCalendarEvent(event);
227
+ return meetingUri ? { event, meetingUri, selected: event === best } : undefined;
228
+ })
229
+ .filter((event): event is GoogleMeetCalendarEventsResult["events"][number] => Boolean(event)),
230
+ };
231
+ }
232
+
233
+ export async function findGoogleMeetCalendarEvent(params: {
234
+ accessToken: string;
235
+ calendarId?: string;
236
+ eventQuery?: string;
237
+ timeMin?: string;
238
+ timeMax?: string;
239
+ maxResults?: number;
240
+ now?: Date;
241
+ }): Promise<GoogleMeetCalendarLookupResult> {
242
+ const result = await listGoogleMeetCalendarEvents(params);
243
+ const selected = result.events.find((event) => event.selected) ?? result.events[0];
244
+ if (!selected) {
245
+ throw new Error("No Google Calendar event with a Google Meet link matched the query");
246
+ }
247
+ return {
248
+ calendarId: result.calendarId,
249
+ event: selected.event,
250
+ meetingUri: selected.meetingUri,
251
+ };
252
+ }