Skip to content

Commit 459d277

Browse files
committed
feat(google-meet): add latest conference command
1 parent dfa52aa commit 459d277

6 files changed

Lines changed: 198 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Docs: https://docs.openclaw.ai
7676
- Plugins/Google Meet: add a `chrome-node` transport so a paired macOS node, such as a Parallels VM, can own Chrome, BlackHole, and SoX while the Gateway machine keeps the agent and model key. Thanks @steipete.
7777
- Plugins/Google Meet: add `googlemeet artifacts` and `googlemeet attendance` commands plus matching tool/gateway actions for conference records, recordings, transcripts and transcript entries, smart notes, and participant sessions. Thanks @steipete.
7878
- Plugins/Google Meet: add markdown and file output for `googlemeet artifacts` and `googlemeet attendance` reports. Thanks @steipete.
79+
- Plugins/Google Meet: add `googlemeet latest` plus matching tool/gateway actions to find the newest conference record for a meeting. Thanks @steipete.
7980
- Plugins/Google Meet: add `googlemeet doctor --oauth` so operators can verify OAuth token refresh, Meet space reads, and side-effecting space creation without printing secrets. Thanks @steipete.
8081
- Plugins/Voice Call: expose the shared `openclaw_agent_consult` realtime tool so live phone calls can ask the full OpenClaw agent for deeper/tool-backed answers. Thanks @steipete.
8182
- Plugins/Voice Call: add `voicecall setup` and a dry-run-by-default `voicecall smoke` command so Twilio/provider readiness can be checked before placing a live test call. Thanks @steipete.

docs/plugins/google-meet.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,7 @@ openclaw googlemeet attendance --meeting https://meet.google.com/abc-defg-hij
638638
If you already know the conference record id, address it directly:
639639

640640
```bash
641+
openclaw googlemeet latest --meeting https://meet.google.com/abc-defg-hij
641642
openclaw googlemeet artifacts --conference-record conferenceRecords/abc123 --json
642643
openclaw googlemeet attendance --conference-record conferenceRecords/abc123 --json
643644
```

extensions/google-meet/index.test.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
createGoogleMeetSpace,
1515
fetchGoogleMeetArtifacts,
1616
fetchGoogleMeetAttendance,
17+
fetchLatestGoogleMeetConferenceRecord,
1718
fetchGoogleMeetSpace,
1819
normalizeGoogleMeetSpaceName,
1920
} from "./src/meet.js";
@@ -343,6 +344,7 @@ describe("google-meet plugin", () => {
343344
"setup_status",
344345
"resolve_space",
345346
"preflight",
347+
"latest",
346348
"artifacts",
347349
"attendance",
348350
"recover_current_tab",
@@ -496,6 +498,32 @@ describe("google-meet plugin", () => {
496498
);
497499
});
498500

501+
it("fetches only the latest Meet conference record for a meeting", async () => {
502+
const fetchMock = stubMeetArtifactsApi();
503+
504+
await expect(
505+
fetchLatestGoogleMeetConferenceRecord({
506+
accessToken: "token",
507+
meeting: "abc-defg-hij",
508+
}),
509+
).resolves.toMatchObject({
510+
input: "abc-defg-hij",
511+
space: { name: "spaces/abc-defg-hij" },
512+
conferenceRecord: { name: "conferenceRecords/rec-1" },
513+
});
514+
515+
const listCall = fetchMock.mock.calls.find(([input]) => {
516+
const url = requestUrl(input);
517+
return url.pathname === "/v2/conferenceRecords";
518+
});
519+
if (!listCall) {
520+
throw new Error("Expected conferenceRecords.list fetch call");
521+
}
522+
const listUrl = requestUrl(listCall[0]);
523+
expect(listUrl.searchParams.get("pageSize")).toBe("1");
524+
expect(listUrl.searchParams.get("filter")).toBe('space.name = "spaces/abc-defg-hij"');
525+
});
526+
499527
it("lists Meet attendance rows with participant sessions", async () => {
500528
const fetchMock = stubMeetArtifactsApi();
501529

@@ -695,6 +723,26 @@ describe("google-meet plugin", () => {
695723
expect(result.details.attendance).toEqual([expect.objectContaining({ displayName: "Alice" })]);
696724
});
697725

726+
it("reports the latest conference record through the tool", async () => {
727+
stubMeetArtifactsApi();
728+
const { tools } = setup();
729+
const tool = tools[0] as {
730+
execute: (
731+
id: string,
732+
params: unknown,
733+
) => Promise<{ details: { conferenceRecord?: { name?: string } } }>;
734+
};
735+
736+
const result = await tool.execute("id", {
737+
action: "latest",
738+
accessToken: "token",
739+
expiresAt: Date.now() + 120_000,
740+
meeting: "abc-defg-hij",
741+
});
742+
743+
expect(result.details.conferenceRecord).toMatchObject({ name: "conferenceRecords/rec-1" });
744+
});
745+
698746
it("fails setup status when the configured Chrome node is not connected", async () => {
699747
const { tools } = setup(
700748
{
@@ -918,6 +966,37 @@ describe("google-meet plugin", () => {
918966
}
919967
});
920968

969+
it("CLI latest prints the latest conference record", async () => {
970+
stubMeetArtifactsApi();
971+
const program = new Command();
972+
const stdout = captureStdout();
973+
registerGoogleMeetCli({
974+
program,
975+
config: resolveGoogleMeetConfig({}),
976+
ensureRuntime: async () => ({}) as unknown as GoogleMeetRuntime,
977+
});
978+
979+
try {
980+
await program.parseAsync(
981+
[
982+
"googlemeet",
983+
"latest",
984+
"--access-token",
985+
"token",
986+
"--expires-at",
987+
String(Date.now() + 120_000),
988+
"--meeting",
989+
"abc-defg-hij",
990+
],
991+
{ from: "user" },
992+
);
993+
expect(stdout.output()).toContain("space: spaces/abc-defg-hij");
994+
expect(stdout.output()).toContain("conference record: conferenceRecords/rec-1");
995+
} finally {
996+
stdout.restore();
997+
}
998+
});
999+
9211000
it("CLI artifacts writes markdown output", async () => {
9221001
stubMeetArtifactsApi();
9231002
const program = new Command();

extensions/google-meet/index.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import {
1919
buildGoogleMeetPreflightReport,
2020
fetchGoogleMeetArtifacts,
2121
fetchGoogleMeetAttendance,
22+
fetchLatestGoogleMeetConferenceRecord,
2223
fetchGoogleMeetSpace,
2324
} from "./src/meet.js";
2425
import { handleGoogleMeetNodeHostCommand } from "./src/node-host.js";
@@ -150,6 +151,7 @@ const GoogleMeetToolSchema = Type.Object({
150151
"setup_status",
151152
"resolve_space",
152153
"preflight",
154+
"latest",
153155
"artifacts",
154156
"attendance",
155157
"recover_current_tab",
@@ -388,6 +390,26 @@ export default definePluginEntry({
388390
},
389391
);
390392

393+
api.registerGatewayMethod(
394+
"googlemeet.latest",
395+
async ({ params, respond }: GatewayRequestHandlerOptions) => {
396+
try {
397+
const raw = asParamRecord(params);
398+
const meeting = resolveMeetingInput(config, raw.meeting);
399+
const token = await resolveGoogleMeetTokenFromParams(config, raw);
400+
respond(
401+
true,
402+
await fetchLatestGoogleMeetConferenceRecord({
403+
accessToken: token.accessToken,
404+
meeting,
405+
}),
406+
);
407+
} catch (err) {
408+
sendError(respond, err);
409+
}
410+
},
411+
);
412+
391413
api.registerGatewayMethod(
392414
"googlemeet.artifacts",
393415
async ({ params, respond }: GatewayRequestHandlerOptions) => {
@@ -563,6 +585,16 @@ export default definePluginEntry({
563585
}),
564586
);
565587
}
588+
case "latest": {
589+
const meeting = resolveMeetingInput(config, raw.meeting);
590+
const token = await resolveGoogleMeetTokenFromParams(config, raw);
591+
return json(
592+
await fetchLatestGoogleMeetConferenceRecord({
593+
accessToken: token.accessToken,
594+
meeting,
595+
}),
596+
);
597+
}
566598
case "artifacts": {
567599
const resolved = await resolveArtifactQueryFromParams(config, raw);
568600
return json(

extensions/google-meet/src/cli.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,11 @@ import {
88
createGoogleMeetSpace,
99
fetchGoogleMeetArtifacts,
1010
fetchGoogleMeetAttendance,
11+
fetchLatestGoogleMeetConferenceRecord,
1112
fetchGoogleMeetSpace,
1213
type GoogleMeetArtifactsResult,
1314
type GoogleMeetAttendanceResult,
15+
type GoogleMeetLatestConferenceRecordResult,
1416
} from "./meet.js";
1517
import {
1618
buildGoogleMeetAuthUrl,
@@ -547,6 +549,18 @@ function writeAttendanceSummary(result: GoogleMeetAttendanceResult): void {
547549
}
548550
}
549551

552+
function writeLatestConferenceRecordSummary(result: GoogleMeetLatestConferenceRecordResult): void {
553+
writeStdoutLine("input: %s", result.input);
554+
writeStdoutLine("space: %s", result.space.name);
555+
if (!result.conferenceRecord) {
556+
writeStdoutLine("conference record: none");
557+
return;
558+
}
559+
writeStdoutLine("conference record: %s", result.conferenceRecord.name);
560+
writeStdoutLine("started: %s", formatOptional(result.conferenceRecord.startTime));
561+
writeStdoutLine("ended: %s", formatOptional(result.conferenceRecord.endTime));
562+
}
563+
550564
function pushMarkdownLine(lines: string[], text = ""): void {
551565
lines.push(text);
552566
}
@@ -974,6 +988,37 @@ export function registerGoogleMeetCli(params: {
974988
}
975989
});
976990

991+
root
992+
.command("latest")
993+
.description("Find the latest Meet conference record for a meeting")
994+
.option("--meeting <value>", "Meet URL, meeting code, or spaces/{id}")
995+
.option("--access-token <token>", "Access token override")
996+
.option("--refresh-token <token>", "Refresh token override")
997+
.option("--client-id <id>", "OAuth client id override")
998+
.option("--client-secret <secret>", "OAuth client secret override")
999+
.option("--expires-at <ms>", "Cached access token expiry as unix epoch milliseconds")
1000+
.option("--json", "Print JSON output", false)
1001+
.action(async (options: ResolveSpaceOptions) => {
1002+
const resolved = resolveTokenOptions(params.config, options);
1003+
const token = await resolveGoogleMeetAccessToken(resolved);
1004+
const result = await fetchLatestGoogleMeetConferenceRecord({
1005+
accessToken: token.accessToken,
1006+
meeting: resolved.meeting,
1007+
});
1008+
if (options.json) {
1009+
writeStdoutJson({
1010+
...result,
1011+
tokenSource: token.refreshed ? "refresh-token" : "cached-access-token",
1012+
});
1013+
return;
1014+
}
1015+
writeLatestConferenceRecordSummary(result);
1016+
writeStdoutLine(
1017+
"token source: %s",
1018+
token.refreshed ? "refresh-token" : "cached-access-token",
1019+
);
1020+
});
1021+
9771022
root
9781023
.command("artifacts")
9791024
.description("List Meet conference records and available participant/artifact metadata")

extensions/google-meet/src/meet.ts

Lines changed: 40 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@ export type GoogleMeetArtifactsResult = {
112112
artifacts: GoogleMeetArtifactsEntry[];
113113
};
114114

115+
export type GoogleMeetLatestConferenceRecordResult = {
116+
input: string;
117+
space: GoogleMeetSpace;
118+
conferenceRecord?: GoogleMeetConferenceRecord;
119+
};
120+
115121
export type GoogleMeetAttendanceRow = {
116122
conferenceRecord: string;
117123
participant: string;
@@ -257,6 +263,7 @@ async function listGoogleMeetCollection<T extends { name?: string }>(params: {
257263
path: string;
258264
collectionKey: string;
259265
query?: Record<string, string | number | boolean | undefined>;
266+
maxItems?: number;
260267
auditContext: string;
261268
errorPrefix: string;
262269
}): Promise<T[]> {
@@ -270,13 +277,17 @@ async function listGoogleMeetCollection<T extends { name?: string }>(params: {
270277
auditContext: params.auditContext,
271278
errorPrefix: params.errorPrefix,
272279
});
273-
items.push(
274-
...assertResourceArray<T>(
275-
payload[params.collectionKey],
276-
params.collectionKey,
277-
params.errorPrefix,
278-
),
280+
const pageItems = assertResourceArray<T>(
281+
payload[params.collectionKey],
282+
params.collectionKey,
283+
params.errorPrefix,
279284
);
285+
const remaining =
286+
typeof params.maxItems === "number" ? Math.max(params.maxItems - items.length, 0) : undefined;
287+
items.push(...(remaining === undefined ? pageItems : pageItems.slice(0, remaining)));
288+
if (typeof params.maxItems === "number" && items.length >= params.maxItems) {
289+
break;
290+
}
280291
pageToken = typeof payload.nextPageToken === "string" ? payload.nextPageToken : undefined;
281292
} while (pageToken);
282293
return items;
@@ -370,6 +381,7 @@ export async function listGoogleMeetConferenceRecords(params: {
370381
accessToken: string;
371382
meeting?: string;
372383
pageSize?: number;
384+
maxItems?: number;
373385
}): Promise<GoogleMeetConferenceRecord[]> {
374386
const filter = params.meeting
375387
? `space.name = "${normalizeGoogleMeetSpaceName(params.meeting)}"`
@@ -382,11 +394,33 @@ export async function listGoogleMeetConferenceRecords(params: {
382394
pageSize: params.pageSize,
383395
filter,
384396
},
397+
maxItems: params.maxItems,
385398
auditContext: "google-meet.conferenceRecords.list",
386399
errorPrefix: "Google Meet conferenceRecords.list",
387400
});
388401
}
389402

403+
export async function fetchLatestGoogleMeetConferenceRecord(params: {
404+
accessToken: string;
405+
meeting: string;
406+
}): Promise<GoogleMeetLatestConferenceRecordResult> {
407+
const space = await fetchGoogleMeetSpace({
408+
accessToken: params.accessToken,
409+
meeting: params.meeting,
410+
});
411+
const [conferenceRecord] = await listGoogleMeetConferenceRecords({
412+
accessToken: params.accessToken,
413+
meeting: space.name,
414+
pageSize: 1,
415+
maxItems: 1,
416+
});
417+
return {
418+
input: params.meeting,
419+
space,
420+
...(conferenceRecord ? { conferenceRecord } : {}),
421+
};
422+
}
423+
390424
export async function listGoogleMeetParticipants(params: {
391425
accessToken: string;
392426
conferenceRecord: string;

0 commit comments

Comments
 (0)