Skill: cyrushuang1995-cmyk/gws-google-workspace (version 2.1.9)

--- name: gws description: "Google Workspace CLI. Make sure to use this skill whenever the user mentions Gmail, email, inbox, send email, read email, check mail, draft email, send invite; Google Drive, upload file, download file, cloud storage, share file; Google Docs, create a document, write a doc; Google Calendar, schedule meeting, check schedule, view events, agenda, share calendar; Google Sheets, spreadsheet; Google Slides, make a slide, presentation; Google Forms, survey, create a form; Google Tasks, to-do, reminder; Google Meet, meeting recording; Contacts, People, address book, contact list; Classroom — or any Google Workspace / G Suite operation, even if they don't say 'gws'. If authorization fails or scope is missing, guide the user through the complete OAuth setup process." license: MIT homepage: https://github.com/googleworkspace/cli metadata: { "openclaw": { "requires": { "bins": ["gws", "jq", "base64", "python3"], "env": ["GOOGLE_WORKSPACE_PROJECT_ID", [REDACTED_SECRET]], "config": ["~/.config/gws/client_secret.json"] }, "install": [ { "id": "npm", "kind": "npm", "package": "@googleworkspace/cli", "global": true, "bins": ["gws"], "label": "Install gws CLI (npm)", }, ], }, } --- # GWS — Google Workspace CLI Google's official Workspace CLI (`@googleworkspace/cli`). Covers Gmail, Drive, Calendar, Sheets, Docs, Slides, Forms, Tasks, People, Meet, and Classroom via Google API Discovery Service. GitHub: https://github.com/googleworkspace/cli ## Setup ```bash export GOOGLE_WORKSPACE_PROJECT_ID=<your-project-id> export GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file gws auth login # first-time auth gws auth login --full # re-auth with all scopes if 403 error ``` Full setup guide: `references/setup.md` --- ## Global Patterns (ALL commands) ### `tail -n +2` before `jq` `gws` writes `Using keyring backend: file` to stdout before JSON. Always skip it: ```bash gws gmail users messages list --params '{"userId":"me"}' 2>/dev/null | tail -n +2 | jq '.' ``` ### `--params` vs `--json` - **`--params`**: URL/query parameters (`userId`, `id`, `q`, `maxResults`) - **`--json`**: Request body (`removeLabelIds`, `raw`, resource JSON) ```bash gws gmail users messages modify \ --json '{"removeLabelIds":["UNREAD"]}' \ --params '{"userId":"me","id":"MSG_ID"}' ``` ### Email body extraction Most emails have only HTML. Try plain text first, then strip HTML tags: ```bash raw=$(gws gmail users messages get --params '{"userId":"me","id":"MSG_ID","format":"full"}' 2>/dev/null | tail -n +2) body=$(echo "$raw" | jq -r '(.payload.parts[]? | select(.mimeType=="text/plain") .body.data // empty)' | head -1 | base64 -d 2>/dev/null) if [ -z "$body" ] || [ ${#body} -lt 10 ]; then body=$(echo "$raw" | jq -r '(.payload.body.data // (.payload.parts[]? | select(.mimeType=="text/html") .body.data // empty))' | head -1 | base64 -d 2>/dev/null | python3 -c "import sys,re; html=sys.stdin.buffer.read().decode('utf-8',errors='ignore'); print(re.sub(r'<[^>]+>',' ',re.sub(r'<style[^>]*>.*?</style>','',html,flags=re.S)).strip()[:500])" 2>/dev/null) fi ``` ### Per-command latency (~2-3s) Use `batchModify` (up to 1000 IDs) or shell `&` + `wait` for parallel execution. ### Discover any command ```bash gws schema <service> # list all methods gws schema <service.method> # parameter details gws <svc> <method> --dry-run # preview without executing ``` --- ## Gmail ### Quick commands ```bash gws gmail +triage # unread inbox summary gws gmail +read --params '{"userId":"me","id":"MSG_ID"}' # read message gws gmail +send --json '{"to":"[REDACTED_SECRET]","subject":"Hi","body":"Text"}' # send email gws gmail +watch # stream new emails ``` ### Common operations ```bash # List unread gws gmail users messages list --params '{"userId":"me","q":"is:unread","maxResults":20}' # Read headers (match by NAME, never by array index) gws gmail users messages get --params '{"userId":"me","id":"ID","format":"metadata","metadataHeaders":["From","Subject","Date"]}' 2>/dev/null | tail -n +2 | jq '.payload.headers | map({(.name): .value}) | add' # Batch mark as read (up to 1000 IDs) IDS=$(gws gmail users messages list --params '{"userId":"me","q":"is:unread","maxResults":100}' 2>/dev/null | tail -n +2 | jq -c '[.messages[].id]') gws gmail users messages batchModify --params '{"userId":"me"}' --json "{\"ids\":$IDS,\"removeLabelIds\":[\"UNREAD\"]}" # Send with attachment: see references/gmail.md # Labels, threads, attachments: see references/gmail.md ``` Search syntax: `is:unread`, `from:xxx`, `subject:keyword`, `label:xxx`, `has:attachment`, `newer_than:1d`, `category:primary|social|updates` 📖 More: labels, attachments, threads, parallel batch read → `references/gmail.md` --- ## Drive ### Common operations ```bash # List files gws drive files list --params '{"pageSize":10,"orderBy":"modifiedTime desc"}' gws drive files list --params '{"q":"mimeType=\"application/vnd.google-apps.spreadsheet\""}' # Upload (two-step: upload → rename, because files create ignores name) cd ~/Downloads gws drive files create --params '{}' --upload file.pdf --upload-content-type application/pdf FILE_ID=$(gws drive files create --params '{}' --upload file.pdf --upload-content-type application/pdf 2>/dev/null | tail -n +2 | jq -r '.id') gws drive files update --params "{\"fileId\":\"$FILE_ID\"}" --json '{"name":"file.pdf"}' # Create folder gws drive files create --params '{}' --json '{"name":"Folder","mimeType":"application/vnd.google-apps.folder"}' # Download / Export gws drive files get --params '{"fileId":"ID","alt":"media"}' --output file.txt gws drive files export --params '{"fileId":"ID","mimeType":"application/pdf"}' --output doc.pdf ``` ⚠️ `--upload` and `--output` only accept relative paths. `cd` to the directory first. ⚠️ `files delete` returns a `saved_file` field — ignore it. 📖 More: copy/move, permissions, storage → `references/drive.md` --- ## Calendar All time parameters use RFC 3339 format. ```bash # List events gws calendar events list --params '{"calendarId":"primary","timeMin":"TODAY_START","timeMax":"TODAY_END","maxResults":20}' # Create event gws calendar events insert --json '{"summary":"Meeting","start":{"dateTime":"START_TIME","timeZone":"TIMEZONE"},"end":{"dateTime":"END_TIME","timeZone":"TIMEZONE"}}' --params '{"calendarId":"primary"}' # Free/busy gws calendar freebusy query --json '{"timeMin":"START","timeMax":"END","items":[{"id":"primary"}]}' ``` 📖 More: update/delete events, ACL, calendar management → `references/calendar.md` --- ## Sheets ```bash # Read gws sheets spreadsheets values get --params '{"spreadsheetId":"ID","range":"Sheet1!A1:B10"}' # Write values (RAW) gws sheets spreadsheets values update --params '{"spreadsheetId":"ID","range":"Sheet1!A1","valueInputOption":"RAW"}' --json '{"values":[["Value"]]}' # Write formulas (must use USER_ENTERED) gws sheets spreadsheets values update --params '{"spreadsheetId":"ID","range":"Sheet1!C1","valueInputOption":"USER_ENTERED"}' --json '{"values":[["=SUM(A1:B10)"]]}' # Append rows gws sheets spreadsheets values append --params '{"spreadsheetId":"ID","range":"Sheet1!A1","valueInputOption":"RAW"}' --json '{"values":[["col1","col2"]]}' ``` 📖 More: row/column ops, formatting, conditional formatting → `references/sheets.md` --- ## Docs ```bash DOC_ID=$(gws docs documents create --json '{"title":"My Doc"}' --params '{}' 2>/dev/null | tail -n +2 | jq -r '.documentId') gws docs documents get --params '{"documentId":"ID"}' 2>/dev/null | tail -n +2 | jq '[.body.content[]|select(.paragraph)|.paragraph.elements[]?|select(.textRun)|.textRun.content]|join("")' gws docs documents batchUpdate --json '{"requests":[{"insertText":{"location":{"index":1},"text":"Hello\n"}}]}' --params '{"documentId":"ID"}' ``` 📖 More: bold, headings, images, bullet lists → `references/docs-slides-forms.md` --- ## Slides Coordinates use EMU units (1 inch = 914400 EMU). ```bash # Create presentation SLIDE_ID=$(gws slides presentations create --json '{"title":"My Slides"}' --params '{}' 2>/dev/null | tail -n +2 | jq -r '.presentationId') # Add text box gws slides presentations batchUpdate --json '{"requests":[{"createShape":{"objectId":"s1","shapeType":"TEXT_BOX","elementProperties":{"pageObjectId":"p1","size":{"width":{"magnitude":4000000,"unit":"EMU"},"height":{"magnitude":300000,"unit":"EMU"}},"transform":{"scaleX":1,"scaleY":1,"translateX":100000,"translateY":100000,"unit":"EMU"}}}},{"insertText":{"objectId":"s1","text":"Hello!"}}]}' --params '{"presentationId":"ID"}' # Add new slide gws slides presentations batchUpdate --json '{"requests":[{"createSlide":{"objectId":"slide2"}}]}' --params '{"presentationId":"ID"}' # Insert image gws slides presentations batchUpdate --json '{"requests":[{"createImage":{"url":"https://example.com/img.png","elementProperties":{"pageObjectId":"p1","size":{"width":{"magnitude":3000000,"unit":"EMU"},"height":{"magnitude":2000000,"unit":"EMU"}}}}}]}' --params '{"presentationId":"ID"}' ``` 📖 More → `references/docs-slides-forms.md` ## Forms ```bash # Create form FORM_ID=$(gws forms forms create --json '{"info":{"title":"Survey"}}' --params '{}' 2>/dev/null | tail -n +2 | jq -r '.formId') # Add question (text) gws forms forms batchUpdate --json '{"requests":[{"createItem":{"location":{"index":0},"item":{"title":"Your name?","questionItem":{"question":{"required":true,"textQuestion":{}}}}}}]}' --params '{"formId":"ID"}' # Add question (radio / checkbox / dropdown / date / scale) gws forms forms batchUpdate --json '{"requests":[{"createItem":{"location":{"index":1},"item":{"title":"Rate 1-5","questionItem":{"question":{"required":true,"scaleQuestion":{"low":1,"high":5}}}}}}]}' --params '{"formId":"ID"}' # View responses gws forms forms responses list --params '{"formId":"ID"}' ``` 📖 More question types → `references/docs-slides-forms.md` ## Tasks ```bash gws tasks tasklists list 2>/dev/null | tail -n +2 | jq '.items[]|{title,id}' gws tasks tasks list --params '{"tasklist":"@default"}' gws tasks tasks insert --json '{"title":"Task","notes":"Desc"}' --params '{"tasklist":"@default"}' gws tasks tasks patch --params '{"tasklist":"@default","task":"TASK_ID"}' --json '{"status":"completed"}' gws tasks tasks delete --params '{"tasklist":"@default","task":"TASK_ID"}' ``` 📖 More → `references/tasks-people-other.md` ## People / Contacts `searchContacts` requires `readMask` — omitting causes 400 error. ```bash gws people people get --params '{"resourceName":"people/me","personFields":"names,emailAddresses"}' gws people people searchContacts --params '{"query":"john","pageSize":10,"readMask":"names,emailAddresses,phoneNumbers"}' gws people connections list --params '{"resourceName":"people/me","personFields":"names,emailAddresses","pageSize":10}' gws people people createContact --json '{"names":[{"givenName":"First","familyName":"Last"}],"emailAddresses":[{"value":"[REDACTED_SECRET]"}]}' --params '{"readMask":"names,emailAddresses"}' gws people contactGroups list --params '{"pageSize":10}' ``` 📖 More → `references/tasks-people-other.md` ## Meet / Classroom ```bash gws meet conferenceRecords list --params '{"pageSize":10}' gws classroom courses list --params '{"pageSize":10}' gws classroom courses students list --params '{"courseId":"COURSE_ID"}' gws classroom courses courseWork list --params '{"courseId":"COURSE_ID"}' ``` 📖 More → `references/tasks-people-other.md` --- ## Workflow Helpers ```bash gws workflow +standup-report # today's meetings + tasks gws workflow +meeting-prep # next meeting prep gws workflow +weekly-digest # weekly meetings + unread count gws workflow +email-to-task --message-id "ID" # Gmail → Tasks ``` --- ## Can't find the right command? 1. **Discover all methods for a service:** `gws schema <service>` (e.g. `gws schema drive`, `gws schema gmail`) 2. **Get parameter details:** `gws schema <service.method>` (e.g. `gws schema drive.files.copy`) 3. **Dry-run to preview:** `gws <svc> <method> --dry-run` 4. **Check references/** for detailed examples per service 5. **Official docs:** https://github.com/googleworkspace/cli --- ## Troubleshooting | Issue | Solution | |-------|----------| | 403 Insufficient scopes | `gws auth login --full` | | No OAuth client | Check `~/.config/gws/client_secret.json` | | API not enabled | Enable in GCP Console → APIs & Services | | jq parse error | Add `tail -n +2` to skip keyring prefix | | Upload shows "Untitled" | Rename with `files update` after upload | | File not found on upload | Use relative paths, `cd` first |

path: references/calendar.md
content: # Calendar Reference

All time parameters use RFC 3339 format.

## Events

```bash
gws calendar events list --params '{"calendarId":"primary","timeMin":"START","timeMax":"END","maxResults":20}'
gws calendar events insert --json '{"summary":"Meeting","start":{"dateTime":"START","timeZone":"Asia/Shanghai"},"end":{"dateTime":"END","timeZone":"Asia/Shanghai"}}' --params '{"calendarId":"primary"}'
gws calendar events update --params '{"calendarId":"primary","eventId":"EVT_ID"}' --json '{"summary":"Updated"}'
gws calendar events delete --params '{"calendarId":"primary","eventId":"EVT_ID"}'
gws calendar events instances --params '{"calendarId":"primary","eventId":"RECURRING_ID"}'
```

## Free/Busy and ACL

```bash
gws calendar freebusy query --json '{"timeMin":"START","timeMax":"END","items":[{"id":"primary"}]}'
gws calendar acl list --params '{"calendarId":"primary"}'
```

## Calendar Management

```bash
gws calendar calendarList list --params '{"maxResults":10}'
gws calendar calendars insert --json '{"summary":"My Calendar"}' --params '{}'
gws calendar calendars delete --params '{"calendarId":"CAL_ID"}'
```


path: references/docs-slides-forms.md
content: # Docs, Slides, and Forms Reference

## Docs

```bash
# Create
DOC_ID=$(gws docs documents create --json '{"title":"My Doc"}' --params '{}' 2>/dev/null | tail -n +2 | jq -r '.documentId')

# Extract plain text
gws docs documents get --params '{"documentId":"ID"}' 2>/dev/null | tail -n +2 | jq '[.body.content[]|select(.paragraph)|.paragraph.elements[]?|select(.textRun)|.textRun.content]|join("")'

# Insert text (index 1 = start)
gws docs documents batchUpdate --json '{"requests":[{"insertText":{"location":{"index":1},"text":"Hello\n"}}]}' --params '{"documentId":"ID"}'

# Bold
gws docs documents batchUpdate --json '{"requests":[{"updateTextStyle":{"range":{"startIndex":1,"endIndex":6},"textStyle":{"bold":true},"fields":"bold"}}]}' --params '{"documentId":"ID"}'

# Heading
gws docs documents batchUpdate --json '{"requests":[{"updateParagraphStyle":{"range":{"startIndex":1,"endIndex":10},"style":{"namedStyleType":"HEADING_1"},"fields":"namedStyleType"}}]}' --params '{"documentId":"ID"}'

# Insert image
gws docs documents batchUpdate --json '{"requests":[{"insertInlineImage":{"uri":"https://example.com/img.png","location":{"index":1}}}]}' --params '{"documentId":"ID"}'

# Bullet list
gws docs documents batchUpdate --json '{"requests":[{"createParagraphBullets":{"range":{"startIndex":1,"endIndex":20},"bulletPreset":"BULLET_DISC_CIRCLE_SQUARE"}}]}' --params '{"documentId":"ID"}'
```

## Slides

Coordinates use EMU units (1 inch = 914400 EMU).

```bash
SLIDE_ID=$(gws slides presentations create --json '{"title":"My Slides"}' --params '{}' 2>/dev/null | tail -n +2 | jq -r '.presentationId')

# Create text box with text
gws slides presentations batchUpdate --json '{"requests":[{"createShape":{"objectId":"s1","shapeType":"TEXT_BOX","elementProperties":{"pageObjectId":"p1","size":{"width":{"magnitude":4000000,"unit":"EMU"},"height":{"magnitude":300000,"unit":"EMU"}},"transform":{"scaleX":1,"scaleY":1,"translateX":100000,"translateY":100000,"unit":"EMU"}}}},{"insertText":{"objectId":"s1","text":"Hello!"}}]}' --params '{"presentationId":"ID"}'

# New slide
gws slides presentations batchUpdate --json '{"requests":[{"createSlide":{"objectId":"slide2"}}]}' --params '{"presentationId":"ID"}'

# Insert image
gws slides presentations batchUpdate --json '{"requests":[{"createImage":{"url":"https://example.com/img.png","elementProperties":{"pageObjectId":"p1","size":{"width":{"magnitude":3000000,"unit":"EMU"},"height":{"magnitude":2000000,"unit":"EMU"}},"transform":{"scaleX":1,"scaleY":1,"translateX":500000,"translateY":500000,"unit":"EMU"}}}}]}' --params '{"presentationId":"ID"}'
```

## Forms

`forms create` only accepts `title`. Add questions via `batchUpdate`.

```bash
FORM_ID=$(gws forms forms create --json '{"info":{"title":"Survey"}}' --params '{}' 2>/dev/null | tail -n +2 | jq -r '.formId')
```

### Question types

```bash
# Text (short/paragraph)
gws forms forms batchUpdate --json '{"requests":[{"createItem":{"location":{"index":0},"item":{"title":"Your name?","questionItem":{"question":{"required":true,"textQuestion":{}}}}}}]}' --params '{"formId":"ID"}'

# Radio (single choice)
gws forms forms batchUpdate --json '{"requests":[{"createItem":{"location":{"index":1},"item":{"title":"Pick one","questionItem":{"question":{"required":true,"choiceQuestion":{"type":"RADIO","options":[{"value":"A"},{"value":"B"},{"value":"C"}]}}}}}}]}' --params '{"formId":"ID"}'

# Checkbox (multi-select)
gws forms forms batchUpdate --json '{"requests":[{"createItem":{"location":{"index":2},"item":{"title":"Select all","questionItem":{"question":{"required":true,"choiceQuestion":{"type":"CHECKBOX","options":[{"value":"X"},{"value":"Y"}]}}}}}}]}' --params '{"formId":"ID"}'

# Dropdown
gws forms forms batchUpdate --json '{"requests":[{"createItem":{"location":{"index":3},"item":{"title":"Choose","questionItem":{"question":{"required":true,"choiceQuestion":{"type":"DROP_DOWN","options":[{"value":"1"},{"value":"2"}]}}}}}}]}' --params '{"formId":"ID"}'

# Date / Time
gws forms forms batchUpdate --json '{"requests":[{"createItem":{"location":{"index":4},"item":{"title":"Date?","questionItem":{"question":{"required":false,"dateQuestion":{"type":"DATE"}}}}}}]}' --params '{"formId":"ID"}'

# Scale (1-5 / 1-10)
gws forms forms batchUpdate --json '{"requests":[{"createItem":{"location":{"index":5},"item":{"title":"Rate","questionItem":{"question":{"required":true,"scaleQuestion":{"low":1,"high":5}}}}}}]}' --params '{"formId":"ID"}'
```

### Read responses

```bash
gws forms forms responses list --params '{"formId":"ID"}'
```


path: references/drive.md
content: # Drive Reference

## Browse and Search

```bash
gws drive files list --params '{"pageSize":10,"orderBy":"modifiedTime desc"}'
gws drive files list --params '{"q":"mimeType=\"application/vnd.google-apps.spreadsheet\""}'
gws drive files list --params '{"q":"'\''FOLDER_ID'\'' in parents"}'
```

## Upload (two-step: upload then rename)

`files create` does not pass `name` to the API — files upload as "Untitled". Upload first, then rename:

```bash
cd ~/Downloads
gws drive files create --params '{}' --upload file.txt --upload-content-type text/plain
gws drive files update --params '{"fileId":"FILE_ID"}' --json '{"name":"real_name.txt"}'
```

Note: `files copy` does respect `name`.

## Create Folder

```bash
gws drive files create --params '{}' --json '{"name":"Folder Name","mimeType":"application/vnd.google-apps.folder"}'
```

## Copy and Move

```bash
gws drive files copy --params '{"fileId":"ID"}' --json '{"name":"Copy of file"}'
gws drive files update --params '{"fileId":"ID","addParents":"FOLDER_ID","removeParents":"root"}' --json '{}'
```

## Download and Export

Native files: `get --alt media`. Google formats (Sheets/Docs/Slides): `export`.

```bash
gws drive files get --params '{"fileId":"ID","alt":"media"}' --output file.txt
gws drive files export --params '{"fileId":"ID","mimeType":"application/pdf"}' --output doc.pdf
gws drive files export --params '{"fileId":"ID","mimeType":"text/csv"}' --output data.csv
```

## Permissions and Storage

```bash
gws drive permissions list --params '{"fileId":"ID","pageSize":10}'
gws drive about get --params '{"fields":"storageQuota"}' 2>/dev/null | tail -n +2 | jq '.storageQuota'
```

## Delete

`files delete` returns `{"status":"success","saved_file":"download.html"}` — ignore the `saved_file` field.

```bash
gws drive files delete --params '{"fileId":"ID"}'
```


path: references/gmail.md
content: # Gmail Reference

## Helper Commands

```bash
gws gmail +triage                                                          # Unread inbox summary
gws gmail +read --params '{"userId":"me","id":"MSG_ID"}'                   # Read message body
gws gmail +reply --params '{"userId":"me","id":"MSG_ID"}' --json '{"body":"Reply"}'
gws gmail +reply-all --params '{"userId":"me","id":"MSG_ID"}' --json '{"body":"Reply"}'
gws gmail +forward --params '{"userId":"me","id":"MSG_ID"}' --json '{"to":"[REDACTED_SECRET]"}'
gws gmail +send --json '{"to":"[REDACTED_SECRET]","subject":"Hi","body":"Text"}'
gws gmail +watch                                                           # Stream new emails (NDJSON)
```

## Search Syntax

Pass via `q` parameter: `is:unread`, `is:starred`, `from:xxx`, `subject:keyword`, `label:xxx`, `has:attachment`, `newer_than:1d`, `after:YYYY/MM/DD`, `before:YYYY/MM/DD`, `category:primary|promotions|social|updates|forums`.

## List and Read

```bash
gws gmail users messages list --params '{"userId":"me","q":"is:unread","maxResults":20}'
gws gmail users messages list --params '{"userId":"me","q":"is:unread"}' --page-all

# Headers (match by name, not index)
gws gmail users messages get --params '{"userId":"me","id":"MSG_ID","format":"metadata","metadataHeaders":["From","Subject","Date"]}' \
  2>/dev/null | tail -n +2 | jq '.payload.headers | map({(.name): .value}) | add'

# Quick preview (no base64, length-limited)
gws gmail users messages get --params '{"userId":"me","id":"MSG_ID","format":"metadata"}' \
  2>/dev/null | tail -n +2 | jq -r '.snippet'
```

## Parallel Batch Read

Use `&` + `wait` to read N messages in ~3s instead of N×3s:

```bash
IDS=$(gws gmail users messages list --params '{"userId":"me","q":"is:unread","maxResults":10}' 2>/dev/null | tail -n +2 | jq -r '.messages[].id')
for id in $IDS; do
  gws gmail users messages get --params "{\"userId\":\"me\",\"id\":\"$id\",\"format\":\"metadata\",\"metadataHeaders\":[\"From\",\"Subject\",\"Date\"]}" \
    2>/dev/null | tail -n +2 | jq -r '.payload.headers | map({(.name): .value}) | add | "\(.From[:30]) | \(.Subject) — \(.Date)"' &
done; wait
```

## Batch Modify (up to 1000 IDs)

```bash
IDS=$(gws gmail users messages list --params '{"userId":"me","q":"is:unread","maxResults":100}' 2>/dev/null | tail -n +2 | jq -c '[.messages[].id]')
gws gmail users messages batchModify --params '{"userId":"me"}' --json "{\"ids\":$IDS,\"removeLabelIds\":[\"UNREAD\"]}"
```

## Single Message Modify

```bash
gws gmail users messages modify --json '{"removeLabelIds":["UNREAD"]}' --params '{"userId":"me","id":"MSG_ID"}'
gws gmail users messages modify --json '{"addLabelIds":["STARRED"],"removeLabelIds":["INBOX"]}' --params '{"userId":"me","id":"MSG_ID"}'
```

## Send Email

```bash
# Plain text
RAW=$(printf "To: [REDACTED_SECRET]\r\nSubject: Hello\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nBody text" | base64 -w0)
gws gmail users messages send --json "{\"raw\":\"$RAW\"}" --params '{"userId":"me"}'

# HTML
RAW=$(printf "To: [REDACTED_SECRET]\r\nSubject: Hello\r\nContent-Type: text/html; charset=utf-8\r\n\r\n<h1>Title</h1><p>Body</p>" | base64 -w0)
gws gmail users messages send --json "{\"raw\":\"$RAW\"}" --params '{"userId":"me"}'
```

## Attachments

```bash
# List attachments
gws gmail users messages get --params '{"userId":"me","id":"MSG_ID","format":"full"}' \
  2>/dev/null | tail -n +2 | jq '[.payload.parts[] | select(.filename != "") | {filename, attachmentId: .body.attachmentId}]'

# Download (cd to target directory first)
cd /path/to/dir
gws gmail users messages attachments get --params '{"userId":"me","messageId":"MSG_ID","id":"ATT_ID"}' --output file.pdf
```

## Threads

```bash
gws gmail users threads list --params '{"userId":"me","q":"is:unread","maxResults":10}'
gws gmail users threads get --params '{"userId":"me","id":"THREAD_ID","format":"metadata"}'
```

## Labels

```bash
gws gmail users labels list --params '{"userId":"me"}' 2>/dev/null | tail -n +2 | jq '.labels[] | {id, name, type}'
gws gmail users labels get --params '{"userId":"me","id":"INBOX"}'
gws gmail users labels create --json '{"name":"Projects/AI","labelListVisibility":"labelShow","messageListVisibility":"show"}' --params '{"userId":"me"}'
```

## Trash / Settings

```bash
gws gmail users messages trash --params '{"userId":"me","id":"MSG_ID"}'
gws gmail users messages untrash --params '{"userId":"me","id":"MSG_ID"}'
gws gmail users settings getVacation --params '{"userId":"me"}'
gws gmail users settings sendAs list --params '{"userId":"me"}'
```


path: references/setup.md
content: # GWS 安装与授权指南

完整的安装、配置和故障排查指南。

---

## 1. 安装 gws CLI

```bash
npm install -g @googleworkspace/cli
```

---

## 2. GCP 项目配置

### 2.1 启用 API

在 [Google Cloud Console](https://console.cloud.google.com) 启用以下 API：

| 服务 | 控制台 URL |
|------|-----------|
| Gmail | `apis/library/gmail.googleapis.com` |
| Drive | `apis/library/drive.googleapis.com` |
| Calendar | `apis/library/calendar.googleapis.com` |
| Sheets | `apis/library/sheets.googleapis.com` |
| Docs | `apis/library/docs.googleapis.com` |
| Slides | `apis/library/slides.googleapis.com` |
| Tasks | `apis/library/tasks.googleapis.com` |
| People | `apis/library/people.googleapis.com` |
| Forms | `apis/library/forms.googleapis.com` |
| Meet | `apis/library/meet.googleapis.com` |
| Classroom | `apis/library/classroom.googleapis.com` |

### 2.2 OAuth 同意屏幕配置

1. 访问 `apis/credentials/consent`
2. 配置应用信息（名称、支持邮箱、开发者联系邮箱）
3. **发布状态**：
   - **Testing 模式**（默认）：refresh_token 7天后过期，需重新授权。适合测试和短期使用。
   - **In production** 模式：refresh_token 长期有效。
   - ⚠️ 切换到 Production 前请确保你理解其安全影响：长期 refresh_token 意味着只要你的 GCP 项目不被撤销，该 token 就会持续有效。建议仅在你完全控制该 GCP 项目时才切换。
   - 如果切换：点击 **"PUBLISH APP"**。注意：如果显示 "Your app requires verification"，这表示 Google 建议你完成应用验证流程。对于个人/内部使用，你可以评估风险后自行决定是否继续。
4. **Scopes 标签页**：添加以下 scope（建议仅添加你实际需要的 scope，最小权限原则）：
   - `https://www.googleapis.com/auth/gmail.modify`
   - `https://www.googleapis.com/auth/drive`
   - `https://www.googleapis.com/auth/calendar`
   - `https://www.googleapis.com/auth/spreadsheets`
   - `https://www.googleapis.com/auth/documents`
   - `https://www.googleapis.com/auth/presentations`
   - `https://www.googleapis.com/auth/tasks`
   - `https://www.googleapis.com/auth/forms.body`
   - `https://www.googleapis.com/auth/meetings.space.created`
   - `https://www.googleapis.com/auth/classroom.courses`
   - `https://www.googleapis.com/auth/userinfo.profile`
   - `https://www.googleapis.com/auth/userinfo.email`
4. **测试用户**（如处于测试模式）：添加你的 Google 账号

> **安全提示**：授权时若看到 "Google hasn't verified this app" 警告，这是正常的——因为你创建的是个人 GCP 项目，未经 Google 官方验证。点击 Advanced → "Go to [app name] (unsafe)" 继续。仅在你信任自己的 GCP 项目时才这样做。

### 2.3 OAuth 凭证

1. 访问 `apis/credentials` → "+ CREATE CREDENTIALS" → "OAuth client ID"
2. 应用类型：**Desktop app**
3. 下载 JSON → 保存为 `~/.config/gws/client_secret.json`

---

## 3. 授权方法

### 方法 A：直接授权（推荐）

```bash
export GOOGLE_WORKSPACE_PROJECT_ID=your-project-id
export GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file

# 完整授权（所有服务）
gws auth login --full

# 或仅授权特定服务
gws auth login -s gmail,drive,calendar
```

授权完成后，凭证保存在 `~/.config/gws/credentials.enc`

### 方法 B：手动授权（无浏览器环境）

```bash
gws auth login --full
# 复制显示的 URL，在浏览器中打开并授权
# 复制回调 URL（http://localhost:xxxx/?code=...）
# 在终端粘贴 code 完成授权
```

---

## 4. 验证授权

```bash
gws auth status
```

应显示：
- `scope_count`: 12+
- `token_valid`: true

---

## 5. 故障排查

### "Insufficient authentication scopes" (403)

```bash
rm ~/.config/gws/token_cache.json
gws auth login --full
```

### "No OAuth client configured"

检查 `~/.config/gws/client_secret.json` 是否存在且命名正确

### "API has not been used in project"

在 GCP Console 中启用对应 API

### 完全重新授权

```bash
rm ~/.config/gws/credentials.enc
rm ~/.config/gws/token_cache.json
gws auth login --full
```

### Token 7天后过期 / 需要频繁重新授权

**原因**：OAuth 应用处于 **Testing** 模式  
**解决**：
1. 去 [GCP Console → OAuth consent screen](https://console.cloud.google.com/auth/audience) 查看 Publishing status
2. 如果你想切换到 **In production**（长期 token），请先评估安全风险：
   - 长期 token 在 GCP 项目被删除/撤销前持续有效
   - 建议使用专用的 GCP 项目，并定期审查已授权的应用
   - 考虑使用 Service Account 或 Domain-wide Delegation 作为更安全的替代方案
3. 确认后点击 **"PUBLISH APP"**
4. 删除旧 token：`rm ~/.config/gws/token_cache.json`
5. 重新授权：`gws auth login --full`

**注意**：切换到 Production 后**必须重新授权**一次，之前的 token 仍然是 7天过期的。

### 检查当前 App 状态

访问：GCP Console → APIs & Services → [OAuth consent screen](https://console.cloud.google.com/auth/audience)

- ✅ **长期有效**：Publishing status = "In production"
- ⚠️ **7天过期**：Publishing status = "Testing"

---

## 6. 环境变量

添加到 `~/.zshrc` 或 `~/.bashrc`：

```bash
export GOOGLE_WORKSPACE_PROJECT_ID=your-gcp-project-id
export GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file
```


path: references/sheets.md
content: # Sheets Reference

Sheet names are not always "Sheet1" — retrieve actual names first:

```bash
SHEET=$(gws sheets spreadsheets get --params '{"spreadsheetId":"ID"}' 2>/dev/null | tail -n +2 | jq -r '.sheets[0].properties.title')
```

## Create, Read, Write

```bash
SHEET_ID=$(gws sheets spreadsheets create --json '{"properties":{"title":"My Sheet"}}' --params '{}' 2>/dev/null | tail -n +2 | jq -r '.spreadsheetId')

# Read
gws sheets spreadsheets values get --params '{"spreadsheetId":"ID","range":"Sheet1!A1:B2"}'

# Write raw values
gws sheets spreadsheets values update --params '{"spreadsheetId":"ID","range":"Sheet1!A1","valueInputOption":"RAW"}' --json '{"values":[["Value"]]}'

# Write formulas (must use USER_ENTERED)
gws sheets spreadsheets values update --params '{"spreadsheetId":"ID","range":"Sheet1!C1","valueInputOption":"USER_ENTERED"}' --json '{"values":[["=AVERAGE(B2:B10)"]]}'

# Append rows
gws sheets spreadsheets values append --params '{"spreadsheetId":"ID","range":"Sheet1!A1","valueInputOption":"RAW"}' --json '{"values":[["col1","col2"]]}'

# Clear (preserves formatting)
gws sheets spreadsheets values clear --params '{"spreadsheetId":"ID","range":"Sheet1!A1:C10"}'
```

## Row/Column Operations

```bash
# Delete row
gws sheets spreadsheets batchUpdate --params '{"spreadsheetId":"ID"}' --json '{"requests":[{"deleteDimension":{"range":{"sheetId":0,"dimension":"ROWS","startIndex":2,"endIndex":3}}}]}'

# Insert column
gws sheets spreadsheets batchUpdate --params '{"spreadsheetId":"ID"}' --json '{"requests":[{"insertDimension":{"range":{"sheetId":0,"dimension":"COLUMNS","startIndex":1,"endIndex":2},"inheritFromBefore":true}}]}'

# Merge cells
gws sheets spreadsheets batchUpdate --params '{"spreadsheetId":"ID"}' --json '{"requests":[{"mergeCells":{"range":{"sheetId":0,"startRowIndex":0,"endRowIndex":1,"startColumnIndex":0,"endColumnIndex":3},"mergeType":"MERGE_ALL"}}]}'

# Set column width
gws sheets spreadsheets batchUpdate --params '{"spreadsheetId":"ID"}' --json '{"requests":[{"updateDimensionProperties":{"range":{"sheetId":0,"dimension":"COLUMNS","startIndex":0,"endIndex":1},"properties":{"pixelSize":200},"fields":"pixelSize"}}]}'
```

## Formatting

```bash
# Cell background color
gws sheets spreadsheets batchUpdate --params '{"spreadsheetId":"ID"}' --json '{"requests":[{"repeatCell":{"range":{"sheetId":0,"startRowIndex":0,"endRowIndex":1},"cell":{"userEnteredFormat":{"backgroundColor":{"red":0.2,"green":0.6,"blue":1}}},"fields":"userEnteredFormat.backgroundColor"}}]}'

# Conditional formatting
gws sheets spreadsheets batchUpdate --params '{"spreadsheetId":"ID"}' --json '{"requests":[{"addConditionalFormatRule":{"rule":{"ranges":[{"sheetId":0,"startRowIndex":1,"endRowIndex":100}],"booleanRule":{"condition":{"type":"NUMBER_GREATER","values":[{"userEnteredValue":"90"}]},"format":{"backgroundColor":{"red":0,"green":1,"blue":0}}}},"index":0}}]}'

# Freeze rows/columns
gws sheets spreadsheets batchUpdate --params '{"spreadsheetId":"ID"}' --json '{"requests":[{"updateSheetProperties":{"properties":{"sheetId":0,"gridProperties":{"frozenRowCount":1,"frozenColumnCount":1}},"fields":"gridProperties.frozenRowCount,gridProperties.frozenColumnCount"}}]}'
```


path: references/tasks-people-other.md
content: # Tasks, People, Meet, Classroom Reference

## Tasks

```bash
gws tasks tasklists list 2>/dev/null | tail -n +2 | jq '.items[]|{title,id}'
gws tasks tasks list --params '{"tasklist":"@default"}'
gws tasks tasks insert --json '{"title":"Task","notes":"Desc"}' --params '{"tasklist":"@default"}'
gws tasks tasks patch --params '{"tasklist":"@default","task":"TASK_ID"}' --json '{"status":"completed"}'
gws tasks tasks delete --params '{"tasklist":"@default","task":"TASK_ID"}'
gws tasks tasks clear --params '{"tasklist":"@default"}'
```

## People

`searchContacts` requires `readMask` — omitting it causes a 400 error.

```bash
gws people people get --params '{"resourceName":"people/me","personFields":"names,emailAddresses"}'
gws people people searchContacts --params '{"query":"john","pageSize":10,"readMask":"names,emailAddresses,phoneNumbers"}'
gws people connections list --params '{"resourceName":"people/me","personFields":"names,emailAddresses","pageSize":10}'
gws people people createContact --json '{"names":[{"givenName":"First","familyName":"Last"}],"emailAddresses":[{"value":"[REDACTED_SECRET]"}]}' --params '{"readMask":"names,emailAddresses"}'
gws people contactGroups list --params '{"pageSize":10}'
```

## Meet / Classroom

```bash
gws meet conferenceRecords list --params '{"pageSize":10}'
gws classroom courses list --params '{"pageSize":10}'
gws classroom courses students list --params '{"courseId":"COURSE_ID"}'
gws classroom courses courseWork list --params '{"courseId":"COURSE_ID"}'
```


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)