> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mirage.strukto.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# GWS

> Google Workspace API client: Discovery passthroughs plus ergonomic helpers.

Mirage's built-in Google Workspace API client follows the command surface of
the upstream [googleworkspace/cli](https://github.com/googleworkspace/cli)
project, with Discovery passthroughs plus ergonomic helpers. The Mirage command
tree is discoverable with `gws --help`.

**Licenses:** The upstream project uses
[Apache-2.0](https://github.com/googleworkspace/cli/blob/main/LICENSE); Mirage's
independent implementation uses
[Apache-2.0](https://github.com/strukto-ai/mirage/blob/main/LICENSE).

## Install

```python theme={null}
from mirage import Workspace
from mirage.commands.cli.builtin.gws import GWS
from mirage.core.google.config import GoogleConfig
from mirage.vfs.gmail import GmailVFS

config = GoogleConfig(client_id="...", client_secret="...", refresh_token="...")
ws = Workspace({"/mail": GmailVFS(config)})
ws.register_cli("gws", GWS, config.model_dump())
```

Two installs under different names are two accounts. In YAML, the same
install rides the `clis:` section; see the [CLI overview](/python/cli/index).

## Verbs

The syntax mirrors the upstream Google Workspace CLI: one passthrough leaf per
Discovery method, plus hand-written helpers under each service.

```bash theme={null}
gws <service> <resource> <method> [--params JSON] [--json JSON]   # API passthrough
gws <service> <helper> [flags]                                    # ergonomic helper
```

Services: `drive`, `sheets`, `docs`, `slides`, `gmail`. The whole
surface is discoverable from the shell:

```bash theme={null}
gws --help                # services
gws drive files --help    # methods on a Discovery resource
gws gmail send --help     # helper flags
```

### API passthrough

Passthrough commands call the corresponding API method directly:
`--params` fills URL path and query parameters, `--json` is the request
body, and the output is the compact API response JSON. List methods
follow `nextPageToken` to the end by default (pages print as NDJSON);
`--page-limit N` stops early.

```bash theme={null}
# List Drive files
gws drive files list --params '{"q": "name contains \'report\'"}'

# Read a Gmail message raw
gws gmail users messages get --params '{"userId": "me", "id": "msg123"}'

# Batch-update a spreadsheet
gws sheets spreadsheets batchUpdate \
  --params '{"spreadsheetId": "SHEET_ID"}' \
  --json '{"requests": [...]}'

# Share a Drive file
gws drive permissions create \
  --params '{"fileId": "FILE_ID"}' \
  --json '{"role": "reader", "type": "anyone"}'
```

### Folder scope

A config carrying `folder_id` scopes what the CLI **creates**, so a file
it makes lands in the same folder a `GDriveVFS` sharing that config
mounts, and the agent's own `ls` shows what it just made:

```python theme={null}
config = GoogleConfig(client_id="...", refresh_token="...", folder_id="FOLDER_ID")
ws = Workspace({"/data": GDriveVFS(config)})
ws.register_cli("gws", GWS, config.model_dump())
```

```bash theme={null}
gws sheets spreadsheets create --json '{"properties": {"title": "Q3"}}'
ls /data                                   # Q3.gsheet.json
```

Three things worth knowing:

* **Two divergences from the official CLI's passthrough.** `drive files
  create` and `copy` get `parents` defaulted into the request body, and
  the Docs/Sheets/Slides `create` methods have no `parents` field at all,
  so mirage issues a second Drive call to move the new file. Both also
  send `supportsAllDrives`, which is what lets a scope name a Shared
  Drive folder.
* **An explicit `parents` array always wins**, and then nothing is
  injected into the query either: you typed the call, you own it. The key
  being present is what counts, so `"parents": []` is honored too rather
  than read as absent.
* **Reads are not scoped.** `gws drive files list` still sees the whole
  account. The scope is about where new files go, not a fence.
* **Helpers are spelled bare, not `+`-prefixed.** Upstream marks its
  hand-written commands with a `+` (`gws docs +write`); mirage's tree does
  not need the marker, because a helper and a Discovery method cannot
  collide inside a typed spec. So an upstream line carrying `+` has to
  drop it here. The flag *names* are upstream's own and match it
  (`--document --text`, `gmail send`'s `--to --subject --body`, `sheets
  read`'s `--spreadsheet --range`); `--tab` is the single addition,
  because upstream's `+write` takes no tab argument and upstream has no
  tab support at all.

### Document tabs

A Google Doc holds one or more tabs, and the Docs API reports them only
when asked. `documents.get` fills the legacy top-level `body` from the
**first tab** and leaves `tabs` empty unless `includeTabsContent` is set;
with it, the content moves under `tabs[].documentTab` and `body` is left
empty. The two shapes are exclusive, so it is a shape switch rather than a
verbosity knob, and tabs nest through `childTabs`.

A `.gdoc.json` on a mount is always the tab-aware shape, because that is
the only one that can represent a document with more than one tab. Through
the passthrough you choose:

```bash theme={null}
# every tab, the shape a mount serves
gws docs documents get --params '{"documentId": "DOC_ID", "includeTabsContent": true}'

# the legacy flat body, which is the FIRST tab only
gws docs documents get --params '{"documentId": "DOC_ID"}'
```

Writes pick a tab the same way. `gws docs write` without `--tab` appends to
the first tab, which is the API's own default for a request that names no
tab; read the ids out of a mounted file with
`jq '[.. | .tabProperties? // empty | .tabId]'`. Note that
`replaceAllText` is one of three requests that instead default to **every**
tab, so a bare `batchUpdate` replacement is document-wide.

### Helpers

| Helper                | Description                                               |
| --------------------- | --------------------------------------------------------- |
| `gws gmail send`      | Send a new email (`--to --subject --body`)                |
| `gws gmail reply`     | Reply to the sender (`--message-id --body`)               |
| `gws gmail reply-all` | Reply to all recipients (To + CC)                         |
| `gws gmail forward`   | Forward a message (`--message-id --to`)                   |
| `gws gmail read`      | One message as processed JSON (`--id`)                    |
| `gws gmail triage`    | Summaries for a search query (`--query --max`)            |
| `gws sheets read`     | Read a cell range (`--spreadsheet --range`)               |
| `gws sheets write`    | Overwrite a range (`--values` / `--json-values`)          |
| `gws sheets append`   | Append rows after a range                                 |
| `gws docs write`      | Append text to a doc or tab (`--document --text [--tab]`) |

```bash theme={null}
gws gmail send --to "user@example.com" --subject "Hello" --body "Hi there"
gws gmail triage --query "is:unread" --max 10
gws sheets read --spreadsheet SHEET_ID --range "Sheet1!A1:C10"
gws sheets append --spreadsheet SHEET_ID --values "alice,42"
gws docs write --document DOC_ID --text "New paragraph"
gws docs write --document DOC_ID --tab TAB_ID --text "Into that tab"
```
