diff --git a/CHANGELOG.md b/CHANGELOG.md
index 509272777..a86eb0499 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,7 @@
### Added
+- Meet: add `meet create/get/update/end/history/participants` commands for Google Meet meeting spaces and conference records. (#468) — thanks @regaw-leinad.
- Forms: add `forms publish` to publish/unpublish existing forms and return the responder URL for automated form creation flows. (#565 / #564) — thanks @bogdanovich.
### Fixed
diff --git a/README.md b/README.md
index b15ff9355..8df49d20e 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# gogcli
`gog` is a script-friendly Google CLI for Gmail, Calendar, Drive, Docs, Sheets,
-Slides, Forms, Apps Script, Contacts, Tasks, People, Classroom, Chat, and
+Slides, Forms, Meet, Apps Script, Contacts, Tasks, People, Classroom, Chat, and
Workspace admin flows.
It is built for terminals, shell scripts, CI, and coding agents:
@@ -303,7 +303,7 @@ accounts.
Common user services:
-- Gmail, Calendar, Drive, Docs, Sheets, Slides, Forms, Apps Script
+- Gmail, Calendar, Drive, Docs, Sheets, Slides, Forms, Meet, Apps Script
- Contacts, People, Tasks, Classroom
- Chat for Workspace accounts
- Backup and local utility commands
@@ -331,6 +331,7 @@ Generated service scope table:
| sheets | yes | Sheets API, Drive API | `https://www.googleapis.com/auth/drive`
`https://www.googleapis.com/auth/spreadsheets` | Export via Drive |
| people | yes | People API | `profile` | OIDC profile scope |
| forms | yes | Forms API | `https://www.googleapis.com/auth/forms.body`
`https://www.googleapis.com/auth/forms.responses.readonly` | |
+| meet | yes | Meet REST API | `https://www.googleapis.com/auth/meetings.space.created`
`https://www.googleapis.com/auth/meetings.space.readonly`
`https://www.googleapis.com/auth/meetings.space.settings` | |
| appscript | yes | Apps Script API | `https://www.googleapis.com/auth/script.projects`
`https://www.googleapis.com/auth/script.deployments`
`https://www.googleapis.com/auth/script.processes` | |
| ads | yes | Google Ads API | `https://www.googleapis.com/auth/adwords` | OAuth scope only |
| groups | no | Cloud Identity API | `https://www.googleapis.com/auth/cloud-identity.groups.readonly` | Workspace only |
diff --git a/docs/commands.generated.md b/docs/commands.generated.md
index fea8a1ad9..fffe0de0f 100644
--- a/docs/commands.generated.md
+++ b/docs/commands.generated.md
@@ -374,6 +374,13 @@ Generated from `gog schema --json`.
- [`gog logout [flags]`](commands/gog-logout.md) - Remove a stored refresh token (alias for 'auth remove')
- [`gog ls (list) [flags]`](commands/gog-ls.md) - List Drive files (alias for 'drive ls')
- [`gog me [flags]`](commands/gog-me.md) - Show your profile (alias for 'people me')
+ - [`gog meet (meeting) [flags]`](commands/gog-meet.md) - Google Meet
+ - [`gog meet (meeting) create (new) [flags]`](commands/gog-meet-create.md) - Create a meeting space
+ - [`gog meet (meeting) end (stop) `](commands/gog-meet-end.md) - End active conference
+ - [`gog meet (meeting) get (info,show) `](commands/gog-meet-get.md) - Get a meeting space
+ - [`gog meet (meeting) history (calls,past) [flags]`](commands/gog-meet-history.md) - List past calls in a meeting
+ - [`gog meet (meeting) participants (people,attendees,who) [flags]`](commands/gog-meet-participants.md) - List participants from the latest call
+ - [`gog meet (meeting) update (edit,set) [flags]`](commands/gog-meet-update.md) - Update space config
- [`gog open (browse) [flags]`](commands/gog-open.md) - Print a best-effort web URL for a Google URL/ID (offline)
- [`gog people (person) [flags]`](commands/gog-people.md) - Google People
- [`gog people (person) get (info,show) `](commands/gog-people-get.md) - Get a user profile by ID
diff --git a/docs/commands/README.md b/docs/commands/README.md
index 271c7b19f..df2e3b2e6 100644
--- a/docs/commands/README.md
+++ b/docs/commands/README.md
@@ -2,7 +2,7 @@
Every `gog` command has a generated docs page. The source of truth is the live CLI schema; run `make docs-commands` after changing command names, flags, help text, aliases, or arguments.
-Generated pages: 471.
+Generated pages: 478.
## Top-level Commands
@@ -29,6 +29,7 @@ Generated pages: 471.
- [gog logout](gog-logout.md) - Remove a stored refresh token (alias for 'auth remove')
- [gog ls](gog-ls.md) - List Drive files (alias for 'drive ls')
- [gog me](gog-me.md) - Show your profile (alias for 'people me')
+- [gog meet](gog-meet.md) - Google Meet
- [gog open](gog-open.md) - Print a best-effort web URL for a Google URL/ID (offline)
- [gog people](gog-people.md) - Google People
- [gog schema](gog-schema.md) - Machine-readable command/flag schema
@@ -417,6 +418,13 @@ Generated pages: 471.
- [gog logout](gog-logout.md) - Remove a stored refresh token (alias for 'auth remove')
- [gog ls](gog-ls.md) - List Drive files (alias for 'drive ls')
- [gog me](gog-me.md) - Show your profile (alias for 'people me')
+ - [gog meet](gog-meet.md) - Google Meet
+ - [gog meet create](gog-meet-create.md) - Create a meeting space
+ - [gog meet end](gog-meet-end.md) - End active conference
+ - [gog meet get](gog-meet-get.md) - Get a meeting space
+ - [gog meet history](gog-meet-history.md) - List past calls in a meeting
+ - [gog meet participants](gog-meet-participants.md) - List participants from the latest call
+ - [gog meet update](gog-meet-update.md) - Update space config
- [gog open](gog-open.md) - Print a best-effort web URL for a Google URL/ID (offline)
- [gog people](gog-people.md) - Google People
- [gog people get](gog-people-get.md) - Get a user profile by ID
diff --git a/docs/commands/gog-auth-add.md b/docs/commands/gog-auth-add.md
index 013b0c0c4..16d064439 100644
--- a/docs/commands/gog-auth-add.md
+++ b/docs/commands/gog-auth-add.md
@@ -44,7 +44,7 @@ gog auth add [flags]
| `--remote` | `bool` | | Remote/server-friendly manual flow (print URL, then exchange code) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`
`--pick`
`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
-| `--services` | `string` | user | Services to authorize: user\|all or comma-separated gmail,calendar,chat,classroom,drive,docs,slides,contacts,tasks,sheets,people,forms,appscript,ads (Keep uses service account: gog auth service-account set) |
+| `--services` | `string` | user | Services to authorize: user\|all or comma-separated gmail,calendar,chat,classroom,drive,docs,slides,contacts,tasks,sheets,people,forms,meet,appscript,ads (Keep uses service account: gog auth service-account set) |
| `--step` | `int` | | Remote auth step: 1=print URL, 2=exchange code |
| `--timeout` | `time.Duration` | | Authorization timeout (manual flows default to 5m) |
| `-v`
`--verbose` | `bool` | | Enable verbose logging |
diff --git a/docs/commands/gog-auth-manage.md b/docs/commands/gog-auth-manage.md
index e134eac3f..ace0a6908 100644
--- a/docs/commands/gog-auth-manage.md
+++ b/docs/commands/gog-auth-manage.md
@@ -36,7 +36,7 @@ gog auth manage (login) [flags]
| `--redirect-host` | `string` | | Hostname for OAuth callback; builds https://{host}/oauth2/callback |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`
`--pick`
`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
-| `--services` | `string` | user | Services to authorize: user\|all or comma-separated gmail,calendar,chat,classroom,drive,docs,slides,contacts,tasks,sheets,people,forms,appscript,ads (Keep uses service account: gog auth service-account set) |
+| `--services` | `string` | user | Services to authorize: user\|all or comma-separated gmail,calendar,chat,classroom,drive,docs,slides,contacts,tasks,sheets,people,forms,meet,appscript,ads (Keep uses service account: gog auth service-account set) |
| `--timeout` | `time.Duration` | 10m | Server timeout duration |
| `-v`
`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
diff --git a/docs/commands/gog-login.md b/docs/commands/gog-login.md
index 44d3db9ff..445090579 100644
--- a/docs/commands/gog-login.md
+++ b/docs/commands/gog-login.md
@@ -44,7 +44,7 @@ gog login [flags]
| `--remote` | `bool` | | Remote/server-friendly manual flow (print URL, then exchange code) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`
`--pick`
`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
-| `--services` | `string` | user | Services to authorize: user\|all or comma-separated gmail,calendar,chat,classroom,drive,docs,slides,contacts,tasks,sheets,people,forms,appscript,ads (Keep uses service account: gog auth service-account set) |
+| `--services` | `string` | user | Services to authorize: user\|all or comma-separated gmail,calendar,chat,classroom,drive,docs,slides,contacts,tasks,sheets,people,forms,meet,appscript,ads (Keep uses service account: gog auth service-account set) |
| `--step` | `int` | | Remote auth step: 1=print URL, 2=exchange code |
| `--timeout` | `time.Duration` | | Authorization timeout (manual flows default to 5m) |
| `-v`
`--verbose` | `bool` | | Enable verbose logging |
diff --git a/docs/commands/gog-meet-create.md b/docs/commands/gog-meet-create.md
new file mode 100644
index 000000000..eba8d8ba8
--- /dev/null
+++ b/docs/commands/gog-meet-create.md
@@ -0,0 +1,44 @@
+# `gog meet create`
+
+> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
+
+Create a meeting space
+
+## Usage
+
+```bash
+gog meet (meeting) create (new) [flags]
+```
+
+## Parent
+
+- [gog meet](gog-meet.md)
+
+## Flags
+
+| Flag | Type | Default | Help |
+| --- | --- | --- | --- |
+| `--access`
`--access-type` | `string` | trusted | Access type: open, trusted, or restricted |
+| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
+| `-a`
`--account`
`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/docs/slides/contacts/tasks/people/sheets/forms/appscript/ads) |
+| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
+| `--color` | `string` | auto | Color output: auto\|always\|never |
+| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
+| `-n`
`--dry-run`
`--dryrun`
`--noop`
`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
+| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
+| `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands |
+| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
+| `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. |
+| `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
+| `--no-input`
`--non-interactive`
`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
+| `--open`
`--browser` | `bool` | | Open the meeting in a browser after creation |
+| `-p`
`--plain`
`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
+| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
+| `--select`
`--pick`
`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
+| `-v`
`--verbose` | `bool` | | Enable verbose logging |
+| `--version` | `kong.VersionFlag` | | Print version and exit |
+
+## See Also
+
+- [gog meet](gog-meet.md)
+- [Command index](README.md)
diff --git a/docs/commands/gog-meet-end.md b/docs/commands/gog-meet-end.md
new file mode 100644
index 000000000..2836f39f3
--- /dev/null
+++ b/docs/commands/gog-meet-end.md
@@ -0,0 +1,42 @@
+# `gog meet end`
+
+> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
+
+End active conference
+
+## Usage
+
+```bash
+gog meet (meeting) end (stop)
+```
+
+## Parent
+
+- [gog meet](gog-meet.md)
+
+## Flags
+
+| Flag | Type | Default | Help |
+| --- | --- | --- | --- |
+| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
+| `-a`
`--account`
`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/docs/slides/contacts/tasks/people/sheets/forms/appscript/ads) |
+| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
+| `--color` | `string` | auto | Color output: auto\|always\|never |
+| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
+| `-n`
`--dry-run`
`--dryrun`
`--noop`
`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
+| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
+| `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands |
+| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
+| `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. |
+| `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
+| `--no-input`
`--non-interactive`
`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
+| `-p`
`--plain`
`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
+| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
+| `--select`
`--pick`
`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
+| `-v`
`--verbose` | `bool` | | Enable verbose logging |
+| `--version` | `kong.VersionFlag` | | Print version and exit |
+
+## See Also
+
+- [gog meet](gog-meet.md)
+- [Command index](README.md)
diff --git a/docs/commands/gog-meet-get.md b/docs/commands/gog-meet-get.md
new file mode 100644
index 000000000..b652a5437
--- /dev/null
+++ b/docs/commands/gog-meet-get.md
@@ -0,0 +1,42 @@
+# `gog meet get`
+
+> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
+
+Get a meeting space
+
+## Usage
+
+```bash
+gog meet (meeting) get (info,show)
+```
+
+## Parent
+
+- [gog meet](gog-meet.md)
+
+## Flags
+
+| Flag | Type | Default | Help |
+| --- | --- | --- | --- |
+| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
+| `-a`
`--account`
`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/docs/slides/contacts/tasks/people/sheets/forms/appscript/ads) |
+| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
+| `--color` | `string` | auto | Color output: auto\|always\|never |
+| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
+| `-n`
`--dry-run`
`--dryrun`
`--noop`
`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
+| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
+| `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands |
+| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
+| `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. |
+| `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
+| `--no-input`
`--non-interactive`
`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
+| `-p`
`--plain`
`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
+| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
+| `--select`
`--pick`
`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
+| `-v`
`--verbose` | `bool` | | Enable verbose logging |
+| `--version` | `kong.VersionFlag` | | Print version and exit |
+
+## See Also
+
+- [gog meet](gog-meet.md)
+- [Command index](README.md)
diff --git a/docs/commands/gog-meet-history.md b/docs/commands/gog-meet-history.md
new file mode 100644
index 000000000..088600557
--- /dev/null
+++ b/docs/commands/gog-meet-history.md
@@ -0,0 +1,46 @@
+# `gog meet history`
+
+> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
+
+List past calls in a meeting
+
+## Usage
+
+```bash
+gog meet (meeting) history (calls,past) [flags]
+```
+
+## Parent
+
+- [gog meet](gog-meet.md)
+
+## Flags
+
+| Flag | Type | Default | Help |
+| --- | --- | --- | --- |
+| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
+| `-a`
`--account`
`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/docs/slides/contacts/tasks/people/sheets/forms/appscript/ads) |
+| `--all`
`--all-pages`
`--allpages` | `bool` | | Fetch all pages |
+| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
+| `--color` | `string` | auto | Color output: auto\|always\|never |
+| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
+| `-n`
`--dry-run`
`--dryrun`
`--noop`
`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
+| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
+| `--fail-empty`
`--non-empty`
`--require-results` | `bool` | | Exit with code 3 if no results |
+| `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands |
+| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
+| `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. |
+| `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
+| `--max`
`--limit` | `int` | 20 | Max results |
+| `--no-input`
`--non-interactive`
`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
+| `--page`
`--cursor` | `string` | | Page token |
+| `-p`
`--plain`
`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
+| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
+| `--select`
`--pick`
`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
+| `-v`
`--verbose` | `bool` | | Enable verbose logging |
+| `--version` | `kong.VersionFlag` | | Print version and exit |
+
+## See Also
+
+- [gog meet](gog-meet.md)
+- [Command index](README.md)
diff --git a/docs/commands/gog-meet-participants.md b/docs/commands/gog-meet-participants.md
new file mode 100644
index 000000000..34494cb8f
--- /dev/null
+++ b/docs/commands/gog-meet-participants.md
@@ -0,0 +1,47 @@
+# `gog meet participants`
+
+> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
+
+List participants from the latest call
+
+## Usage
+
+```bash
+gog meet (meeting) participants (people,attendees,who) [flags]
+```
+
+## Parent
+
+- [gog meet](gog-meet.md)
+
+## Flags
+
+| Flag | Type | Default | Help |
+| --- | --- | --- | --- |
+| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
+| `-a`
`--account`
`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/docs/slides/contacts/tasks/people/sheets/forms/appscript/ads) |
+| `--all`
`--all-pages`
`--allpages` | `bool` | | Fetch all pages |
+| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
+| `--color` | `string` | auto | Color output: auto\|always\|never |
+| `--conference` | `string` | | Specific conference ID (default: most recent call) |
+| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
+| `-n`
`--dry-run`
`--dryrun`
`--noop`
`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
+| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
+| `--fail-empty`
`--non-empty`
`--require-results` | `bool` | | Exit with code 3 if no results |
+| `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands |
+| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
+| `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. |
+| `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
+| `--max`
`--limit` | `int` | 50 | Max results |
+| `--no-input`
`--non-interactive`
`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
+| `--page`
`--cursor` | `string` | | Page token |
+| `-p`
`--plain`
`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
+| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
+| `--select`
`--pick`
`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
+| `-v`
`--verbose` | `bool` | | Enable verbose logging |
+| `--version` | `kong.VersionFlag` | | Print version and exit |
+
+## See Also
+
+- [gog meet](gog-meet.md)
+- [Command index](README.md)
diff --git a/docs/commands/gog-meet-update.md b/docs/commands/gog-meet-update.md
new file mode 100644
index 000000000..e857b313d
--- /dev/null
+++ b/docs/commands/gog-meet-update.md
@@ -0,0 +1,43 @@
+# `gog meet update`
+
+> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
+
+Update space config
+
+## Usage
+
+```bash
+gog meet (meeting) update (edit,set) [flags]
+```
+
+## Parent
+
+- [gog meet](gog-meet.md)
+
+## Flags
+
+| Flag | Type | Default | Help |
+| --- | --- | --- | --- |
+| `--access`
`--access-type` | `string` | | Access type: open, trusted, or restricted |
+| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
+| `-a`
`--account`
`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/docs/slides/contacts/tasks/people/sheets/forms/appscript/ads) |
+| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
+| `--color` | `string` | auto | Color output: auto\|always\|never |
+| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
+| `-n`
`--dry-run`
`--dryrun`
`--noop`
`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
+| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
+| `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands |
+| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
+| `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. |
+| `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
+| `--no-input`
`--non-interactive`
`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
+| `-p`
`--plain`
`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
+| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
+| `--select`
`--pick`
`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
+| `-v`
`--verbose` | `bool` | | Enable verbose logging |
+| `--version` | `kong.VersionFlag` | | Print version and exit |
+
+## See Also
+
+- [gog meet](gog-meet.md)
+- [Command index](README.md)
diff --git a/docs/commands/gog-meet.md b/docs/commands/gog-meet.md
new file mode 100644
index 000000000..635388ceb
--- /dev/null
+++ b/docs/commands/gog-meet.md
@@ -0,0 +1,51 @@
+# `gog meet`
+
+> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
+
+Google Meet
+
+## Usage
+
+```bash
+gog meet (meeting) [flags]
+```
+
+## Parent
+
+- [gog](gog.md)
+
+## Subcommands
+
+- [gog meet create](gog-meet-create.md) - Create a meeting space
+- [gog meet end](gog-meet-end.md) - End active conference
+- [gog meet get](gog-meet-get.md) - Get a meeting space
+- [gog meet history](gog-meet-history.md) - List past calls in a meeting
+- [gog meet participants](gog-meet-participants.md) - List participants from the latest call
+- [gog meet update](gog-meet-update.md) - Update space config
+
+## Flags
+
+| Flag | Type | Default | Help |
+| --- | --- | --- | --- |
+| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
+| `-a`
`--account`
`--acct` | `string` | | Account email for API commands (gmail/calendar/chat/classroom/drive/docs/slides/contacts/tasks/people/sheets/forms/appscript/ads) |
+| `--client` | `string` | | OAuth client name (selects stored credentials + token bucket) |
+| `--color` | `string` | auto | Color output: auto\|always\|never |
+| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
+| `-n`
`--dry-run`
`--dryrun`
`--noop`
`--preview` | `bool` | | Do not make changes; print intended actions and exit successfully |
+| `--enable-commands` | `string` | | Comma-separated list of enabled commands; dot paths allowed (restricts CLI) |
+| `-y`
`--force`
`--assume-yes`
`--yes` | `bool` | | Skip confirmations for destructive commands |
+| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
+| `-h`
`--help` | `kong.helpFlag` | | Show context-sensitive help. |
+| `-j`
`--json`
`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
+| `--no-input`
`--non-interactive`
`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
+| `-p`
`--plain`
`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
+| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
+| `--select`
`--pick`
`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
+| `-v`
`--verbose` | `bool` | | Enable verbose logging |
+| `--version` | `kong.VersionFlag` | | Print version and exit |
+
+## See Also
+
+- [gog](gog.md)
+- [Command index](README.md)
diff --git a/docs/commands/gog.md b/docs/commands/gog.md
index 57798833e..c9c9923a2 100644
--- a/docs/commands/gog.md
+++ b/docs/commands/gog.md
@@ -39,6 +39,7 @@ gog [flags]
- [gog logout](gog-logout.md) - Remove a stored refresh token (alias for 'auth remove')
- [gog ls](gog-ls.md) - List Drive files (alias for 'drive ls')
- [gog me](gog-me.md) - Show your profile (alias for 'people me')
+- [gog meet](gog-meet.md) - Google Meet
- [gog open](gog-open.md) - Print a best-effort web URL for a Google URL/ID (offline)
- [gog people](gog-people.md) - Google People
- [gog schema](gog-schema.md) - Machine-readable command/flag schema
diff --git a/internal/cmd/execute_meet_test.go b/internal/cmd/execute_meet_test.go
new file mode 100644
index 000000000..915c705db
--- /dev/null
+++ b/internal/cmd/execute_meet_test.go
@@ -0,0 +1,312 @@
+package cmd
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "google.golang.org/api/meet/v2"
+ "google.golang.org/api/option"
+)
+
+func newTestMeetService(t *testing.T, handler http.Handler) {
+ t.Helper()
+
+ origNew := newMeetService
+ t.Cleanup(func() { newMeetService = origNew })
+
+ srv := httptest.NewServer(handler)
+ t.Cleanup(srv.Close)
+
+ svc, err := meet.NewService(context.Background(),
+ option.WithoutAuthentication(),
+ option.WithHTTPClient(srv.Client()),
+ option.WithEndpoint(srv.URL+"/"),
+ )
+ if err != nil {
+ t.Fatalf("NewService: %v", err)
+ }
+
+ newMeetService = func(context.Context, string) (*meet.Service, error) { return svc, nil }
+}
+
+func meetSpaceResponse() map[string]any {
+ return map[string]any{
+ "name": "spaces/abc123",
+ "meetingUri": "https://meet.google.com/abc-defg-hij",
+ "meetingCode": "abc-defg-hij",
+ "config": map[string]any{
+ "accessType": "TRUSTED",
+ "entryPointAccess": "ALL",
+ },
+ }
+}
+
+func TestExecute_MeetCreate_JSON(t *testing.T) {
+ newTestMeetService(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if !(r.URL.Path == "/v2/spaces" && r.Method == http.MethodPost) {
+ http.NotFound(w, r)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(meetSpaceResponse())
+ }))
+
+ out := captureStdout(t, func() {
+ _ = captureStderr(t, func() {
+ if err := Execute([]string{"--json", "--account", "a@b.com", "meet", "create"}); err != nil {
+ t.Fatalf("Execute: %v", err)
+ }
+ })
+ })
+
+ var parsed struct {
+ Created bool `json:"created"`
+ MeetingURI string `json:"meeting_uri"`
+ MeetingCode string `json:"meeting_code"`
+ }
+
+ if err := json.Unmarshal([]byte(out), &parsed); err != nil {
+ t.Fatalf("json parse: %v\nout=%q", err, out)
+ }
+
+ if !parsed.Created {
+ t.Fatal("expected created=true")
+ }
+
+ if parsed.MeetingCode != "abc-defg-hij" {
+ t.Fatalf("unexpected meeting_code: %q", parsed.MeetingCode)
+ }
+
+ if parsed.MeetingURI != "https://meet.google.com/abc-defg-hij" {
+ t.Fatalf("unexpected meeting_uri: %q", parsed.MeetingURI)
+ }
+}
+
+func TestExecute_MeetCreate_Text(t *testing.T) {
+ newTestMeetService(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if !(r.URL.Path == "/v2/spaces" && r.Method == http.MethodPost) {
+ http.NotFound(w, r)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(meetSpaceResponse())
+ }))
+
+ out := captureStdout(t, func() {
+ _ = captureStderr(t, func() {
+ if err := Execute([]string{"--plain", "--account", "a@b.com", "meet", "create"}); err != nil {
+ t.Fatalf("Execute: %v", err)
+ }
+ })
+ })
+
+ if !strings.Contains(out, "meeting_code\tabc-defg-hij") {
+ t.Fatalf("expected meeting_code in output, got: %q", out)
+ }
+
+ if !strings.Contains(out, "meeting_uri\thttps://meet.google.com/abc-defg-hij") {
+ t.Fatalf("expected meeting_uri in output, got: %q", out)
+ }
+
+ if !strings.Contains(out, "access\ttrusted") {
+ t.Fatalf("expected access in output, got: %q", out)
+ }
+}
+
+func TestExecute_MeetCreate_DryRun(t *testing.T) {
+ newTestMeetService(t, http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
+ t.Fatal("should not call API in dry-run mode")
+ http.Error(w, "unexpected", http.StatusInternalServerError)
+ }))
+
+ out := captureStdout(t, func() {
+ _ = captureStderr(t, func() {
+ err := Execute([]string{"--json", "--dry-run", "--account", "a@b.com", "meet", "create"})
+ if err != nil && ExitCode(err) != 0 {
+ t.Fatalf("Execute: %v", err)
+ }
+ })
+ })
+
+ var parsed struct {
+ DryRun bool `json:"dry_run"`
+ Op string `json:"op"`
+ }
+
+ if err := json.Unmarshal([]byte(out), &parsed); err != nil {
+ t.Fatalf("json parse: %v\nout=%q", err, out)
+ }
+
+ if !parsed.DryRun {
+ t.Fatal("expected dry_run=true")
+ }
+
+ if parsed.Op != "meet.spaces.create" {
+ t.Fatalf("unexpected op: %q", parsed.Op)
+ }
+}
+
+func TestExecute_MeetGet_JSON(t *testing.T) {
+ newTestMeetService(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ if !(r.URL.Path == "/v2/spaces/abc-defg-hij" && r.Method == http.MethodGet) {
+ http.NotFound(w, r)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(meetSpaceResponse())
+ }))
+
+ out := captureStdout(t, func() {
+ _ = captureStderr(t, func() {
+ if err := Execute([]string{"--json", "--account", "a@b.com", "meet", "get", "abc-defg-hij"}); err != nil {
+ t.Fatalf("Execute: %v", err)
+ }
+ })
+ })
+
+ var parsed struct {
+ MeetingCode string `json:"meeting_code"`
+ }
+
+ if err := json.Unmarshal([]byte(out), &parsed); err != nil {
+ t.Fatalf("json parse: %v\nout=%q", err, out)
+ }
+
+ if parsed.MeetingCode != "abc-defg-hij" {
+ t.Fatalf("unexpected meeting_code: %q", parsed.MeetingCode)
+ }
+}
+
+func TestExecute_MeetHistory_JSON(t *testing.T) {
+ newTestMeetService(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch {
+ case r.URL.Path == "/v2/spaces/abc-defg-hij" && r.Method == http.MethodGet:
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(meetSpaceResponse())
+ case r.URL.Path == "/v2/conferenceRecords" && r.Method == http.MethodGet:
+ if got, want := r.URL.Query().Get("filter"), `space.name = "spaces/abc123"`; got != want {
+ t.Fatalf("unexpected filter: %q, want %q", got, want)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "conferenceRecords": []map[string]any{
+ {
+ "name": "conferenceRecords/rec1",
+ "space": "spaces/abc123",
+ "startTime": "2026-03-20T10:00:00Z",
+ "endTime": "2026-03-20T11:00:00Z",
+ },
+ },
+ })
+ default:
+ http.NotFound(w, r)
+ }
+ }))
+
+ out := captureStdout(t, func() {
+ _ = captureStderr(t, func() {
+ if err := Execute([]string{"--json", "--account", "a@b.com", "meet", "history", "abc-defg-hij"}); err != nil {
+ t.Fatalf("Execute: %v", err)
+ }
+ })
+ })
+
+ var parsed struct {
+ MeetingCode string `json:"meeting_code"`
+ Conferences []struct {
+ Name string `json:"name"`
+ } `json:"conferences"`
+ }
+
+ if err := json.Unmarshal([]byte(out), &parsed); err != nil {
+ t.Fatalf("json parse: %v\nout=%q", err, out)
+ }
+
+ if parsed.MeetingCode != "abc-defg-hij" {
+ t.Fatalf("unexpected meeting_code: %q", parsed.MeetingCode)
+ }
+
+ if len(parsed.Conferences) != 1 || parsed.Conferences[0].Name != "conferenceRecords/rec1" {
+ t.Fatalf("unexpected conferences: %#v", parsed.Conferences)
+ }
+}
+
+func TestExecute_MeetParticipants_JSON(t *testing.T) {
+ newTestMeetService(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ switch {
+ case r.URL.Path == "/v2/spaces/abc-defg-hij" && r.Method == http.MethodGet:
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(meetSpaceResponse())
+ case r.URL.Path == "/v2/conferenceRecords" && r.Method == http.MethodGet:
+ if got, want := r.URL.Query().Get("filter"), `space.name = "spaces/abc123"`; got != want {
+ t.Fatalf("unexpected filter: %q, want %q", got, want)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "conferenceRecords": []map[string]any{
+ {
+ "name": "conferenceRecords/rec1",
+ "space": "spaces/abc123",
+ "startTime": "2026-03-20T10:00:00Z",
+ "endTime": "2026-03-20T11:00:00Z",
+ },
+ },
+ })
+ case r.URL.Path == "/v2/conferenceRecords/rec1/participants" && r.Method == http.MethodGet:
+ w.Header().Set("Content-Type", "application/json")
+ _ = json.NewEncoder(w).Encode(map[string]any{
+ "participants": []map[string]any{
+ {
+ "name": "conferenceRecords/rec1/participants/p1",
+ "earliestStartTime": "2026-03-20T10:00:00Z",
+ "latestEndTime": "2026-03-20T11:00:00Z",
+ "signedinUser": map[string]any{
+ "displayName": "Dan Wager",
+ "user": "users/123",
+ },
+ },
+ },
+ })
+ default:
+ http.NotFound(w, r)
+ }
+ }))
+
+ out := captureStdout(t, func() {
+ _ = captureStderr(t, func() {
+ if err := Execute([]string{"--json", "--account", "a@b.com", "meet", "participants", "abc-defg-hij"}); err != nil {
+ t.Fatalf("Execute: %v", err)
+ }
+ })
+ })
+
+ var parsed struct {
+ MeetingCode string `json:"meeting_code"`
+ Participants []struct {
+ SignedinUser struct {
+ DisplayName string `json:"displayName"`
+ } `json:"signedinUser"`
+ } `json:"participants"`
+ }
+
+ if err := json.Unmarshal([]byte(out), &parsed); err != nil {
+ t.Fatalf("json parse: %v\nout=%q", err, out)
+ }
+
+ if parsed.MeetingCode != "abc-defg-hij" {
+ t.Fatalf("unexpected meeting_code: %q", parsed.MeetingCode)
+ }
+
+ if len(parsed.Participants) != 1 || parsed.Participants[0].SignedinUser.DisplayName != "Dan Wager" {
+ t.Fatalf("unexpected participants: %#v", parsed.Participants)
+ }
+}
diff --git a/internal/cmd/help_snapshot_test.go b/internal/cmd/help_snapshot_test.go
index ebfbae757..40894f8a1 100644
--- a/internal/cmd/help_snapshot_test.go
+++ b/internal/cmd/help_snapshot_test.go
@@ -66,6 +66,18 @@ func TestHelpSnapshot_Forms(t *testing.T) {
)
}
+func TestHelpSnapshot_Meet(t *testing.T) {
+ out := captureHelpOutput(t, "meet", "--help")
+ requireHelpContains(t, out,
+ "\n create",
+ "\n get",
+ "\n update",
+ "\n end",
+ "\n history",
+ "\n participants",
+ )
+}
+
func TestHelpSnapshot_Admin(t *testing.T) {
out := captureHelpOutput(t, "admin", "--help")
requireHelpContains(t, out,
diff --git a/internal/cmd/meet.go b/internal/cmd/meet.go
new file mode 100644
index 000000000..e948ab75a
--- /dev/null
+++ b/internal/cmd/meet.go
@@ -0,0 +1,213 @@
+package cmd
+
+import (
+ "context"
+ "os"
+ "os/exec"
+ "runtime"
+ "strings"
+
+ "google.golang.org/api/meet/v2"
+
+ "github.com/steipete/gogcli/internal/googleapi"
+ "github.com/steipete/gogcli/internal/outfmt"
+ "github.com/steipete/gogcli/internal/ui"
+)
+
+// openMeetBrowser opens the meeting URL in the default browser.
+var openMeetBrowser = func(url string) error {
+ var cmd *exec.Cmd
+
+ switch runtime.GOOS {
+ case "darwin":
+ cmd = exec.Command("open", url) //nolint:gosec // executable is fixed; arg is meeting URL
+ case "windows":
+ cmd = exec.Command("rundll32", "url.dll,FileProtocolHandler", url) //nolint:gosec // executable is fixed; arg is meeting URL
+ default:
+ cmd = exec.Command("xdg-open", url) //nolint:gosec // executable is fixed; arg is meeting URL
+ }
+
+ return cmd.Start()
+}
+
+var newMeetService = googleapi.NewMeet
+
+type MeetCmd struct {
+ Create MeetCreateCmd `cmd:"" name:"create" aliases:"new" help:"Create a meeting space"`
+ Get MeetGetCmd `cmd:"" name:"get" aliases:"info,show" help:"Get a meeting space"`
+ Update MeetUpdateCmd `cmd:"" name:"update" aliases:"edit,set" help:"Update space config"`
+ End MeetEndCmd `cmd:"" name:"end" aliases:"stop" help:"End active conference"`
+ History MeetHistoryCmd `cmd:"" name:"history" aliases:"calls,past" help:"List past calls in a meeting"`
+ Participants MeetParticipantsCmd `cmd:"" name:"participants" aliases:"people,attendees,who" help:"List participants from the latest call"`
+}
+
+// MeetCreateCmd creates a new meeting space.
+type MeetCreateCmd struct {
+ Access string `name:"access" aliases:"access-type" help:"Access type: open, trusted, or restricted" default:"trusted"`
+ EntryPoint string `name:"entry-point" aliases:"entry-point-access" help:"Entry point access: all or creator-only" default:"all" hidden:""`
+ Open bool `name:"open" aliases:"browser" help:"Open the meeting in a browser after creation"`
+}
+
+func (c *MeetCreateCmd) Run(ctx context.Context, flags *RootFlags) error {
+ accessType, err := parseMeetAccessType(c.Access)
+ if err != nil {
+ return err
+ }
+
+ entryPointAccess, err := parseMeetEntryPointAccess(c.EntryPoint)
+ if err != nil {
+ return err
+ }
+
+ if dryRunErr := dryRunExit(ctx, flags, "meet.spaces.create", map[string]any{
+ "access_type": accessType,
+ "entry_point_access": entryPointAccess,
+ }); dryRunErr != nil {
+ return dryRunErr
+ }
+
+ _, svc, err := requireMeetService(ctx, flags)
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ space := &meet.Space{
+ Config: &meet.SpaceConfig{
+ AccessType: accessType,
+ EntryPointAccess: entryPointAccess,
+ },
+ }
+
+ created, err := svc.Spaces.Create(space).Context(ctx).Do()
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ if outfmt.IsJSON(ctx) {
+ if err := outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
+ "created": true,
+ "name": created.Name,
+ "meeting_uri": created.MeetingUri,
+ "meeting_code": created.MeetingCode,
+ "config": created.Config,
+ }); err != nil {
+ return err
+ }
+
+ return openMeetingIfRequested(c.Open, created.MeetingUri)
+ }
+
+ u := ui.FromContext(ctx)
+ printMeetSpace(u, created)
+
+ return openMeetingIfRequested(c.Open, created.MeetingUri)
+}
+
+// MeetGetCmd gets a meeting space by meeting code.
+type MeetGetCmd struct {
+ MeetingCode string `arg:"" name:"meeting-code" help:"Meeting code (e.g. abc-defg-hij)"`
+}
+
+func (c *MeetGetCmd) Run(ctx context.Context, flags *RootFlags) error {
+ spaceName := normalizeMeetSpaceName(c.MeetingCode)
+ if spaceName == "" {
+ return usage("empty meeting code")
+ }
+
+ _, svc, err := requireMeetService(ctx, flags)
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ space, err := svc.Spaces.Get(spaceName).Context(ctx).Do()
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ if outfmt.IsJSON(ctx) {
+ return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
+ "name": space.Name,
+ "meeting_uri": space.MeetingUri,
+ "meeting_code": space.MeetingCode,
+ "config": space.Config,
+ })
+ }
+
+ u := ui.FromContext(ctx)
+ printMeetSpace(u, space)
+
+ return nil
+}
+
+func printMeetSpace(u *ui.UI, space *meet.Space) {
+ if u == nil || space == nil {
+ return
+ }
+
+ u.Out().Printf("meeting_code\t%s", space.MeetingCode)
+ u.Out().Printf("meeting_uri\t%s", space.MeetingUri)
+
+ if space.Config != nil {
+ u.Out().Printf("access\t%s", strings.ToLower(space.Config.AccessType))
+ }
+
+ if space.ActiveConference != nil && space.ActiveConference.ConferenceRecord != "" {
+ u.Out().Printf("active_conference\t%s", space.ActiveConference.ConferenceRecord)
+ }
+}
+
+// normalizeMeetSpaceName accepts either a full resource name ("spaces/xxx")
+// or a bare meeting code ("xxx-yyyy-zzz") and returns a value suitable for
+// the spaces.get API, which accepts both formats.
+func normalizeMeetSpaceName(input string) string {
+ input = strings.TrimSpace(input)
+ if input == "" {
+ return ""
+ }
+
+ if strings.HasPrefix(input, "spaces/") {
+ return input
+ }
+
+ return "spaces/" + input
+}
+
+// resolveMeetSpace resolves a meeting code to the full Space resource.
+// This is needed because some API methods (patch, endActiveConference)
+// require the canonical resource name (e.g. "spaces/KP0uKCifZgYB"),
+// which differs from the meeting code (e.g. "abc-defg-hij").
+func resolveMeetSpace(ctx context.Context, svc *meet.Service, input string) (*meet.Space, error) {
+ return svc.Spaces.Get(normalizeMeetSpaceName(input)).Context(ctx).Do()
+}
+
+func openMeetingIfRequested(shouldOpen bool, meetingURI string) error {
+ if !shouldOpen || strings.TrimSpace(meetingURI) == "" {
+ return nil
+ }
+
+ return openMeetBrowser(meetingURI)
+}
+
+func parseMeetAccessType(s string) (string, error) {
+ switch strings.TrimSpace(strings.ToLower(s)) {
+ case "trusted", "":
+ return "TRUSTED", nil
+ case "open":
+ return "OPEN", nil
+ case "restricted":
+ return "RESTRICTED", nil
+ default:
+ return "", usagef("invalid --access %q (expected open, trusted, or restricted)", s)
+ }
+}
+
+func parseMeetEntryPointAccess(s string) (string, error) {
+ switch strings.TrimSpace(strings.ToLower(s)) {
+ case literalAll, "":
+ return "ALL", nil
+ case "creator-only", "creator_only", "creatoronly":
+ return "CREATOR_APP_ONLY", nil
+ default:
+ return "", usagef("invalid --entry-point %q (expected all or creator-only)", s)
+ }
+}
diff --git a/internal/cmd/meet_helpers.go b/internal/cmd/meet_helpers.go
new file mode 100644
index 000000000..c8fef20fc
--- /dev/null
+++ b/internal/cmd/meet_helpers.go
@@ -0,0 +1,62 @@
+package cmd
+
+import (
+ "fmt"
+ "strings"
+
+ "google.golang.org/api/meet/v2"
+
+ "github.com/steipete/gogcli/internal/errfmt"
+)
+
+// wrapMeetError provides helpful error messages for common Meet API issues.
+func wrapMeetError(err error) error {
+ if err == nil {
+ return nil
+ }
+
+ errStr := err.Error()
+
+ if strings.Contains(errStr, "accessNotConfigured") ||
+ strings.Contains(errStr, "Meet REST API has not been used") {
+ return errfmt.NewUserFacingError(
+ "Meet REST API is not enabled; enable it at: https://console.developers.google.com/apis/api/meet.googleapis.com/overview",
+ err,
+ )
+ }
+
+ if strings.Contains(errStr, "insufficientPermissions") ||
+ strings.Contains(errStr, "insufficient authentication scopes") {
+ return errfmt.NewUserFacingError(
+ "Insufficient permissions for Meet API; re-authenticate with: gog auth add --services meet",
+ err,
+ )
+ }
+
+ return err
+}
+
+func meetSpaceNameFilter(spaceName string) string {
+ return fmt.Sprintf("space.name = %q", spaceName)
+}
+
+// participantDisplayName extracts a human-readable name from a participant.
+func participantDisplayName(p *meet.Participant) string {
+ if p == nil {
+ return ""
+ }
+
+ if p.SignedinUser != nil && p.SignedinUser.DisplayName != "" {
+ return p.SignedinUser.DisplayName
+ }
+
+ if p.AnonymousUser != nil && p.AnonymousUser.DisplayName != "" {
+ return p.AnonymousUser.DisplayName
+ }
+
+ if p.PhoneUser != nil && p.PhoneUser.DisplayName != "" {
+ return p.PhoneUser.DisplayName
+ }
+
+ return p.Name
+}
diff --git a/internal/cmd/meet_history.go b/internal/cmd/meet_history.go
new file mode 100644
index 000000000..7ef551ee6
--- /dev/null
+++ b/internal/cmd/meet_history.go
@@ -0,0 +1,127 @@
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "strings"
+
+ "google.golang.org/api/meet/v2"
+
+ "github.com/steipete/gogcli/internal/outfmt"
+ "github.com/steipete/gogcli/internal/ui"
+)
+
+// MeetHistoryCmd lists past conferences (calls) for a meeting.
+type MeetHistoryCmd struct {
+ MeetingCode string `arg:"" name:"meeting-code" help:"Meeting code (e.g. abc-defg-hij)"`
+ Max int `name:"max" aliases:"limit" help:"Max results" default:"20"`
+ Page string `name:"page" aliases:"cursor" help:"Page token"`
+ All bool `name:"all" aliases:"all-pages,allpages" help:"Fetch all pages"`
+ FailEmpty bool `name:"fail-empty" aliases:"non-empty,require-results" help:"Exit with code 3 if no results"`
+}
+
+func (c *MeetHistoryCmd) Run(ctx context.Context, flags *RootFlags) error {
+ u := ui.FromContext(ctx)
+
+ if strings.TrimSpace(c.MeetingCode) == "" {
+ return usage("empty meeting code")
+ }
+
+ if c.Max <= 0 {
+ return usage("--max must be > 0")
+ }
+
+ _, svc, err := requireMeetService(ctx, flags)
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ // Resolve the meeting code to the canonical space name for filtering.
+ space, err := resolveMeetSpace(ctx, svc, c.MeetingCode)
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ filter := meetSpaceNameFilter(space.Name)
+
+ fetch := func(pageToken string) ([]*meet.ConferenceRecord, string, error) {
+ call := svc.ConferenceRecords.List().
+ PageSize(int64(c.Max)).
+ Filter(filter).
+ Context(ctx)
+ if strings.TrimSpace(pageToken) != "" {
+ call = call.PageToken(pageToken)
+ }
+
+ resp, err := call.Do()
+ if err != nil {
+ return nil, "", wrapMeetError(err)
+ }
+
+ return resp.ConferenceRecords, resp.NextPageToken, nil
+ }
+
+ var records []*meet.ConferenceRecord
+
+ nextPageToken := ""
+
+ if c.All {
+ all, err := collectAllPages(c.Page, fetch)
+ if err != nil {
+ return err
+ }
+
+ records = all
+ } else {
+ var err error
+
+ records, nextPageToken, err = fetch(c.Page)
+ if err != nil {
+ return err
+ }
+ }
+
+ if outfmt.IsJSON(ctx) {
+ if err := outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
+ "meeting_code": c.MeetingCode,
+ "conferences": records,
+ "nextPageToken": nextPageToken,
+ }); err != nil {
+ return err
+ }
+
+ if len(records) == 0 {
+ return failEmptyExit(c.FailEmpty)
+ }
+
+ return nil
+ }
+
+ if len(records) == 0 {
+ u.Err().Printf("No past calls found for %s", c.MeetingCode)
+
+ return failEmptyExit(c.FailEmpty)
+ }
+
+ w, flush := tableWriter(ctx)
+ defer flush()
+
+ fmt.Fprintln(w, "START\tEND\tCONFERENCE")
+
+ for _, r := range records {
+ if r == nil {
+ continue
+ }
+
+ fmt.Fprintf(w, "%s\t%s\t%s\n",
+ sanitizeTab(r.StartTime),
+ sanitizeTab(r.EndTime),
+ sanitizeTab(strings.TrimPrefix(r.Name, "conferenceRecords/")),
+ )
+ }
+
+ printNextPageHint(u, nextPageToken)
+
+ return nil
+}
diff --git a/internal/cmd/meet_participants.go b/internal/cmd/meet_participants.go
new file mode 100644
index 000000000..3c2a13395
--- /dev/null
+++ b/internal/cmd/meet_participants.go
@@ -0,0 +1,164 @@
+package cmd
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "strings"
+
+ "google.golang.org/api/meet/v2"
+
+ "github.com/steipete/gogcli/internal/outfmt"
+ "github.com/steipete/gogcli/internal/ui"
+)
+
+// MeetParticipantsCmd lists participants from the most recent call in a meeting,
+// or from a specific conference if --conference is provided.
+type MeetParticipantsCmd struct {
+ MeetingCode string `arg:"" name:"meeting-code" help:"Meeting code (e.g. abc-defg-hij)"`
+ Conference string `name:"conference" help:"Specific conference ID (default: most recent call)"`
+ Max int `name:"max" aliases:"limit" help:"Max results" default:"50"`
+ Page string `name:"page" aliases:"cursor" help:"Page token"`
+ All bool `name:"all" aliases:"all-pages,allpages" help:"Fetch all pages"`
+ FailEmpty bool `name:"fail-empty" aliases:"non-empty,require-results" help:"Exit with code 3 if no results"`
+}
+
+func (c *MeetParticipantsCmd) Run(ctx context.Context, flags *RootFlags) error {
+ u := ui.FromContext(ctx)
+
+ if strings.TrimSpace(c.MeetingCode) == "" {
+ return usage("empty meeting code")
+ }
+
+ if c.Max <= 0 {
+ return usage("--max must be > 0")
+ }
+
+ _, svc, err := requireMeetService(ctx, flags)
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ // Determine which conference to list participants for.
+ conferenceName, err := resolveConference(ctx, svc, c.MeetingCode, c.Conference)
+ if err != nil {
+ return err
+ }
+
+ fetch := func(pageToken string) ([]*meet.Participant, string, error) {
+ call := svc.ConferenceRecords.Participants.List(conferenceName).
+ PageSize(int64(c.Max)).
+ Context(ctx)
+ if strings.TrimSpace(pageToken) != "" {
+ call = call.PageToken(pageToken)
+ }
+
+ resp, err := call.Do()
+ if err != nil {
+ return nil, "", wrapMeetError(err)
+ }
+
+ return resp.Participants, resp.NextPageToken, nil
+ }
+
+ var participants []*meet.Participant
+
+ nextPageToken := ""
+
+ if c.All {
+ all, err := collectAllPages(c.Page, fetch)
+ if err != nil {
+ return err
+ }
+
+ participants = all
+ } else {
+ var err error
+
+ participants, nextPageToken, err = fetch(c.Page)
+ if err != nil {
+ return err
+ }
+ }
+
+ if outfmt.IsJSON(ctx) {
+ if err := outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
+ "meeting_code": c.MeetingCode,
+ "conference": conferenceName,
+ "participants": participants,
+ "nextPageToken": nextPageToken,
+ }); err != nil {
+ return err
+ }
+
+ if len(participants) == 0 {
+ return failEmptyExit(c.FailEmpty)
+ }
+
+ return nil
+ }
+
+ if len(participants) == 0 {
+ u.Err().Printf("No participants found for %s", c.MeetingCode)
+
+ return failEmptyExit(c.FailEmpty)
+ }
+
+ w, flush := tableWriter(ctx)
+ defer flush()
+
+ fmt.Fprintln(w, "DISPLAY_NAME\tJOINED\tLEFT")
+
+ for _, p := range participants {
+ if p == nil {
+ continue
+ }
+
+ fmt.Fprintf(w, "%s\t%s\t%s\n",
+ sanitizeTab(participantDisplayName(p)),
+ sanitizeTab(p.EarliestStartTime),
+ sanitizeTab(p.LatestEndTime),
+ )
+ }
+
+ printNextPageHint(u, nextPageToken)
+
+ return nil
+}
+
+// resolveConference determines the conference record name to use.
+// If an explicit conference ID is provided, it is used directly.
+// Otherwise, the most recent conference for the meeting is looked up.
+func resolveConference(ctx context.Context, svc *meet.Service, meetingCode, conferenceOverride string) (string, error) {
+ if conferenceOverride != "" {
+ name := strings.TrimSpace(conferenceOverride)
+ if !strings.HasPrefix(name, "conferenceRecords/") {
+ name = "conferenceRecords/" + name
+ }
+
+ return name, nil
+ }
+
+ // Look up the most recent conference for this meeting.
+ space, err := resolveMeetSpace(ctx, svc, meetingCode)
+ if err != nil {
+ return "", wrapMeetError(err)
+ }
+
+ filter := meetSpaceNameFilter(space.Name)
+
+ resp, err := svc.ConferenceRecords.List().
+ Filter(filter).
+ PageSize(1).
+ Context(ctx).
+ Do()
+ if err != nil {
+ return "", wrapMeetError(err)
+ }
+
+ if len(resp.ConferenceRecords) == 0 {
+ return "", usagef("no past calls found for %s", meetingCode)
+ }
+
+ return resp.ConferenceRecords[0].Name, nil
+}
diff --git a/internal/cmd/meet_update_end.go b/internal/cmd/meet_update_end.go
new file mode 100644
index 000000000..069130d5e
--- /dev/null
+++ b/internal/cmd/meet_update_end.go
@@ -0,0 +1,139 @@
+package cmd
+
+import (
+ "context"
+ "os"
+ "strings"
+
+ "google.golang.org/api/meet/v2"
+
+ "github.com/steipete/gogcli/internal/outfmt"
+ "github.com/steipete/gogcli/internal/ui"
+)
+
+// MeetUpdateCmd updates the configuration of a meeting space.
+type MeetUpdateCmd struct {
+ MeetingCode string `arg:"" name:"meeting-code" help:"Meeting code (e.g. abc-defg-hij)"`
+ Access string `name:"access" aliases:"access-type" help:"Access type: open, trusted, or restricted"`
+ EntryPoint string `name:"entry-point" aliases:"entry-point-access" help:"Entry point access: all or creator-only" hidden:""`
+}
+
+func (c *MeetUpdateCmd) Run(ctx context.Context, flags *RootFlags) error {
+ if strings.TrimSpace(c.MeetingCode) == "" {
+ return usage("empty meeting code")
+ }
+
+ var updateMask []string
+
+ patch := &meet.Space{Config: &meet.SpaceConfig{}}
+
+ if c.Access != "" {
+ accessType, err := parseMeetAccessType(c.Access)
+ if err != nil {
+ return err
+ }
+
+ patch.Config.AccessType = accessType
+ updateMask = append(updateMask, "config.accessType")
+ }
+
+ if c.EntryPoint != "" {
+ entryPointAccess, err := parseMeetEntryPointAccess(c.EntryPoint)
+ if err != nil {
+ return err
+ }
+
+ patch.Config.EntryPointAccess = entryPointAccess
+ updateMask = append(updateMask, "config.entryPointAccess")
+ }
+
+ if len(updateMask) == 0 {
+ return usage("at least one of --access or --entry-point is required")
+ }
+
+ _, svc, err := requireMeetService(ctx, flags)
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ space, err := resolveMeetSpace(ctx, svc, c.MeetingCode)
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ if dryRunErr := dryRunExit(ctx, flags, "meet.spaces.patch", map[string]any{
+ "meeting_code": c.MeetingCode,
+ "update_mask": strings.Join(updateMask, ","),
+ "config": patch.Config,
+ }); dryRunErr != nil {
+ return dryRunErr
+ }
+
+ updated, err := svc.Spaces.Patch(space.Name, patch).
+ UpdateMask(strings.Join(updateMask, ",")).
+ Context(ctx).
+ Do()
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ if outfmt.IsJSON(ctx) {
+ return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
+ "updated": true,
+ "name": updated.Name,
+ "meeting_uri": updated.MeetingUri,
+ "meeting_code": updated.MeetingCode,
+ "config": updated.Config,
+ })
+ }
+
+ u := ui.FromContext(ctx)
+ printMeetSpace(u, updated)
+
+ return nil
+}
+
+// MeetEndCmd ends an active conference in a meeting space.
+type MeetEndCmd struct {
+ MeetingCode string `arg:"" name:"meeting-code" help:"Meeting code (e.g. abc-defg-hij)"`
+}
+
+func (c *MeetEndCmd) Run(ctx context.Context, flags *RootFlags) error {
+ if strings.TrimSpace(c.MeetingCode) == "" {
+ return usage("empty meeting code")
+ }
+
+ _, svc, err := requireMeetService(ctx, flags)
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ space, err := resolveMeetSpace(ctx, svc, c.MeetingCode)
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ if confirmErr := confirmDestructive(ctx, flags, "end active conference in "+c.MeetingCode); confirmErr != nil {
+ return confirmErr
+ }
+
+ req := &meet.EndActiveConferenceRequest{}
+
+ _, err = svc.Spaces.EndActiveConference(space.Name, req).Context(ctx).Do()
+ if err != nil {
+ return wrapMeetError(err)
+ }
+
+ if outfmt.IsJSON(ctx) {
+ return outfmt.WriteJSON(ctx, os.Stdout, map[string]any{
+ "ended": true,
+ "meeting_code": c.MeetingCode,
+ })
+ }
+
+ u := ui.FromContext(ctx)
+ u.Out().Printf("ended\ttrue")
+ u.Out().Printf("meeting_code\t%s", c.MeetingCode)
+
+ return nil
+}
diff --git a/internal/cmd/root.go b/internal/cmd/root.go
index fb7a92a75..8d44b7a34 100644
--- a/internal/cmd/root.go
+++ b/internal/cmd/root.go
@@ -81,6 +81,7 @@ type CLI struct {
Keep KeepCmd `cmd:"" help:"Google Keep (Workspace only)"`
Sheets SheetsCmd `cmd:"" aliases:"sheet" help:"Google Sheets"`
Forms FormsCmd `cmd:"" aliases:"form" help:"Google Forms"`
+ Meet MeetCmd `cmd:"" aliases:"meeting" help:"Google Meet"`
AppScript AppScriptCmd `cmd:"" name:"appscript" aliases:"script,apps-script" help:"Google Apps Script"`
Config ConfigCmd `cmd:"" help:"Manage configuration"`
ExitCodes AgentExitCodesCmd `cmd:"" name:"exit-codes" aliases:"exitcodes" help:"Print stable exit codes (alias for 'agent exit-codes')"`
diff --git a/internal/cmd/service_helpers.go b/internal/cmd/service_helpers.go
index 811445d2b..75c65c961 100644
--- a/internal/cmd/service_helpers.go
+++ b/internal/cmd/service_helpers.go
@@ -8,6 +8,7 @@ import (
"google.golang.org/api/docs/v1"
"google.golang.org/api/drive/v3"
"google.golang.org/api/gmail/v1"
+ "google.golang.org/api/meet/v2"
"google.golang.org/api/sheets/v4"
)
@@ -35,6 +36,10 @@ func requireClassroomService(ctx context.Context, flags *RootFlags) (string, *cl
return requireGoogleService(ctx, flags, newClassroomService)
}
+func requireMeetService(ctx context.Context, flags *RootFlags) (string, *meet.Service, error) {
+ return requireGoogleService(ctx, flags, newMeetService)
+}
+
func requireSheetsService(ctx context.Context, flags *RootFlags) (string, *sheets.Service, error) {
return requireGoogleService(ctx, flags, newSheetsService)
}
diff --git a/internal/googleapi/meet.go b/internal/googleapi/meet.go
new file mode 100644
index 000000000..01ac60fc0
--- /dev/null
+++ b/internal/googleapi/meet.go
@@ -0,0 +1,20 @@
+package googleapi
+
+import (
+ "context"
+ "fmt"
+
+ "google.golang.org/api/meet/v2"
+
+ "github.com/steipete/gogcli/internal/googleauth"
+)
+
+func NewMeet(ctx context.Context, email string) (*meet.Service, error) {
+ if opts, err := optionsForAccount(ctx, googleauth.ServiceMeet, email); err != nil {
+ return nil, fmt.Errorf("meet options: %w", err)
+ } else if svc, err := meet.NewService(ctx, opts...); err != nil {
+ return nil, fmt.Errorf("create meet service: %w", err)
+ } else {
+ return svc, nil
+ }
+}
diff --git a/internal/googleauth/service.go b/internal/googleauth/service.go
index 2c8a36a75..e4d3fe653 100644
--- a/internal/googleauth/service.go
+++ b/internal/googleauth/service.go
@@ -22,6 +22,7 @@ const (
ServicePeople Service = "people"
ServiceSheets Service = "sheets"
ServiceForms Service = "forms"
+ ServiceMeet Service = "meet"
ServiceAppScript Service = "appscript"
ServiceAds Service = "ads"
ServiceGroups Service = "groups"
@@ -83,6 +84,7 @@ var serviceOrder = []Service{
ServiceSheets,
ServicePeople,
ServiceForms,
+ ServiceMeet,
ServiceAppScript,
ServiceAds,
ServiceGroups,
@@ -196,6 +198,15 @@ var serviceInfoByService = map[Service]serviceInfo{
user: true,
apis: []string{"Forms API"},
},
+ ServiceMeet: {
+ scopes: []string{
+ "https://www.googleapis.com/auth/meetings.space.created",
+ "https://www.googleapis.com/auth/meetings.space.readonly",
+ "https://www.googleapis.com/auth/meetings.space.settings",
+ },
+ user: true,
+ apis: []string{"Meet REST API"},
+ },
ServiceAppScript: {
scopes: []string{
"https://www.googleapis.com/auth/script.projects",
@@ -551,6 +562,12 @@ func scopesForServiceWithOptions(service Service, opts ScopeOptions) ([]string,
formBodyScope,
"https://www.googleapis.com/auth/forms.responses.readonly",
}, nil
+ case ServiceMeet:
+ if opts.Readonly {
+ return []string{"https://www.googleapis.com/auth/meetings.space.readonly"}, nil
+ }
+
+ return Scopes(service)
case ServiceAppScript:
if opts.Readonly {
return []string{
diff --git a/internal/googleauth/service_test.go b/internal/googleauth/service_test.go
index 5217d897c..a7ed1f663 100644
--- a/internal/googleauth/service_test.go
+++ b/internal/googleauth/service_test.go
@@ -66,7 +66,7 @@ func TestExtractCodeAndState_Errors(t *testing.T) {
func TestAllServices(t *testing.T) {
svcs := AllServices()
- if len(svcs) != 17 {
+ if len(svcs) != 18 {
t.Fatalf("unexpected: %v", svcs)
}
seen := make(map[Service]bool)
@@ -75,7 +75,7 @@ func TestAllServices(t *testing.T) {
seen[s] = true
}
- for _, want := range []Service{ServiceGmail, ServiceCalendar, ServiceChat, ServiceClassroom, ServiceDrive, ServiceDocs, ServiceSlides, ServiceContacts, ServiceTasks, ServicePeople, ServiceSheets, ServiceForms, ServiceAppScript, ServiceAds, ServiceGroups, ServiceKeep, ServiceAdmin} {
+ for _, want := range []Service{ServiceGmail, ServiceCalendar, ServiceChat, ServiceClassroom, ServiceDrive, ServiceDocs, ServiceSlides, ServiceContacts, ServiceTasks, ServicePeople, ServiceSheets, ServiceForms, ServiceMeet, ServiceAppScript, ServiceAds, ServiceGroups, ServiceKeep, ServiceAdmin} {
if !seen[want] {
t.Fatalf("missing %q", want)
}
@@ -84,7 +84,7 @@ func TestAllServices(t *testing.T) {
func TestUserServices(t *testing.T) {
svcs := UserServices()
- if len(svcs) != 14 {
+ if len(svcs) != 15 {
t.Fatalf("unexpected: %v", svcs)
}
@@ -97,7 +97,7 @@ func TestUserServices(t *testing.T) {
seenDocs = true
case ServiceSlides:
seenSlides = true
- case ServiceForms, ServiceAppScript, ServiceAds:
+ case ServiceForms, ServiceMeet, ServiceAppScript, ServiceAds:
// expected user services
case ServiceKeep:
t.Fatalf("unexpected keep in user services")
@@ -114,7 +114,7 @@ func TestUserServices(t *testing.T) {
}
func TestUserServiceCSV(t *testing.T) {
- want := "gmail,calendar,chat,classroom,drive,docs,slides,contacts,tasks,sheets,people,forms,appscript,ads"
+ want := "gmail,calendar,chat,classroom,drive,docs,slides,contacts,tasks,sheets,people,forms,meet,appscript,ads"
if got := UserServiceCSV(); got != want {
t.Fatalf("unexpected user services csv: %q", got)
}
diff --git a/scripts/live-test.sh b/scripts/live-test.sh
index 0bac98d84..ea18cc7c9 100755
--- a/scripts/live-test.sh
+++ b/scripts/live-test.sh
@@ -159,6 +159,7 @@ source "$ROOT_DIR/scripts/live-tests/contacts.sh"
source "$ROOT_DIR/scripts/live-tests/people.sh"
source "$ROOT_DIR/scripts/live-tests/workspace.sh"
source "$ROOT_DIR/scripts/live-tests/classroom.sh"
+source "$ROOT_DIR/scripts/live-tests/meet.sh"
ensure_test_account
@@ -187,5 +188,6 @@ run_contacts_tests
run_people_tests
run_workspace_tests
run_classroom_tests
+run_meet_tests
echo "Live tests complete."
diff --git a/scripts/live-tests/meet.sh b/scripts/live-tests/meet.sh
new file mode 100755
index 000000000..0e4624086
--- /dev/null
+++ b/scripts/live-tests/meet.sh
@@ -0,0 +1,20 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+run_meet_tests() {
+ if skip "meet"; then
+ echo "==> meet (skipped)"
+ return 0
+ fi
+
+ local space_json meeting_code
+ echo "==> meet create"
+ space_json=$(gog meet create --json)
+ meeting_code=$(echo "$space_json" | "$PY" -c "import sys,json; print(json.load(sys.stdin)['meeting_code'])")
+ [ -n "$meeting_code" ] || { echo "Failed to parse meeting code" >&2; exit 1; }
+
+ run_required "meet" "meet get" gog meet get "$meeting_code" --json >/dev/null
+ run_required "meet" "meet update" gog meet update "$meeting_code" --access open --json >/dev/null
+ run_required "meet" "meet history" gog meet history "$meeting_code" --json --max 1 >/dev/null
+}