feat(raw): add lossless API dump commands

Co-authored-by: Ali Karbassi <ali@karbassi.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Peter Steinberger 2026-05-04 07:55:15 +01:00
parent 8b8fd09fa2
commit 7b288cc922
No known key found for this signature in database
57 changed files with 2860 additions and 5 deletions

View File

@ -5,6 +5,8 @@
### Added
- Install: publish a GHCR Docker image for release tags, with a non-root runtime image and file-keyring docs for container automation. (#539, #444) — thanks @HuckOps and @rdehuyss.
- Gmail: add `--sanitize-content` (`--safe`) to `gmail get` and `gmail thread get` for agent-oriented sanitized content output without raw Gmail payloads in JSON. (#238, #220) — thanks @urasmutlu.
- Raw API dumps: add `docs raw`, `sheets raw`, `slides raw`, `drive raw`, `gmail raw`, `calendar raw`, `people raw`, `contacts raw`, `tasks raw`, and `forms raw` subcommands for lossless Google API JSON output, with `--pretty`, Drive raw redaction defaults, Sheets grid-data warnings, and a raw-output security audit. (#495, #496) — thanks @karbassi.
- Drive: add `--fields` to `drive ls` and `drive get` so callers can pass Drive API field masks for fields beyond the default JSON set. (#495) — thanks @karbassi.
- Agent safety: add baked safety-profile builds for fail-closed agent binaries, with `agent-safe`, `readonly`, and `full` profiles, filtered help/schema output, docs, and build tooling. (#366, #239) — thanks @drewburchfield.
- Calendar: add `--with-meet` to `calendar update` for adding Google Meet conferencing to existing events. (#538) — thanks @alexisperumal.
- Calendar: add `calendar move` / `calendar transfer` to move an event to another calendar and change its organizer. (#448) — thanks @markusbkoch.

View File

@ -1159,6 +1159,15 @@ gog drive unshare <fileId> --permission-id <permissionId>
# Shared drives (Team Drives)
gog drive drives --max 100
# Request extra fields from the Drive API (closes #486)
gog drive ls --fields "files(id,name,thumbnailLink),nextPageToken"
gog drive get <fileId> --fields "id,name,thumbnailLink,imageMediaMetadata"
# Raw API dump (lossless JSON for scripting/LLMs)
gog drive raw <fileId> # fields=*, sensitive fields redacted by default
gog drive raw <fileId> --fields "id,name,thumbnailLink" # honors user-named fields verbatim
gog drive raw <fileId> --pretty
```
### Docs / Slides / Sheets
@ -1187,6 +1196,8 @@ gog docs write <docId> --file ./body.md --replace --markdown
gog docs write <docId> --file ./body.md --append --markdown
gog docs find-replace <docId> "old" "new"
gog docs find-replace <docId> "old" "new" --tab "Notes"
gog docs raw <docId> # Lossless JSON dump of Documents.Get (LLM/scripting)
gog docs raw <docId> --pretty
# Slides
gog slides info <presentationId>
@ -1204,6 +1215,7 @@ gog slides insert-text <presentationId> <objectId> - < long-content.md
gog slides insert-text <presentationId> <objectId> "New body" --replace
gog slides replace-text <presentationId> "{{name}}" "Acme Corp"
gog slides replace-text <presentationId> "TODO" "DONE" --match-case --page <slideId1> --page <slideId2>
gog slides raw <presentationId> # Lossless JSON dump of Presentations.Get
# Sheets
gog sheets copy <spreadsheetId> "My Sheet Copy"
@ -1223,8 +1235,36 @@ gog sheets links <spreadsheetId> 'Sheet1!A1:B10'
gog sheets add-tab <spreadsheetId> <tabName> --index 0
gog sheets rename-tab <spreadsheetId> <oldName> <newName>
gog sheets delete-tab <spreadsheetId> <tabName> --force
gog sheets raw <spreadsheetId> # Lossless JSON dump of Spreadsheets.Get
gog sheets raw <spreadsheetId> --include-grid-data # Include cell-level data (off by default)
# Other raw dumps (gmail, calendar, people, contacts, tasks, forms)
gog gmail raw <messageId> # Lossless JSON dump of Users.Messages.Get (default format=full)
gog gmail raw <messageId> --format raw # Gmail's native format=raw (base64url RFC822)
gog calendar raw <calendarId> <eventId> # Lossless JSON dump of Events.Get
gog people raw people/<resourceName> # Lossless JSON dump of People.Get
gog contacts raw people/<resourceName> # Same endpoint, exposed under the contacts group
gog tasks raw <tasklistId> <taskId> # Lossless JSON dump of Tasks.Get
gog forms raw <formId> # Lossless JSON dump of Forms.Get
```
**Raw vs other read subcommands.** Use `raw` when you need the full
canonical Google API response as JSON (e.g. feeding a doc into an LLM,
or scripting against structural fields `cat`/`structure` drop). Other
read commands are lossier on purpose:
- `docs info`, `sheets metadata`, `slides info`, `drive get` → metadata only (cheap)
- `docs cat`, `docs structure` → plain text / simplified structure
- `docs export`, `sheets export`, `slides export`, `drive download` → converted file formats (pdf/docx/xlsx/pptx/md)
- `<group> raw` → full API response as JSON (verbose, lossless)
`drive raw` defaults to `fields=*` and redacts a small set of
capability/token-shaped fields (thumbnailLink, webContentLink,
exportLinks, resourceKey, appProperties, properties,
contentHints.thumbnail.image). When `--fields` is supplied the response
is returned verbatim — the user named the field, they get it. See
[`docs/raw-audit.md`](docs/raw-audit.md) for the redaction rationale.
### Contacts
```bash

View File

@ -77,6 +77,7 @@ Generated from `gog schema --json`.
- [`gog calendar (cal) move (transfer) <calendarId> <eventId> <destinationCalendarId> [flags]`](commands/gog-calendar-move.md) - Move an event to another calendar
- [`gog calendar (cal) out-of-office (ooo) --from=STRING --to=STRING [<calendarId>] [flags]`](commands/gog-calendar-out-of-office.md) - Create an Out of Office event
- [`gog calendar (cal) propose-time <calendarId> <eventId> [flags]`](commands/gog-calendar-propose-time.md) - Generate URL to propose a new meeting time (browser-only feature)
- [`gog calendar (cal) raw <calendarId> <eventId> [flags]`](commands/gog-calendar-raw.md) - Dump raw Google Calendar API response as JSON (Events.Get; lossless; for scripting and LLM consumption)
- [`gog calendar (cal) respond (rsvp,reply) <calendarId> <eventId> [flags]`](commands/gog-calendar-respond.md) - Respond to an event invitation
- [`gog calendar (cal) search (find,query) <query> [flags]`](commands/gog-calendar-search.md) - Search events
- [`gog calendar (cal) subscribe (sub,add-calendar) <calendarId> [flags]`](commands/gog-calendar-subscribe.md) - Add a calendar to your calendar list
@ -200,6 +201,7 @@ Generated from `gog schema --json`.
- [`gog contacts (contact) other delete <resourceName>`](commands/gog-contacts-other-delete.md) - Delete an other contact
- [`gog contacts (contact) other list [flags]`](commands/gog-contacts-other-list.md) - List other contacts
- [`gog contacts (contact) other search <query> ... [flags]`](commands/gog-contacts-other-search.md) - Search other contacts
- [`gog contacts (contact) raw <identifier> [flags]`](commands/gog-contacts-raw.md) - Dump raw People API response as JSON (People.Get; lossless; for scripting and LLM consumption)
- [`gog contacts (contact) search <query> ... [flags]`](commands/gog-contacts-search.md) - Search contacts by name/email/phone
- [`gog contacts (contact) update (edit,set) <resourceName> [flags]`](commands/gog-contacts-update.md) - Update a contact
- [`gog docs (doc) <command> [flags]`](commands/gog-docs.md) - Google Docs (export via Drive)
@ -223,6 +225,7 @@ Generated from `gog schema --json`.
- [`gog docs (doc) info (get,show) <docId>`](commands/gog-docs-info.md) - Get Google Doc metadata
- [`gog docs (doc) insert <docId> [<content>] [flags]`](commands/gog-docs-insert.md) - Insert text at a specific position
- [`gog docs (doc) list-tabs <docId>`](commands/gog-docs-list-tabs.md) - List all tabs in a Google Doc
- [`gog docs (doc) raw <docId> [flags]`](commands/gog-docs-raw.md) - Dump raw Google Docs API response as JSON (Documents.Get; lossless; for scripting and LLM consumption)
- [`gog docs (doc) rename-tab <docId> [flags]`](commands/gog-docs-rename-tab.md) - Rename a tab in a Google Doc
- [`gog docs (doc) sed <docId> [<expression>] [flags]`](commands/gog-docs-sed.md) - Regex find/replace (sed-style: s/pattern/replacement/g)
- [`gog docs (doc) structure (struct) <docId> [flags]`](commands/gog-docs-structure.md) - Show document structure with numbered paragraphs
@ -241,11 +244,12 @@ Generated from `gog schema --json`.
- [`gog drive (drv) delete (rm,del) <fileId> [flags]`](commands/gog-drive-delete.md) - Move a file to trash (use --permanent to delete forever)
- [`gog drive (drv) download <fileId> [flags]`](commands/gog-drive-download.md) - Download a file (exports Google Docs formats)
- [`gog drive (drv) drives [flags]`](commands/gog-drive-drives.md) - List shared drives (Team Drives)
- [`gog drive (drv) get <fileId>`](commands/gog-drive-get.md) - Get file metadata
- [`gog drive (drv) get <fileId> [flags]`](commands/gog-drive-get.md) - Get file metadata
- [`gog drive (drv) ls [flags]`](commands/gog-drive-ls.md) - List files in a folder (default: root)
- [`gog drive (drv) mkdir <name> [flags]`](commands/gog-drive-mkdir.md) - Create a folder
- [`gog drive (drv) move <fileId> [flags]`](commands/gog-drive-move.md) - Move a file to a different folder
- [`gog drive (drv) permissions <fileId> [flags]`](commands/gog-drive-permissions.md) - List permissions on a file
- [`gog drive (drv) raw <fileId> [flags]`](commands/gog-drive-raw.md) - Dump raw Google Drive API response as JSON (Files.Get; lossless; for scripting and LLM consumption)
- [`gog drive (drv) rename <fileId> <newName>`](commands/gog-drive-rename.md) - Rename a file or folder
- [`gog drive (drv) search <query> ... [flags]`](commands/gog-drive-search.md) - Full-text search across Drive
- [`gog drive (drv) share <fileId> [flags]`](commands/gog-drive-share.md) - Share a file or folder
@ -259,6 +263,7 @@ Generated from `gog schema --json`.
- [`gog forms (form) delete-question (delete-q,dq,rm-q) <formId> <index>`](commands/gog-forms-delete-question.md) - Delete a question by index
- [`gog forms (form) get (info,show) <formId>`](commands/gog-forms-get.md) - Get a form
- [`gog forms (form) move-question (move-q,mq) <formId> <oldIndex> <newIndex>`](commands/gog-forms-move-question.md) - Move a question to a new position
- [`gog forms (form) raw <formId> [flags]`](commands/gog-forms-raw.md) - Dump raw Google Forms API response as JSON (Forms.Get; lossless; for scripting and LLM consumption)
- [`gog forms (form) responses <command>`](commands/gog-forms-responses.md) - Form responses
- [`gog forms (form) responses get (info,show) <formId> <responseId>`](commands/gog-forms-responses-get.md) - Get a form response
- [`gog forms (form) responses list (ls) <formId> [flags]`](commands/gog-forms-responses-list.md) - List form responses
@ -297,6 +302,7 @@ Generated from `gog schema --json`.
- [`gog gmail (mail,email) messages (message,msg,msgs) <command>`](commands/gog-gmail-messages.md) - Message operations
- [`gog gmail (mail,email) messages (message,msg,msgs) modify (update,edit,set) <messageId> [flags]`](commands/gog-gmail-messages-modify.md) - Modify labels on a single message
- [`gog gmail (mail,email) messages (message,msg,msgs) search (find,query,ls,list) <query> ... [flags]`](commands/gog-gmail-messages-search.md) - Search messages using Gmail query syntax
- [`gog gmail (mail,email) raw <messageId> [flags]`](commands/gog-gmail-raw.md) - Dump raw Gmail API response as JSON (Users.Messages.Get; lossless; for scripting and LLM consumption)
- [`gog gmail (mail,email) search (find,query,ls,list) <query> ... [flags]`](commands/gog-gmail-search.md) - Search threads using Gmail query syntax
- [`gog gmail (mail,email) send [flags]`](commands/gog-gmail-send.md) - Send an email
- [`gog gmail (mail,email) settings <command>`](commands/gog-gmail-settings.md) - Settings and admin
@ -366,6 +372,7 @@ Generated from `gog schema --json`.
- [`gog people (person) <command> [flags]`](commands/gog-people.md) - Google People
- [`gog people (person) get (info,show) <userId>`](commands/gog-people-get.md) - Get a user profile by ID
- [`gog people (person) me`](commands/gog-people-me.md) - Show your profile (people/me)
- [`gog people (person) raw <userId> [flags]`](commands/gog-people-raw.md) - Dump raw People API response as JSON (People.Get; lossless; for scripting and LLM consumption)
- [`gog people (person) relations [<userId>] [flags]`](commands/gog-people-relations.md) - Get user relations
- [`gog people (person) search (find,query) <query> ... [flags]`](commands/gog-people-search.md) - Search the Workspace directory
- [`gog schema (help-json,helpjson) [<command> ...] [flags]`](commands/gog-schema.md) - Machine-readable command/flag schema
@ -401,6 +408,7 @@ Generated from `gog schema --json`.
- [`gog sheets (sheet) named-ranges (namedranges,nr) update (edit,set) <spreadsheetId> <nameOrId> [flags]`](commands/gog-sheets-named-ranges-update.md) - Update a named range
- [`gog sheets (sheet) notes <spreadsheetId> <range>`](commands/gog-sheets-notes.md) - Get cell notes from a range
- [`gog sheets (sheet) number-format <spreadsheetId> <range> [flags]`](commands/gog-sheets-number-format.md) - Apply number format to a range
- [`gog sheets (sheet) raw <spreadsheetId> [flags]`](commands/gog-sheets-raw.md) - Dump raw Google Sheets API response as JSON (Spreadsheets.Get; lossless; for scripting and LLM consumption)
- [`gog sheets (sheet) read-format (get-format,format-read) <spreadsheetId> <range> [flags]`](commands/gog-sheets-read-format.md) - Read cell formatting from a range
- [`gog sheets (sheet) rename-tab (rename-sheet) <spreadsheetId> <oldName> <newName>`](commands/gog-sheets-rename-tab.md) - Rename a tab/sheet in a spreadsheet
- [`gog sheets (sheet) resize-columns <spreadsheetId> <columns> [flags]`](commands/gog-sheets-resize-columns.md) - Resize sheet columns
@ -419,6 +427,7 @@ Generated from `gog schema --json`.
- [`gog slides (slide) info (get,show) <presentationId>`](commands/gog-slides-info.md) - Get Google Slides presentation metadata
- [`gog slides (slide) insert-text <presentationId> <objectId> <text> [flags]`](commands/gog-slides-insert-text.md) - Insert text into an existing page element (shape or table) by objectId
- [`gog slides (slide) list-slides <presentationId>`](commands/gog-slides-list-slides.md) - List all slides with their object IDs
- [`gog slides (slide) raw <presentationId> [flags]`](commands/gog-slides-raw.md) - Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)
- [`gog slides (slide) read-slide <presentationId> <slideId>`](commands/gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
- [`gog slides (slide) replace-slide <presentationId> <slideId> <image> [flags]`](commands/gog-slides-replace-slide.md) - Replace the image on an existing slide in-place
- [`gog slides (slide) replace-text <presentationId> <find> <replacement> [flags]`](commands/gog-slides-replace-text.md) - Find-and-replace text across a presentation
@ -435,6 +444,7 @@ Generated from `gog schema --json`.
- [`gog tasks (task) lists <command>`](commands/gog-tasks-lists.md) - List task lists
- [`gog tasks (task) lists create (add,new) <title> ...`](commands/gog-tasks-lists-create.md) - Create a task list
- [`gog tasks (task) lists list [flags]`](commands/gog-tasks-lists-list.md) - List task lists
- [`gog tasks (task) raw <tasklistId> <taskId> [flags]`](commands/gog-tasks-raw.md) - Dump raw Google Tasks API response as JSON (Tasks.Get; lossless; for scripting and LLM consumption)
- [`gog tasks (task) undo (uncomplete,undone) <tasklistId> <taskId>`](commands/gog-tasks-undo.md) - Mark task needs action
- [`gog tasks (task) update (edit,set) <tasklistId> <taskId> [flags]`](commands/gog-tasks-update.md) - Update a task
- [`gog time <command> [flags]`](commands/gog-time.md) - Local time utilities

View File

@ -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: 440.
Generated pages: 450.
## Top-level Commands
@ -120,6 +120,7 @@ Generated pages: 440.
- [gog calendar move](gog-calendar-move.md) - Move an event to another calendar
- [gog calendar out-of-office](gog-calendar-out-of-office.md) - Create an Out of Office event
- [gog calendar propose-time](gog-calendar-propose-time.md) - Generate URL to propose a new meeting time (browser-only feature)
- [gog calendar raw](gog-calendar-raw.md) - Dump raw Google Calendar API response as JSON (Events.Get; lossless; for scripting and LLM consumption)
- [gog calendar respond](gog-calendar-respond.md) - Respond to an event invitation
- [gog calendar search](gog-calendar-search.md) - Search events
- [gog calendar subscribe](gog-calendar-subscribe.md) - Add a calendar to your calendar list
@ -243,6 +244,7 @@ Generated pages: 440.
- [gog contacts other delete](gog-contacts-other-delete.md) - Delete an other contact
- [gog contacts other list](gog-contacts-other-list.md) - List other contacts
- [gog contacts other search](gog-contacts-other-search.md) - Search other contacts
- [gog contacts raw](gog-contacts-raw.md) - Dump raw People API response as JSON (People.Get; lossless; for scripting and LLM consumption)
- [gog contacts search](gog-contacts-search.md) - Search contacts by name/email/phone
- [gog contacts update](gog-contacts-update.md) - Update a contact
- [gog docs](gog-docs.md) - Google Docs (export via Drive)
@ -266,6 +268,7 @@ Generated pages: 440.
- [gog docs info](gog-docs-info.md) - Get Google Doc metadata
- [gog docs insert](gog-docs-insert.md) - Insert text at a specific position
- [gog docs list-tabs](gog-docs-list-tabs.md) - List all tabs in a Google Doc
- [gog docs raw](gog-docs-raw.md) - Dump raw Google Docs API response as JSON (Documents.Get; lossless; for scripting and LLM consumption)
- [gog docs rename-tab](gog-docs-rename-tab.md) - Rename a tab in a Google Doc
- [gog docs sed](gog-docs-sed.md) - Regex find/replace (sed-style: s/pattern/replacement/g)
- [gog docs structure](gog-docs-structure.md) - Show document structure with numbered paragraphs
@ -289,6 +292,7 @@ Generated pages: 440.
- [gog drive mkdir](gog-drive-mkdir.md) - Create a folder
- [gog drive move](gog-drive-move.md) - Move a file to a different folder
- [gog drive permissions](gog-drive-permissions.md) - List permissions on a file
- [gog drive raw](gog-drive-raw.md) - Dump raw Google Drive API response as JSON (Files.Get; lossless; for scripting and LLM consumption)
- [gog drive rename](gog-drive-rename.md) - Rename a file or folder
- [gog drive search](gog-drive-search.md) - Full-text search across Drive
- [gog drive share](gog-drive-share.md) - Share a file or folder
@ -302,6 +306,7 @@ Generated pages: 440.
- [gog forms delete-question](gog-forms-delete-question.md) - Delete a question by index
- [gog forms get](gog-forms-get.md) - Get a form
- [gog forms move-question](gog-forms-move-question.md) - Move a question to a new position
- [gog forms raw](gog-forms-raw.md) - Dump raw Google Forms API response as JSON (Forms.Get; lossless; for scripting and LLM consumption)
- [gog forms responses](gog-forms-responses.md) - Form responses
- [gog forms responses get](gog-forms-responses-get.md) - Get a form response
- [gog forms responses list](gog-forms-responses-list.md) - List form responses
@ -340,6 +345,7 @@ Generated pages: 440.
- [gog gmail messages](gog-gmail-messages.md) - Message operations
- [gog gmail messages modify](gog-gmail-messages-modify.md) - Modify labels on a single message
- [gog gmail messages search](gog-gmail-messages-search.md) - Search messages using Gmail query syntax
- [gog gmail raw](gog-gmail-raw.md) - Dump raw Gmail API response as JSON (Users.Messages.Get; lossless; for scripting and LLM consumption)
- [gog gmail search](gog-gmail-search.md) - Search threads using Gmail query syntax
- [gog gmail send](gog-gmail-send.md) - Send an email
- [gog gmail settings](gog-gmail-settings.md) - Settings and admin
@ -409,6 +415,7 @@ Generated pages: 440.
- [gog people](gog-people.md) - Google People
- [gog people get](gog-people-get.md) - Get a user profile by ID
- [gog people me](gog-people-me.md) - Show your profile (people/me)
- [gog people raw](gog-people-raw.md) - Dump raw People API response as JSON (People.Get; lossless; for scripting and LLM consumption)
- [gog people relations](gog-people-relations.md) - Get user relations
- [gog people search](gog-people-search.md) - Search the Workspace directory
- [gog schema](gog-schema.md) - Machine-readable command/flag schema
@ -444,6 +451,7 @@ Generated pages: 440.
- [gog sheets named-ranges update](gog-sheets-named-ranges-update.md) - Update a named range
- [gog sheets notes](gog-sheets-notes.md) - Get cell notes from a range
- [gog sheets number-format](gog-sheets-number-format.md) - Apply number format to a range
- [gog sheets raw](gog-sheets-raw.md) - Dump raw Google Sheets API response as JSON (Spreadsheets.Get; lossless; for scripting and LLM consumption)
- [gog sheets read-format](gog-sheets-read-format.md) - Read cell formatting from a range
- [gog sheets rename-tab](gog-sheets-rename-tab.md) - Rename a tab/sheet in a spreadsheet
- [gog sheets resize-columns](gog-sheets-resize-columns.md) - Resize sheet columns
@ -462,6 +470,7 @@ Generated pages: 440.
- [gog slides info](gog-slides-info.md) - Get Google Slides presentation metadata
- [gog slides insert-text](gog-slides-insert-text.md) - Insert text into an existing page element (shape or table) by objectId
- [gog slides list-slides](gog-slides-list-slides.md) - List all slides with their object IDs
- [gog slides raw](gog-slides-raw.md) - Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)
- [gog slides read-slide](gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
- [gog slides replace-slide](gog-slides-replace-slide.md) - Replace the image on an existing slide in-place
- [gog slides replace-text](gog-slides-replace-text.md) - Find-and-replace text across a presentation
@ -478,6 +487,7 @@ Generated pages: 440.
- [gog tasks lists](gog-tasks-lists.md) - List task lists
- [gog tasks lists create](gog-tasks-lists-create.md) - Create a task list
- [gog tasks lists list](gog-tasks-lists-list.md) - List task lists
- [gog tasks raw](gog-tasks-raw.md) - Dump raw Google Tasks API response as JSON (Tasks.Get; lossless; for scripting and LLM consumption)
- [gog tasks undo](gog-tasks-undo.md) - Mark task needs action
- [gog tasks update](gog-tasks-update.md) - Update a task
- [gog time](gog-time.md) - Local time utilities

View File

@ -0,0 +1,43 @@
# `gog calendar raw`
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
Dump raw Google Calendar API response as JSON (Events.Get; lossless; for scripting and LLM consumption)
## Usage
```bash
gog calendar (cal) raw <calendarId> <eventId> [flags]
```
## Parent
- [gog calendar](gog-calendar.md)
## Flags
| Flag | Type | Default | Help |
| --- | --- | --- | --- |
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
| `-a`<br>`--account`<br>`--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`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--pretty` | `bool` | | Pretty-print JSON (default: compact single-line) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
## See Also
- [gog calendar](gog-calendar.md)
- [Command index](README.md)

View File

@ -31,6 +31,7 @@ gog calendar (cal) <command> [flags]
- [gog calendar move](gog-calendar-move.md) - Move an event to another calendar
- [gog calendar out-of-office](gog-calendar-out-of-office.md) - Create an Out of Office event
- [gog calendar propose-time](gog-calendar-propose-time.md) - Generate URL to propose a new meeting time (browser-only feature)
- [gog calendar raw](gog-calendar-raw.md) - Dump raw Google Calendar API response as JSON (Events.Get; lossless; for scripting and LLM consumption)
- [gog calendar respond](gog-calendar-respond.md) - Respond to an event invitation
- [gog calendar search](gog-calendar-search.md) - Search events
- [gog calendar subscribe](gog-calendar-subscribe.md) - Add a calendar to your calendar list

View File

@ -0,0 +1,44 @@
# `gog contacts raw`
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
Dump raw People API response as JSON (People.Get; lossless; for scripting and LLM consumption)
## Usage
```bash
gog contacts (contact) raw <identifier> [flags]
```
## Parent
- [gog contacts](gog-contacts.md)
## Flags
| Flag | Type | Default | Help |
| --- | --- | --- | --- |
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
| `-a`<br>`--account`<br>`--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`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `--person-fields` | `string` | | People API personFields mask (default: broad set) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--pretty` | `bool` | | Pretty-print JSON (default: compact single-line) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
## See Also
- [gog contacts](gog-contacts.md)
- [Command index](README.md)

View File

@ -23,6 +23,7 @@ gog contacts (contact) <command> [flags]
- [gog contacts get](gog-contacts-get.md) - Get a contact
- [gog contacts list](gog-contacts-list.md) - List contacts
- [gog contacts other](gog-contacts-other.md) - Other contacts
- [gog contacts raw](gog-contacts-raw.md) - Dump raw People API response as JSON (People.Get; lossless; for scripting and LLM consumption)
- [gog contacts search](gog-contacts-search.md) - Search contacts by name/email/phone
- [gog contacts update](gog-contacts-update.md) - Update a contact

View File

@ -0,0 +1,43 @@
# `gog docs raw`
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
Dump raw Google Docs API response as JSON (Documents.Get; lossless; for scripting and LLM consumption)
## Usage
```bash
gog docs (doc) raw <docId> [flags]
```
## Parent
- [gog docs](gog-docs.md)
## Flags
| Flag | Type | Default | Help |
| --- | --- | --- | --- |
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
| `-a`<br>`--account`<br>`--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`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--pretty` | `bool` | | Pretty-print JSON (default: compact single-line) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
## See Also
- [gog docs](gog-docs.md)
- [Command index](README.md)

View File

@ -30,6 +30,7 @@ gog docs (doc) <command> [flags]
- [gog docs info](gog-docs-info.md) - Get Google Doc metadata
- [gog docs insert](gog-docs-insert.md) - Insert text at a specific position
- [gog docs list-tabs](gog-docs-list-tabs.md) - List all tabs in a Google Doc
- [gog docs raw](gog-docs-raw.md) - Dump raw Google Docs API response as JSON (Documents.Get; lossless; for scripting and LLM consumption)
- [gog docs rename-tab](gog-docs-rename-tab.md) - Rename a tab in a Google Doc
- [gog docs sed](gog-docs-sed.md) - Regex find/replace (sed-style: s/pattern/replacement/g)
- [gog docs structure](gog-docs-structure.md) - Show document structure with numbered paragraphs

View File

@ -7,7 +7,7 @@ Get file metadata
## Usage
```bash
gog drive (drv) get <fileId>
gog drive (drv) get <fileId> [flags]
```
## Parent
@ -25,6 +25,7 @@ gog drive (drv) get <fileId>
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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) |
| `--fields` | `string` | | Drive API field mask (overrides the default set; e.g. 'id,name,thumbnailLink') |
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |

View File

@ -27,6 +27,7 @@ gog drive (drv) ls [flags]
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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) |
| `--fields` | `string` | | Drive API field mask (overrides the default set; e.g. 'files(id,name,thumbnailLink),nextPageToken') |
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |

View File

@ -0,0 +1,44 @@
# `gog drive raw`
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
Dump raw Google Drive API response as JSON (Files.Get; lossless; for scripting and LLM consumption)
## Usage
```bash
gog drive (drv) raw <fileId> [flags]
```
## Parent
- [gog drive](gog-drive.md)
## Flags
| Flag | Type | Default | Help |
| --- | --- | --- | --- |
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
| `-a`<br>`--account`<br>`--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`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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) |
| `--fields` | `string` | | Drive API field mask (default: * with sensitive fields redacted client-side). Set explicitly to disable redaction. |
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--pretty` | `bool` | | Pretty-print JSON (default: compact single-line) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
## See Also
- [gog drive](gog-drive.md)
- [Command index](README.md)

View File

@ -26,6 +26,7 @@ gog drive (drv) <command> [flags]
- [gog drive mkdir](gog-drive-mkdir.md) - Create a folder
- [gog drive move](gog-drive-move.md) - Move a file to a different folder
- [gog drive permissions](gog-drive-permissions.md) - List permissions on a file
- [gog drive raw](gog-drive-raw.md) - Dump raw Google Drive API response as JSON (Files.Get; lossless; for scripting and LLM consumption)
- [gog drive rename](gog-drive-rename.md) - Rename a file or folder
- [gog drive search](gog-drive-search.md) - Full-text search across Drive
- [gog drive share](gog-drive-share.md) - Share a file or folder

View File

@ -0,0 +1,43 @@
# `gog forms raw`
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
Dump raw Google Forms API response as JSON (Forms.Get; lossless; for scripting and LLM consumption)
## Usage
```bash
gog forms (form) raw <formId> [flags]
```
## Parent
- [gog forms](gog-forms.md)
## Flags
| Flag | Type | Default | Help |
| --- | --- | --- | --- |
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
| `-a`<br>`--account`<br>`--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`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--pretty` | `bool` | | Pretty-print JSON (default: compact single-line) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
## See Also
- [gog forms](gog-forms.md)
- [Command index](README.md)

View File

@ -21,6 +21,7 @@ gog forms (form) <command> [flags]
- [gog forms delete-question](gog-forms-delete-question.md) - Delete a question by index
- [gog forms get](gog-forms-get.md) - Get a form
- [gog forms move-question](gog-forms-move-question.md) - Move a question to a new position
- [gog forms raw](gog-forms-raw.md) - Dump raw Google Forms API response as JSON (Forms.Get; lossless; for scripting and LLM consumption)
- [gog forms responses](gog-forms-responses.md) - Form responses
- [gog forms update](gog-forms-update.md) - Update form title, description, or settings
- [gog forms watch](gog-forms-watch.md) - Response watches (push notifications)

View File

@ -0,0 +1,44 @@
# `gog gmail raw`
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
Dump raw Gmail API response as JSON (Users.Messages.Get; lossless; for scripting and LLM consumption)
## Usage
```bash
gog gmail (mail,email) raw <messageId> [flags]
```
## Parent
- [gog gmail](gog-gmail.md)
## Flags
| Flag | Type | Default | Help |
| --- | --- | --- | --- |
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
| `-a`<br>`--account`<br>`--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`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--format` | `string` | full | Gmail format: full\|metadata\|minimal\|raw (default: full; note: 'raw' here means Gmail's base64url RFC822 blob, NOT the gog raw subcommand sense) |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--pretty` | `bool` | | Pretty-print JSON (default: compact single-line) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
## See Also
- [gog gmail](gog-gmail.md)
- [Command index](README.md)

View File

@ -27,6 +27,7 @@ gog gmail (mail,email) <command> [flags]
- [gog gmail labels](gog-gmail-labels.md) - Label operations
- [gog gmail mark-read](gog-gmail-mark-read.md) - Mark messages as read
- [gog gmail messages](gog-gmail-messages.md) - Message operations
- [gog gmail raw](gog-gmail-raw.md) - Dump raw Gmail API response as JSON (Users.Messages.Get; lossless; for scripting and LLM consumption)
- [gog gmail search](gog-gmail-search.md) - Search threads using Gmail query syntax
- [gog gmail send](gog-gmail-send.md) - Send an email
- [gog gmail settings](gog-gmail-settings.md) - Settings and admin

View File

@ -27,6 +27,7 @@ gog ls (list) [flags]
| `--disable-commands` | `string` | | Comma-separated list of disabled commands; dot paths allowed |
| `-n`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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) |
| `--fields` | `string` | | Drive API field mask (overrides the default set; e.g. 'files(id,name,thumbnailLink),nextPageToken') |
| `-y`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |

View File

@ -0,0 +1,44 @@
# `gog people raw`
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
Dump raw People API response as JSON (People.Get; lossless; for scripting and LLM consumption)
## Usage
```bash
gog people (person) raw <userId> [flags]
```
## Parent
- [gog people](gog-people.md)
## Flags
| Flag | Type | Default | Help |
| --- | --- | --- | --- |
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
| `-a`<br>`--account`<br>`--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`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `--person-fields` | `string` | | People API personFields mask (default: broad set; pass a narrower list to reduce output) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--pretty` | `bool` | | Pretty-print JSON (default: compact single-line) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
## See Also
- [gog people](gog-people.md)
- [Command index](README.md)

View File

@ -18,6 +18,7 @@ gog people (person) <command> [flags]
- [gog people get](gog-people-get.md) - Get a user profile by ID
- [gog people me](gog-people-me.md) - Show your profile (people/me)
- [gog people raw](gog-people-raw.md) - Dump raw People API response as JSON (People.Get; lossless; for scripting and LLM consumption)
- [gog people relations](gog-people-relations.md) - Get user relations
- [gog people search](gog-people-search.md) - Search the Workspace directory

View File

@ -0,0 +1,44 @@
# `gog sheets raw`
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
Dump raw Google Sheets API response as JSON (Spreadsheets.Get; lossless; for scripting and LLM consumption)
## Usage
```bash
gog sheets (sheet) raw <spreadsheetId> [flags]
```
## Parent
- [gog sheets](gog-sheets.md)
## Flags
| Flag | Type | Default | Help |
| --- | --- | --- | --- |
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
| `-a`<br>`--account`<br>`--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`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `--include-grid-data` | `bool` | | Include cell-level grid data in the response (off by default; payloads can be large and may contain secrets in formulas) |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--pretty` | `bool` | | Pretty-print JSON (default: compact single-line) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
## See Also
- [gog sheets](gog-sheets.md)
- [Command index](README.md)

View File

@ -35,6 +35,7 @@ gog sheets (sheet) <command> [flags]
- [gog sheets named-ranges](gog-sheets-named-ranges.md) - Manage named ranges
- [gog sheets notes](gog-sheets-notes.md) - Get cell notes from a range
- [gog sheets number-format](gog-sheets-number-format.md) - Apply number format to a range
- [gog sheets raw](gog-sheets-raw.md) - Dump raw Google Sheets API response as JSON (Spreadsheets.Get; lossless; for scripting and LLM consumption)
- [gog sheets read-format](gog-sheets-read-format.md) - Read cell formatting from a range
- [gog sheets rename-tab](gog-sheets-rename-tab.md) - Rename a tab/sheet in a spreadsheet
- [gog sheets resize-columns](gog-sheets-resize-columns.md) - Resize sheet columns

View File

@ -0,0 +1,43 @@
# `gog slides raw`
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)
## Usage
```bash
gog slides (slide) raw <presentationId> [flags]
```
## Parent
- [gog slides](gog-slides.md)
## Flags
| Flag | Type | Default | Help |
| --- | --- | --- | --- |
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
| `-a`<br>`--account`<br>`--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`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--pretty` | `bool` | | Pretty-print JSON (default: compact single-line) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
## See Also
- [gog slides](gog-slides.md)
- [Command index](README.md)

View File

@ -26,6 +26,7 @@ gog slides (slide) <command> [flags]
- [gog slides info](gog-slides-info.md) - Get Google Slides presentation metadata
- [gog slides insert-text](gog-slides-insert-text.md) - Insert text into an existing page element (shape or table) by objectId
- [gog slides list-slides](gog-slides-list-slides.md) - List all slides with their object IDs
- [gog slides raw](gog-slides-raw.md) - Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)
- [gog slides read-slide](gog-slides-read-slide.md) - Read slide content: speaker notes, text elements, and images
- [gog slides replace-slide](gog-slides-replace-slide.md) - Replace the image on an existing slide in-place
- [gog slides replace-text](gog-slides-replace-text.md) - Find-and-replace text across a presentation

View File

@ -0,0 +1,43 @@
# `gog tasks raw`
> Generated from `gog schema --json`. Do not edit this page by hand; run `make docs-commands`.
Dump raw Google Tasks API response as JSON (Tasks.Get; lossless; for scripting and LLM consumption)
## Usage
```bash
gog tasks (task) raw <tasklistId> <taskId> [flags]
```
## Parent
- [gog tasks](gog-tasks.md)
## Flags
| Flag | Type | Default | Help |
| --- | --- | --- | --- |
| `--access-token` | `string` | | Use provided access token directly (bypasses stored refresh tokens; token expires in ~1h) |
| `-a`<br>`--account`<br>`--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`<br>`--dry-run`<br>`--dryrun`<br>`--noop`<br>`--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`<br>`--force`<br>`--assume-yes`<br>`--yes` | `bool` | | Skip confirmations for destructive commands |
| `--gmail-no-send` | `bool` | false | Block Gmail send operations (agent safety) |
| `-h`<br>`--help` | `kong.helpFlag` | | Show context-sensitive help. |
| `-j`<br>`--json`<br>`--machine` | `bool` | false | Output JSON to stdout (best for scripting) |
| `--no-input`<br>`--non-interactive`<br>`--noninteractive` | `bool` | | Never prompt; fail instead (useful for CI) |
| `-p`<br>`--plain`<br>`--tsv` | `bool` | false | Output stable, parseable text to stdout (TSV; no colors) |
| `--pretty` | `bool` | | Pretty-print JSON (default: compact single-line) |
| `--results-only` | `bool` | | In JSON mode, emit only the primary result (drops envelope fields like nextPageToken) |
| `--select`<br>`--pick`<br>`--project` | `string` | | In JSON mode, select comma-separated fields (best-effort; supports dot paths). Desire path: use --fields for most commands. |
| `-v`<br>`--verbose` | `bool` | | Enable verbose logging |
| `--version` | `kong.VersionFlag` | | Print version and exit |
## See Also
- [gog tasks](gog-tasks.md)
- [Command index](README.md)

View File

@ -23,6 +23,7 @@ gog tasks (task) <command> [flags]
- [gog tasks get](gog-tasks-get.md) - Get a task
- [gog tasks list](gog-tasks-list.md) - List tasks
- [gog tasks lists](gog-tasks-lists.md) - List task lists
- [gog tasks raw](gog-tasks-raw.md) - Dump raw Google Tasks API response as JSON (Tasks.Get; lossless; for scripting and LLM consumption)
- [gog tasks undo](gog-tasks-undo.md) - Mark task needs action
- [gog tasks update](gog-tasks-update.md) - Update a task

174
docs/raw-audit.md Normal file
View File

@ -0,0 +1,174 @@
# `gog <group> raw` — Sensitive Field Audit
This document records the security audit performed before shipping the
`gog <group> raw <id>` subcommands, which dump the canonical Google API
response as JSON for programmatic / LLM consumption.
## Redaction rule
`raw` applies field-level redaction **only when the user did not explicitly
name a field via `--fields`**. Rationale:
- The default (implicit `fields=*` for Drive, or no mask for the other
APIs) pulls in capability URLs and third-partystashed metadata that
callers rarely want and can leak if piped into an LLM, shared in a bug
report, or committed to a repo.
- When a caller writes `--fields "id,name,thumbnailLink"`, they named
`thumbnailLink` deliberately. Redacting a user-named field would be
surprising and user-hostile.
Summary: **redact what the user didn't ask for; honor what they did.**
## Per-endpoint Findings: Workspace Content
### 1. `docs.Documents.Get``gog docs raw`
REST ref: <https://developers.google.com/docs/api/reference/rest/v1/documents/get>
Go type: <https://pkg.go.dev/google.golang.org/api/docs/v1#Document>
| Field | Risk | Default handling |
|---|---|---|
| `inlineObjects.*.embeddedObject.imageProperties.contentUri` | Short-lived (~30 min) bearer-style authenticated image URL | **Ship as-is** |
| `inlineObjects.*.embeddedObject.imageProperties.sourceUri` | May reference private source URLs | **Ship as-is** |
**Why not redact:** the Docs API has no field mask, so the principled
"redact only what the user didn't name" rule has no escape hatch.
These image URIs are short-lived (~30 min) and require the caller's
auth anyway — substantially lower risk than Drive's `thumbnailLink`.
The lossless guarantee is valued more than the marginal hardening.
No credentials, tokens, or OAuth metadata in the response.
### 2. `sheets.Spreadsheets.Get``gog sheets raw`
REST ref: <https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get>
Go type: <https://pkg.go.dev/google.golang.org/api/sheets/v4#Spreadsheet>
| Field | Risk | Default handling |
|---|---|---|
| `developerMetadata` | Third-party apps may stash arbitrary KV including secrets | **Warn on stderr if present**, do not redact |
| `sheets[].data.rowData.values[].userEnteredValue.formulaValue` (only with `--include-grid-data`) | Formulas can embed API keys via `IMPORTRANGE`, hardcoded tokens in cells | **Warn on stderr when `--include-grid-data` is set**, do not redact |
`--include-grid-data` is **off by default** because grid payloads can be
multi-MB and are the primary leakage vector.
No redaction applied to Sheets output; warnings only. Redacting cell
content would defeat the purpose of a lossless dump.
### 3. `slides.Presentations.Get``gog slides raw`
REST ref: <https://developers.google.com/slides/api/reference/rest/v1/presentations/get>
Go type: <https://pkg.go.dev/google.golang.org/api/slides/v1#Presentation>
| Field | Risk | Default handling |
|---|---|---|
| `slides[].pageElements[].image.contentUrl` | Short-lived authenticated image URL (same class as Docs `contentUri`) | **Ship as-is** |
| `slides[].pageElements[].image.sourceUrl` | Possibly private origin URL | **Ship as-is** |
| `slides[].pageElements[].video.url` | Drive video refs may carry signed access | **Ship as-is** |
Same rationale as Docs: no field mask, short-lived URLs, auth-gated,
lower risk than Drive. Lossless guarantee preferred.
### 4. `drive.Files.Get` with `fields=*``gog drive raw` *(highest risk)*
REST ref: <https://developers.google.com/drive/api/reference/rest/v3/files/get>
Go type: <https://pkg.go.dev/google.golang.org/api/drive/v3#File>
| Field | Risk | Default handling |
|---|---|---|
| `thumbnailLink` | Time-limited signed URL that bypasses normal auth for ~hours. Classic leak vector. | Redact |
| `webContentLink` | Direct download URL; capability URL | Redact |
| `exportLinks` | Per-MIME authenticated export URLs | Redact |
| `resourceKey` | Capability token for link-shared files; effectively a shared secret | Redact |
| `appProperties` | Arbitrary app-stashed KV; apps commonly misuse for secrets | Redact |
| `properties` | Public custom properties, still frequently (mis)used for tokens | Redact |
| `contentHints.thumbnail.image` | Base64 thumbnail bytes; large and unnecessary | Redact |
| `permissions[].emailAddress`, `owners[].emailAddress`, `sharingUser`, `lastModifyingUser`, `trashingUser` | Non-collaborator emails (PII) when `fields=*` enumerates full ACL | Not redacted — caller already has access to the file; enumeration is a conscious `--fields` choice |
**Reminder:** all of the above are redacted **only when `--fields` is not
set**. Passing `--fields "id,name,thumbnailLink"` returns `thumbnailLink`
verbatim — the user named it.
## Per-endpoint Findings: Other Services
### 5. `gmail.Users.Messages.Get``gog gmail raw`
REST ref: <https://developers.google.com/gmail/api/reference/rest/v1/users.messages/get>
Go type: <https://pkg.go.dev/google.golang.org/api/gmail/v1#Message>
The subcommand name "raw" collides with Gmail's native `format=raw`
(base64url-encoded RFC822 blob). `gog gmail raw` defaults to
`format=FULL` (full parsed Message struct) and exposes `--format
full|metadata|minimal|raw` for users who want Gmail's RAW. Help text
documents both senses.
| Field | Risk | Default handling |
|---|---|---|
| `payload.body.data` (base64url) | Email body; user already has read access | Ship as-is |
| `payload.headers` | May contain `Received-SPF`, `DKIM-Signature`, routing metadata | Ship as-is |
| `raw` (when `--format=raw`) | Full RFC822 source including original attachments | Ship as-is — user asked for it |
No credential leakage risk. Caller already holds the Gmail scope.
### 6. `calendar.Events.Get``gog calendar raw`
REST ref: <https://developers.google.com/calendar/api/v3/reference/events/get>
Go type: <https://pkg.go.dev/google.golang.org/api/calendar/v3#Event>
| Field | Risk | Default handling |
|---|---|---|
| `attendees[].email` | Attendee emails (PII) | Ship as-is — caller already on the event ACL |
| `conferenceData.entryPoints[].uri` | Meeting URLs (Meet/Zoom) including passwords in params | Ship as-is — user asked for the event |
| `extendedProperties.private` / `extendedProperties.shared` | App-stashed KV; third-party apps may use for secrets | Ship as-is (same rationale as Sheets `developerMetadata`) |
No redaction. The risk surface here is fundamentally the same as
simply reading the event in the Calendar UI.
### 7. `people.People.Get``gog people raw` / `gog contacts raw`
REST ref: <https://developers.google.com/people/api/rest/v1/people/get>
Go type: <https://pkg.go.dev/google.golang.org/api/people/v1#Person>
Both `gog people raw` and `gog contacts raw` call the same underlying
`people.Get` endpoint. Requires `--person-fields` (Google's field mask
for the People API, required on every request).
| Field | Risk | Default handling |
|---|---|---|
| `emailAddresses`, `phoneNumbers`, `addresses`, `biographies` | PII; user's own contacts | Ship as-is |
| `userDefined[]` | Arbitrary KV custom fields; may store secrets | Ship as-is |
| `metadata.sources[].profileMetadata.userTypes` | Account type disclosure | Ship as-is |
### 8. `tasks.Tasks.Get``gog tasks raw`
REST ref: <https://developers.google.com/tasks/reference/rest/v1/tasks/get>
Go type: <https://pkg.go.dev/google.golang.org/api/tasks/v1#Task>
| Field | Risk | Default handling |
|---|---|---|
| `notes` | User-entered free text | Ship as-is |
| `links[].link` | External URLs attached to a task | Ship as-is |
No sensitivity concerns beyond the caller's own task data.
### 9. `forms.Forms.Get``gog forms raw`
REST ref: <https://developers.google.com/forms/api/reference/rest/v1/forms/get>
Go type: <https://pkg.go.dev/google.golang.org/api/forms/v1#Form>
| Field | Risk | Default handling |
|---|---|---|
| `items[].questionItem.question.grading` | Correct answers for graded forms | Ship as-is — caller is the form owner |
| `linkedSheetId` | ID of the responses spreadsheet | Ship as-is |
No redaction. The form owner already has access to everything here.
## Cross-cutting observations
- Google APIs never return OAuth access tokens, refresh tokens, or client
secrets in resource responses. The risk is capability URLs and
app-stashed custom metadata, not credential disclosure in the API
contract itself.
- `gog drive raw` is the most dangerous command; the others are modest in
comparison.
- This audit covers all currently shipped `raw` subcommands.

View File

@ -8,6 +8,7 @@ type CalendarCmd struct {
Alias CalendarAliasCmd `cmd:"" name:"alias" help:"Manage calendar aliases"`
Events CalendarEventsCmd `cmd:"" name:"events" aliases:"list,ls" help:"List events from a calendar or all calendars"`
Event CalendarEventCmd `cmd:"" name:"event" aliases:"get,info,show" help:"Get event"`
Raw CalendarRawCmd `cmd:"" name:"raw" help:"Dump raw Google Calendar API response as JSON (Events.Get; lossless; for scripting and LLM consumption)"`
Create CalendarCreateCmd `cmd:"" name:"create" aliases:"add,new" help:"Create an event"`
Update CalendarUpdateCmd `cmd:"" name:"update" aliases:"edit,set" help:"Update an event"`
Move CalendarMoveCmd `cmd:"" name:"move" aliases:"transfer" help:"Move an event to another calendar"`

View File

@ -0,0 +1,52 @@
package cmd
import (
"context"
"errors"
"os"
"github.com/steipete/gogcli/internal/outfmt"
)
// CalendarRawCmd dumps the full Events.Get response as JSON, using the
// existing calendar-resolution helper so short names, "primary", and email
// aliases all work the same as they do for `calendar event`.
//
// REST reference: https://developers.google.com/calendar/api/v3/reference/events/get
// Go type: https://pkg.go.dev/google.golang.org/api/calendar/v3#Event
type CalendarRawCmd struct {
CalendarID string `arg:"" name:"calendarId" help:"Calendar ID (e.g. 'primary', an email, or a calendar ID)"`
EventID string `arg:"" name:"eventId" help:"Event ID"`
Pretty bool `name:"pretty" help:"Pretty-print JSON (default: compact single-line)"`
}
func (c *CalendarRawCmd) Run(ctx context.Context, flags *RootFlags) error {
eventID := normalizeCalendarEventID(c.EventID)
if eventID == "" {
return usage("empty eventId")
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newCalendarService(ctx, account)
if err != nil {
return err
}
calendarID, err := resolveCalendarSelector(ctx, svc, c.CalendarID, false)
if err != nil {
return err
}
event, err := svc.Events.Get(calendarID, eventID).Context(ctx).Do()
if err != nil {
return err
}
if event == nil {
return errors.New("event not found")
}
return outfmt.WriteRaw(ctx, os.Stdout, event, outfmt.RawOptions{Pretty: c.Pretty})
}

View File

@ -0,0 +1,130 @@
package cmd
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"google.golang.org/api/calendar/v3"
"google.golang.org/api/option"
)
func newCalendarRawTestServer(t *testing.T, status int, body map[string]any) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Calendar API uses /calendars/{calId}/events/{evId} for Events.Get
// and /users/me/calendarList for list operations used by the resolver.
path := strings.TrimPrefix(r.URL.Path, "/calendar/v3")
if strings.HasPrefix(path, "/users/me/calendarList") {
// Respond with an empty list so the resolver treats the input as a literal ID.
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{}})
return
}
if !strings.Contains(path, "/events/") || r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
if status != 0 {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{"code": status, "message": "mock error"},
})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(body)
}))
}
func installMockCalendarService(t *testing.T, srv *httptest.Server) {
t.Helper()
orig := newCalendarService
t.Cleanup(func() { newCalendarService = orig })
svc, err := calendar.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newCalendarService = func(context.Context, string) (*calendar.Service, error) { return svc, nil }
}
func fullCalendarEventResponse(id string) map[string]any {
return map[string]any{
"id": id,
"summary": "Lunch",
"start": map[string]any{"dateTime": "2026-04-08T12:00:00Z"},
"end": map[string]any{"dateTime": "2026-04-08T13:00:00Z"},
"attendees": []map[string]any{
{"email": "a@b.com", "responseStatus": "accepted"},
},
}
}
func TestCalendarRaw_HappyPath(t *testing.T) {
srv := newCalendarRawTestServer(t, 0, fullCalendarEventResponse("ev1"))
defer srv.Close()
installMockCalendarService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
if err := runKong(t, &CalendarRawCmd{}, []string{"primary", "ev1"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
var got map[string]any
if err := json.Unmarshal([]byte(out), &got); err != nil {
t.Fatalf("invalid JSON: %v\nraw: %s", err, out)
}
if got["id"] != "ev1" {
t.Fatalf("expected id=ev1, got: %v", got["id"])
}
if _, ok := got["attendees"]; !ok {
t.Fatalf("expected attendees in raw output")
}
}
func TestCalendarRaw_APIError(t *testing.T) {
srv := newCalendarRawTestServer(t, http.StatusInternalServerError, nil)
defer srv.Close()
installMockCalendarService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &CalendarRawCmd{}, []string{"primary", "ev1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 500")
}
})
}
func TestCalendarRaw_NotFound(t *testing.T) {
srv := newCalendarRawTestServer(t, http.StatusNotFound, nil)
defer srv.Close()
installMockCalendarService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &CalendarRawCmd{}, []string{"primary", "ev1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 404")
}
})
}
func TestCalendarRaw_EmptyEventID(t *testing.T) {
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
if err := (&CalendarRawCmd{CalendarID: "primary"}).Run(ctx, flags); err == nil {
t.Fatalf("expected error on empty eventId")
}
}

View File

@ -22,6 +22,7 @@ type ContactsCmd struct {
Delete ContactsDeleteCmd `cmd:"" name:"delete" aliases:"rm,del,remove" help:"Delete a contact"`
Directory ContactsDirectoryCmd `cmd:"" name:"directory" help:"Directory contacts"`
Other ContactsOtherCmd `cmd:"" name:"other" help:"Other contacts"`
Raw ContactsRawCmd `cmd:"" name:"raw" help:"Dump raw People API response as JSON (People.Get; lossless; for scripting and LLM consumption)"`
}
type ContactsSearchCmd struct {

View File

@ -37,6 +37,44 @@ type DocsCmd struct {
Sed DocsSedCmd `cmd:"" name:"sed" help:"Regex find/replace (sed-style: s/pattern/replacement/g)"`
Clear DocsClearCmd `cmd:"" name:"clear" help:"Clear all content from a Google Doc"`
Structure DocsStructureCmd `cmd:"" name:"structure" aliases:"struct" help:"Show document structure with numbered paragraphs"`
Raw DocsRawCmd `cmd:"" name:"raw" help:"Dump raw Google Docs API response as JSON (Documents.Get; lossless; for scripting and LLM consumption)"`
}
// DocsRawCmd dumps the full Documents.Get response as JSON, with no Fields
// restriction. Intended for programmatic / LLM consumption where the caller
// wants the canonical Google Docs API tree (tables, suggestions, per-run
// styling, list nesting, named ranges, inline objects) that `info` drops.
//
// REST reference: https://developers.google.com/docs/api/reference/rest/v1/documents/get
// Go type: https://pkg.go.dev/google.golang.org/api/docs/v1#Document
type DocsRawCmd struct {
DocID string `arg:"" name:"docId" help:"Doc ID"`
Pretty bool `name:"pretty" help:"Pretty-print JSON (default: compact single-line)"`
}
func (c *DocsRawCmd) Run(ctx context.Context, flags *RootFlags) error {
id := strings.TrimSpace(c.DocID)
if id == "" {
return usage("empty docId")
}
svc, err := requireDocsService(ctx, flags)
if err != nil {
return err
}
doc, err := svc.Documents.Get(id).Context(ctx).Do()
if err != nil {
if isDocsNotFound(err) {
return fmt.Errorf("doc not found or not a Google Doc (id=%s)", id)
}
return err
}
if doc == nil {
return errors.New("doc not found")
}
return outfmt.WriteRaw(ctx, os.Stdout, doc, outfmt.RawOptions{Pretty: c.Pretty})
}
type DocsExportCmd struct {

View File

@ -0,0 +1,172 @@
package cmd
import (
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"google.golang.org/api/docs/v1"
"google.golang.org/api/option"
"github.com/steipete/gogcli/internal/ui"
)
// fullDocResponse returns a richer Document payload than DocsInfoCmd would
// ever request (DocsInfoCmd restricts via Fields). This proves `raw` drops
// that restriction and exposes the full API tree.
func fullDocResponse(id string) map[string]any {
return map[string]any{
"documentId": id,
"title": "Full Doc",
"revisionId": "rev1",
"body": map[string]any{
"content": []any{
map[string]any{
"startIndex": 1,
"endIndex": 10,
"paragraph": map[string]any{
"elements": []any{
map[string]any{
"textRun": map[string]any{
"content": "hello world\n",
},
},
},
},
},
},
},
"namedStyles": map[string]any{
"styles": []any{map[string]any{"namedStyleType": "NORMAL_TEXT"}},
},
}
}
// newDocsRawTestServer builds a test Docs server; if status != 0 it returns
// that status instead of a successful response.
func newDocsRawTestServer(t *testing.T, status int, body map[string]any) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/v1/documents/") || r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
if status != 0 {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{"code": status, "message": "mock error"},
})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(body)
}))
}
func installMockDocsService(t *testing.T, srv *httptest.Server) {
t.Helper()
orig := newDocsService
t.Cleanup(func() { newDocsService = orig })
docSvc, err := docs.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newDocsService = func(context.Context, string) (*docs.Service, error) { return docSvc, nil }
}
func rawTestContext(t *testing.T) context.Context {
t.Helper()
u, err := ui.New(ui.Options{Stdout: io.Discard, Stderr: io.Discard, Color: "never"})
if err != nil {
t.Fatalf("ui.New: %v", err)
}
return ui.WithUI(context.Background(), u)
}
func TestDocsRaw_HappyPath(t *testing.T) {
srv := newDocsRawTestServer(t, 0, fullDocResponse("doc1"))
defer srv.Close()
installMockDocsService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
cmd := &DocsRawCmd{}
if err := runKong(t, cmd, []string{"doc1"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
var got map[string]any
if err := json.Unmarshal([]byte(out), &got); err != nil {
t.Fatalf("output is not valid JSON: %v\nraw: %s", err, out)
}
// Bare struct: top-level keys must be Document fields, not a wrapper.
if got["documentId"] != "doc1" {
t.Fatalf("expected documentId=doc1, got: %v", got["documentId"])
}
// The whole point of raw: body.content must be present (info -j drops it).
body, ok := got["body"].(map[string]any)
if !ok {
t.Fatalf("expected body object in output, got: %v", got["body"])
}
if _, ok := body["content"]; !ok {
t.Fatalf("expected body.content in raw output")
}
}
func TestDocsRaw_APIError(t *testing.T) {
srv := newDocsRawTestServer(t, http.StatusInternalServerError, nil)
defer srv.Close()
installMockDocsService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
cmd := &DocsRawCmd{}
err := runKong(t, cmd, []string{"doc1"}, ctx, flags)
if err == nil {
t.Fatalf("expected error on 500, got nil")
}
})
}
func TestDocsRaw_NotFound(t *testing.T) {
srv := newDocsRawTestServer(t, http.StatusNotFound, nil)
defer srv.Close()
installMockDocsService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
cmd := &DocsRawCmd{}
err := runKong(t, cmd, []string{"doc1"}, ctx, flags)
if err == nil {
t.Fatalf("expected error on 404")
}
if !strings.Contains(err.Error(), "not found") {
t.Fatalf("expected 'not found' in error, got: %v", err)
}
})
}
func TestDocsRaw_EmptyDocID(t *testing.T) {
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
err := (&DocsRawCmd{}).Run(ctx, flags)
if err == nil {
t.Fatalf("expected error on empty docId")
}
}

View File

@ -2,6 +2,7 @@ package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
@ -12,6 +13,7 @@ import (
"strings"
"google.golang.org/api/drive/v3"
gapi "google.golang.org/api/googleapi"
"github.com/steipete/gogcli/internal/googleapi"
"github.com/steipete/gogcli/internal/outfmt"
@ -82,6 +84,94 @@ type DriveCmd struct {
URL DriveURLCmd `cmd:"" name:"url" help:"Print web URLs for files"`
Comments DriveCommentsCmd `cmd:"" name:"comments" help:"Manage comments on files"`
Drives DriveDrivesCmd `cmd:"" name:"drives" help:"List shared drives (Team Drives)"`
Raw DriveRawCmd `cmd:"" name:"raw" help:"Dump raw Google Drive API response as JSON (Files.Get; lossless; for scripting and LLM consumption)"`
}
// driveRawSensitiveFields is the set of top-level File fields redacted from
// `gog drive raw` output when the user did not name them via --fields. See
// docs/raw-audit.md for the rationale per field.
var driveRawSensitiveFields = []string{
"thumbnailLink",
"webContentLink",
"exportLinks",
"resourceKey",
"appProperties",
"properties",
}
// DriveRawCmd dumps the full Files.Get response as JSON. Uses fields=* by
// default to expose the entire File resource. When --fields is absent the
// command redacts a small set of capability/token-shaped fields (see
// driveRawSensitiveFields); when --fields is explicitly set the response is
// returned verbatim, honoring exactly what the user asked for. This means
// passing `--fields "id,name,thumbnailLink"` returns thumbnailLink as
// requested.
//
// REST reference: https://developers.google.com/drive/api/reference/rest/v3/files/get
// Go type: https://pkg.go.dev/google.golang.org/api/drive/v3#File
type DriveRawCmd struct {
FileID string `arg:"" name:"fileId" help:"File ID"`
Fields string `name:"fields" help:"Drive API field mask (default: * with sensitive fields redacted client-side). Set explicitly to disable redaction."`
Pretty bool `name:"pretty" help:"Pretty-print JSON (default: compact single-line)"`
}
func (c *DriveRawCmd) Run(ctx context.Context, flags *RootFlags) error {
fileID := strings.TrimSpace(c.FileID)
if fileID == "" {
return usage("empty fileId")
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newDriveService(ctx, account)
if err != nil {
return err
}
userSetFields := strings.TrimSpace(c.Fields) != ""
mask := "*"
if userSetFields {
mask = c.Fields
}
f, err := svc.Files.Get(fileID).
SupportsAllDrives(true).
Fields(gapi.Field(mask)).
Context(ctx).
Do()
if err != nil {
return err
}
if f == nil {
return errors.New("file not found")
}
// Round-trip through JSON so we can redact by key when needed.
raw, err := json.Marshal(f)
if err != nil {
return fmt.Errorf("marshal drive file: %w", err)
}
var m map[string]any
if err := json.Unmarshal(raw, &m); err != nil {
return fmt.Errorf("unmarshal drive file: %w", err)
}
// Redact only when the user did not explicitly request fields.
if !userSetFields {
for _, key := range driveRawSensitiveFields {
delete(m, key)
}
// contentHints.thumbnail.image is the one nested leak.
if hints, ok := m["contentHints"].(map[string]any); ok {
if thumb, ok := hints["thumbnail"].(map[string]any); ok {
delete(thumb, "image")
}
}
}
return outfmt.WriteRaw(ctx, os.Stdout, m, outfmt.RawOptions{Pretty: c.Pretty})
}
type DriveLsCmd struct {
@ -91,6 +181,7 @@ type DriveLsCmd struct {
Parent string `name:"parent" help:"Folder ID to list (default: root)"`
All bool `name:"all" aliases:"global" help:"List all accessible files (mutually exclusive with --parent)"`
AllDrives bool `name:"all-drives" help:"Include shared drives (default: true; use --no-all-drives for My Drive only)" default:"true" negatable:"_"`
Fields string `name:"fields" help:"Drive API field mask (overrides the default set; e.g. 'files(id,name,thumbnailLink),nextPageToken')"`
}
type DriveSearchCmd struct {
@ -105,6 +196,7 @@ type DriveSearchCmd struct {
type DriveGetCmd struct {
FileID string `arg:"" name:"fileId" help:"File ID"`
Fields string `name:"fields" help:"Drive API field mask (overrides the default set; e.g. 'id,name,thumbnailLink')"`
}
func (c *DriveGetCmd) Run(ctx context.Context, flags *RootFlags) error {
@ -123,9 +215,13 @@ func (c *DriveGetCmd) Run(ctx context.Context, flags *RootFlags) error {
return err
}
mask := driveFileGetFields
if strings.TrimSpace(c.Fields) != "" {
mask = c.Fields
}
f, err := svc.Files.Get(fileID).
SupportsAllDrives(true).
Fields(driveFileGetFields).
Fields(gapi.Field(mask)).
Context(ctx).
Do()
if err != nil {

View File

@ -0,0 +1,138 @@
package cmd
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"google.golang.org/api/drive/v3"
"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
)
type driveFieldsHit struct {
lastFields atomic.Value // string
}
func newDriveFieldsTestServer(t *testing.T, handler func(r *http.Request) map[string]any, hit *driveFieldsHit) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if hit != nil {
hit.lastFields.Store(r.URL.Query().Get("fields"))
}
body := handler(r)
if body == nil {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(body)
}))
}
// TestDriveGet_FieldsFlag proves --fields on `gog drive get` maps to the
// Drive API fields parameter, enabling requests for fields the hard-coded
// default did not include (e.g. thumbnailLink). Closes #486.
func TestDriveGet_FieldsFlag(t *testing.T) {
hit := &driveFieldsHit{}
srv := newDriveFieldsTestServer(t, func(r *http.Request) map[string]any {
if !strings.Contains(r.URL.Path, "/files/f1") {
return nil
}
return map[string]any{
"id": "f1",
"name": "photo.png",
"mimeType": "image/png",
"thumbnailLink": "https://drive.google.com/thumb/f1",
}
}, hit)
defer srv.Close()
installMockDriveService(t, srv)
u, err := ui.New(ui.Options{Stdout: nil, Stderr: nil, Color: "never"})
if err != nil {
t.Fatalf("ui.New: %v", err)
}
ctx := ui.WithUI(context.Background(), u)
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
if err := runKong(t, &DriveGetCmd{}, []string{"f1", "--fields", "id,name,thumbnailLink"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
got, _ := hit.lastFields.Load().(string)
if !strings.Contains(got, "thumbnailLink") {
t.Fatalf("expected Drive API fields param to contain thumbnailLink, got: %q", got)
}
// Output should wrap under "file" per existing drive get -j contract.
var envelope map[string]any
if err := json.Unmarshal([]byte(out), &envelope); err != nil {
t.Fatalf("invalid JSON: %v\nraw: %s", err, out)
}
file, ok := envelope["file"].(map[string]any)
if !ok {
t.Fatalf("expected file envelope, got: %v", envelope)
}
if file["thumbnailLink"] != "https://drive.google.com/thumb/f1" {
t.Fatalf("expected thumbnailLink passthrough, got: %v", file["thumbnailLink"])
}
}
// TestDriveLs_FieldsFlag proves --fields on `gog drive ls` maps through to
// the Drive API list fields parameter so consumers can request fields not
// in the hard-coded default set.
func TestDriveLs_FieldsFlag(t *testing.T) {
hit := &driveFieldsHit{}
srv := newDriveFieldsTestServer(t, func(r *http.Request) map[string]any {
path := strings.TrimPrefix(r.URL.Path, "/drive/v3")
if path != "/files" {
return nil
}
return map[string]any{
"files": []map[string]any{
{
"id": "f1",
"name": "photo.png",
"mimeType": "image/png",
"thumbnailLink": "https://drive.google.com/thumb/f1",
},
},
}
}, hit)
defer srv.Close()
installMockDriveService(t, srv)
u, err := ui.New(ui.Options{Stdout: nil, Stderr: nil, Color: "never"})
if err != nil {
t.Fatalf("ui.New: %v", err)
}
ctx := ui.WithUI(context.Background(), u)
ctx = outfmt.WithMode(ctx, outfmt.Mode{JSON: true})
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
if err := runKong(t, &DriveLsCmd{}, []string{"--fields", "files(id,name,thumbnailLink)"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
got, _ := hit.lastFields.Load().(string)
if !strings.Contains(got, "thumbnailLink") {
t.Fatalf("expected Drive API fields param to contain thumbnailLink, got: %q", got)
}
if !strings.Contains(out, "thumbnailLink") {
t.Fatalf("expected thumbnailLink in output, got: %q", out)
}
}
// Silence unused package warning when the test only references drive.Service
// indirectly through installMockDriveService.
var _ = drive.Service{}

View File

@ -7,6 +7,7 @@ import (
"strings"
"google.golang.org/api/drive/v3"
gapi "google.golang.org/api/googleapi"
"github.com/steipete/gogcli/internal/outfmt"
"github.com/steipete/gogcli/internal/ui"
@ -18,6 +19,7 @@ type driveFileListOptions struct {
page string
allDrives bool
driveID string
fields string // optional field mask override
}
func (c *DriveLsCmd) Run(ctx context.Context, flags *RootFlags) error {
@ -45,6 +47,7 @@ func (c *DriveLsCmd) Run(ctx context.Context, flags *RootFlags) error {
max: c.Max,
page: c.Page,
allDrives: c.AllDrives,
fields: c.Fields,
})
if err != nil {
return err
@ -99,7 +102,11 @@ func listDriveFiles(ctx context.Context, svc *drive.Service, opts driveFileListO
PageToken(opts.page).
OrderBy("modifiedTime desc")
call = driveFilesListCallWithDriveSupport(call, opts.allDrives, opts.driveID)
return call.Fields(driveFileListFields).Context(ctx).Do()
mask := driveFileListFields
if strings.TrimSpace(opts.fields) != "" {
mask = opts.fields
}
return call.Fields(gapi.Field(mask)).Context(ctx).Do()
}
func writeDriveFileList(ctx context.Context, resp *drive.FileList, emptyMessage string) error {

View File

@ -0,0 +1,181 @@
package cmd
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"google.golang.org/api/drive/v3"
"google.golang.org/api/option"
)
type driveRawHit struct {
lastFields atomic.Value // string
}
func newDriveRawTestServer(t *testing.T, status int, body map[string]any, hit *driveRawHit) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/drive/v3")
if !strings.HasPrefix(path, "/files/") || r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
if hit != nil {
hit.lastFields.Store(r.URL.Query().Get("fields"))
}
if status != 0 {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{"code": status, "message": "mock error"},
})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(body)
}))
}
func installMockDriveService(t *testing.T, srv *httptest.Server) {
t.Helper()
orig := newDriveService
t.Cleanup(func() { newDriveService = orig })
svc, err := drive.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newDriveService = func(context.Context, string) (*drive.Service, error) { return svc, nil }
}
// sensitiveDriveFile returns a File response containing every sensitive
// field the audit says should be redacted by default.
func sensitiveDriveFile(id string) map[string]any {
return map[string]any{
"id": id,
"name": "secrets.txt",
"mimeType": "text/plain",
"thumbnailLink": "https://drive.google.com/thumb/XYZ?token=LEAK",
"webContentLink": "https://drive.google.com/download?id=XYZ",
"exportLinks": map[string]any{"application/pdf": "https://drive.google.com/export?id=XYZ"},
"resourceKey": "rk-CAPABILITY-SECRET",
"appProperties": map[string]any{"api_token": "sk-live-0000"},
"properties": map[string]any{"webhook": "https://hooks.example/private"},
"webViewLink": "https://docs.google.com/open?id=XYZ",
"createdTime": "2026-01-01T00:00:00Z",
"modifiedTime": "2026-01-02T00:00:00Z",
}
}
func TestDriveRaw_DefaultRedactsSensitiveFields(t *testing.T) {
hit := &driveRawHit{}
srv := newDriveRawTestServer(t, 0, sensitiveDriveFile("f1"), hit)
defer srv.Close()
installMockDriveService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
if err := runKong(t, &DriveRawCmd{}, []string{"f1"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
// Default must request fields=* from the API.
if got, _ := hit.lastFields.Load().(string); got != "*" {
t.Fatalf("expected fields=* by default, got: %q", got)
}
var fileOut map[string]any
if err := json.Unmarshal([]byte(out), &fileOut); err != nil {
t.Fatalf("invalid JSON: %v\nraw: %s", err, out)
}
// Safe fields remain.
if fileOut["id"] != "f1" {
t.Fatalf("expected id=f1, got: %v", fileOut["id"])
}
if fileOut["name"] != "secrets.txt" {
t.Fatalf("expected name present, got: %v", fileOut["name"])
}
// Sensitive fields stripped.
for _, key := range []string{"thumbnailLink", "webContentLink", "exportLinks", "resourceKey", "appProperties", "properties"} {
if _, present := fileOut[key]; present {
t.Fatalf("expected %q to be redacted from default output, got: %v", key, fileOut[key])
}
}
}
func TestDriveRaw_ExplicitFieldsHonorsUserChoice(t *testing.T) {
hit := &driveRawHit{}
srv := newDriveRawTestServer(t, 0, sensitiveDriveFile("f1"), hit)
defer srv.Close()
installMockDriveService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
if err := runKong(t, &DriveRawCmd{}, []string{"f1", "--fields", "id,name,thumbnailLink"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
// The Google client library can munge the fields value slightly, but it
// must contain the user's requested field names.
got, _ := hit.lastFields.Load().(string)
if !strings.Contains(got, "thumbnailLink") {
t.Fatalf("expected thumbnailLink in fields query, got: %q", got)
}
var fileOut map[string]any
if err := json.Unmarshal([]byte(out), &fileOut); err != nil {
t.Fatalf("invalid JSON: %v\nraw: %s", err, out)
}
// User-named field must NOT be redacted.
if fileOut["thumbnailLink"] != "https://drive.google.com/thumb/XYZ?token=LEAK" {
t.Fatalf("expected thumbnailLink to be preserved when user named it, got: %v", fileOut["thumbnailLink"])
}
}
func TestDriveRaw_APIError(t *testing.T) {
srv := newDriveRawTestServer(t, http.StatusInternalServerError, nil, nil)
defer srv.Close()
installMockDriveService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &DriveRawCmd{}, []string{"f1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 500")
}
})
}
func TestDriveRaw_NotFound(t *testing.T) {
srv := newDriveRawTestServer(t, http.StatusNotFound, nil, nil)
defer srv.Close()
installMockDriveService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &DriveRawCmd{}, []string{"f1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 404")
}
})
}
func TestDriveRaw_EmptyID(t *testing.T) {
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
if err := (&DriveRawCmd{}).Run(ctx, flags); err == nil {
t.Fatalf("expected error on empty id")
}
}

View File

@ -24,6 +24,7 @@ type FormsCmd struct {
MoveQuestion FormsMoveQuestionCmd `cmd:"" name:"move-question" aliases:"move-q,mq" help:"Move a question to a new position"`
Responses FormsResponsesCmd `cmd:"" name:"responses" help:"Form responses"`
Watch FormsWatchCmd `cmd:"" name:"watch" aliases:"watches" help:"Response watches (push notifications)"`
Raw FormsRawCmd `cmd:"" name:"raw" help:"Dump raw Google Forms API response as JSON (Forms.Get; lossless; for scripting and LLM consumption)"`
}
type FormsResponsesCmd struct {

45
internal/cmd/forms_raw.go Normal file
View File

@ -0,0 +1,45 @@
package cmd
import (
"context"
"errors"
"os"
"strings"
"github.com/steipete/gogcli/internal/outfmt"
)
// FormsRawCmd dumps the full Forms.Get response as JSON.
//
// REST reference: https://developers.google.com/forms/api/reference/rest/v1/forms/get
// Go type: https://pkg.go.dev/google.golang.org/api/forms/v1#Form
type FormsRawCmd struct {
FormID string `arg:"" name:"formId" help:"Form ID"`
Pretty bool `name:"pretty" help:"Pretty-print JSON (default: compact single-line)"`
}
func (c *FormsRawCmd) Run(ctx context.Context, flags *RootFlags) error {
formID := strings.TrimSpace(c.FormID)
if formID == "" {
return usage("empty formId")
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newFormsService(ctx, account)
if err != nil {
return err
}
form, err := svc.Forms.Get(formID).Context(ctx).Do()
if err != nil {
return err
}
if form == nil {
return errors.New("form not found")
}
return outfmt.WriteRaw(ctx, os.Stdout, form, outfmt.RawOptions{Pretty: c.Pretty})
}

View File

@ -0,0 +1,122 @@
package cmd
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
formsapi "google.golang.org/api/forms/v1"
"google.golang.org/api/option"
)
func newFormsRawTestServer(t *testing.T, status int, body map[string]any) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.URL.Path, "/forms/") || r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
if status != 0 {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{"code": status, "message": "mock error"},
})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(body)
}))
}
func installMockFormsService(t *testing.T, srv *httptest.Server) {
t.Helper()
orig := newFormsService
t.Cleanup(func() { newFormsService = orig })
svc, err := formsapi.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newFormsService = func(context.Context, string) (*formsapi.Service, error) { return svc, nil }
}
func fullFormResponse(id string) map[string]any {
return map[string]any{
"formId": id,
"info": map[string]any{
"title": "Survey",
"description": "Tell us what you think",
},
"items": []map[string]any{
{"itemId": "q1", "title": "How are you?"},
},
}
}
func TestFormsRaw_HappyPath(t *testing.T) {
srv := newFormsRawTestServer(t, 0, fullFormResponse("form1"))
defer srv.Close()
installMockFormsService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
if err := runKong(t, &FormsRawCmd{}, []string{"form1"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
var got map[string]any
if err := json.Unmarshal([]byte(out), &got); err != nil {
t.Fatalf("invalid JSON: %v\nraw: %s", err, out)
}
if got["formId"] != "form1" {
t.Fatalf("expected formId=form1, got: %v", got["formId"])
}
if _, ok := got["items"]; !ok {
t.Fatalf("expected items in raw output")
}
}
func TestFormsRaw_APIError(t *testing.T) {
srv := newFormsRawTestServer(t, http.StatusInternalServerError, nil)
defer srv.Close()
installMockFormsService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &FormsRawCmd{}, []string{"form1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 500")
}
})
}
func TestFormsRaw_NotFound(t *testing.T) {
srv := newFormsRawTestServer(t, http.StatusNotFound, nil)
defer srv.Close()
installMockFormsService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &FormsRawCmd{}, []string{"form1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 404")
}
})
}
func TestFormsRaw_EmptyID(t *testing.T) {
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
if err := (&FormsRawCmd{}).Run(ctx, flags); err == nil {
t.Fatalf("expected error on empty id")
}
}

View File

@ -9,6 +9,7 @@ type GmailCmd struct {
Messages GmailMessagesCmd `cmd:"" name:"messages" aliases:"message,msg,msgs" group:"Read" help:"Message operations"`
Thread GmailThreadCmd `cmd:"" name:"thread" aliases:"threads,read" group:"Organize" help:"Thread operations (get, modify)"`
Get GmailGetCmd `cmd:"" name:"get" aliases:"info,show" group:"Read" help:"Get a message (full|metadata|raw)"`
Raw GmailRawCmd `cmd:"" name:"raw" group:"Read" help:"Dump raw Gmail API response as JSON (Users.Messages.Get; lossless; for scripting and LLM consumption)"`
Attachment GmailAttachmentCmd `cmd:"" name:"attachment" group:"Read" help:"Download a single attachment"`
URL GmailURLCmd `cmd:"" name:"url" group:"Read" help:"Print Gmail web URLs for threads"`
History GmailHistoryCmd `cmd:"" name:"history" group:"Read" help:"Gmail history"`

View File

@ -21,6 +21,7 @@ type GmailGetCmd struct {
const (
gmailFormatFull = "full"
gmailFormatMetadata = "metadata"
gmailFormatMinimal = "minimal"
gmailFormatRaw = "raw"
)

64
internal/cmd/gmail_raw.go Normal file
View File

@ -0,0 +1,64 @@
package cmd
import (
"context"
"errors"
"fmt"
"os"
"strings"
"github.com/steipete/gogcli/internal/outfmt"
)
// GmailRawCmd dumps the full Users.Messages.Get response as JSON. Note the
// naming collision: "raw" is both the gog-side subcommand name (meaning
// "dump the full API response") and a Gmail API `format=raw` value meaning
// "base64url-encoded RFC822 source". This command defaults to
// `format=full` (the structured parsed message). Pass `--format raw` to
// get Gmail's native RAW — the base64url blob will still appear as the
// `raw` field inside the JSON response.
//
// REST reference: https://developers.google.com/gmail/api/reference/rest/v1/users.messages/get
// Go type: https://pkg.go.dev/google.golang.org/api/gmail/v1#Message
type GmailRawCmd struct {
MessageID string `arg:"" name:"messageId" help:"Message ID"`
Format string `name:"format" help:"Gmail format: full|metadata|minimal|raw (default: full; note: 'raw' here means Gmail's base64url RFC822 blob, NOT the gog raw subcommand sense)" default:"full"`
Pretty bool `name:"pretty" help:"Pretty-print JSON (default: compact single-line)"`
}
func (c *GmailRawCmd) Run(ctx context.Context, flags *RootFlags) error {
messageID := normalizeGmailMessageID(strings.TrimSpace(c.MessageID))
if messageID == "" {
return usage("empty messageId")
}
format := strings.TrimSpace(c.Format)
if format == "" {
format = gmailFormatFull
}
switch format {
case gmailFormatFull, gmailFormatMetadata, gmailFormatMinimal, gmailFormatRaw:
default:
return fmt.Errorf("invalid --format: %q (expected full|metadata|minimal|raw)", format)
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newGmailService(ctx, account)
if err != nil {
return err
}
msg, err := svc.Users.Messages.Get("me", messageID).Format(format).Context(ctx).Do()
if err != nil {
return err
}
if msg == nil {
return errors.New("message not found")
}
return outfmt.WriteRaw(ctx, os.Stdout, msg, outfmt.RawOptions{Pretty: c.Pretty})
}

View File

@ -0,0 +1,170 @@
package cmd
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"google.golang.org/api/gmail/v1"
"google.golang.org/api/option"
)
type gmailRawHit struct {
lastFormat atomic.Value // string
}
func newGmailRawTestServer(t *testing.T, status int, body map[string]any, hit *gmailRawHit) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.URL.Path, "/users/me/messages/") || r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
if hit != nil {
hit.lastFormat.Store(r.URL.Query().Get("format"))
}
if status != 0 {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{"code": status, "message": "mock error"},
})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(body)
}))
}
func installMockGmailService(t *testing.T, srv *httptest.Server) {
t.Helper()
orig := newGmailService
t.Cleanup(func() { newGmailService = orig })
svc, err := gmail.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newGmailService = func(context.Context, string) (*gmail.Service, error) { return svc, nil }
}
func fullGmailMessageResponse(id string) map[string]any {
return map[string]any{
"id": id,
"threadId": "t1",
"labelIds": []string{"INBOX"},
"snippet": "hello world",
"payload": map[string]any{
"mimeType": "text/plain",
"headers": []map[string]any{
{"name": "From", "value": "a@b.com"},
{"name": "Subject", "value": "hi"},
},
},
}
}
func TestGmailRaw_HappyPath_DefaultFormatFull(t *testing.T) {
hit := &gmailRawHit{}
srv := newGmailRawTestServer(t, 0, fullGmailMessageResponse("m1"), hit)
defer srv.Close()
installMockGmailService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
if err := runKong(t, &GmailRawCmd{}, []string{"m1"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
if got, _ := hit.lastFormat.Load().(string); got != "full" {
t.Fatalf("expected default format=full, got: %q", got)
}
var fileOut map[string]any
if err := json.Unmarshal([]byte(out), &fileOut); err != nil {
t.Fatalf("invalid JSON: %v\nraw: %s", err, out)
}
if fileOut["id"] != "m1" {
t.Fatalf("expected id=m1, got: %v", fileOut["id"])
}
// Bare struct — no wrapper.
if _, wrapped := fileOut["message"]; wrapped {
t.Fatalf("raw output must not be wrapped, got: %v", fileOut)
}
}
func TestGmailRaw_FormatPropagation(t *testing.T) {
for _, fmt := range []string{"full", "metadata", "minimal", "raw"} {
t.Run(fmt, func(t *testing.T) {
hit := &gmailRawHit{}
srv := newGmailRawTestServer(t, 0, fullGmailMessageResponse("m1"), hit)
defer srv.Close()
installMockGmailService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &GmailRawCmd{}, []string{"m1", "--format", fmt}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
if got, _ := hit.lastFormat.Load().(string); got != fmt {
t.Fatalf("expected format=%s in request, got: %q", fmt, got)
}
})
}
}
func TestGmailRaw_InvalidFormat(t *testing.T) {
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
err := (&GmailRawCmd{MessageID: "m1", Format: "bogus"}).Run(ctx, flags)
if err == nil {
t.Fatalf("expected error on invalid format")
}
}
func TestGmailRaw_APIError(t *testing.T) {
srv := newGmailRawTestServer(t, http.StatusInternalServerError, nil, nil)
defer srv.Close()
installMockGmailService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &GmailRawCmd{}, []string{"m1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 500")
}
})
}
func TestGmailRaw_NotFound(t *testing.T) {
srv := newGmailRawTestServer(t, http.StatusNotFound, nil, nil)
defer srv.Close()
installMockGmailService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &GmailRawCmd{}, []string{"m1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 404")
}
})
}
func TestGmailRaw_EmptyID(t *testing.T) {
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
if err := (&GmailRawCmd{}).Run(ctx, flags); err == nil {
t.Fatalf("expected error on empty id")
}
}

View File

@ -13,6 +13,7 @@ type PeopleCmd struct {
Get PeopleGetCmd `cmd:"" name:"get" aliases:"info,show" help:"Get a user profile by ID"`
Search PeopleSearchCmd `cmd:"" name:"search" aliases:"find,query" help:"Search the Workspace directory"`
Relations PeopleRelationsCmd `cmd:"" name:"relations" help:"Get user relations"`
Raw PeopleRawCmd `cmd:"" name:"raw" help:"Dump raw People API response as JSON (People.Get; lossless; for scripting and LLM consumption)"`
}
type PeopleMeCmd struct{}

View File

@ -0,0 +1,76 @@
package cmd
import (
"context"
"errors"
"os"
"strings"
"github.com/steipete/gogcli/internal/outfmt"
)
// defaultPeopleRawMask is the field mask used when the user does not
// supply --person-fields. Covers the commonly useful Person fields.
const defaultPeopleRawMask = "names,emailAddresses,phoneNumbers,organizations,urls,addresses,biographies,birthdays,photos,metadata,relations,userDefined,memberships,events,imClients,interests,locales,nicknames,occupations,skills"
// PeopleRawCmd dumps the full People.Get response as JSON. Requires the
// People API field mask (set via --person-fields). Defaults to a broad
// set covering commonly useful Person resource fields.
//
// REST reference: https://developers.google.com/people/api/rest/v1/people/get
// Go type: https://pkg.go.dev/google.golang.org/api/people/v1#Person
type PeopleRawCmd struct {
UserID string `arg:"" name:"userId" help:"Person resource name (people/...) or email"`
PersonFields string `name:"person-fields" help:"People API personFields mask (default: broad set; pass a narrower list to reduce output)"`
Pretty bool `name:"pretty" help:"Pretty-print JSON (default: compact single-line)"`
}
func (c *PeopleRawCmd) Run(ctx context.Context, flags *RootFlags) error {
return runPeopleRaw(ctx, flags, c.UserID, c.PersonFields, c.Pretty)
}
// ContactsRawCmd mirrors PeopleRawCmd but lives under the `contacts` group
// for users who think of these operations in contact terms. Wraps the
// same underlying People.Get call.
//
// REST reference: https://developers.google.com/people/api/rest/v1/people/get
type ContactsRawCmd struct {
Identifier string `arg:"" name:"identifier" help:"Contact resource name (people/...) or email"`
PersonFields string `name:"person-fields" help:"People API personFields mask (default: broad set)"`
Pretty bool `name:"pretty" help:"Pretty-print JSON (default: compact single-line)"`
}
func (c *ContactsRawCmd) Run(ctx context.Context, flags *RootFlags) error {
return runPeopleRaw(ctx, flags, c.Identifier, c.PersonFields, c.Pretty)
}
func runPeopleRaw(ctx context.Context, flags *RootFlags, id, fields string, pretty bool) error {
resource := normalizePeopleResource(id)
if resource == "" {
return usage("required: resource name or email")
}
mask := defaultPeopleRawMask
if trimmed := strings.TrimSpace(fields); trimmed != "" {
mask = trimmed
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newPeopleContactsService(ctx, account)
if err != nil {
return wrapPeopleAPIError(err)
}
person, err := svc.People.Get(resource).PersonFields(mask).Context(ctx).Do()
if err != nil {
return wrapPeopleAPIError(err)
}
if person == nil {
return errors.New("person not found")
}
return outfmt.WriteRaw(ctx, os.Stdout, person, outfmt.RawOptions{Pretty: pretty})
}

View File

@ -0,0 +1,138 @@
package cmd
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"google.golang.org/api/option"
"google.golang.org/api/people/v1"
)
func newPeopleRawTestServer(t *testing.T, status int, body map[string]any) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.URL.Path, "/people/") || r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
if status != 0 {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{"code": status, "message": "mock error"},
})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(body)
}))
}
func installMockPeopleContactsService(t *testing.T, srv *httptest.Server) {
t.Helper()
orig := newPeopleContactsService
t.Cleanup(func() { newPeopleContactsService = orig })
svc, err := people.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newPeopleContactsService = func(context.Context, string) (*people.Service, error) { return svc, nil }
}
func fullPersonResponse(id string) map[string]any {
return map[string]any{
"resourceName": id,
"etag": "abc",
"names": []map[string]any{
{"displayName": "Ada Lovelace", "givenName": "Ada", "familyName": "Lovelace"},
},
"emailAddresses": []map[string]any{
{"value": "ada@example.com"},
},
}
}
func TestPeopleRaw_HappyPath(t *testing.T) {
srv := newPeopleRawTestServer(t, 0, fullPersonResponse("people/c1"))
defer srv.Close()
installMockPeopleContactsService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
if err := runKong(t, &PeopleRawCmd{}, []string{"people/c1"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
var got map[string]any
if err := json.Unmarshal([]byte(out), &got); err != nil {
t.Fatalf("invalid JSON: %v\nraw: %s", err, out)
}
if got["resourceName"] != "people/c1" {
t.Fatalf("expected resourceName=people/c1, got: %v", got["resourceName"])
}
if _, ok := got["names"]; !ok {
t.Fatalf("expected names in raw output")
}
}
func TestContactsRaw_HappyPath(t *testing.T) {
srv := newPeopleRawTestServer(t, 0, fullPersonResponse("people/c1"))
defer srv.Close()
installMockPeopleContactsService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
if err := runKong(t, &ContactsRawCmd{}, []string{"people/c1"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
var got map[string]any
if err := json.Unmarshal([]byte(out), &got); err != nil {
t.Fatalf("invalid JSON: %v\nraw: %s", err, out)
}
if got["resourceName"] != "people/c1" {
t.Fatalf("expected resourceName=people/c1, got: %v", got["resourceName"])
}
}
func TestPeopleRaw_APIError(t *testing.T) {
srv := newPeopleRawTestServer(t, http.StatusInternalServerError, nil)
defer srv.Close()
installMockPeopleContactsService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &PeopleRawCmd{}, []string{"people/c1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 500")
}
})
}
func TestPeopleRaw_EmptyID(t *testing.T) {
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
if err := (&PeopleRawCmd{}).Run(ctx, flags); err == nil {
t.Fatalf("expected error on empty id")
}
}
func TestContactsRaw_EmptyID(t *testing.T) {
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
if err := (&ContactsRawCmd{}).Run(ctx, flags); err == nil {
t.Fatalf("expected error on empty id")
}
}

View File

@ -3,6 +3,7 @@ package cmd
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"strings"
@ -43,6 +44,7 @@ type SheetsCmd struct {
Links SheetsLinksCmd `cmd:"" name:"links" aliases:"hyperlinks" help:"Get cell hyperlinks from a range"`
Named SheetsNamedRangesCmd `cmd:"" name:"named-ranges" aliases:"namedranges,nr" help:"Manage named ranges"`
Metadata SheetsMetadataCmd `cmd:"" name:"metadata" aliases:"info" help:"Get spreadsheet metadata"`
Raw SheetsRawCmd `cmd:"" name:"raw" help:"Dump raw Google Sheets API response as JSON (Spreadsheets.Get; lossless; for scripting and LLM consumption)"`
Create SheetsCreateCmd `cmd:"" name:"create" aliases:"new" help:"Create a new spreadsheet"`
Copy SheetsCopyCmd `cmd:"" name:"copy" aliases:"cp,duplicate" help:"Copy a Google Sheet"`
Export SheetsExportCmd `cmd:"" name:"export" aliases:"download,dl" help:"Export a Google Sheet (pdf|xlsx|csv) via Drive"`
@ -416,6 +418,52 @@ func (c *SheetsClearCmd) Run(ctx context.Context, flags *RootFlags) error {
return nil
}
// SheetsRawCmd dumps the full Spreadsheets.Get response as JSON, with no
// Fields restriction. `--include-grid-data` opts into returning cell-level
// data; it is off by default because grid payloads can be multi-MB and are
// the primary leakage vector (formulas may embed API keys or tokens).
//
// REST reference: https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets/get
// Go type: https://pkg.go.dev/google.golang.org/api/sheets/v4#Spreadsheet
type SheetsRawCmd struct {
SpreadsheetID string `arg:"" name:"spreadsheetId" help:"Spreadsheet ID"`
IncludeGridData bool `name:"include-grid-data" help:"Include cell-level grid data in the response (off by default; payloads can be large and may contain secrets in formulas)"`
Pretty bool `name:"pretty" help:"Pretty-print JSON (default: compact single-line)"`
}
func (c *SheetsRawCmd) Run(ctx context.Context, flags *RootFlags) error {
u := ui.FromContext(ctx)
spreadsheetID := normalizeGoogleID(strings.TrimSpace(c.SpreadsheetID))
if spreadsheetID == "" {
return usage("empty spreadsheetId")
}
_, svc, err := requireSheetsService(ctx, flags)
if err != nil {
return err
}
call := svc.Spreadsheets.Get(spreadsheetID).Context(ctx)
if c.IncludeGridData {
call = call.IncludeGridData(true)
u.Err().Println("warning: --include-grid-data may expose cell-level formulas that contain API keys or hardcoded secrets")
}
resp, err := call.Do()
if err != nil {
return err
}
if resp == nil {
return errors.New("spreadsheet not found")
}
if len(resp.DeveloperMetadata) > 0 {
u.Err().Println("warning: response contains developerMetadata which may hold third-party app secrets")
}
return outfmt.WriteRaw(ctx, os.Stdout, resp, outfmt.RawOptions{Pretty: c.Pretty})
}
type SheetsMetadataCmd struct {
SpreadsheetID string `arg:"" name:"spreadsheetId" help:"Spreadsheet ID"`
}

View File

@ -0,0 +1,189 @@
package cmd
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"net/http/httptest"
"strings"
"sync/atomic"
"testing"
"google.golang.org/api/option"
"google.golang.org/api/sheets/v4"
"github.com/steipete/gogcli/internal/ui"
)
type sheetsRawHit struct {
includeGridData atomic.Bool
}
func newSheetsRawTestServer(t *testing.T, status int, body map[string]any, hit *sheetsRawHit) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/sheets/v4")
path = strings.TrimPrefix(path, "/v4")
if !strings.HasPrefix(path, "/spreadsheets/") || r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
if hit != nil && r.URL.Query().Get("includeGridData") == "true" {
hit.includeGridData.Store(true)
}
if status != 0 {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{"code": status, "message": "mock error"},
})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(body)
}))
}
func installMockSheetsService(t *testing.T, srv *httptest.Server) {
t.Helper()
orig := newSheetsService
t.Cleanup(func() { newSheetsService = orig })
svc, err := sheets.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newSheetsService = func(context.Context, string) (*sheets.Service, error) { return svc, nil }
}
func fullSheetResponse(id string) map[string]any {
return map[string]any{
"spreadsheetId": id,
"spreadsheetUrl": "http://example.com/" + id,
"properties": map[string]any{
"title": "Full Sheet",
"locale": "en_US",
"timeZone": "UTC",
},
"sheets": []map[string]any{
{
"properties": map[string]any{
"sheetId": 1,
"title": "Sheet1",
"gridProperties": map[string]any{
"rowCount": 100,
"columnCount": 26,
},
},
},
},
}
}
func rawContextWithStderr(t *testing.T, stderr io.Writer) context.Context {
t.Helper()
u, err := ui.New(ui.Options{Stdout: io.Discard, Stderr: stderr, Color: "never"})
if err != nil {
t.Fatalf("ui.New: %v", err)
}
return ui.WithUI(context.Background(), u)
}
func TestSheetsRaw_HappyPath_NoGridDataByDefault(t *testing.T) {
hit := &sheetsRawHit{}
srv := newSheetsRawTestServer(t, 0, fullSheetResponse("s1"), hit)
defer srv.Close()
installMockSheetsService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
if err := runKong(t, &SheetsRawCmd{}, []string{"s1"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
if hit.includeGridData.Load() {
t.Fatalf("--include-grid-data should not be set by default")
}
var got map[string]any
if err := json.Unmarshal([]byte(out), &got); err != nil {
t.Fatalf("invalid JSON: %v\nraw: %s", err, out)
}
if got["spreadsheetId"] != "s1" {
t.Fatalf("expected spreadsheetId=s1, got: %v", got["spreadsheetId"])
}
if _, ok := got["sheets"]; !ok {
t.Fatalf("expected sheets in raw output")
}
}
func TestSheetsRaw_IncludeGridDataFlag(t *testing.T) {
hit := &sheetsRawHit{}
srv := newSheetsRawTestServer(t, 0, fullSheetResponse("s1"), hit)
defer srv.Close()
installMockSheetsService(t, srv)
var stderr bytes.Buffer
ctx := rawContextWithStderr(t, &stderr)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &SheetsRawCmd{}, []string{"s1", "--include-grid-data"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
if !hit.includeGridData.Load() {
t.Fatalf("expected includeGridData=true in request")
}
// Audit requires a stderr warning when grid data is included.
if !strings.Contains(stderr.String(), "grid") {
t.Fatalf("expected stderr warning mentioning 'grid', got: %q", stderr.String())
}
}
func TestSheetsRaw_APIError(t *testing.T) {
srv := newSheetsRawTestServer(t, http.StatusInternalServerError, nil, nil)
defer srv.Close()
installMockSheetsService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
err := runKong(t, &SheetsRawCmd{}, []string{"s1"}, ctx, flags)
if err == nil {
t.Fatalf("expected error on 500")
}
})
}
func TestSheetsRaw_NotFound(t *testing.T) {
srv := newSheetsRawTestServer(t, http.StatusNotFound, nil, nil)
defer srv.Close()
installMockSheetsService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
err := runKong(t, &SheetsRawCmd{}, []string{"s1"}, ctx, flags)
if err == nil {
t.Fatalf("expected error on 404")
}
})
}
func TestSheetsRaw_EmptyID(t *testing.T) {
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
if err := (&SheetsRawCmd{}).Run(ctx, flags); err == nil {
t.Fatalf("expected error on empty id")
}
}

View File

@ -35,6 +35,45 @@ type SlidesCmd struct {
ReplaceSlide SlidesReplaceSlideCmd `cmd:"" name:"replace-slide" help:"Replace the image on an existing slide in-place"`
InsertText SlidesInsertTextCmd `cmd:"" name:"insert-text" help:"Insert text into an existing page element (shape or table) by objectId"`
ReplaceText SlidesReplaceTextCmd `cmd:"" name:"replace-text" help:"Find-and-replace text across a presentation"`
Raw SlidesRawCmd `cmd:"" name:"raw" help:"Dump raw Google Slides API response as JSON (Presentations.Get; lossless; for scripting and LLM consumption)"`
}
// SlidesRawCmd dumps the full Presentations.Get response as JSON. The
// Slides API has no field mask, so output is unconditionally lossless.
// Note: response may contain short-lived authenticated image/video URLs
// (see docs/raw-audit.md for the risk assessment).
//
// REST reference: https://developers.google.com/slides/api/reference/rest/v1/presentations/get
// Go type: https://pkg.go.dev/google.golang.org/api/slides/v1#Presentation
type SlidesRawCmd struct {
PresentationID string `arg:"" name:"presentationId" help:"Presentation ID"`
Pretty bool `name:"pretty" help:"Pretty-print JSON (default: compact single-line)"`
}
func (c *SlidesRawCmd) Run(ctx context.Context, flags *RootFlags) error {
id := strings.TrimSpace(c.PresentationID)
if id == "" {
return usage("empty presentationId")
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newSlidesService(ctx, account)
if err != nil {
return err
}
pres, err := svc.Presentations.Get(id).Context(ctx).Do()
if err != nil {
return err
}
if pres == nil {
return errors.New("presentation not found")
}
return outfmt.WriteRaw(ctx, os.Stdout, pres, outfmt.RawOptions{Pretty: c.Pretty})
}
type SlidesExportCmd struct {

View File

@ -0,0 +1,133 @@
package cmd
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"google.golang.org/api/option"
"google.golang.org/api/slides/v1"
)
func newSlidesRawTestServer(t *testing.T, status int, body map[string]any) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/slides/v1")
path = strings.TrimPrefix(path, "/v1")
if !strings.HasPrefix(path, "/presentations/") || r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
if status != 0 {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{"code": status, "message": "mock error"},
})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(body)
}))
}
func installMockSlidesService(t *testing.T, srv *httptest.Server) {
t.Helper()
orig := newSlidesService
t.Cleanup(func() { newSlidesService = orig })
svc, err := slides.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newSlidesService = func(context.Context, string) (*slides.Service, error) { return svc, nil }
}
func fullPresentationResponse(id string) map[string]any {
return map[string]any{
"presentationId": id,
"title": "Full Deck",
"slides": []map[string]any{
{
"objectId": "slide1",
"pageElements": []map[string]any{
{
"objectId": "e1",
"shape": map[string]any{
"shapeType": "TEXT_BOX",
},
},
},
},
},
"masters": []map[string]any{{"objectId": "master1"}},
"layouts": []map[string]any{{"objectId": "layout1"}},
}
}
func TestSlidesRaw_HappyPath(t *testing.T) {
srv := newSlidesRawTestServer(t, 0, fullPresentationResponse("p1"))
defer srv.Close()
installMockSlidesService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
if err := runKong(t, &SlidesRawCmd{}, []string{"p1"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
var got map[string]any
if err := json.Unmarshal([]byte(out), &got); err != nil {
t.Fatalf("invalid JSON: %v\nraw: %s", err, out)
}
if got["presentationId"] != "p1" {
t.Fatalf("expected presentationId=p1, got: %v", got["presentationId"])
}
if _, ok := got["slides"]; !ok {
t.Fatalf("expected slides in raw output")
}
}
func TestSlidesRaw_APIError(t *testing.T) {
srv := newSlidesRawTestServer(t, http.StatusInternalServerError, nil)
defer srv.Close()
installMockSlidesService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &SlidesRawCmd{}, []string{"p1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 500")
}
})
}
func TestSlidesRaw_NotFound(t *testing.T) {
srv := newSlidesRawTestServer(t, http.StatusNotFound, nil)
defer srv.Close()
installMockSlidesService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &SlidesRawCmd{}, []string{"p1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 404")
}
})
}
func TestSlidesRaw_EmptyID(t *testing.T) {
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
if err := (&SlidesRawCmd{}).Run(ctx, flags); err == nil {
t.Fatalf("expected error on empty id")
}
}

View File

@ -16,4 +16,5 @@ type TasksCmd struct {
Undo TasksUndoCmd `cmd:"" name:"undo" help:"Mark task needs action" aliases:"uncomplete,undone"`
Delete TasksDeleteCmd `cmd:"" name:"delete" aliases:"rm,del,remove" help:"Delete a task"`
Clear TasksClearCmd `cmd:"" name:"clear" help:"Clear completed tasks"`
Raw TasksRawCmd `cmd:"" name:"raw" help:"Dump raw Google Tasks API response as JSON (Tasks.Get; lossless; for scripting and LLM consumption)"`
}

54
internal/cmd/tasks_raw.go Normal file
View File

@ -0,0 +1,54 @@
package cmd
import (
"context"
"errors"
"os"
"strings"
"github.com/steipete/gogcli/internal/outfmt"
)
// TasksRawCmd dumps the full Tasks.Get response as JSON.
//
// REST reference: https://developers.google.com/tasks/reference/rest/v1/tasks/get
// Go type: https://pkg.go.dev/google.golang.org/api/tasks/v1#Task
type TasksRawCmd struct {
TasklistID string `arg:"" name:"tasklistId" help:"Task list ID"`
TaskID string `arg:"" name:"taskId" help:"Task ID"`
Pretty bool `name:"pretty" help:"Pretty-print JSON (default: compact single-line)"`
}
func (c *TasksRawCmd) Run(ctx context.Context, flags *RootFlags) error {
tasklistID := strings.TrimSpace(c.TasklistID)
taskID := strings.TrimSpace(c.TaskID)
if tasklistID == "" {
return usage("empty tasklistId")
}
if taskID == "" {
return usage("empty taskId")
}
account, err := requireAccount(flags)
if err != nil {
return err
}
svc, err := newTasksService(ctx, account)
if err != nil {
return err
}
tasklistID, err = resolveTasklistID(ctx, svc, tasklistID)
if err != nil {
return err
}
task, err := svc.Tasks.Get(tasklistID, taskID).Context(ctx).Do()
if err != nil {
return err
}
if task == nil {
return errors.New("task not found")
}
return outfmt.WriteRaw(ctx, os.Stdout, task, outfmt.RawOptions{Pretty: c.Pretty})
}

View File

@ -0,0 +1,127 @@
package cmd
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"google.golang.org/api/option"
"google.golang.org/api/tasks/v1"
)
func newTasksRawTestServer(t *testing.T, status int, body map[string]any) *httptest.Server {
t.Helper()
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// resolveTasklistID may list tasklists; return an empty list so the resolver falls through to the literal ID.
if strings.HasSuffix(r.URL.Path, "/users/@me/lists") && r.Method == http.MethodGet {
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{"items": []any{}})
return
}
if !strings.Contains(r.URL.Path, "/lists/") || !strings.Contains(r.URL.Path, "/tasks/") || r.Method != http.MethodGet {
http.NotFound(w, r)
return
}
if status != 0 {
w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]any{
"error": map[string]any{"code": status, "message": "mock error"},
})
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(body)
}))
}
func installMockTasksService(t *testing.T, srv *httptest.Server) {
t.Helper()
orig := newTasksService
t.Cleanup(func() { newTasksService = orig })
svc, err := tasks.NewService(context.Background(),
option.WithoutAuthentication(),
option.WithHTTPClient(srv.Client()),
option.WithEndpoint(srv.URL+"/"),
)
if err != nil {
t.Fatalf("NewService: %v", err)
}
newTasksService = func(context.Context, string) (*tasks.Service, error) { return svc, nil }
}
func fullTaskResponse(id string) map[string]any {
return map[string]any{
"id": id,
"title": "Buy milk",
"status": "needsAction",
"notes": "2% if possible",
}
}
func TestTasksRaw_HappyPath(t *testing.T) {
srv := newTasksRawTestServer(t, 0, fullTaskResponse("t1"))
defer srv.Close()
installMockTasksService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
out := captureStdout(t, func() {
if err := runKong(t, &TasksRawCmd{}, []string{"list1", "t1"}, ctx, flags); err != nil {
t.Fatalf("run: %v", err)
}
})
var got map[string]any
if err := json.Unmarshal([]byte(out), &got); err != nil {
t.Fatalf("invalid JSON: %v\nraw: %s", err, out)
}
if got["id"] != "t1" {
t.Fatalf("expected id=t1, got: %v", got["id"])
}
if got["notes"] != "2% if possible" {
t.Fatalf("expected notes passthrough, got: %v", got["notes"])
}
}
func TestTasksRaw_APIError(t *testing.T) {
srv := newTasksRawTestServer(t, http.StatusInternalServerError, nil)
defer srv.Close()
installMockTasksService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &TasksRawCmd{}, []string{"list1", "t1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 500")
}
})
}
func TestTasksRaw_NotFound(t *testing.T) {
srv := newTasksRawTestServer(t, http.StatusNotFound, nil)
defer srv.Close()
installMockTasksService(t, srv)
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
_ = captureStdout(t, func() {
if err := runKong(t, &TasksRawCmd{}, []string{"list1", "t1"}, ctx, flags); err == nil {
t.Fatalf("expected error on 404")
}
})
}
func TestTasksRaw_EmptyIDs(t *testing.T) {
ctx := rawTestContext(t)
flags := &RootFlags{Account: "a@b.com"}
if err := (&TasksRawCmd{}).Run(ctx, flags); err == nil {
t.Fatalf("expected error on empty tasklistId")
}
if err := (&TasksRawCmd{TasklistID: "list1"}).Run(ctx, flags); err == nil {
t.Fatalf("expected error on empty taskId")
}
}

36
internal/outfmt/raw.go Normal file
View File

@ -0,0 +1,36 @@
package outfmt
import (
"context"
"encoding/json"
"fmt"
"io"
)
// RawOptions configures WriteRaw.
type RawOptions struct {
// Pretty emits indented JSON (2-space). Default is compact single-line.
Pretty bool
}
// WriteRaw marshals v as JSON and writes it to w, emitting the value bare
// (no envelope/wrapper). Intended for `gog <group> raw` subcommands that
// expose the canonical Google API response for programmatic consumption.
//
// Compact by default; pass RawOptions{Pretty: true} for indented output.
// Always appends a trailing newline for pipe friendliness.
// HTML escaping is disabled so URLs with & survive unchanged.
func WriteRaw(_ context.Context, w io.Writer, v any, opts RawOptions) error {
enc := json.NewEncoder(w)
enc.SetEscapeHTML(false)
if opts.Pretty {
enc.SetIndent("", " ")
}
if err := enc.Encode(v); err != nil {
return fmt.Errorf("encode raw json: %w", err)
}
return nil
}

109
internal/outfmt/raw_test.go Normal file
View File

@ -0,0 +1,109 @@
package outfmt
import (
"bytes"
"context"
"encoding/json"
"strings"
"testing"
)
func TestWriteRaw_CompactByDefault(t *testing.T) {
var buf bytes.Buffer
in := map[string]any{"id": "abc", "nested": map[string]any{"k": "v"}}
if err := WriteRaw(context.Background(), &buf, in, RawOptions{}); err != nil {
t.Fatalf("err: %v", err)
}
out := buf.String()
if strings.Contains(out, " ") || strings.Contains(out, "\n ") {
t.Fatalf("expected compact output, got: %q", out)
}
// Must still end with newline for pipe friendliness.
if !strings.HasSuffix(out, "\n") {
t.Fatalf("expected trailing newline, got: %q", out)
}
var round map[string]any
if err := json.Unmarshal([]byte(out), &round); err != nil {
t.Fatalf("output is not valid JSON: %v", err)
}
if round["id"] != "abc" {
t.Fatalf("unexpected id: %v", round["id"])
}
}
func TestWriteRaw_Pretty(t *testing.T) {
var buf bytes.Buffer
in := map[string]any{"id": "abc", "nested": map[string]any{"k": "v"}}
if err := WriteRaw(context.Background(), &buf, in, RawOptions{Pretty: true}); err != nil {
t.Fatalf("err: %v", err)
}
out := buf.String()
if !strings.Contains(out, "\n ") {
t.Fatalf("expected indented output, got: %q", out)
}
var round map[string]any
if err := json.Unmarshal([]byte(out), &round); err != nil {
t.Fatalf("pretty output is not valid JSON: %v", err)
}
}
func TestWriteRaw_NoHTMLEscape(t *testing.T) {
var buf bytes.Buffer
in := map[string]any{"url": "https://example.com/?a=1&b=2"}
if err := WriteRaw(context.Background(), &buf, in, RawOptions{}); err != nil {
t.Fatalf("err: %v", err)
}
if strings.Contains(buf.String(), "\\u0026") {
t.Fatalf("expected raw & in output, got: %q", buf.String())
}
}
func TestWriteRaw_BareStruct_NoWrapper(t *testing.T) {
// raw must emit the value as-is, with no envelope/wrapper added.
type apiResp struct {
DocumentId string `json:"documentId"`
Title string `json:"title"`
}
var buf bytes.Buffer
if err := WriteRaw(context.Background(), &buf, apiResp{DocumentId: "d1", Title: "t"}, RawOptions{}); err != nil {
t.Fatalf("err: %v", err)
}
var round map[string]any
if err := json.Unmarshal(buf.Bytes(), &round); err != nil {
t.Fatalf("invalid json: %v", err)
}
if _, hasWrapper := round["document"]; hasWrapper {
t.Fatalf("raw output must not be wrapped under a key, got: %v", round)
}
if round["documentId"] != "d1" {
t.Fatalf("expected documentId=d1, got: %v", round["documentId"])
}
}
func TestWriteRaw_MarshalError(t *testing.T) {
var buf bytes.Buffer
// channels cannot be marshaled to JSON.
err := WriteRaw(context.Background(), &buf, make(chan int), RawOptions{})
if err == nil {
t.Fatalf("expected error marshaling channel")
}
}