Full Code of googleworkspace/cli for AI

main e9970db26fb3 cached
196 files
1.4 MB
345.7k tokens
1329 symbols
1 requests
Download .txt
Showing preview only (1,466K chars total). Download the full file or copy to clipboard to get everything.
Repository: googleworkspace/cli
Branch: main
Commit: e9970db26fb3
Files: 196
Total size: 1.4 MB

Directory structure:
gitextract_nx_35fca/

├── .agent/
│   ├── skills/
│   │   └── vhs.md
│   └── workflows/
│       └── verify-skills.md
├── .changeset/
│   ├── README.md
│   └── config.json
├── .claude/
│   └── settings.json
├── .gemini/
│   ├── config.yaml
│   └── style_guide.md
├── .github/
│   ├── CODEOWNERS
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── labeler.yml
│   └── workflows/
│       ├── automation.yml
│       ├── ci.yml
│       ├── cla.yml
│       ├── coverage.yml
│       ├── generate-skills.yml
│       ├── policy.yml
│       ├── publish-skills.yml
│       ├── release-changesets.yml
│       ├── release.yml
│       └── stale.yml
├── .gitignore
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── CONTEXT.md
├── Cargo.toml
├── LICENSE
├── README.md
├── SECURITY.md
├── art/
│   ├── features.txt
│   ├── intro.txt
│   ├── outro.txt
│   ├── qr.txt
│   ├── scene1.txt
│   ├── scene2.txt
│   ├── scene2b.txt
│   ├── scene3.txt
│   ├── scene3b.txt
│   ├── scene4.txt
│   ├── scene5.txt
│   ├── scene6.txt
│   ├── scene7.txt
│   ├── scene8.txt
│   └── scene9.txt
├── dist-workspace.toml
├── docs/
│   ├── CODE_OF_CONDUCT.md
│   ├── CONTRIBUTING.md
│   ├── demo.tape
│   └── skills.md
├── flake.nix
├── gemini-extension.json
├── lefthook.yml
├── package.json
├── registry/
│   ├── personas.yaml
│   └── recipes.yaml
├── scripts/
│   ├── coverage.sh
│   ├── show-art.sh
│   ├── tag-release.sh
│   └── version-sync.sh
├── skills/
│   ├── gws-admin-reports/
│   │   └── SKILL.md
│   ├── gws-calendar/
│   │   └── SKILL.md
│   ├── gws-calendar-agenda/
│   │   └── SKILL.md
│   ├── gws-calendar-insert/
│   │   └── SKILL.md
│   ├── gws-chat/
│   │   └── SKILL.md
│   ├── gws-chat-send/
│   │   └── SKILL.md
│   ├── gws-classroom/
│   │   └── SKILL.md
│   ├── gws-docs/
│   │   └── SKILL.md
│   ├── gws-docs-write/
│   │   └── SKILL.md
│   ├── gws-drive/
│   │   └── SKILL.md
│   ├── gws-drive-upload/
│   │   └── SKILL.md
│   ├── gws-events/
│   │   └── SKILL.md
│   ├── gws-events-renew/
│   │   └── SKILL.md
│   ├── gws-events-subscribe/
│   │   └── SKILL.md
│   ├── gws-forms/
│   │   └── SKILL.md
│   ├── gws-gmail/
│   │   └── SKILL.md
│   ├── gws-gmail-forward/
│   │   └── SKILL.md
│   ├── gws-gmail-read/
│   │   └── SKILL.md
│   ├── gws-gmail-reply/
│   │   └── SKILL.md
│   ├── gws-gmail-reply-all/
│   │   └── SKILL.md
│   ├── gws-gmail-send/
│   │   └── SKILL.md
│   ├── gws-gmail-triage/
│   │   └── SKILL.md
│   ├── gws-gmail-watch/
│   │   └── SKILL.md
│   ├── gws-keep/
│   │   └── SKILL.md
│   ├── gws-meet/
│   │   └── SKILL.md
│   ├── gws-modelarmor/
│   │   └── SKILL.md
│   ├── gws-modelarmor-create-template/
│   │   └── SKILL.md
│   ├── gws-modelarmor-sanitize-prompt/
│   │   └── SKILL.md
│   ├── gws-modelarmor-sanitize-response/
│   │   └── SKILL.md
│   ├── gws-people/
│   │   └── SKILL.md
│   ├── gws-shared/
│   │   └── SKILL.md
│   ├── gws-sheets/
│   │   └── SKILL.md
│   ├── gws-sheets-append/
│   │   └── SKILL.md
│   ├── gws-sheets-read/
│   │   └── SKILL.md
│   ├── gws-slides/
│   │   └── SKILL.md
│   ├── gws-tasks/
│   │   └── SKILL.md
│   ├── gws-workflow/
│   │   └── SKILL.md
│   ├── gws-workflow-email-to-task/
│   │   └── SKILL.md
│   ├── gws-workflow-file-announce/
│   │   └── SKILL.md
│   ├── gws-workflow-meeting-prep/
│   │   └── SKILL.md
│   ├── gws-workflow-standup-report/
│   │   └── SKILL.md
│   ├── gws-workflow-weekly-digest/
│   │   └── SKILL.md
│   ├── persona-content-creator/
│   │   └── SKILL.md
│   ├── persona-customer-support/
│   │   └── SKILL.md
│   ├── persona-event-coordinator/
│   │   └── SKILL.md
│   ├── persona-exec-assistant/
│   │   └── SKILL.md
│   ├── persona-hr-coordinator/
│   │   └── SKILL.md
│   ├── persona-it-admin/
│   │   └── SKILL.md
│   ├── persona-project-manager/
│   │   └── SKILL.md
│   ├── persona-researcher/
│   │   └── SKILL.md
│   ├── persona-sales-ops/
│   │   └── SKILL.md
│   ├── persona-team-lead/
│   │   └── SKILL.md
│   ├── recipe-backup-sheet-as-csv/
│   │   └── SKILL.md
│   ├── recipe-batch-invite-to-event/
│   │   └── SKILL.md
│   ├── recipe-block-focus-time/
│   │   └── SKILL.md
│   ├── recipe-bulk-download-folder/
│   │   └── SKILL.md
│   ├── recipe-collect-form-responses/
│   │   └── SKILL.md
│   ├── recipe-compare-sheet-tabs/
│   │   └── SKILL.md
│   ├── recipe-copy-sheet-for-new-month/
│   │   └── SKILL.md
│   ├── recipe-create-classroom-course/
│   │   └── SKILL.md
│   ├── recipe-create-doc-from-template/
│   │   └── SKILL.md
│   ├── recipe-create-events-from-sheet/
│   │   └── SKILL.md
│   ├── recipe-create-expense-tracker/
│   │   └── SKILL.md
│   ├── recipe-create-feedback-form/
│   │   └── SKILL.md
│   ├── recipe-create-gmail-filter/
│   │   └── SKILL.md
│   ├── recipe-create-meet-space/
│   │   └── SKILL.md
│   ├── recipe-create-presentation/
│   │   └── SKILL.md
│   ├── recipe-create-shared-drive/
│   │   └── SKILL.md
│   ├── recipe-create-task-list/
│   │   └── SKILL.md
│   ├── recipe-create-vacation-responder/
│   │   └── SKILL.md
│   ├── recipe-draft-email-from-doc/
│   │   └── SKILL.md
│   ├── recipe-email-drive-link/
│   │   └── SKILL.md
│   ├── recipe-find-free-time/
│   │   └── SKILL.md
│   ├── recipe-find-large-files/
│   │   └── SKILL.md
│   ├── recipe-forward-labeled-emails/
│   │   └── SKILL.md
│   ├── recipe-generate-report-from-sheet/
│   │   └── SKILL.md
│   ├── recipe-label-and-archive-emails/
│   │   └── SKILL.md
│   ├── recipe-log-deal-update/
│   │   └── SKILL.md
│   ├── recipe-organize-drive-folder/
│   │   └── SKILL.md
│   ├── recipe-plan-weekly-schedule/
│   │   └── SKILL.md
│   ├── recipe-post-mortem-setup/
│   │   └── SKILL.md
│   ├── recipe-reschedule-meeting/
│   │   └── SKILL.md
│   ├── recipe-review-meet-participants/
│   │   └── SKILL.md
│   ├── recipe-review-overdue-tasks/
│   │   └── SKILL.md
│   ├── recipe-save-email-attachments/
│   │   └── SKILL.md
│   ├── recipe-save-email-to-doc/
│   │   └── SKILL.md
│   ├── recipe-schedule-recurring-event/
│   │   └── SKILL.md
│   ├── recipe-send-team-announcement/
│   │   └── SKILL.md
│   ├── recipe-share-doc-and-notify/
│   │   └── SKILL.md
│   ├── recipe-share-event-materials/
│   │   └── SKILL.md
│   ├── recipe-share-folder-with-team/
│   │   └── SKILL.md
│   ├── recipe-sync-contacts-to-sheet/
│   │   └── SKILL.md
│   └── recipe-watch-drive-changes/
│       └── SKILL.md
├── src/
│   ├── auth.rs
│   ├── auth_commands.rs
│   ├── client.rs
│   ├── commands.rs
│   ├── credential_store.rs
│   ├── discovery.rs
│   ├── error.rs
│   ├── executor.rs
│   ├── formatter.rs
│   ├── fs_util.rs
│   ├── generate_skills.rs
│   ├── helpers/
│   │   ├── README.md
│   │   ├── calendar.rs
│   │   ├── chat.rs
│   │   ├── docs.rs
│   │   ├── drive.rs
│   │   ├── events/
│   │   │   ├── mod.rs
│   │   │   ├── renew.rs
│   │   │   └── subscribe.rs
│   │   ├── gmail/
│   │   │   ├── forward.rs
│   │   │   ├── mod.rs
│   │   │   ├── read.rs
│   │   │   ├── reply.rs
│   │   │   ├── send.rs
│   │   │   ├── triage.rs
│   │   │   └── watch.rs
│   │   ├── mod.rs
│   │   ├── modelarmor.rs
│   │   ├── script.rs
│   │   ├── sheets.rs
│   │   └── workflows.rs
│   ├── logging.rs
│   ├── main.rs
│   ├── oauth_config.rs
│   ├── output.rs
│   ├── schema.rs
│   ├── services.rs
│   ├── setup.rs
│   ├── setup_tui.rs
│   ├── text.rs
│   ├── timezone.rs
│   ├── token_storage.rs
│   └── validate.rs
└── templates/
    └── modelarmor/
        └── jailbreak.json

================================================
FILE CONTENTS
================================================

================================================
FILE: .agent/skills/vhs.md
================================================
---
description: Writing and editing VHS `.tape` files for terminal demo GIFs
---

# VHS Tape Files

[VHS](https://github.com/charmbracelet/vhs) records terminal sessions into GIFs/MP4s/WebMs from `.tape` scripts. Run with `vhs demo.tape`.

## Critical Syntax Rules

### Type command and inline directives

`Type`, `Sleep`, `Enter` are **separate directives on the same line**, delimited by the closing `"` of the `Type` string. The most common bug is forgetting to close the `Type` string, which causes `Sleep`/`Enter` to be typed literally into the terminal.

```
# ✅ CORRECT — closing " before Sleep
Type "echo hello" Sleep 300ms Enter

# ❌ WRONG — Sleep and Enter are typed as literal text
Type "echo hello Sleep 300ms Enter
```

### Type with @speed override

Override typing speed per-command with `@<time>` immediately after `Type` (no space):

```
Type@80ms '{"pageSize": 2}' Sleep 100ms
```

### Quoting

- Double quotes `"..."` are the standard Type delimiter
- Single quotes `'...'` also work and are useful when the typed content contains double quotes (e.g. JSON)
- Escape quotes inside strings with backticks: `` Type `VAR="value"` ``
- When building shell commands with nested quotes, split across multiple `Type` lines:

```
Type "gws drive files list --params '" Sleep 100ms
Type@80ms '{"pageSize": 2, "fields": "nextPageToken,files(id)"}' Sleep 100ms
Type "' --page-all" Sleep 300ms Enter
```

> **Pitfall**: Every `Type` line that is followed by `Sleep` or `Enter` on the same line MUST close its string first. Audit each line to ensure the quote is closed before any directive.

## Settings (top of file only)

Settings must appear before any non-setting command (except `Output`). `TypingSpeed` is the only setting that can be changed mid-tape.

```
Output demo.gif

Set Shell "bash"
Set FontSize 14
Set Width 1200
Set Height 1200
Set Theme "Catppuccin Mocha"
Set WindowBar Colorful
Set WindowBarSize 40
Set TypingSpeed 40ms
Set Padding 20
```

## Common Commands

| Command | Example | Notes |
|---|---|---|
| `Output` | `Output demo.gif` | `.gif`, `.mp4`, `.webm` |
| `Type` | `Type "ls -la"` | Type characters |
| `Type@<time>` | `Type@80ms "slow"` | Override typing speed |
| `Sleep` | `Sleep 2s`, `Sleep 300ms` | Pause recording |
| `Enter` | `Enter` | Press enter |
| `Hide` / `Show` | `Hide` ... `Show` | Hide setup commands |
| `Ctrl+<key>` | `Ctrl+C` | Key combos |
| `Tab`, `Space`, `Backspace` | `Tab 2` | Optional repeat count |
| `Up`, `Down`, `Left`, `Right` | `Up 3` | Arrow keys |
| `Wait` | `Wait /pattern/` | Wait for regex on screen |
| `Screenshot` | `Screenshot out.png` | Capture frame |
| `Env` | `Env FOO "bar"` | Set env var |
| `Source` | `Source other.tape` | Include another tape |
| `Require` | `Require jq` | Assert program exists |

## Hide/Show for Setup

Use `Hide`/`Show` to run setup commands (e.g. setting `$PATH`, clearing screen) without recording them:

```
Hide
Type "export PATH=$PWD/target/release:$PATH" Enter
Type "clear" Enter
Sleep 2s
Show
```

## Checklist When Editing Tape Files

1. **Every `Type` string must be closed** before `Sleep`/`Enter` on the same line
2. **Multi-line Type sequences** that build a single shell command: ensure the final line closes its string and includes `Enter`
3. **Sleep durations** after commands should be long enough for the command to finish (network calls may need 8s+)
4. **Settings go at the top** — only `TypingSpeed` can appear later
5. **Test locally** with `vhs <file>.tape` before committing


================================================
FILE: .agent/workflows/verify-skills.md
================================================
---
description: Verify all skills/*/SKILL.md files against actual CLI output for accuracy
---

# Verify Skills

Ensure every `skills/*/SKILL.md` file is accurate and optimized for AI agent consumption.

## Steps

1. **List all skill files**

```bash
find skills -name SKILL.md | sort
```

2. **Get top-level help for every service**

// turbo
```bash
for svc in drive sheets gmail calendar admin admin-reports docs slides tasks people chat vault groupssettings reseller licensing apps-script; do
  echo "=== $svc ==="
  ./target/debug/gws $svc --help 2>&1
  echo
done
```

3. **Get sub-resource help for key services** (spot-check method names used in examples)

// turbo
```bash
./target/debug/gws drive files --help 2>&1
./target/debug/gws gmail users messages --help 2>&1
./target/debug/gws sheets spreadsheets --help 2>&1
./target/debug/gws sheets spreadsheets values --help 2>&1
./target/debug/gws calendar events --help 2>&1
./target/debug/gws people people --help 2>&1
./target/debug/gws chat spaces --help 2>&1
./target/debug/gws vault matters --help 2>&1
./target/debug/gws admin users --help 2>&1
./target/debug/gws tasks tasks --help 2>&1
```

4. **For each SKILL.md, verify the following against the CLI `--help` output:**

   - [ ] **Resource names** match exactly (e.g., `files`, `spreadsheets`, `users`)
   - [ ] **Method names** match exactly (e.g., `list`, `insert`, `batchUpdate`, `getContent`)
   - [ ] **Nested resource paths** are correct (e.g., `spreadsheets values get`, not `values get`)
   - [ ] **Alias** mentioned in the file matches `services.rs` (e.g., `gws script` for apps-script)
   - [ ] **API version** in the header is correct
   - [ ] **Example commands** use valid `--params` and `--json` flag syntax
   - [ ] **No OAuth scopes section** — scopes should not be listed in skill files
   - [ ] **Tips section** contains accurate, actionable advice

5. **Cross-check `shared/SKILL.md`** covers:

   - [ ] `--fields` / field mask syntax
   - [ ] CLI syntax (`--params`, `--json`, `--output`, `--upload`, `--page-all`, `--page-limit`, `--page-delay`)
   - [ ] Authentication (`GOOGLE_WORKSPACE_CLI_CREDENTIALS`, `GOOGLE_WORKSPACE_API_KEY`)
   - [ ] Auto-pagination (`--page-all`) with NDJSON output
   - [ ] `gws schema <method>` introspection
   - [ ] Error handling JSON structure
   - [ ] Binary download with `--output`
   - [ ] Version override (`--api-version`, colon syntax)

6. **Fix any issues found** — update the SKILL.md files directly.

7. **Rebuild and re-verify** if any examples were changed.

// turbo
```bash
cargo build 2>&1
```


================================================
FILE: .changeset/README.md
================================================
# Changesets

Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
with multi-package repos, or single-package repos to help you version and publish your code. You can
find the full documentation for it [in our repository](https://github.com/changesets/changesets)

We have a quick list of common questions to get you started engaging with this project in
[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)


================================================
FILE: .changeset/config.json
================================================
{
  "$schema": "https://unpkg.com/@changesets/config@3.1.2/schema.json",
  "changelog": "@changesets/cli/changelog",
  "commit": false,
  "fixed": [],
  "linked": [],
  "access": "public",
  "baseBranch": "main",
  "updateInternalDependencies": "patch",
  "ignore": []
}


================================================
FILE: .claude/settings.json
================================================
{
  "attribution": {
    "commit": ""
  }
}


================================================
FILE: .gemini/config.yaml
================================================
code_review:
  comment_severity_threshold: HIGH
  pull_request_opened:
    help: false
    summary: true
    code_review: true
    include_drafts: false


================================================
FILE: .gemini/style_guide.md
================================================
# Code Review Style Guide

## Project Architecture

`gws` is a Rust CLI that dynamically generates commands from Google Discovery Documents at runtime. It does NOT use generated Rust crates (`google-drive3`, etc.) for API interaction. Do not suggest adding API-specific crates to `Cargo.toml`.

For additional context, read `AGENTS.md`.

## Security: Trusted vs Untrusted Inputs

This CLI is frequently invoked by AI/LLM agents. CLI arguments may be adversarial.

- **CLI arguments (untrusted)** — Must validate paths against traversal (`../../`), reject control characters, percent-encode URL path segments, and use `reqwest .query()` for query parameters. Validators: `validate_safe_output_dir()`, `validate_safe_dir_path()`, `encode_path_segment()`, `validate_resource_name()`.
- **Environment variables (trusted)** — Set by the user in their shell profile, `.env` file, or deployment config. Do NOT flag missing path validation on environment variable values. This is consistent with `XDG_CONFIG_HOME`, `CARGO_HOME`, etc.

## Test Coverage

The `codecov/patch` check requires new/modified lines to be covered by tests. Prefer extracting testable helper functions over embedding logic in `main`/`run`. Tests should cover both happy paths and rejection paths (e.g., pass `../../.ssh` and assert `Err`).

## Changesets

Every PR must include a `.changeset/<name>.md` file. The package name **must** be `"@googleworkspace/cli"` (not `"googleworkspace-cli"`). Use `patch` for fixes/chores, `minor` for features, `major` for breaking changes.

## PR Scope

Review comments must stay within the PR's stated scope. If you spot an improvement opportunity that is unrelated to the PR's purpose (e.g., refactoring constants, adding support for a different credential type, making an unrelated function atomic), mark it as a **follow-up** suggestion — not a blocking review comment. Do not request changes that expand the PR beyond its original intent.

Examples of scope creep to avoid:
- A bug-fix PR should not grow into a refactoring PR.
- Adding constants for strings used elsewhere is a separate cleanup task.
- Making a pre-existing function atomic is an enhancement, not a fix for the current PR.

## Severity Calibration

Mark issues as **critical** only when they cause data loss, security vulnerabilities, or incorrect behavior under normal conditions. Theoretical failures in infallible system APIs (e.g., `tokio::signal::ctrl_c()` registration) are **low** severity — do not label them critical. Contradicting a prior review suggestion (e.g., suggesting `expect()` then flagging `expect()` as wrong) erodes trust; verify consistency with earlier comments before posting.


================================================
FILE: .github/CODEOWNERS
================================================
# Codeowners

# Core engine code strictly requires your review
# Isolates agents to `skills/` or `src/helpers/` unless absolutely necessary
/src/main.rs @jpoehnelt
/src/executor.rs @jpoehnelt
/src/discovery.rs @jpoehnelt
/src/commands.rs @jpoehnelt
/src/auth.rs @jpoehnelt
/src/schema.rs @jpoehnelt


================================================
FILE: .github/PULL_REQUEST_TEMPLATE.md
================================================
## Description

Please include a summary of the change and which issue is fixed. If adding a new feature or command, please include the output of running it with `--dry-run` to prove the JSON request body matches the Discovery Document schema.

**Dry Run Output:**
```json
// Paste --dry-run output here if applicable
```

## Checklist:

- [ ] My code follows the `AGENTS.md` guidelines (no generated `google-*` crates).
- [ ] I have run `cargo fmt --all` to format the code perfectly.
- [ ] I have run `cargo clippy -- -D warnings` and resolved all warnings.
- [ ] I have added tests that prove my fix is effective or that my feature works.
- [ ] I have provided a Changeset file (e.g. via `pnpx changeset`) to document my changes.


================================================
FILE: .github/labeler.yml
================================================
# Labels applied to PRs based on changed files.
# Used by the actions/labeler action in .github/workflows/automation.yml

"area: auth":
  - changed-files:
      - any-glob-to-any-file:
          - src/auth.rs
          - src/auth_commands.rs
          - src/setup.rs
          - src/accounts.rs
          - src/credential_store.rs
          - src/token_storage.rs
          - src/oauth_config.rs

"area: discovery":
  - changed-files:
      - any-glob-to-any-file:
          - src/discovery.rs
          - src/services.rs

"area: http":
  - changed-files:
      - any-glob-to-any-file:
          - src/executor.rs
          - src/client.rs

"area: tui":
  - changed-files:
      - any-glob-to-any-file:
          - src/setup_tui.rs

"area: mcp":
  - changed-files:
      - any-glob-to-any-file:
          - src/mcp_server.rs

"area: skills":
  - changed-files:
      - any-glob-to-any-file:
          - src/generate_skills.rs
          - skills/**

"area: docs":
  - changed-files:
      - any-glob-to-any-file:
          - "*.md"
          - docs/**

"area: distribution":
  - changed-files:
      - any-glob-to-any-file:
          - .github/workflows/release.yml
          - .github/workflows/release-changesets.yml
          - dist-workspace.toml
          - Cargo.toml

"area: core":
  - changed-files:
      - any-glob-to-any-file:
          - src/main.rs
          - src/commands.rs
          - src/error.rs
          - src/formatter.rs
          - src/fs_util.rs
          - src/helpers/**
          - src/text.rs
          - src/validate.rs
          - src/schema.rs


================================================
FILE: .github/workflows/automation.yml
================================================
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: Automation

on:
  push:
    branches: [main]
  pull_request_target:
    types: [opened, synchronize, reopened]
  pull_request_review:
    types: [submitted]

permissions:
  contents: write
  issues: write
  pull-requests: write

jobs:
  format:
    name: Format
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }}

      - name: Install Rust
        uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          components: rustfmt

      - name: Run cargo fmt
        run: cargo fmt --all

      - name: Commit and push
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add -A
          git diff --cached --quiet || git commit -m "style: cargo fmt" && git push

  file-labeler:
    name: File Labeler
    if: github.event_name == 'pull_request_target'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5
        with:
          repo-token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }}
          sync-labels: true

  gemini-review:
    name: Gemini Review
    if: >-
      github.event_name == 'pull_request_target' &&
      github.event.action == 'synchronize'
    runs-on: ubuntu-latest
    concurrency:
      group: gemini-review-${{ github.event.pull_request.number }}
      cancel-in-progress: true
    steps:
      - name: Remove reviewed label
        uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            try {
              await github.rest.issues.removeLabel({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.payload.pull_request.number,
                name: 'gemini: reviewed',
              });
            } catch (e) {
              // Label not present — ignore
            }

      - name: Debounce
        run: sleep 60

      - name: Trigger Gemini Code Assist review
        uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
        with:
          github-token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }}
          script: |
            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.payload.pull_request.number,
              body: '/gemini review',
            });

  gemini-reviewed:
    name: Gemini Reviewed
    if: >-
      github.event_name == 'pull_request_review' &&
      github.event.review.user.login == 'gemini-code-assist[bot]'
    runs-on: ubuntu-latest
    steps:
      - name: Add reviewed label if review matches HEAD
        uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const pr = await github.rest.pulls.get({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: context.payload.pull_request.number,
            });

            if (context.payload.review.commit_id !== pr.data.head.sha) {
              console.log(`Review is for ${context.payload.review.commit_id} but HEAD is ${pr.data.head.sha} — skipping label`);
              return;
            }

            try {
              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: context.payload.pull_request.number,
                labels: ['gemini: reviewed'],
              });
            } catch (e) {
              if (e.status === 403) {
                console.log(`Token cannot add labels for this review event (${e.message}) — skipping`);
                return;
              }
              throw e;
            }


================================================
FILE: .github/workflows/ci.yml
================================================
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

env:
  CARGO_TERM_COLOR: always
  SCCACHE_GHA_ENABLED: "true"
  SCCACHE_IGNORE_SERVER_IO_ERROR: "true"

jobs:
  changes:
    name: Detect Changes
    runs-on: ubuntu-latest
    outputs:
      rust: ${{ steps.filter.outputs.rust }}
      nix: ${{ steps.filter.outputs.nix }}
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
      - uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
        id: filter
        with:
          filters: |
            rust:
              - '**/*.rs'
              - 'Cargo.toml'
              - 'Cargo.lock'
              - 'build.rs'
              - '.cargo/**'
            nix:
              - 'flake.nix'
              - 'flake.lock'

  test:
    name: Test
    needs: changes
    if: needs.changes.outputs.rust == 'true' || github.event_name == 'push'
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest]
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

      - name: Install Rust
        uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable

      - name: Setup sccache
        id: sccache
        uses: mozilla-actions/sccache-action@2df7dbab909c49ab7d3382d05da469f3f975c2d6 # v0.0.7
        continue-on-error: true

      - name: Enable sccache
        if: steps.sccache.outcome == 'success'
        shell: bash
        run: |
          if sccache --start-server 2>/dev/null; then
            echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
          else
            echo "::warning::sccache server failed to start, building without cache"
          fi

      - name: Cache cargo
        uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2
        with:
          key: test-${{ matrix.os }}

      - name: Run tests
        run: cargo test --verbose

  nix:
    name: Nix
    needs: changes
    if: needs.changes.outputs.rust == 'true' || needs.changes.outputs.nix == 'true' || github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
      - name: Install Nix
        uses: DeterminateSystems/nix-installer-action@d96bc962e61b3049ce8128d03d57a1144fa96539 # main
      - name: Magic Nix Cache
        uses: DeterminateSystems/magic-nix-cache-action@cec65ff6f104850203b152861d3f9e5f1747885d # main
      - name: Check flake
        run: nix flake check
      - name: Build flake
        run: nix build .

  lint:
    name: Lint
    needs: changes
    if: needs.changes.outputs.rust == 'true' || github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

      - name: Install Rust
        uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          components: rustfmt, clippy

      - name: Setup sccache
        id: sccache
        uses: mozilla-actions/sccache-action@2df7dbab909c49ab7d3382d05da469f3f975c2d6 # v0.0.7
        continue-on-error: true

      - name: Enable sccache
        if: steps.sccache.outcome == 'success'
        shell: bash
        run: |
          if sccache --start-server 2>/dev/null; then
            echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
          else
            echo "::warning::sccache server failed to start, building without cache"
          fi

      - name: Cache cargo
        uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2
        with:
          key: lint

      - name: Check formatting
        run: |
          if ! cargo fmt --all -- --check; then
            echo "::error::Cargo fmt failed. Please run 'cargo fmt --all' locally and commit the changes."
            exit 1
          fi

      - name: Clippy
        run: cargo clippy -- -D warnings


  skills:
    name: Verify Skills
    needs: changes
    if: needs.changes.outputs.rust == 'true' || github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

      - name: Install Rust
        uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable

      - name: Setup sccache
        id: sccache
        uses: mozilla-actions/sccache-action@2df7dbab909c49ab7d3382d05da469f3f975c2d6 # v0.0.7
        continue-on-error: true

      - name: Enable sccache
        if: steps.sccache.outcome == 'success'
        shell: bash
        run: |
          if sccache --start-server 2>/dev/null; then
            echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
          else
            echo "::warning::sccache server failed to start, building without cache"
          fi

      - name: Cache cargo
        uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2
        with:
          key: skills

      - name: Regenerate skills
        run: cargo run -- generate-skills --output-dir skills

      - name: Check for drift
        run: |
          if ! git diff --exit-code skills/; then
            echo "::warning::Skills are out of date — the hourly auto-sync PR will fix this automatically."
          fi

  build-linux:
    name: Build (Linux x86_64)
    needs: changes
    if: needs.changes.outputs.rust == 'true' || github.event_name == 'push'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

      - name: Install Rust
        uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          targets: x86_64-unknown-linux-gnu

      - name: Setup sccache
        id: sccache
        uses: mozilla-actions/sccache-action@2df7dbab909c49ab7d3382d05da469f3f975c2d6 # v0.0.7
        continue-on-error: true

      - name: Enable sccache
        if: steps.sccache.outcome == 'success'
        shell: bash
        run: |
          if sccache --start-server 2>/dev/null; then
            echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
          else
            echo "::warning::sccache server failed to start, building without cache"
          fi

      - name: Cache cargo
        uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2
        with:
          key: build-x86_64-unknown-linux-gnu
          cache-targets: "false"

      - name: Build
        run: cargo build --release --target x86_64-unknown-linux-gnu

      - name: Upload binary
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
        with:
          name: gws-linux-x86_64
          path: target/x86_64-unknown-linux-gnu/release/gws
          retention-days: 1

  build:
    name: Build
    needs: [smoketest, changes]
    if: |
      always() && !cancelled() && !failure()
      && (needs.changes.outputs.rust == 'true' || github.event_name == 'push')
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            target: aarch64-unknown-linux-gnu
          - os: macos-latest
            target: x86_64-apple-darwin
          - os: macos-latest
            target: aarch64-apple-darwin
          - os: windows-latest
            target: x86_64-pc-windows-msvc
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

      - name: Install Rust
        uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          targets: ${{ matrix.target }}

      - name: Setup sccache
        id: sccache
        uses: mozilla-actions/sccache-action@2df7dbab909c49ab7d3382d05da469f3f975c2d6 # v0.0.7
        continue-on-error: true

      - name: Enable sccache
        if: steps.sccache.outcome == 'success'
        shell: bash
        run: |
          if sccache --start-server 2>/dev/null; then
            echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"
          else
            echo "::warning::sccache server failed to start, building without cache"
          fi

      - name: Cache cargo
        uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2
        with:
          key: build-${{ matrix.target }}
          cache-targets: "false"

      - name: Install cross-compilation tools
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: |
          sudo apt-get update
          sudo apt-get install -y gcc-aarch64-linux-gnu

      - name: Disable Windows Defender scanning for cargo
        if: runner.os == 'Windows'
        run: |
          Add-MpPreference -ExclusionPath "$env:USERPROFILE\.cargo"
          Add-MpPreference -ExclusionPath "$env:USERPROFILE\.rustup"
          Add-MpPreference -ExclusionPath "${{ github.workspace }}\target"

      - name: Build
        run: cargo build --release --target ${{ matrix.target }}
        env:
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc

  smoketest:
    name: API Smoketest
    needs: build-linux
    if: github.event.pull_request.head.repo.full_name == github.repository || github.event_name != 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

      - name: Download binary
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
        with:
          name: gws-linux-x86_64
          path: ./bin

      - name: Make binary executable
        run: chmod +x ./bin/gws

      - name: Decode credentials
        env:
          GOOGLE_CREDENTIALS_JSON: ${{ secrets.GOOGLE_CREDENTIALS_JSON }}
        run: |
          if [ -z "$GOOGLE_CREDENTIALS_JSON" ]; then
            echo "::error::GOOGLE_CREDENTIALS_JSON secret is not set"
            exit 1
          fi
          echo "$GOOGLE_CREDENTIALS_JSON" | base64 -d > /tmp/credentials.json

      - name: Smoketest — help
        run: ./bin/gws --help

      - name: Smoketest — schema introspection
        run: ./bin/gws schema drive.files.list | jq -e '.httpMethod'

      - name: Smoketest — Drive files list
        env:
          GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE: /tmp/credentials.json
        run: |
          ./bin/gws drive files list \
            --params '{"pageSize": 1, "fields": "files(id,mimeType)"}' \
            | jq -e '.files'

      - name: Smoketest — Gmail messages
        env:
          GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE: /tmp/credentials.json
        run: |
          ./bin/gws gmail users messages list \
            --params '{"userId": "me", "maxResults": 1, "fields": "messages(id)"}' \
            | jq -e '.messages'

      - name: Smoketest — Calendar events
        env:
          GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE: /tmp/credentials.json
        run: |
          ./bin/gws calendar events list \
            --params '{"calendarId": "primary", "maxResults": 1, "fields": "kind,items(id,status)"}' \
            | jq -e '.kind'

      - name: Smoketest — Slides presentation
        env:
          GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE: /tmp/credentials.json
        run: |
          ./bin/gws slides presentations get \
            --params '{"presentationId": "1knOKD_87JWE4qsEbO4r5O91IxTER5ybBBhOJgZ1yLFI", "fields": "presentationId,slides(objectId)"}' \
            | jq -e '.presentationId'

      - name: Smoketest — pagination
        env:
          GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE: /tmp/credentials.json
        run: |
          LINES=$(./bin/gws drive files list \
            --params '{"pageSize": 1, "fields": "nextPageToken,files(id)"}' \
            --page-all --page-limit 2 \
            | wc -l)
          if [ "$LINES" -lt 2 ]; then
            echo "::error::Expected at least 2 NDJSON lines from pagination, got $LINES"
            exit 1
          fi

      - name: Smoketest — error handling
        run: |
          if ./bin/gws fakeservice list 2>&1; then
            echo "::error::Expected exit code 1 for unknown service"
            exit 1
          fi

      - name: Cleanup credentials
        if: always()
        run: rm -f /tmp/credentials.json


================================================
FILE: .github/workflows/cla.yml
================================================
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: CLA

on:
  check_run:
    types: [completed]

permissions:
  pull-requests: write

jobs:
  cla-label:
    name: CLA Label
    if: github.event.check_run.name == 'cla/google'
    runs-on: ubuntu-latest
    steps:
      - name: Update CLA label
        uses: actions/github-script@f28e40c7f34bde8b3046d885e986cb6290c5673b # v7
        with:
          github-token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }}
          script: |
            const cr = context.payload.check_run;
            const passed = cr.conclusion === 'success';

            for (const pr of cr.pull_requests) {
              const labels = passed
                ? { add: 'cla: yes', remove: 'cla: no' }
                : { add: 'cla: no', remove: 'cla: yes' };

              await github.rest.issues.addLabels({
                owner: context.repo.owner,
                repo: context.repo.repo,
                issue_number: pr.number,
                labels: [labels.add],
              });

              try {
                await github.rest.issues.removeLabel({
                  owner: context.repo.owner,
                  repo: context.repo.repo,
                  issue_number: pr.number,
                  name: labels.remove,
                });
              } catch (e) {
                // Label not present — ignore
              }
            }


================================================
FILE: .github/workflows/coverage.yml
================================================
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: Coverage

on:
  push:
    branches: [main]
    paths: ['**/*.rs', 'Cargo.toml', 'Cargo.lock']
  pull_request:
    branches: [main]
    paths: ['**/*.rs', 'Cargo.toml', 'Cargo.lock']

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
  coverage:
    name: Coverage
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
      
      - name: Install Rust
        uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable
        with:
          components: llvm-tools-preview
          
      - name: Install cargo-llvm-cov
        uses: taiki-e/install-action@a37010ded18ff788be4440302bd6830b1ae50d8b # cargo-llvm-cov
        with:
          tool: cargo-llvm-cov
        
      - name: Generate code coverage
        run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info
        
      - name: Upload coverage to Codecov
        uses: codecov/codecov-action@b9fd7d16f6d7d1b5d2bec1a2887e65ceed900238 # v4
        with:
          files: lcov.info
          fail_ci_if_error: false
          token: ${{ secrets.CODECOV_TOKEN }}


================================================
FILE: .github/workflows/generate-skills.yml
================================================
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: Generate Skills

on:
  schedule:
    - cron: "0 * * * *" # Hourly — keeps skills in sync with Discovery API changes
  workflow_dispatch: # Manual trigger
  push:
    branches-ignore:
      - main # main is kept up to date by PR merges

concurrency:
  group: generate-skills-${{ github.ref_name }}
  cancel-in-progress: true

env:
  CARGO_TERM_COLOR: always

jobs:
  generate:
    name: Generate and commit skills
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write

    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          # For cron/dispatch: check out main. For push: check out the branch.
          ref: ${{ github.head_ref || github.ref_name }}
          token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }}

      - name: Install Rust
        uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable

      - name: Setup sccache
        uses: mozilla-actions/sccache-action@2df7dbab909c49ab7d3382d05da469f3f975c2d6 # v0.0.7

      - name: Cache cargo
        uses: Swatinem/rust-cache@ad397744b0d591a723ab90405b7247fac0e6b8db # v2
        with:
          key: generate-skills-ubuntu

      - name: Generate skills
        run: cargo run -- generate-skills

      - name: Check for changes
        id: diff
        run: |
          if git diff --quiet skills/ docs/skills.md; then
            echo "changed=false" >> "$GITHUB_OUTPUT"
          else
            echo "changed=true" >> "$GITHUB_OUTPUT"
          fi

      # --- Cron / workflow_dispatch: open a PR against main ---
      - name: Create changeset for sync PR
        if: >-
          steps.diff.outputs.changed == 'true' &&
          (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
        run: |
          mkdir -p .changeset
          cat > .changeset/sync-skills.md << 'EOF'
          ---
          "@googleworkspace/cli": patch
          ---

          Sync generated skills with latest Google Discovery API specs
          EOF

      - name: Create or update sync PR
        if: >-
          steps.diff.outputs.changed == 'true' &&
          (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch')
        uses: peter-evans/create-pull-request@22a9089034f40e5a961c8808d113e2c98fb63676 # v7
        with:
          token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }}
          branch: chore/sync-skills
          title: "chore: sync skills with Discovery API"
          body: |
            Automated PR — the Google Discovery API specs have changed and the
            generated skill files are out of date.

            Created by the **Generate Skills** workflow (`generate-skills.yml`).
          commit-message: "chore: regenerate skills from Discovery API"
          add-paths: |
            skills/
            docs/skills.md
            .changeset/sync-skills.md
          delete-branch: true

      # --- Push events (non-main branches): commit directly ---
      - name: Commit and push if changed
        if: >-
          steps.diff.outputs.changed == 'true' &&
          github.event_name == 'push'
        env:
          GITHUB_TOKEN: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }}
        run: |
          git config user.name  "googleworkspace-bot"
          git config user.email "googleworkspace-bot@users.noreply.github.com"

          git add skills/ docs/skills.md
          git commit -m "chore: regenerate skills [skip ci]"
          git push


================================================
FILE: .github/workflows/policy.yml
================================================
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: Policy

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.ref != 'refs/heads/main' }}

jobs:
  policy-check:
    name: Policy Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          fetch-depth: 0
      - name: Enforce AGENTS.md rules
        run: |
          if grep -qE "^google-[a-zA-Z0-9_-]+[[:space:]]*=" Cargo.toml; then
            echo "::error file=Cargo.toml::Violates AGENTS.md: Adding generated google-* crates is prohibited. The CLI uses dynamic schema discovery at runtime."
            exit 1
          fi
          echo "Policy check passed."
      - name: Enforce Changeset File
        if: github.event_name == 'pull_request'
        run: |
          git fetch origin ${{ github.base_ref }}
          # Skip changeset requirement if no Rust/Cargo files changed
          if ! git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -qE '\.(rs)$|^Cargo\.(toml|lock)$'; then
            echo "No Rust/Cargo files changed; skipping changeset requirement."
            exit 0
          fi
          if ! git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -q "^.changeset/.*\.md$"; then
            echo "::error::A Changeset file is required! Please run 'npx changeset' or manually create a markdown file in the .changeset directory describing your changes to automatically version and release this PR."
            exit 1
          fi
          echo "Changeset file found!"
      - name: Validate Changeset Package Name
        if: github.event_name == 'pull_request'
        run: |
          for f in $(git diff --name-only origin/${{ github.base_ref }}...HEAD | grep "^.changeset/.*\.md$"); do
            if grep -q '"googleworkspace-cli"' "$f"; then
              echo "::error file=$f::Wrong package name. Use '\"@googleworkspace/cli\"' not '\"googleworkspace-cli\"'."
              exit 1
            fi
          done
          echo "Changeset package names valid!"


================================================
FILE: .github/workflows/publish-skills.yml
================================================
name: Publish OpenClaw Skills

on:
  push:
    branches: [main]
    paths:
      - "skills/**"
      - ".github/workflows/publish-skills.yml"
  pull_request:
    branches: [main]
    paths:
      - "skills/**"
      - ".github/workflows/publish-skills.yml"
  schedule:
    - cron: "0 * * * *" # Hourly, to drip-publish past rate limits
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: false

jobs:
  publish:
    runs-on: ubuntu-latest
    permissions:
      contents: read
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4

      - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
        with:
          node-version: "20"

      - name: Install ClawHub CLI
        run: npm i -g clawhub@0.7.0

      - name: Authenticate ClawHub
        env:
          CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
        run: |
          if [ -z "$CLAWHUB_TOKEN" ]; then
            echo "::error::CLAWHUB_TOKEN secret is not set"
            exit 1
          fi
          clawhub login --token "$CLAWHUB_TOKEN"

      - name: Publish skills
        run: |
          if [ "${{ github.event_name }}" = "pull_request" ]; then
            clawhub sync --root skills --all --dry-run
          else
            clawhub sync --root skills --all
          fi


================================================
FILE: .github/workflows/release-changesets.yml
================================================
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: Release (Changeset)

on:
  workflow_dispatch:
  push:
    branches:
      - main

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: false

jobs:
  release:
    name: Release
    runs-on: ubuntu-latest
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
        with:
          token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }}

      - name: Install Rust
        uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable

      - name: Install Nix
        uses: cachix/install-nix-action@08dcb3a5e62fa31e2da3d490afc4176ef55ecd72 # v30
        with:
          github_access_token: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }}

      - uses: pnpm/action-setup@c5ba7f7862a0f64c1b1a05fbac13e0b8e86ba08c # v4

      - name: Setup Node.js
        uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
        with:
          node-version: '22'
          cache: 'pnpm'

      - name: Install Dependencies
        run: pnpm install

      - run: |
          git config --global user.name "googleworkspace-bot"
          git config --global user.email "googleworkspace-bot@google.com"

      - name: Create Release Pull Request or Tag
        id: changesets
        uses: changesets/action@6a0a831ff30acef54f2c6aa1cbbc1096b066edaf # v1
        with:
          version: pnpm run version-sync
          publish: pnpm run tag-release
          commit: 'chore: release versions'
          title: 'chore: release versions'
          setupGitUser: false
        env:
          GITHUB_TOKEN: ${{ secrets.GOOGLEWORKSPACE_BOT_TOKEN }}


================================================
FILE: .github/workflows/release.yml
================================================
# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist
#
# Copyright 2022-2024, axodotdev
# SPDX-License-Identifier: MIT or Apache-2.0
#
# CI that:
#
# * checks for a Git Tag that looks like a release
# * builds artifacts with dist (archives, installers, hashes)
# * uploads those artifacts to temporary workflow zip
# * on success, uploads the artifacts to a GitHub Release
#
# Note that the GitHub Release will be created with a generated
# title/body based on your changelogs.

name: Release
permissions:
  "contents": "write"

# This task will run whenever you push a git tag that looks like a version
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
#
# If PACKAGE_NAME is specified, then the announcement will be for that
# package (erroring out if it doesn't have the given version or isn't dist-able).
#
# If PACKAGE_NAME isn't specified, then the announcement will be for all
# (dist-able) packages in the workspace with that version (this mode is
# intended for workspaces with only one dist-able package, or with all dist-able
# packages versioned/released in lockstep).
#
# If you push multiple tags at once, separate instances of this workflow will
# spin up, creating an independent announcement for each one. However, GitHub
# will hard limit this to 3 tags per commit, as it will assume more tags is a
# mistake.
#
# If there's a prerelease-style suffix to the version, then the release(s)
# will be marked as a prerelease.
on:
  pull_request:
  push:
    tags:
      - '**[0-9]+.[0-9]+.[0-9]+*'

jobs:
  # Run 'dist plan' (or host) to determine what tasks we need to do
  plan:
    runs-on: "ubuntu-22.04"
    outputs:
      val: ${{ steps.plan.outputs.manifest }}
      tag: ${{ !github.event.pull_request && github.ref_name || '' }}
      tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
      publishing: ${{ !github.event.pull_request }}
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          persist-credentials: false
          submodules: recursive
      - name: Install dist
        # we specify bash to get pipefail; it guards against the `curl` command
        # failing. otherwise `sh` won't catch that `curl` returned non-0
        shell: bash
        run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.31.0/cargo-dist-installer.sh | sh"
      - name: Cache dist
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
        with:
          name: cargo-dist-cache
          path: ~/.cargo/bin/dist
      # sure would be cool if github gave us proper conditionals...
      # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
      # functionality based on whether this is a pull_request, and whether it's from a fork.
      # (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
      # but also really annoying to build CI around when it needs secrets to work right.)
      - id: plan
        run: |
          dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json
          echo "dist ran successfully"
          cat plan-dist-manifest.json
          echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
      - name: "Upload dist-manifest.json"
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
        with:
          name: artifacts-plan-dist-manifest
          path: plan-dist-manifest.json

  # Build and packages all the platform-specific things
  build-local-artifacts:
    name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
    # Let the initial task tell us to not run (currently very blunt)
    needs:
      - plan
    if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
    strategy:
      fail-fast: false
      # Target platforms/runners are computed by dist in create-release.
      # Each member of the matrix has the following arguments:
      #
      # - runner: the github runner
      # - dist-args: cli flags to pass to dist
      # - install-dist: expression to run to install dist on the runner
      #
      # Typically there will be:
      # - 1 "global" task that builds universal installers
      # - N "local" tasks that build each platform's binaries and platform-specific installers
      matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
    runs-on: ${{ matrix.runner }}
    container: ${{ matrix.container && matrix.container.image || null }}
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
    permissions:
      "attestations": "write"
      "contents": "read"
      "id-token": "write"
    steps:
      - name: enable windows longpaths
        run: |
          git config --global core.longpaths true
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          persist-credentials: false
          submodules: recursive
      - name: Install Rust non-interactively if not already installed
        if: ${{ matrix.container }}
        run: |
          if ! command -v cargo > /dev/null 2>&1; then
            curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
            echo "$HOME/.cargo/bin" >> $GITHUB_PATH
          fi
      - name: Install dist
        run: ${{ matrix.install_dist.run }}
      # Get the dist-manifest
      - name: Fetch local artifacts
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
        with:
          pattern: artifacts-*
          path: target/distrib/
          merge-multiple: true
      - name: Install dependencies
        run: |
          ${{ matrix.packages_install }}
      - name: Build artifacts
        run: |
          # Actually do builds and make zips and whatnot
          dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json
          echo "dist ran successfully"
      - name: Attest
        uses: actions/attest-build-provenance@43d14bc2b83dec42d39ecae14e916627a18bb661 # v3
        with:
          subject-path: "target/distrib/*${{ join(matrix.targets, ', ') }}*"
      - id: cargo-dist
        name: Post-build
        # We force bash here just because github makes it really hard to get values up
        # to "real" actions without writing to env-vars, and writing to env-vars has
        # inconsistent syntax between shell and powershell.
        shell: bash
        run: |
          # Parse out what we just built and upload it to scratch storage
          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
          dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
          echo "EOF" >> "$GITHUB_OUTPUT"

          cp dist-manifest.json "$BUILD_MANIFEST_NAME"
      - name: "Upload artifacts"
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
        with:
          name: artifacts-build-local-${{ join(matrix.targets, '_') }}
          path: |
            ${{ steps.cargo-dist.outputs.paths }}
            ${{ env.BUILD_MANIFEST_NAME }}

  # Build and package all the platform-agnostic(ish) things
  build-global-artifacts:
    needs:
      - plan
      - build-local-artifacts
    runs-on: "ubuntu-22.04"
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          persist-credentials: false
          submodules: recursive
      - name: Install cached dist
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
        with:
          name: cargo-dist-cache
          path: ~/.cargo/bin/
      - run: chmod +x ~/.cargo/bin/dist
      # Get all the local artifacts for the global tasks to use (for e.g. checksums)
      - name: Fetch local artifacts
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
        with:
          pattern: artifacts-*
          path: target/distrib/
          merge-multiple: true
      - id: cargo-dist
        shell: bash
        run: |
          dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json
          echo "dist ran successfully"

          # Parse out what we just built and upload it to scratch storage
          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
          jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
          echo "EOF" >> "$GITHUB_OUTPUT"

          cp dist-manifest.json "$BUILD_MANIFEST_NAME"
      - name: "Upload artifacts"
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
        with:
          name: artifacts-build-global
          path: |
            ${{ steps.cargo-dist.outputs.paths }}
            ${{ env.BUILD_MANIFEST_NAME }}
  # Determines if we should publish/announce
  host:
    needs:
      - plan
      - build-local-artifacts
      - build-global-artifacts
    # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine)
    if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    runs-on: "ubuntu-22.04"
    outputs:
      val: ${{ steps.host.outputs.manifest }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          persist-credentials: false
          submodules: recursive
      - name: Install cached dist
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
        with:
          name: cargo-dist-cache
          path: ~/.cargo/bin/
      - run: chmod +x ~/.cargo/bin/dist
      # Fetch artifacts from scratch-storage
      - name: Fetch artifacts
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
        with:
          pattern: artifacts-*
          path: target/distrib/
          merge-multiple: true
      - id: host
        shell: bash
        run: |
          dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
          echo "artifacts uploaded and released successfully"
          cat dist-manifest.json
          echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
      - name: "Upload dist-manifest.json"
        uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6
        with:
          # Overwrite the previous copy
          name: artifacts-dist-manifest
          path: dist-manifest.json
      # Create a GitHub Release while uploading all files to it
      - name: "Download GitHub Artifacts"
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
        with:
          pattern: artifacts-*
          path: artifacts
          merge-multiple: true
      - name: Cleanup
        run: |
          # Remove the granular manifests
          rm -f artifacts/*-dist-manifest.json
      - name: Create GitHub Release
        env:
          PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
          ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
          ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
          RELEASE_COMMIT: "${{ github.sha }}"
        run: |
          # Write and read notes from a file to avoid quoting breaking things
          echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt

          gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*

  publish-npm:
    needs:
      - plan
      - host
    runs-on: "ubuntu-22.04"
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      PLAN: ${{ needs.plan.outputs.val }}
    if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
    steps:
      - name: Fetch npm packages
        uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7
        with:
          pattern: artifacts-*
          path: npm/
          merge-multiple: true
      - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
        with:
          node-version: '20.x'
          registry-url: 'https://wombat-dressing-room.appspot.com'
      - run: |
          for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith("-npm-package.tar.gz")] | any)'); do
            pkg=$(echo "$release" | jq '.artifacts[] | select(endswith("-npm-package.tar.gz"))' --raw-output)
            npm publish --access public "./npm/${pkg}"
          done
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

  announce:
    needs:
      - plan
      - host
      - publish-npm
    # use "always() && ..." to allow us to wait for all publish jobs while
    # still allowing individual publish jobs to skip themselves (for prereleases).
    # "host" however must run to completion, no skipping allowed!
    if: ${{ always() && needs.host.result == 'success' && (needs.publish-npm.result == 'skipped' || needs.publish-npm.result == 'success') }}
    runs-on: "ubuntu-22.04"
    env:
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          persist-credentials: false
          submodules: recursive


================================================
FILE: .github/workflows/stale.yml
================================================
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

name: 'Close Stale PRs'
on:
  schedule:
    - cron: '30 1 * * *'
  workflow_dispatch:

permissions:
  pull-requests: write

jobs:
  stale:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/stale@5bef64f19d7facfb25b37b414482c7164d639639 # v9
        with:
          days-before-issue-stale: -1
          days-before-issue-close: -1
          days-before-pr-stale: 3
          days-before-pr-close: 0
          stale-pr-message: 'This PR has been inactive for 72 hours. Closing to keep the queue clean.'
          close-pr-message: 'This PR was closed because it has been stalled for 72 hours. Feel free to magically reopen it if you want to continue working on it!'
          exempt-pr-labels: 'keep-alive'


================================================
FILE: .gitignore
================================================
# Rust
/target/
**/*.rs.bk
bin/
lcov.info


.emails/

# Node
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm

# Build outputs
/dist/
*.tsbuildinfo
/bin/gws-native*

# IDE
.idea/
.vscode/
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Environment
.env
.env.local
.env.*.local

# Test artifacts
*.log
coverage/
.nyc_output/
download.txt
files.jsonl

# Plans (local design docs)
docs/plans/

# Generated
demo.mp4
download.html

================================================
FILE: AGENTS.md
================================================
# AGENTS.md

## Project Overview

`gws` is a Rust CLI tool for interacting with Google Workspace APIs. It dynamically generates its command surface at runtime by parsing Google Discovery Service JSON documents.

> [!IMPORTANT]
> **Dynamic Discovery**: This project does NOT use generated Rust crates (e.g., `google-drive3`) for API interaction. Instead, it fetches the Discovery JSON at runtime and builds `clap` commands dynamically. When adding a new service, you only need to register it in `src/services.rs` and verify the Discovery URL pattern in `src/discovery.rs`. Do NOT add new crates to `Cargo.toml` for standard Google APIs.

> [!NOTE]
> **Package Manager**: Use `pnpm` instead of `npm` for Node.js package management in this repository.

## Build & Test

> [!IMPORTANT]
> **Test Coverage**: The `codecov/patch` check requires that new or modified lines are covered by tests. When adding code, extract testable helper functions rather than embedding logic in `main`/`run` where it's hard to unit-test. Run `cargo test` locally and verify new branches are exercised.

```bash
cargo build          # Build in dev mode
cargo clippy -- -D warnings  # Lint check
cargo test           # Run tests
```

## Changesets

Every PR must include a changeset file. Create one at `.changeset/<descriptive-name>.md`:

```markdown
---
"@googleworkspace/cli": patch
---

Brief description of the change
```

Use `patch` for fixes/chores, `minor` for new features, `major` for breaking changes. The CI policy check will fail without a changeset.

## Architecture

The CLI uses a **two-phase argument parsing** strategy:

1. Parse argv to extract the service name (e.g., `drive`)
2. Fetch the service's Discovery Document, build a dynamic `clap::Command` tree, then re-parse

### Source Layout

| File                      | Purpose                                                                                   |
| ------------------------- | ----------------------------------------------------------------------------------------- |
| `src/main.rs`             | Entrypoint, two-phase CLI parsing, method resolution                                      |
| `src/discovery.rs`        | Serde models for Discovery Document + fetch/cache                                         |
| `src/services.rs`         | Service alias → Discovery API name/version mapping                                        |
| `src/auth.rs`             | OAuth2 token acquisition via env vars, encrypted credentials, or ADC                      |
| `src/credential_store.rs` | AES-256-GCM encryption/decryption of credential files                                     |
| `src/auth_commands.rs`    | `gws auth` subcommands: `login`, `logout`, `setup`, `status`, `export`                    |
| `src/commands.rs`         | Recursive `clap::Command` builder from Discovery resources                                |
| `src/executor.rs`         | HTTP request construction, response handling, schema validation                           |
| `src/schema.rs`           | `gws schema` command — introspect API method schemas                                      |
| `src/error.rs`            | Structured JSON error output                                                              |
| `src/logging.rs`          | Opt-in structured logging (stderr + file) via `tracing`                                   |
| `src/timezone.rs`         | Account timezone resolution: `--timezone` flag, Calendar Settings API, 24h cache           |

## Demo Videos

Demo recordings are generated with [VHS](https://github.com/charmbracelet/vhs) (`.tape` files).

```bash
vhs docs/demo.tape
```

### VHS quoting rules

- Use **double quotes** for simple strings: `Type "gws --help" Enter`
- Use **backtick quotes** when the typed text contains JSON with double quotes:
  ```
  Type `gws drive files list --params '{"pageSize":5}'` Enter
  ```
  `\"` escapes inside double-quoted `Type` strings are **not supported** by VHS and will cause parse errors.

### Scene art

ASCII art title cards live in `art/`. The `scripts/show-art.sh` helper clears the screen and cats the file. Portrait scenes use `scene*.txt`; landscape chapters use `long-*.txt`.

## Input Validation & URL Safety

> [!IMPORTANT]
> This CLI is frequently invoked by AI/LLM agents. Always assume inputs can be adversarial — validate paths against traversal (`../../.ssh`), restrict format strings to allowlists, reject control characters, and encode user values before embedding them in URLs.

> [!NOTE]
> **Environment variables are trusted inputs.** The validation rules above apply to **CLI arguments** that may be passed by untrusted AI agents. Environment variables (e.g. `GOOGLE_WORKSPACE_CLI_CONFIG_DIR`) are set by the user themselves — in their shell profile, `.env` file, or deployment config — and are not subject to path traversal validation. This is consistent with standard conventions like `XDG_CONFIG_HOME`, `CARGO_HOME`, etc.

### Path Safety (`src/validate.rs`)

When adding new helpers or CLI flags that accept file paths, **always validate** using the shared helpers:

| Scenario                               | Validator                                | Rejects                                                              |
| -------------------------------------- | ---------------------------------------- | -------------------------------------------------------------------- |
| File path for writing (`--output-dir`) | `validate::validate_safe_output_dir()`   | Absolute paths, `../` traversal, symlinks outside CWD, control chars |
| File path for reading (`--dir`)        | `validate::validate_safe_dir_path()`     | Absolute paths, `../` traversal, symlinks outside CWD, control chars |
| Enum/allowlist values (`--msg-format`) | clap `value_parser` (see `gmail/mod.rs`) | Any value not in the allowlist                                       |

```rust
// In your argument parser:
if let Some(output_dir) = matches.get_one::<String>("output-dir") {
    crate::validate::validate_safe_output_dir(output_dir)?;
    builder.output_dir(Some(output_dir.clone()));
}
```

### URL Encoding (`src/helpers/mod.rs`)

User-supplied values embedded in URL **path segments** must be percent-encoded. Use the shared helper:

```rust
// CORRECT — encodes slashes, spaces, and special characters
let url = format!(
    "https://www.googleapis.com/drive/v3/files/{}",
    crate::helpers::encode_path_segment(file_id),
);

// WRONG — raw user input in URL path
let url = format!("https://www.googleapis.com/drive/v3/files/{}", file_id);
```

For **query parameters**, use reqwest's `.query()` builder which handles encoding automatically:

```rust
// CORRECT — reqwest encodes query values
client.get(url).query(&[("q", user_query)]).send().await?;

// WRONG — manual string interpolation in query strings
let url = format!("{}?q={}", base_url, user_query);
```

### Resource Name Validation (`src/helpers/mod.rs`)

When a user-supplied string is used as a GCP resource identifier (project ID, topic name, space name, etc.) that gets embedded in a URL path, validate it first:

```rust
// Validates the string does not contain path traversal segments (`..`), control characters, or URL-breaking characters like `?` and `#`.
let project = crate::helpers::validate_resource_name(&project_id)?;
let url = format!("https://pubsub.googleapis.com/v1/projects/{}/topics/my-topic", project);
```

This prevents injection of query parameters, path traversal, or other malicious payloads through resource name arguments like `--project` or `--space`.

### Checklist for New Features

When adding a new helper or CLI command:

1. **File paths** → Use `validate_safe_output_dir` / `validate_safe_dir_path`
2. **Enum flags** → Constrain via clap `value_parser` or `validate_msg_format`
3. **URL path segments** → Use `encode_path_segment()`
4. **Query parameters** → Use reqwest `.query()` builder
5. **Resource names** (project IDs, space names, topic names) → Use `validate_resource_name()`
6. **Write tests** for both the happy path AND the rejection path (e.g., pass `../../.ssh` and assert `Err`)

## PR Labels

Use these labels to categorize pull requests and issues:

- `area: discovery` — Discovery document fetching, caching, parsing
- `area: http` — Request execution, URL building, response handling
- `area: docs` — README, contributing guides, documentation
- `area: tui` — Setup wizard, picker, input fields
- `area: distribution` — Nix flake, cargo-dist, npm packaging, install methods
- `area: auth` — OAuth, credentials, multi-account, ADC
- `area: skills` — AI skill generation and management

## Environment Variables

### Authentication

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_TOKEN` | Pre-obtained OAuth2 access token (highest priority; bypasses all credential file loading) |
| `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` | Path to OAuth credentials JSON (no default; if unset, falls back to encrypted credentials in `~/.config/gws/`) |
| `GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND` | Keyring backend: `keyring` (default, uses OS keyring with file fallback) or `file` (file only, for Docker/CI/headless) |

| `GOOGLE_APPLICATION_CREDENTIALS` | Standard Google ADC path; used as fallback when no gws-specific credentials are configured |

### Configuration

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` | Override the config directory (default: `~/.config/gws`) |

### OAuth Client

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_CLIENT_ID` | OAuth client ID (for `gws auth login` when no `client_secret.json` is saved) |
| `GOOGLE_WORKSPACE_CLI_CLIENT_SECRET` | OAuth client secret (paired with `CLIENT_ID` above) |

### Sanitization (Model Armor)

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE` | Default Model Armor template (overridden by `--sanitize` flag) |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_MODE` | `warn` (default) or `block` |

### Helpers

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_PROJECT_ID` | GCP project ID override for quota/billing and fallback for helper commands (overridden by `--project` flag) |

### Logging

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_LOG` | Log level filter for stderr output (e.g., `gws=debug`). Off by default. |
| `GOOGLE_WORKSPACE_CLI_LOG_FILE` | Directory for JSON-line log files with daily rotation. Off by default. |

All variables can also live in a `.env` file (loaded via `dotenvy`).


================================================
FILE: CHANGELOG.md
================================================
# @googleworkspace/cli

## 0.18.1

### Patch Changes

- a87037b: Handle SIGTERM in `gws gmail +watch` and `gws events +subscribe` for clean container shutdown.

  Long-running pull loops now exit gracefully on SIGTERM (in addition to Ctrl+C),
  enabling clean shutdown under Kubernetes, Docker, and systemd.

## 0.18.0

### Minor Changes

- 908cf73: feat(gmail): auto-populate From header with display name from send-as settings

  Fetch the user's send-as identities to set the From header with a display name in all mail helpers (+send, +reply, +reply-all, +forward), matching Gmail web client behavior. Also enriches bare `--from` emails with their configured display name.

- 6e4daaf: Gmail helpers rollup: mail-builder migration, --attach flag (upload endpoint), +read helper

  - Migrate `+send`, `+reply`, `+reply-all`, and `+forward` to the `mail-builder` crate for RFC-compliant MIME construction
  - Add `--from` flag to `+send` for send-as alias support
  - Add `-a`/`--attach` flag to all mail helpers (`+send`, `+reply`, `+reply-all`, `+forward`) with `mime_guess2` auto-detection, 25MB size validation, and upload endpoint support (35MB API limit vs 5MB metadata-only)
  - Add `+read` helper to extract message body and headers (text, HTML, or JSON output)
  - Make `OriginalMessage.thread_id` optional (`Option<String>`) for draft compatibility
  - RFC 2822 display name quoting is handled natively by `mail-builder`
  - Introduce `UploadSource` enum in executor for type-safe upload strategies

### Patch Changes

- 1e90380: fix(gmail): remove dead `--attachment` arg from `+send`

  The `+send` subcommand defined a duplicate `"attachment"` arg alongside the
  `"attach"` arg already provided by `common_mail_args`. Since `parse_attachments`
  reads `"attach"`, the `--attachment` flag was silently ignored. Removed the
  dead duplicate.

- 908cf73: fix(gmail): handle reply-all to own message correctly

  Reply-all to a message you sent no longer errors with "No To recipient remains." The original To recipients are now used as reply targets, matching Gmail web client behavior.

- 2e909ae: Consolidate terminal sanitization, coloring, and output helpers into a new `output.rs` module. Fixes raw ANSI escape codes in `watch.rs` that bypassed `NO_COLOR` and TTY detection, upgrades `sanitize_for_terminal` to also strip dangerous Unicode characters (bidi overrides, zero-width spaces, directional isolates), and sanitizes previously raw API error body and user query outputs.

## 0.17.0

### Minor Changes

- 1b0a21f: feat: support google meet video conferencing in calendar +insert

### Patch Changes

- 811fe7b: Fix critical security vulnerability (TOCTOU/Symlink race) in atomic file writes.

  The atomic_write and atomic_write_async utilities now use:

  - Randomized temporary filenames to prevent predictability.
  - O_EXCL creation flags to prevent following pre-existing symlinks.
  - Strict 0600 permissions from the moment of file creation on Unix systems.
  - Redundant post-write permission calls have been removed to close race windows.

- b241a5b: fix(security): cap Retry-After sleep, sanitize upload mimeType, and validate --upload/--output paths
- 6f92e5b: Stderr/output hygiene rollup: route diagnostics to stderr, add colored error labels, propagate auth errors.

  - **triage.rs**: "No messages found" sent to stderr so stdout stays valid JSON for pipes
  - **modelarmor.rs**: response body printed only on success; error message now includes body for diagnostics
  - **error.rs**: colored `error[variant]:` labels on stderr (respects `NO_COLOR` env var), `hint:` prefix for accessNotConfigured guidance
  - **calendar, chat, docs, drive, script, sheets**: auth failures now propagate as `GwsError::Auth` instead of silently proceeding unauthenticated (dry-run still works without auth)

- 398e80c: Sync generated skills with latest Google Discovery API specs
- 8458104: Extend input validation to reject dangerous Unicode characters (zero-width chars, bidi overrides, Unicode line/paragraph separators) that were not caught by the previous ASCII-range check

## 0.16.0

### Minor Changes

- 47afe5f: Use Google account timezone instead of machine-local time for day-boundary calculations in calendar and workflow helpers. Adds `--timezone` flag to `+agenda` for explicit override. Timezone is fetched from Calendar Settings API and cached for 24 hours.

### Patch Changes

- c61b9cb: fix(gmail): RFC 2047 encode non-ASCII display names in To/From/Cc/Bcc headers

  Fixes mojibake when sending emails to recipients with non-ASCII display names (e.g. Japanese, Spanish accented characters). The new `encode_address_header()` function parses mailbox lists, encodes only the display-name portion via RFC 2047 Base64, and leaves email addresses untouched.

## 0.15.0

### Minor Changes

- 6f3e090: Add opt-in structured HTTP request logging via `tracing`

  New environment variables:

  - `GOOGLE_WORKSPACE_CLI_LOG`: stderr log filter (e.g., `gws=debug`)
  - `GOOGLE_WORKSPACE_CLI_LOG_FILE`: directory for JSON log files with daily rotation

  Logging is completely silent by default (zero overhead). Only PII-free metadata is logged: API method ID, HTTP method, status code, latency, and content-type.

## 0.14.0

### Minor Changes

- dc561e0: Add `--upload-content-type` flag and smart MIME inference for multipart uploads

  Previously, multipart uploads used the metadata `mimeType` field for both the Drive
  metadata and the media part's `Content-Type` header. This made it impossible to upload
  a file in one format (e.g. Markdown) and have Drive convert it to another (e.g. Google Docs),
  because the media `Content-Type` and the target `mimeType` must differ for import conversions.

  The new `--upload-content-type` flag allows setting the media `Content-Type` explicitly.
  When omitted, the media type is now inferred from the file extension before falling back
  to the metadata `mimeType`. This matches Google Drive's model where metadata `mimeType`
  is the _target_ type (what the file should become) while the media `Content-Type` is the
  _source_ type (what the bytes are).

  This means import conversions now work automatically:

  ```bash
  # Extension inference detects text/markdown → conversion just works
  gws drive files create \
    --json '{"name":"My Doc","mimeType":"application/vnd.google-apps.document"}' \
    --upload notes.md

  # Explicit flag still available as an override
  gws drive files create \
    --json '{"name":"My Doc","mimeType":"application/vnd.google-apps.document"}' \
    --upload notes.md \
    --upload-content-type text/markdown
  ```

### Patch Changes

- 945ac91: Stream multipart uploads to avoid OOM on large files. File content is now streamed in chunks via `ReaderStream` instead of being read entirely into memory, reducing memory usage from O(file_size) to O(64 KB).

## 0.13.3

### Patch Changes

- 8ef27a2: fix(calendar): use local timezone for agenda day boundaries instead of UTC
- 4d7b420: Fix `+append --json-values` flattening multi-row arrays into a single row by preserving the `Vec<Vec<String>>` row structure through to the API request body
- bb94016: fix(security): validate space name in chat +send to prevent path traversal
- 4b827cd: chore: fix maintainer email typo in flake.nix and harden coverage.sh
- 44767ed: Map People service to `contacts` and `directory` scope prefixes so `gws auth login -s people` includes the required OAuth scopes
- 8fce003: fix(docs): correct flag names in recipes (--spreadsheet-id, --attendees, --duration)
- 21b1840: Expose `repeated: true` in `gws schema` output and expand JSON arrays into repeated query parameters for `repeated` fields
- 1346d47: Sync generated skills with latest Google Discovery API specs
- 957b999: test(gmail): add unit tests for +triage argument parsing and format selection

## 0.13.2

### Patch Changes

- 3dcf818: Refresh OAuth access tokens for long-running Gmail watch and Workspace Events subscribe helpers before each Pub/Sub and Gmail request.
- 86ea6de: Validate `--subscription` resource name in `gmail +watch` and deduplicate `PUBSUB_API_BASE` constant.

## 0.13.1

### Patch Changes

- 510024f: Centralize token cache filenames as constants and support ServiceAccount credentials at the default plaintext path
- 510024f: Auto-recover from stale encrypted credentials after upgrade: remove undecryptable `credentials.enc` and fall through to other credential sources (plaintext, ADC) instead of hard-erroring. Also sync encryption key file backup when keyring has key but file is missing.
- e104106: Add shell tips section to gws-shared skill warning about zsh `!` history expansion, and replace single quotes with double quotes around sheet ranges containing `!` in recipes and skill examples

## 0.13.0

### Minor Changes

- 9d937af: Add `--html` flag to `+send`, `+reply`, `+reply-all`, and `+forward` for HTML email composition.

### Patch Changes

- 2df32ee: Document helper commands (`+` prefix) in README

  Adds a "Helper Commands" section to the Advanced Usage chapter explaining
  the `+` prefix convention, listing all 24 helper commands across 10 services
  with descriptions and usage examples.

## 0.12.0

### Minor Changes

- 247e27a: Add structured exit codes for scriptable error handling

  `gws` now exits with a type-specific code instead of always using `1`:

  | Code | Meaning                                                         |
  | ---- | --------------------------------------------------------------- |
  | `0`  | Success                                                         |
  | `1`  | API error — Google returned a 4xx/5xx response                  |
  | `2`  | Auth error — credentials missing, expired, or invalid           |
  | `3`  | Validation error — bad arguments, unknown service, invalid flag |
  | `4`  | Discovery error — could not fetch the API schema document       |
  | `5`  | Internal error — unexpected failure                             |

  Exit codes are documented in `gws --help` and in the README.

### Patch Changes

- 087066f: Fix `gws auth login` encrypted credential persistence by enabling native keyring backends for the `keyring` crate on supported desktop platforms instead of silently falling back to the in-memory mock store.

## 0.11.1

### Patch Changes

- adbca87: Fix `--format csv` for array-of-arrays responses (e.g. Sheets values API)

## 0.11.0

### Minor Changes

- 4d4b09f: Add `--cc` and `--bcc` flags to `+send`, `--to` and `--bcc` to `+reply` and `+reply-all`, and `--bcc` to `+forward`.

## 0.10.0

### Minor Changes

- 8d89325: Add `GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND` env var for explicit keyring backend selection (`keyring` or `file`). Fixes credential key loss in Docker/keyring-less environments by never deleting `.encryption_key` and always persisting it as a fallback.

### Patch Changes

- 06aa698: fix(auth): dynamically fetch scopes from Discovery docs when `-s` specifies services not in static scope lists
- 06aa698: fix(auth): format extract_scopes_from_doc and deduplicate dynamic scopes
- 5e7d120: Bring `+forward` behavior in line with Gmail's web UI: keep the forward in the sender's original thread, add a blank line between the forwarded message metadata and body, and remove the spurious closing delimiter.
- 2782cf1: Fix gmail +triage 403 error by using gmail.readonly scope instead of gmail.modify to avoid conflict with gmail.metadata scope that does not support the q parameter

## 0.9.1

### Patch Changes

- 5872dbe: Stop persisting encryption key to `.encryption_key` file when OS keyring is available. Existing file-based keys are migrated into the keyring and the file is removed on next CLI invocation.

## 0.9.0

### Minor Changes

- 7d15365: feat(gmail): add +reply, +reply-all, and +forward helpers

  Adds three new Gmail helper commands:

  - `+reply` -- reply to a message with automatic threading
  - `+reply-all` -- reply to all recipients with --remove/--cc support
  - `+forward` -- forward a message to new recipients

### Patch Changes

- 08716f8: Fix garbled non-ASCII email subjects in `gmail +send` by RFC 2047 encoding the Subject header and adding MIME-Version/Content-Type headers.
- f083eb9: Improve `gws auth setup` project creation failures in step 3:
  - Detect Google Cloud Terms of Service precondition failures and show actionable guidance (`gcloud auth list`, account verification, Console ToS URL).
  - Detect invalid project ID format / already-in-use errors and show clearer guidance.
  - In interactive setup, keep the wizard open and re-prompt for a new project ID instead of exiting immediately on create failures.
- 789e7f1: Switch reqwest TLS from bundled Mozilla roots to native OS certificate store

  This allows the CLI to trust custom or corporate CA certificates installed
  in the system trust store, fixing TLS errors in enterprise environments.

## 0.8.1

### Patch Changes

- 4d41e52: Prioritize local project configuration and `GOOGLE_WORKSPACE_PROJECT_ID` over global Application Default Credentials (ADC) for quota attribution. This fixes 403 errors when the Drive API is disabled in a global gcloud project but enabled in the project configured for gws.

## 0.8.0

### Minor Changes

- dd3fc90: Remove `mcp` command

## 0.7.0

### Minor Changes

- e1505af: Remove multi-account, domain-wide delegation, and impersonation support. Removes `gws auth list`, `gws auth default`, `--account` flag, `GOOGLE_WORKSPACE_CLI_ACCOUNT` and `GOOGLE_WORKSPACE_CLI_IMPERSONATED_USER` env vars.

### Patch Changes

- 54b3b31: Move x-goog-user-project header from default client headers to API request builder, fixing Discovery Document fetches failing with 403 when the quota project lacks certain APIs enabled

## 0.6.3

### Patch Changes

- 322529d: Document all environment variables and enable GOOGLE_WORKSPACE_CLI_CONFIG_DIR in release builds
- 2173a92: Send x-goog-user-project header when using ADC with a quota_project_id
- 1f47420: fix: extract CLA label job into dedicated workflow to prevent feedback loop

  The Automation workflow's `check_run: [completed]` trigger caused a feedback
  loop — every workflow completion fired a check_run event, re-triggering
  Automation, which produced another check_run event, and so on. Moving the
  CLA label job to its own `cla.yml` workflow eliminates the trigger from
  Automation entirely.

- 132c3b1: fix: warn on credential file permission failures instead of ignoring

  Replaced silent `let _ =` on `set_permissions` calls in `save_encrypted`
  with `eprintln!` warnings so users are aware if their credential files
  end up with insecure permissions. Also log keyring access failures
  instead of silently falling through to file storage.

- a2cc523: Add `x86_64-unknown-linux-musl` build target for Linux musl/static binary support
- c86b964: Fix multi-account selection: MCP server now respects `GOOGLE_WORKSPACE_CLI_ACCOUNT` env var (#221), and `--account` flag before service name no longer causes parse errors (#181)
- ff53538: Fix scope selection to use first (broadest) scope instead of all method scopes, preventing gmail.metadata restrictions from blocking query parameters
- c80eb52: Replace strip_suffix(".readonly").unwrap() with unwrap_or fallback

  Two call sites used `.strip_suffix(".readonly").unwrap()` which would
  panic if a scope URL marked as `is_readonly` didn't actually end with
  ".readonly". While the current data makes this unlikely, using
  `unwrap_or` is a defensive improvement that prevents potential panics
  from inconsistent discovery data.

- 9a780d7: Log token cache decryption/parse errors instead of silently swallowing

  Previously, `load_from_disk` used four nested `if let Ok` blocks that
  silently returned an empty map on any failure. When the encryption key
  changed or the cache was corrupted, tokens silently stopped loading and
  users were forced to re-authenticate with no explanation.

  Now logs specific warnings to stderr for decryption failures, invalid
  UTF-8, and JSON parse errors, with a hint to re-authenticate.

- 6daf90d: Fix MCP tool schemas to conditionally include `body`, `upload`, and `page_all` properties only when the underlying Discovery Document method supports them. `body` is included only when a request body is defined, `upload` only when `supportsMediaUpload` is true, and `page_all` only when the method has a `pageToken` parameter. Also drops empty `body: {}` objects that LLMs commonly send on GET methods, preventing 400 errors from Google APIs.

## 0.6.2

### Patch Changes

- 28fa25a: Clean up nits from PR #175 auth fix

  - Update stale docstring on `resolve_account` to match new fallthrough behavior
  - Add breadcrumb comment on string-based error matching in `main.rs`
  - Move identity scope injection before authenticator build for readability

## 0.6.1

### Patch Changes

- 88cb65c: chore: add automation workflow for auto-fmt, CLA labeling, and file-based PR triage
- a926e3f: Fix auth failures when accounts.json registry is missing

  Three related bugs caused all API calls to fail with "Access denied. No credentials provided" even after a successful `gws auth login`:

  1. `resolve_account()` rejected valid `credentials.enc` as "legacy" when `accounts.json` was absent, instead of using them.
  2. `main.rs` silently swallowed all auth errors, masking real failures behind a generic message.
  3. `auth login` didn't include `openid`/`email` scopes, so `fetch_userinfo_email()` couldn't identify the user, causing credentials to be saved without an `accounts.json` entry.

- cb1f988: Add Content-Length: 0 header for POST/PUT/PATCH requests with no body to fix HTTP 411 errors
- 3d59b2e: fix: isolate flaky auth tests from host ADC credentials

## 0.6.0

### Minor Changes

- b38b760: Add Application Default Credentials (ADC) support.

  `gws` now discovers ADC as a fourth credential source, after the encrypted
  and plaintext credential files. The lookup order is:

  1. `GOOGLE_WORKSPACE_CLI_TOKEN` env var (raw access token, highest priority)
  2. `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` env var
  3. Encrypted credentials (`~/.config/gws/credentials.enc`)
  4. Plaintext credentials (`~/.config/gws/credentials.json`)
  5. **ADC** — `GOOGLE_APPLICATION_CREDENTIALS` env var (hard error if file missing), then
     `~/.config/gcloud/application_default_credentials.json` (silent if absent)

  This means `gcloud auth application-default login --client-id-file=client_secret.json`
  is now a fully supported auth flow — no need to run `gws auth login` separately.
  Both `authorized_user` and `service_account` ADC formats are supported.

## 0.5.0

### Minor Changes

- 9cf6e0e: Add `--tool-mode compact|full` flag to `gws mcp`. Compact mode exposes one tool per service plus a `gws_discover` meta-tool, reducing context window usage from 200-400 tools to ~26.

### Patch Changes

- 0a16d0b: Add `-s`/`--services` flag to `gws auth login` to filter the scope picker
  by service name (e.g. `-s drive,gmail,sheets`). Also expands the workspace
  admin scope blocklist to include `chat.admin.*` and `classroom.*` patterns.
- 5205467: fix(setup): drain stale keypresses between TUI screen transitions

## 0.4.4

### Patch Changes

- e1e08eb: Fix highlight color on light terminal themes by using reverse video instead of a dark-gray background

## 0.4.3

### Patch Changes

- fc6bc95: Exclude Workspace-admin-only scopes from the "Recommended" scope preset.

  Scopes that require Google Workspace domain-admin access (`apps.*`,
  `cloud-identity.*`, `ediscovery`, `directory.readonly`, `groups`) now return
  `400 invalid_scope` when used by personal `@gmail.com` accounts. These scopes
  are no longer included in the "Recommended" template, preventing login failures
  for non-Workspace users.

  Workspace admins can still select these scopes manually via the "Full Access"
  template or by picking them individually in the scope picker.

  Adds a new `is_workspace_admin_scope()` helper (mirroring the existing
  `is_app_only_scope()`) that centralises this detection logic.

- 2aa6084: docs: Comprehensive README overhaul addressing user feedback.

  Added a Prerequisites section prior to the Quick Start to highlight the optional `gcloud` dependency.
  Expanded the Authentication section with a decision matrix to help users choose the correct authentication path.
  Added prominent warnings about OAuth "testing mode" limitations (the 25-scope cap) and the strict requirement to explicitly add the authorizing account as a "Test user" (#130).
  Added a dedicated Troubleshooting section detailing fixes for common OAuth consent errors, "Access blocked" issues, and `redirect_uri_mismatch` failures.
  Included shell escaping examples for Google Sheets A1 notation (`!`).
  Clarified the `npm` installation rationale and added explicit links to pre-built native binaries on GitHub Releases.

## 0.4.2

### Patch Changes

- d3e90e4: fix: use ~/.config/gws on all platforms for consistent config path

  Previously used `dirs::config_dir()` which resolves to different paths per OS
  (e.g. ~/Library/Application Support/gws on macOS, %APPDATA%\gws on Windows),
  contradicting the documented ~/.config/gws/ path. Now uses ~/.config/gws/
  everywhere with a fallback to the legacy OS-specific path for existing installs.

## 0.4.1

### Patch Changes

- dbda001: Add "Enter project ID manually" option to project picker in `gws auth setup`.

  Users with large numbers of GCP projects often hit the 10-second listing timeout.
  The picker now includes a "⌨ Enter project ID manually" item so users can type a
  known project ID directly without waiting for `gcloud projects list` to complete.

## 0.4.0

### Minor Changes

- 87e4bb1: Add Linux ARM64 build targets (aarch64-unknown-linux-gnu and aarch64-unknown-linux-musl) to cargo-dist, enabling prebuilt binaries for ARM64 Linux users via npm, the shell installer, and GitHub Releases.
- d1825f9: ### Multi-Account Support

  Add support for managing multiple Google accounts with per-account credential storage.

  **New features:**

  - `--account EMAIL` global flag available on every command
  - `GOOGLE_WORKSPACE_CLI_ACCOUNT` environment variable as fallback
  - `gws auth login --account EMAIL` — associates credentials with a specific account
  - `gws auth list` — lists all registered accounts
  - `gws auth default EMAIL` — sets the default account
  - `gws auth logout --account EMAIL` — removes a specific account
  - `login_hint` in OAuth URL for automatic account pre-selection in browser
  - Email validation via Google userinfo endpoint after OAuth flow

  **Breaking change:** Existing users must run `gws auth login` again after upgrading. The credential storage format has changed from a single `credentials.enc` to per-account files (`credentials.<b64-email>.enc`) with an `accounts.json` registry.

### Patch Changes

- a6994ad: Filter out `apps.alerts` scopes from user OAuth login flow since they require service account with domain-wide delegation
- 1ad4f34: fix: replace unwrap() calls with proper error handling in MCP server

  Replaced four `unwrap()` calls in `mcp_server.rs` that could panic the MCP
  server process with graceful error handling. Also added a warning log when
  authentication silently falls back to unauthenticated mode.

- a1be14f: fix: drain stdout pipe to prevent project listing timeout during auth setup

  Fixed `gws auth setup` timing out at step 3 (GCP project selection) for users
  with many projects. The `gcloud projects list` stdout pipe was only read after
  the child process exited, causing a deadlock when output exceeded the OS pipe
  buffer (~64 KB). Stdout is now drained in a background thread to prevent the
  pipe from filling up.

- 364542b: fix: reject DEL character (0x7F) in input validation

  The `reject_control_chars` helper rejected bytes 0x00–0x1F but allowed
  the DEL character (0x7F), which is also an ASCII control character. This
  could allow malformed input from LLM agents to bypass validation.

- 75cec1b: Fix URL template expansion so media upload endpoints substitute path parameters and avoid iterative replacement side effects.
- ed409e3: Harden URL and path construction across helper modules (gmail/watch, modelarmor, discovery)
- 263a8e5: fix: use gcloud.cmd on Windows and show platform-correct config paths

  On Windows, gcloud is installed as `gcloud.cmd` which Rust's `Command`
  cannot find without the extension. Also replaced hardcoded `~/.config/gws/`
  in error messages with the actual platform-resolved path.

## 0.3.5

### Patch Changes

- 4bca693: fix: credential masking panic and silent token write errors

  Fixed `gws auth export` masking which panicked on short strings and showed
  the entire secret instead of masking it. Also fixed silent token cache write
  failures in `save_to_disk` that returned `Ok(())` even when the write failed.

- f84ce37: Fix URL template path expansion to safely encode path parameters, including
  Sheets `range` values with Unicode and reserved characters. `{var}` expansions
  now encode as a path segment, `{+var}` preserves slashes while encoding each
  segment, and invalid path parameter/template mismatches fail fast.
- eb0347a: fix: correct author email typo in package.json
- 70d0cdd: Fix Slides presentations.get failure caused by flatPath placeholder mismatch

  When a Discovery Document's `flatPath` uses placeholder names that don't match
  the method's parameter names (e.g., `{presentationsId}` vs `presentationId`),
  `build_url` now falls back to the `path` field which uses RFC 6570 operators
  that resolve correctly.

  Fixes #118

- 37ab483: Add flake.nix for nix & NixOS installs
- 1991d53: Add prominent disclaimer that this is not an officially supported Google product to README, --help, and --version output

## 0.3.4

### Patch Changes

- 704928b: fix(setup): enable APIs individually and surface gcloud errors

  Previously `gws auth setup` used a single batch `gcloud services enable` call
  for all Workspace APIs. If any one API failed, the entire batch was marked as
  failed and stderr was silently discarded. APIs are now enabled individually and
  in parallel, with error messages surfaced to the user.

## 0.3.3

### Patch Changes

- 92e66a3: Add `gws version` as a bare subcommand alongside `gws --version` and `gws -V`

## 0.3.2

### Patch Changes

- 8fadbd6: Smarter truncation of method and resource descriptions from discovery docs. Descriptions now truncate at sentence boundaries when possible, fall back to word boundaries with an ellipsis, and strip markdown links to reclaim character budget. Fixes #64.

## 0.3.1

### Patch Changes

- b3669e0: Add hourly cron to generate-skills workflow to auto-sync skills with upstream Google Discovery API changes via PR
- e8d533e: Add workflow to publish OpenClaw skills to ClawHub
- 3b38c8d: Sync generated skills with latest Google Discovery API specs

## 0.3.0

### Minor Changes

- 670267f: feat: add `gws mcp` Model Context Protocol server

  Adds a new `gws mcp` subcommand that starts an MCP server over stdio,
  exposing Google Workspace APIs as structured tools to any MCP-compatible
  client (Claude Desktop, Gemini CLI, VS Code, etc.).

### Patch Changes

- 8c1042a: Fix x-goog-api-client header format to use `gl-rust/gws-<version>`
- 3de9762: Fix docs: `gws setup` → `gws auth setup` (fixes #56, #57)

## 0.2.2

### Patch Changes

- f281797: docs(auth): add manual Google Cloud OAuth client setup and browser-assisted login guidance

  Adds step-by-step guidance for creating a Desktop OAuth client in Google Cloud Console,
  where to place `client_secret.json`, and how humans/agents can complete browser consent
  (including unverified app and scope-selection prompts).

- ee2e216: Narrow default OAuth scopes to avoid `Error 403: restricted_client` on unverified apps and add a `--full` flag for broader access (fixes #25). Replace the cryptic non-interactive setup error with actionable step-by-step OAuth console instructions (fixes #24).
- de2787e: feat(error): detect disabled APIs and guide users to enable them

  When the Google API returns a 403 `accessNotConfigured` error (i.e., the
  required API has not been enabled for the GCP project), `gws` now:

  - Extracts the GCP Console enable URL from the error message body.
  - Prints the original error JSON to stdout (machine-readable, unchanged shape
    except for an optional new `enable_url` field added to the error object).
  - Prints a human-readable hint with the direct enable URL to stderr, along
    with instructions to retry after enabling.

  This prevents a dead-end experience where users see a raw 403 JSON blob
  with no guidance. The JSON output is backward-compatible; only an optional
  `enable_url` field is added when the URL is parseable from the message.

  Fixes #31

- 9935dde: ci: auto-generate and commit skills on PR branch pushes
- 4b868c7: docs: add community guidance to gws-shared skill and gws --help output

  Encourages agents and users to star the repository and directs bug reports
  and feature requests to GitHub Issues, with guidance to check for existing
  issues before opening new ones.

- 0603bce: fix: atomic credential file writes to prevent corruption on crash or Ctrl-C
- 666f9a8: fix(auth): support --help / -h flag on auth subcommand
- bcd2401: fix: flatten nested objects in table output and fix multi-byte char truncation panic
- ee35e4a: fix: warn to stderr when unknown --format value is provided
- e094b02: fix: YAML block scalar for strings with `#`/`:`, and repeated CSV/table headers with `--page-all`

  **Bug 1 — YAML output: `drive#file` rendered as block scalar**

  Strings containing `#` or `:` (e.g. `drive#file`, `https://…`) were
  incorrectly emitted as YAML block scalars (`|`), producing output like:

  ```yaml
  kind: |
    drive#file
  ```

  Block scalars add an implicit trailing newline which changes the string
  value and produces invalid-looking output. The fix restricts block
  scalar to strings that genuinely contain newlines; all other strings
  are double-quoted, which is safe for any character sequence.

  **Bug 2 — `--page-all` with `--format csv` / `--format table` repeats headers**

  When paginating with `--page-all`, each page printed its own header row,
  making the combined output unusable for downstream processing:

  ```
  id,kind,name          ← page 1 header
  1,drive#file,foo.txt
  id,kind,name          ← page 2 header (unexpected!)
  2,drive#file,bar.txt
  ```

  Column headers (and the table separator line) are now emitted only for
  the first page; continuation pages contain data rows only.

- 173d155: fix: add YAML document separators (---) when paginating with --page-all --format yaml
- 214fc18: ci: skip smoketest on fork pull requests

## 0.2.1

### Patch Changes

- 6ae7427: fix(auth): stabilize encrypted credential key fallback across sessions

  When the OS keyring returned `NoEntry`, the previous code could generate
  a fresh random key on each process invocation instead of reusing one.
  This caused `credentials.enc` written by `gws auth login` to be
  unreadable by subsequent commands.

  Changes:

  - Always prefer an existing `.encryption_key` file before generating a new key
  - When generating a new key, persist it to `.encryption_key` as a stable fallback
  - Best-effort write new keys into the keyring as well
  - Fix `OnceLock` race: return the already-cached key if `set` loses a race

  Fixes #27

## 0.2.0

### Minor Changes

- b0d0b95: Add workflow helpers, personas, and 50 consumer-focused recipes

  - Add `gws workflow` subcommand with 5 built-in helpers: `+standup-report`, `+meeting-prep`, `+email-to-task`, `+weekly-digest`, `+file-announce`
  - Add 10 agent personas (exec-assistant, project-manager, sales-ops, etc.) with curated skill sets
  - Add `docs/skills.md` skills index and `registry/recipes.yaml` with 50 multi-step recipes for Gmail, Drive, Docs, Calendar, and Sheets
  - Update README with skills index link and accurate skill count
  - Fix lefthook pre-commit to run fmt and clippy sequentially

### Patch Changes

- 90adcb4: fix: percent-encode path parameters to prevent path traversal
- e71ce29: Fix Gemini extension installation issue by removing redundant authentication settings and update the documentation.
- 90adcb4: fix: harden input validation for AI/LLM callers

  - Add `src/validate.rs` with `validate_safe_output_dir`, `validate_msg_format`, and `validate_safe_dir_path` helpers
  - Validate `--output-dir` against path traversal in `gmail +watch` and `events +subscribe`
  - Validate `--msg-format` against allowlist (full, metadata, minimal, raw) in `gmail +watch`
  - Validate `--dir` against path traversal in `script +push`
  - Add clap `value_parser` constraint for `--msg-format`
  - Document input validation patterns in `AGENTS.md`

- 90adcb4: Security: Harden validate_resource_name and fix Gmail watch path traversal
- 90adcb4: Replace manual `urlencoded()` with reqwest `.query()` builder for safer URL encoding
- c11d3c4: Added test coverage for `EncryptedTokenStorage::new` initialization.
- 7664357: Add test for missing error path in load_client_config
- 90adcb4: fix: add shared URL safety helpers for path params (`encode_path_segment`, `validate_resource_name`)
- 90adcb4: fix: warn on stderr when API calls fail silently

## 0.1.5

### Patch Changes

- d29f41e: Fix README typography and spacing

## 0.1.4

### Patch Changes

- adb2cfa: Fix OAuth login failing with "no refresh token" error by decrypting the token cache before parsing and supporting the HashMap token format used by EncryptedTokenStorage
- d990dcc: Improve README branding by making the hero banner full-width.

## 0.1.3

### Patch Changes

- c714f4b: Fix npm package name to publish as @googleworkspace/cli instead of gws

## 0.1.2

### Patch Changes

- 3cd4d52: Fix release pipeline to sync Cargo.toml version with changesets and create git tags for private packages

## 0.1.1

### Patch Changes

- a0ad089: Speed up CI builds with Swatinem/rust-cache, sccache, and build artifact reuse for smoketests
- 30d929b: Optimize demo GIF and improve README


================================================
FILE: CLAUDE.md
================================================
When contributing to this repository, you must strictly follow all guidelines outlined in the AGENTS.md file.


================================================
FILE: CONTEXT.md
================================================
# Google Workspace CLI (`gws`) Context

The `gws` CLI provides dynamic access to Google Workspace APIs (Drive, Gmail, Calendar, Sheets, Admin, etc.) by parsing Discovery Documents at runtime.

## Rules of Engagement for Agents

* **Schema Discovery:** *If you don't know the exact JSON payload structure, run `gws schema <resource>.<method>` first to inspect the schema before executing.*
* **Context Window Protection:** *Workspace APIs (like Drive and Gmail) return massive JSON blobs. ALWAYS use field masks when listing or getting resources by appending `--params '{"fields": "id,name"}'` to avoid overwhelming your context window.*
* **Dry-Run Safety:** *Always use the `--dry-run` flag for mutating operations (create, update, delete) to validate your JSON payload before actual execution.*

## Core Syntax

```bash
gws <service> <resource> [sub-resource] <method> [flags]
```

Use `--help` to get help on the available commands.

```bash
gws --help
gws <service> --help
gws <service> <resource> --help
gws <service> <resource> <method> --help
```

### Key Flags

-   `--params '<JSON>'`: URL/query parameters (e.g., `id`, `q`, `pageSize`).
-   `--json '<JSON>'`: Request body for POST/PUT/PATCH methods.
-   `--page-all`: Auto-paginates results and outputs NDJSON (one JSON object per line).
-   `--fields '<MASK>'`: Limits the response fields (critical for AI context window efficiency).
-   `--upload <PATH>`: Files for multipart uploads (e.g., `drive files create`).
-   `--output <PATH>`: Destination for binary downloads (e.g., `drive files get`).
-   `--sanitize <TEMPLATE>`: Sanitizes output using Google Cloud Model Armor.

## Usage Patterns

### 1. Reading Data (GET/LIST)
Always use `--fields` to minimize tokens.

```bash
# List Drive files (efficient)
gws drive files list --params '{"q": "name contains \"Report\"", "pageSize": 10}' --fields "files(id,name,mimeType)"

# Get Gmail message details
gws gmail users messages get --params '{"userId": "me", "id": "MSG_123"}'
```

### 2. Writing Data (POST/PUT/PATCH)
Use `--json` for the request body.

```bash
# Send Email
gws gmail users messages send --params '{"userId": "me"}' --json '{"raw": "BASE64..."}'

# Create Spreadsheet
gws sheets spreadsheets create --json '{"properties": {"title": "Q4 Budget"}}'
```

### 3. Pagination (NDJSON)
Use `--page-all` for listing large collections. The output is Newline Delimited JSON.

```bash
# Stream all users
gws admin users list --params '{"domain": "example.com"}' --page-all
```

### 4. Schema Introspection
If unsure about parameters or body structure, check the schema:

```bash
gws schema drive.files.list
gws schema sheets.spreadsheets.create
```


================================================
FILE: Cargo.toml
================================================
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[package]
name = "gws"
version = "0.18.1"
edition = "2021"
description = "Google Workspace CLI — dynamic command surface from Discovery Service"
license = "Apache-2.0"
repository = "https://github.com/googleworkspace/cli"
homepage = "https://github.com/googleworkspace/cli"
readme = "README.md"
authors = ["Justin Poehnelt"]
keywords = ["cli", "google-workspace", "google", "drive", "gmail"]
categories = ["command-line-utilities", "web-programming"]

[[bin]]
name = "gws"
path = "src/main.rs"



[dependencies]
tempfile = "3"
aes-gcm = "0.10"
anyhow = "1"
clap = { version = "4", features = ["derive", "string"] }
dirs = "5"
dotenvy = "0.15"
hostname = "0.4"
reqwest = { version = "0.12", features = ["json", "stream", "rustls-tls-native-roots"], default-features = false }
rand = "0.8"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sha2 = "0.10"
thiserror = "2"
tokio = { version = "1", features = ["full"] }
yup-oauth2 = "12"
futures-util = "0.3"
tokio-util = { version = "0.7", features = ["io"] }
bytes = "1"
base64 = "0.22.1"
derive_builder = "0.20.2"
ratatui = "0.30.0"
crossterm = "0.29.0"
chrono = "0.4.44"
chrono-tz = "0.10"
iana-time-zone = "0.1"
mail-builder = "0.4"
async-trait = "0.1.89"
serde_yaml = "0.9.34"
percent-encoding = "2.3.2"
zeroize = { version = "1.8.2", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }
tracing-appender = "0.2"
uuid = { version = "1.22.0", features = ["v4", "v5"] }
mime_guess2 = "2.3.1"

[target.'cfg(target_os = "macos")'.dependencies]
keyring = { version = "3.6.3", features = ["apple-native"] }

[target.'cfg(target_os = "windows")'.dependencies]
keyring = { version = "3.6.3", features = ["windows-native"] }

[target.'cfg(not(any(target_os = "macos", target_os = "windows")))'.dependencies]
keyring = "3.6.3"


# The profile that 'cargo dist' will build with
[profile.dist]
inherits = "release"
lto = "thin"

[dev-dependencies]
serial_test = "3.4.0"


================================================
FILE: LICENSE
================================================

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: README.md
================================================
<h1 align="center">gws</h1>

**One CLI for all of Google Workspace — built for humans and AI agents.**<br>
Drive, Gmail, Calendar, and every Workspace API. Zero boilerplate. Structured JSON output. 40+ agent skills included.

> [!NOTE]
> This is **not** an officially supported Google product.

<p>
  <a href="https://www.npmjs.com/package/@googleworkspace/cli"><img src="https://img.shields.io/npm/v/@googleworkspace/cli" alt="npm version"></a>
  <a href="https://github.com/googleworkspace/cli/blob/main/LICENSE"><img src="https://img.shields.io/github/license/googleworkspace/cli" alt="license"></a>
  <a href="https://github.com/googleworkspace/cli/actions/workflows/ci.yml"><img src="https://img.shields.io/github/actions/workflow/status/googleworkspace/cli/ci.yml?branch=main&label=CI" alt="CI status"></a>
  <a href="https://www.npmjs.com/package/@googleworkspace/cli"><img src="https://img.shields.io/npm/unpacked-size/@googleworkspace/cli" alt="install size"></a>
</p>
<br>

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

`gws` doesn't ship a static list of commands. It reads Google's own [Discovery Service](https://developers.google.com/discovery) at runtime and builds its entire command surface dynamically. When Google Workspace adds an API endpoint or method, `gws` picks it up automatically.

> [!IMPORTANT]
> This project is under active development. Expect breaking changes as we march toward v1.0.

## Contents

- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Why gws?](#why-gws)
- [Authentication](#authentication)
- [AI Agent Skills](#ai-agent-skills)
- [Advanced Usage](#advanced-usage)
- [Environment Variables](#environment-variables)
- [Exit Codes](#exit-codes)
- [Architecture](#architecture)
- [Troubleshooting](#troubleshooting)
- [Development](#development)

## Prerequisites

- **Node.js 18+** — for `npm install` (or download a pre-built binary from [GitHub Releases](https://github.com/googleworkspace/cli/releases))
- **A Google Cloud project** — required for OAuth credentials. You can create one via the [Google Cloud Console](https://console.cloud.google.com/) or with the [`gcloud` CLI](https://cloud.google.com/sdk/docs/install) or with the `gws auth setup` command.
- **A Google account** with access to Google Workspace

## Installation

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

> The npm package bundles pre-built native binaries for your OS and architecture.
> No Rust toolchain required.

Pre-built binaries are also available on the [GitHub Releases](https://github.com/googleworkspace/cli/releases) page.

Or build from source:

```bash
cargo install --git https://github.com/googleworkspace/cli --locked
```

A Nix flake is also available at `github:googleworkspace/cli`

```bash
nix run github:googleworkspace/cli
```

On macOS and Linux, you can also install via [Homebrew](https://brew.sh/):

```bash
brew install googleworkspace-cli
```

## Quick Start

```bash
gws auth setup     # walks you through Google Cloud project config
gws auth login     # subsequent OAuth login
gws drive files list --params '{"pageSize": 5}'
```

## Why gws?

**For humans** — stop writing `curl` calls against REST docs. `gws` gives you `--help` on every resource, `--dry-run` to preview requests, and auto‑pagination.

**For AI agents** — every response is structured JSON. Pair it with the included agent skills and your LLM can manage Workspace without custom tooling.

```bash
# List the 10 most recent files
gws drive files list --params '{"pageSize": 10}'

# Create a spreadsheet
gws sheets spreadsheets create --json '{"properties": {"title": "Q1 Budget"}}'

# Send a Chat message
gws chat spaces messages create \
  --params '{"parent": "spaces/xyz"}' \
  --json '{"text": "Deploy complete."}' \
  --dry-run

# Introspect any method's request/response schema
gws schema drive.files.list

# Stream paginated results as NDJSON
gws drive files list --params '{"pageSize": 100}' --page-all | jq -r '.files[].name'
```

## Authentication

The CLI supports multiple auth workflows so it works on your laptop, in CI, and on a server.

### Which setup should I use?

| I have… | Use |
|---|---|
| `gcloud` installed and authenticated | [`gws auth setup`](#interactive-local-desktop) (fastest) |
| A GCP project but no `gcloud` | [Manual OAuth setup](#manual-oauth-setup-google-cloud-console) |
| An existing OAuth access token | [`GOOGLE_WORKSPACE_CLI_TOKEN`](#pre-obtained-access-token) |
| Existing Credentials | [`GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE`](#service-account-server-to-server) |

### Interactive (local desktop)

Credentials are encrypted at rest (AES-256-GCM) with the key stored in your OS keyring (or `~/.config/gws/.encryption_key` when `GOOGLE_WORKSPACE_CLI_KEYRING_BACKEND=file`).

```bash
gws auth setup       # one-time: creates a Cloud project, enables APIs, logs you in
gws auth login       # subsequent scope selection and login
```

> `gws auth setup` requires the [`gcloud` CLI](https://cloud.google.com/sdk/docs/install). If you don't have `gcloud`, use the [manual setup](#manual-oauth-setup-google-cloud-console) below instead.

> [!WARNING]
> **Scope limits in testing mode:** If your OAuth app is unverified (testing mode),
> Google limits consent to ~25 scopes. The `recommended` scope preset includes 85+
> scopes and **will fail** for unverified apps (especially for `@gmail.com` accounts).
> Choose individual services instead to filter the scope picker:
> ```bash
> gws auth login -s drive,gmail,sheets
> ```


### Manual OAuth setup (Google Cloud Console)

Use this when `gws auth setup` cannot automate project/client creation, or when you want explicit control.

1. Open Google Cloud Console in the target project:
   - OAuth consent screen: `https://console.cloud.google.com/apis/credentials/consent?project=<PROJECT_ID>`
   - Credentials: `https://console.cloud.google.com/apis/credentials?project=<PROJECT_ID>`
2. Configure OAuth branding/audience if prompted:
   - App type: **External** (testing mode is fine)
3. Add your account under **Test users**
4. Create an OAuth client:
   - Type: **Desktop app**
5. Download the client JSON and save it to:
   - `~/.config/gws/client_secret.json`

> [!IMPORTANT]
> **You must add yourself as a test user.** In the OAuth consent screen, click
> **Test users → Add users** and enter your Google account email. Without this,
> login will fail with a generic "Access blocked" error.

Then run:

```bash
gws auth login
```

### Browser-assisted auth (human or agent)

You can complete OAuth either manually or with browser automation.

- **Human flow**: run `gws auth login`, open the printed URL, approve scopes.
- **Agent-assisted flow**: the agent opens the URL, selects account, handles consent prompts, and returns control once the localhost callback succeeds.

If consent shows **"Google hasn't verified this app"** (testing mode), click **Continue**.
If scope checkboxes appear, select required scopes (or **Select all**) before continuing.

### Headless / CI (export flow)

1. Complete interactive auth on a machine with a browser.
2. Export credentials:
   ```bash
   gws auth export --unmasked > credentials.json
   ```
3. On the headless machine:
   ```bash
   export GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=/path/to/credentials.json
   gws drive files list   # just works
   ```

### Service Account (server-to-server)

Point to your key file; no login needed.

```bash
export GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=/path/to/service-account.json
gws drive files list
```

### Pre-obtained Access Token

Useful when another tool (e.g. `gcloud`) already mints tokens for your environment.

```bash
export GOOGLE_WORKSPACE_CLI_TOKEN=$(gcloud auth print-access-token)
```

### Precedence

| Priority | Source                 | Set via                                 |
| -------- | ---------------------- | --------------------------------------- |
| 1        | Access token           | `GOOGLE_WORKSPACE_CLI_TOKEN`            |
| 2        | Credentials file       | `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` |
| 3        | Encrypted credentials  | `gws auth login`                        |
| 4        | Plaintext credentials  | `~/.config/gws/credentials.json`        |

Environment variables can also live in a `.env` file.

## AI Agent Skills

The repo ships 100+ Agent Skills (`SKILL.md` files) — one for every supported API, plus higher-level helpers for common workflows and 50 curated recipes for Gmail, Drive, Docs, Calendar, and Sheets. See the full [Skills Index](docs/skills.md) for the complete list.

```bash
# Install all skills at once
npx skills add https://github.com/googleworkspace/cli

# Or pick only what you need
npx skills add https://github.com/googleworkspace/cli/tree/main/skills/gws-drive
npx skills add https://github.com/googleworkspace/cli/tree/main/skills/gws-gmail
```

<details>
<summary>OpenClaw setup</summary>

```bash
# Symlink all skills (stays in sync with repo)
ln -s $(pwd)/skills/gws-* ~/.openclaw/skills/

# Or copy specific skills
cp -r skills/gws-drive skills/gws-gmail ~/.openclaw/skills/
```

The `gws-shared` skill includes an `install` block so OpenClaw auto-installs the CLI via `npm` if `gws` isn't on PATH.

</details>

## Gemini CLI Extension

1. Authenticate the CLI first:

   ```bash
   gws auth setup
   ```

2. Install the extension into the Gemini CLI:
   ```bash
   gemini extensions install https://github.com/googleworkspace/cli
   ```

Installing this extension gives your Gemini CLI agent direct access to all `gws` commands and Google Workspace agent skills. Because `gws` handles its own authentication securely, you simply need to authenticate your terminal once prior to using the agent, and the extension will automatically inherit your credentials.

## Advanced Usage

### Multipart Uploads

```bash
gws drive files create --json '{"name": "report.pdf"}' --upload ./report.pdf
```

### Pagination

| Flag                | Description                                    | Default |
| ------------------- | ---------------------------------------------- | ------- |
| `--page-all`        | Auto-paginate, one JSON line per page (NDJSON) | off     |
| `--page-limit <N>`  | Max pages to fetch                             | 10      |
| `--page-delay <MS>` | Delay between pages                            | 100 ms  |

### Google Sheets — Shell Escaping

Sheets ranges use `!` which bash interprets as history expansion. Always wrap values in **single quotes**:

```bash
# Read cells A1:C10 from "Sheet1"
gws sheets spreadsheets values get \
  --params '{"spreadsheetId": "SPREADSHEET_ID", "range": "Sheet1!A1:C10"}'

# Append rows
gws sheets spreadsheets values append \
  --params '{"spreadsheetId": "ID", "range": "Sheet1!A1", "valueInputOption": "USER_ENTERED"}' \
  --json '{"values": [["Name", "Score"], ["Alice", 95]]}'
```

### Helper Commands

Some services ship hand-crafted helper commands alongside the auto-generated Discovery surface. Helper commands are prefixed with `+` so they are visually distinct and never collide with Discovery-generated method names.

Time-aware helpers (`+agenda`, `+standup-report`, `+weekly-digest`, `+meeting-prep`) automatically use your **Google account timezone** (fetched from Calendar Settings API and cached for 24 hours). Override with `--timezone`/`--tz` on `+agenda`, or set the `--timezone` flag for explicit control.

Run `gws <service> --help` to see both Discovery methods and helper commands together.

```bash
gws gmail --help      # shows +send, +reply, +reply-all, +forward, +triage, +watch …
gws calendar --help   # shows +insert, +agenda …
gws drive --help      # shows +upload …
```

**Full helper reference:**

| Service | Command | Description |
|---------|---------|-------------|
| `gmail` | `+send` | Send an email |
| `gmail` | `+reply` | Reply to a message (handles threading automatically) |
| `gmail` | `+reply-all` | Reply-all to a message |
| `gmail` | `+forward` | Forward a message to new recipients |
| `gmail` | `+triage` | Show unread inbox summary (sender, subject, date) |
| `gmail` | `+watch` | Watch for new emails and stream them as NDJSON |
| `sheets` | `+append` | Append a row to a spreadsheet |
| `sheets` | `+read` | Read values from a spreadsheet |
| `docs` | `+write` | Append text to a document |
| `chat` | `+send` | Send a message to a space |
| `drive` | `+upload` | Upload a file with automatic metadata |
| `calendar` | `+insert` | Create a new event |
| `calendar` | `+agenda` | Show upcoming events (uses Google account timezone; override with `--timezone`) |
| `script` | `+push` | Replace all files in an Apps Script project with local files |
| `workflow` | `+standup-report` | Today's meetings + open tasks as a standup summary |
| `workflow` | `+meeting-prep` | Prepare for your next meeting: agenda, attendees, and linked docs |
| `workflow` | `+email-to-task` | Convert a Gmail message into a Google Tasks entry |
| `workflow` | `+weekly-digest` | Weekly summary: this week's meetings + unread email count |
| `workflow` | `+file-announce` | Announce a Drive file in a Chat space |
| `events` | `+subscribe` | Subscribe to Workspace events and stream them as NDJSON |
| `events` | `+renew` | Renew/reactivate Workspace Events subscriptions |
| `modelarmor` | `+sanitize-prompt` | Sanitize a user prompt through a Model Armor template |
| `modelarmor` | `+sanitize-response` | Sanitize a model response through a Model Armor template |
| `modelarmor` | `+create-template` | Create a new Model Armor template |

**Examples:**

```bash
# Send an email
gws gmail +send --to alice@example.com --subject "Hello" --body "Hi there"

# Reply to a message
gws gmail +reply --message-id MESSAGE_ID --body "Thanks!"

# Append a row to a spreadsheet
gws sheets +append --spreadsheet SPREADSHEET_ID --values "Alice,95"

# Show today's calendar agenda
gws calendar +agenda

# Upload a file to Drive
gws drive +upload ./report.pdf --name "Q1 Report"

# Morning standup summary
gws workflow +standup-report

# Show today's agenda in a specific timezone
gws calendar +agenda --today --timezone America/New_York
```

### Model Armor (Response Sanitization)

Integrate [Google Cloud Model Armor](https://cloud.google.com/security/products/model-armor) to scan API responses for prompt injection before they reach your agent.

```bash
gws gmail users messages get --params '...' \
  --sanitize "projects/P/locations/L/templates/T"
```

| Variable                                 | Description                  |
| ---------------------------------------- | ---------------------------- |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE` | Default Model Armor template |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_MODE`     | `warn` (default) or `block`  |

## Environment Variables

All variables are optional. See [`.env.example`](.env.example) for a copy-paste template.

| Variable | Description |
|---|---|
| `GOOGLE_WORKSPACE_CLI_TOKEN` | Pre-obtained OAuth2 access token (highest priority) |
| `GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE` | Path to OAuth credentials JSON (user or service account) |
| `GOOGLE_WORKSPACE_CLI_CLIENT_ID` | OAuth client ID (alternative to `client_secret.json`) |
| `GOOGLE_WORKSPACE_CLI_CLIENT_SECRET` | OAuth client secret (paired with `CLIENT_ID`) |
| `GOOGLE_WORKSPACE_CLI_CONFIG_DIR` | Override config directory (default: `~/.config/gws`) |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_TEMPLATE` | Default Model Armor template |
| `GOOGLE_WORKSPACE_CLI_SANITIZE_MODE` | `warn` (default) or `block` |
| `GOOGLE_WORKSPACE_CLI_LOG` | Log level for stderr (e.g., `gws=debug`). Off by default. |
| `GOOGLE_WORKSPACE_CLI_LOG_FILE` | Directory for JSON log files with daily rotation. Off by default. |
| `GOOGLE_WORKSPACE_PROJECT_ID` | GCP project ID override for quota/billing and fallback for helper commands |

Environment variables can also be set in a `.env` file (loaded via [dotenvy](https://crates.io/crates/dotenvy)).

## Exit Codes

`gws` uses structured exit codes so scripts can branch on the failure type without parsing error output.

| Code | Meaning | Example cause |
|------|---------|---------------|
| `0` | Success | Command completed normally |
| `1` | API error | Google returned a 4xx/5xx response |
| `2` | Auth error | Credentials missing, expired, or invalid |
| `3` | Validation error | Bad arguments, unknown service, invalid flag |
| `4` | Discovery error | Could not fetch the API schema document |
| `5` | Internal error | Unexpected failure |

```bash
gws drive files list --params '{"fileId": "bad"}'
echo $?   # 1 — API error

gws unknown-service files list
echo $?   # 3 — validation error (unknown service)
```

## Architecture

`gws` uses a **two-phase parsing** strategy:

1. Read `argv[1]` to identify the service (e.g. `drive`)
2. Fetch the service's Discovery Document (cached 24 h)
3. Build a `clap::Command` tree from the document's resources and methods
4. Re-parse the remaining arguments
5. Authenticate, build the HTTP request, execute

All output — success, errors, download metadata — is structured JSON.

## Troubleshooting

### "Access blocked" or 403 during login

Your OAuth app is in **testing mode** and your account is not listed as a test user.

**Fix:** Open the [OAuth consent screen](https://console.cloud.google.com/apis/credentials/consent) in your GCP project → **Test users** → **Add users** → enter your Google account email. Then retry `gws auth login`.

### "Google hasn't verified this app"

Expected when your app is in testing mode. Click **Advanced** → **Go to \<app name\> (unsafe)** to proceed. This is safe for personal use; verification is only required to publish the app to other users.

### Too many scopes / consent screen error

Unverified (testing mode) apps are limited to ~25 OAuth scopes. The `recommended` scope preset includes many scopes and will exceed this limit.

**Fix:** Select only the scopes you need:

```bash
gws auth login --scopes drive,gmail,calendar
```

### `gcloud` CLI not found

`gws auth setup` requires the `gcloud` CLI to automate project creation. You have three options:

1. [Install gcloud](https://cloud.google.com/sdk/docs/install) and use `gcloud` directly.
2. Re-run `gws auth setup` which wraps `gcloud` calls.
3. Skip `gcloud` entirely — set up OAuth credentials manually in the [Cloud Console](#manual-oauth-setup-google-cloud-console)

### `redirect_uri_mismatch`

The OAuth client was not created as a **Desktop app** type. In the [Credentials](https://console.cloud.google.com/apis/credentials) page, delete the existing client, create a new one with type **Desktop app**, and download the new JSON.

### API not enabled — `accessNotConfigured`

If a required Google API is not enabled for your GCP project, you will see a
403 error with reason `accessNotConfigured`:

```json
{
  "error": {
    "code": 403,
    "message": "Gmail API has not been used in project 549352339482 ...",
    "reason": "accessNotConfigured",
    "enable_url": "https://console.developers.google.com/apis/api/gmail.googleapis.com/overview?project=549352339482"
  }
}
```

`gws` also prints an actionable hint to **stderr**:

```
💡 API not enabled for your GCP project.
   Enable it at: https://console.developers.google.com/apis/api/gmail.googleapis.com/overview?project=549352339482
   After enabling, wait a few seconds and retry your command.
```

**Steps to fix:**

1. Click the `enable_url` link (or copy it from the `enable_url` JSON field).
2. In the GCP Console, click **Enable**.
3. Wait ~10 seconds, then retry your `gws` command.

> [!TIP]
> You can also run `gws auth setup` which walks you through enabling all required
> APIs for your project automatically.

## Development

```bash
cargo build                       # dev build
cargo clippy -- -D warnings       # lint
cargo test                        # unit tests
./scripts/coverage.sh             # HTML coverage report → target/llvm-cov/html/
```

## License

Apache-2.0

## Disclaimer

> [!CAUTION]
> This is **not** an officially supported Google product.


================================================
FILE: SECURITY.md
================================================
# Report a security issue

To report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). We use
[https://g.co/vulnz](https://g.co/vulnz) for our intake, and do coordination and disclosure here on
GitHub (including using GitHub Security Advisory). The Google Security Team will
respond within 5 working days of your report on [https://g.co/vulnz](https://g.co/vulnz).


================================================
FILE: art/features.txt
================================================

        ╔════════════════════════════════════════════════╗
        ║                                                ║
        ║           ✨  Feature Roll  ✨                 ║
        ║                                                ║
        ║      ✅ Compatible with Headless Envs          ║
        ║      🔒 Can be Scoped to Read-Only             ║
        ║      🦀 Zero Runtime (Static Binary)           ║
        ║      📄 Auto-Pagination (NDJSON)               ║
        ║      🧠 Type-Safe Discovery Schemas            ║
        ║                                                ║
        ╚════════════════════════════════════════════════╝


================================================
FILE: art/intro.txt
================================================
          
          ╔════════════════════════════════════════════════╗
          ║                                                ║
          ║           ██████╗ ██╗     ██╗███████╗          ║
          ║          ██╔════╝ ██║     ██║██╔════╝          ║
          ║          ██║  ███╗██║█╗   ██║███████╗          ║
          ║          ██║   ██║██║███╗ ██║╚════██║          ║
          ║          ╚██████╔╝╚███╔ ███╔╝███████║          ║
          ║           ╚═════╝  ╚══╝ ╚══╝ ╚══════╝          ║
          ║                                                ║
          ║              Google Workspace CLI              ║
          ║             ─────────────────────              ║
          ║              One tool. Every API.              ║
          ║                                                ║
          ╚════════════════════════════════════════════════╝
        
                  ⭐ github.com/googleworkspace/cli
        
                    npm i -g @googleworkspace/cli
        
                  ────────────────────────────────
                         Built with Rust. 🦀
                  ────────────────────────────────
        
                       🚀 Drive    📧 Gmail              
                       📅 Calendar 📊 Sheets             
                       📝 Docs     🎨 Slides             
                       💬 Chat     👥 Admin              
  

================================================
FILE: art/outro.txt
================================================
          
          ╔════════════════════════════════════════════════╗
          ║                                                ║
          ║           ██████╗ ██╗     ██╗███████╗          ║
          ║          ██╔════╝ ██║     ██║██╔════╝          ║
          ║          ██║  ███╗██║█╗   ██║███████╗          ║
          ║          ██║   ██║██║███╗ ██║╚════██║          ║
          ║          ╚██████╔╝╚███╔ ███╔╝███████║          ║
          ║           ╚═════╝  ╚══╝ ╚══╝ ╚══════╝          ║
          ║                                                ║
          ║              Google Workspace CLI              ║
          ║             ─────────────────────              ║
          ║              One tool. Every API.              ║
          ║                                                ║
          ╚════════════════════════════════════════════════╝
        
                  ⭐ github.com/googleworkspace/cli
        
                    npm i -g @googleworkspace/cli
        
                  ────────────────────────────────
                         Built with Rust. 🦀
                  ────────────────────────────────
        
                       🚀 Drive    📧 Gmail              
                       📅 Calendar 📊 Sheets             
                       📝 Docs     🎨 Slides             
                       💬 Chat     👥 Admin              
  

================================================
FILE: art/qr.txt
================================================
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              
                                                              



================================================
FILE: art/scene1.txt
================================================
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 🤖 WHAT IS GWS?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 A single CLI for ALL Google Workspace APIs.

 Perfect for:

  🤖 AI agents
  📜 Shell scripts
  ⚡ Power users
  📊 Automation


================================================
FILE: art/scene2.txt
================================================
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 📂 EXPLORE SERVICES
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


================================================
FILE: art/scene2b.txt
================================================
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 🔍 INSPECT DRIVE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


================================================
FILE: art/scene3.txt
================================================
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 🔍 INTROSPECT APIS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 What params does an API
 method accept? Just ask.



================================================
FILE: art/scene3b.txt
================================================
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 📊 JSON SCHEMAS
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━


================================================
FILE: art/scene4.txt
================================================
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 🗂️  List files in a folder
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━



================================================
FILE: art/scene5.txt
================================================
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 📤 Upload to Drive
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━



================================================
FILE: art/scene6.txt
================================================
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 📧 Send an email
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━



================================================
FILE: art/scene7.txt
================================================
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 📅 Schedule a meeting
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━



================================================
FILE: art/scene8.txt
================================================
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 📊 Log data to Sheets
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━



================================================
FILE: art/scene9.txt
================================================
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 ♾️  Paginate all pages
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

 --page-all streams NDJSON
 from every page.
 Pipe to jq for processing.



================================================
FILE: dist-workspace.toml
================================================
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

[workspace]
members = ["cargo:."]

# Config for 'cargo dist'
[dist]
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
cargo-dist-version = "0.31.0"
# CI backends to support
ci = "github"
# The installers to generate for each app
installers = ["shell", "powershell", "npm"]
# Publish jobs to run
publish-jobs = ["npm"]
npm-scope = "@googleworkspace"
# Enable github attestations
github-attestations = true
npm-package = "cli"
# Target platforms to build apps for (Rust target-triple syntax)
targets = ["aarch64-apple-darwin", "aarch64-unknown-linux-gnu", "aarch64-unknown-linux-musl", "x86_64-apple-darwin", "x86_64-unknown-linux-gnu", "x86_64-unknown-linux-musl", "x86_64-pc-windows-msvc"]
# Which actions to run on pull requests
pr-run-mode = "plan"
# Don't overwrite release.yml on `cargo dist init` (preserves custom npm registry config)
allow-dirty = ["ci"]
# The archive format to use for windows builds (defaults .zip)
# Using .zip routes through PowerShell's Expand-Archive, which correctly
# handles Windows paths. Using .tar.gz causes failures in Git Bash because
# MSYS tar interprets "C:" as a remote host (issue #152).
windows-archive = ".zip"
# The archive format to use for non-windows builds (defaults .tar.xz)
unix-archive = ".tar.gz"


================================================
FILE: docs/CODE_OF_CONDUCT.md
================================================
# Code of Conduct

## Our Pledge

In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of
experience, education, socio-economic status, nationality, personal appearance,
race, religion, or sexual identity and orientation.

## Our Standards

Examples of behavior that contributes to creating a positive environment
include:

*   Using welcoming and inclusive language
*   Being respectful of differing viewpoints and experiences
*   Gracefully accepting constructive criticism
*   Focusing on what is best for the community
*   Showing empathy towards other community members

Examples of unacceptable behavior by participants include:

*   The use of sexualized language or imagery and unwelcome sexual attention or
    advances
*   Trolling, insulting/derogatory comments, and personal or political attacks
*   Public or private harassment
*   Publishing others' private information, such as a physical or electronic
    address, without explicit permission
*   Other conduct which could reasonably be considered inappropriate in a
    professional setting

## Our Responsibilities

Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.

Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening,
offensive, or harmful.

## Scope

This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.

This Code of Conduct also applies outside the project spaces when the Project
Steward has a reasonable belief that an individual's behavior may have a
negative impact on the project or its community.

## Conflict Resolution

We do not believe that all conflict is bad; healthy debate and disagreement
often yield positive results. However, it is never okay to be disrespectful or
to engage in behavior that violates the project’s code of conduct.

If you see someone violating the code of conduct, you are encouraged to address
the behavior directly with those involved. Many issues can be resolved quickly
and easily, and this gives people more control over the outcome of their
dispute. If you are unable to resolve the matter for any reason, or if the
behavior is threatening or harassing, report it. We are dedicated to providing
an environment where participants feel welcome and safe.

Reports should be directed to the Project Maintainers (opensource@google.com). It is the Project Steward’s duty to
receive and address reported violations of the code of conduct. They will then
work with a committee consisting of representatives from the Open Source
Programs Office and the Google Open Source Strategy team. If for any reason you
are uncomfortable reaching out to the Project Steward, please email
opensource@google.com.

We will investigate every complaint, but you may not receive a direct response.
We will use our discretion in determining when and how to follow up on reported
incidents, which may range from not taking action to permanent expulsion from
the project and project-sponsored spaces. We will notify the accused of the
report and provide them an opportunity to discuss it before any action is taken.
The identity of the reporter will be omitted from the details of the report
supplied to the accused. In potentially harmful situations, such as ongoing
harassment or threats to anyone's safety, we may take action without notice.

## Attribution

This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
available at
https://www.contributor-covenant.org/version/1/4/code-of-conduct.html


================================================
FILE: docs/CONTRIBUTING.md
================================================
# How to contribute

We'd love to accept your patches and contributions to this project.

## Before you begin

### Sign our Contributor License Agreement

Contributions to this project must be accompanied by a
[Contributor License Agreement](https://cla.developers.google.com/about) (CLA).
You (or your employer) retain the copyright to your contribution; this simply
gives us permission to use and redistribute your contributions as part of the
project.

If you or your current employer have already signed the Google CLA (even if it
was for a different project), you probably don't need to do it again.

Visit <https://cla.developers.google.com/> to see your current agreements or to
sign a new one.

### Review our community guidelines

This project follows
[Google's Open Source Community Guidelines](https://opensource.google/conduct/).

## Contribution process

### Code reviews

All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.

### Updating CI Smoketest Credentials

If the OAuth refresh token used in the GitHub Actions smoketest expires or needs additional scopes, you can generate a new one and update the repository secret using the GitHub CLI (`gh`).

1. **Set the credentials file path to output plaintext JSON**:
   ```bash
   export GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE=smoketest-creds.json
   ```

2. **Authenticate with the required scopes**:
   ```bash
   cargo run -- auth login --scopes https://www.googleapis.com/auth/drive,https://www.googleapis.com/auth/gmail.readonly,https://www.googleapis.com/auth/calendar.readonly,https://www.googleapis.com/auth/presentations.readonly,https://www.googleapis.com/auth/tasks.readonly
   ```

3. **Export and set the GitHub actions secret**:
   ```bash
   cargo run --quiet -- auth export --unmasked | base64 | gh secret set GOOGLE_CREDENTIALS_JSON
   ```

4. **Clean up**:
   ```bash
   rm smoketest-creds.json
   unset GOOGLE_WORKSPACE_CLI_CREDENTIALS_FILE
   ```

## Development Patterns

### Changesets

Every PR must include a changeset file at `.changeset/<descriptive-name>.md`:

```markdown
---
"@googleworkspace/cli": patch
---

Brief description of the change
```

Use `patch` for fixes/chores, `minor` for new features, `major` for breaking changes.

### Input Validation & URL Safety

This CLI is designed to be invoked by AI/LLM agents, so all user-supplied inputs must be treated as potentially adversarial. See [AGENTS.md](../AGENTS.md#input-validation--url-safety) for the full reference. The key rules are:

| What you're doing | What to use |
|---|---|
| Accepting a file path (`--output-dir`, `--dir`) | `validate::validate_safe_output_dir()` or `validate_safe_dir_path()` |
| Embedding a value in a URL path segment | `helpers::encode_path_segment()` |
| Passing query parameters | reqwest `.query()` builder (never string interpolation) |
| Using a resource name in a URL (`--project`, `--space`) | `helpers::validate_resource_name()` |
| Accepting an enum flag (`--msg-format`) | clap `value_parser` (see `gmail/mod.rs`) |

### Testing Expectations

- All new validation logic must include **both happy-path and error-path tests**
- Tests that modify the process CWD must use `#[serial]` from `serial_test`
- Tempdir paths should be canonicalized before use to handle macOS `/var` → `/private/var` symlinks
- Run the full suite before submitting: `cargo test && cargo clippy -- -D warnings`

================================================
FILE: docs/demo.tape
================================================
# GWS CLI — README Demo
# Run: vhs docs/demo.tape
#
# All cosmetic sleeps minimized. API sleeps kept for responses.
# Single line commands for reliability.

Output docs/demo.mp4
Output docs/demo.gif

Set Shell "bash"
Set FontSize 18
Set Width 800
Set Height 500
Set TypingSpeed 1ms
Set Padding 30
Set LineHeight 1.3

# ── Setup (hidden) ──
Hide
# Mock gemini CLI for deterministic demo
Type 'function gemini() { echo "Why do Java developers wear glasses? Because they don'"'"'t C#."; }' Enter
Type "export -f gemini" Enter
Type "export PATH=$PWD/target/release:$PWD/target/debug:$PATH" Enter
Type "set -e" Enter
Sleep 1s
Type `DEMO=$(gws drive files create --json '{"name":"gws-demo","mimeType":"application/vnd.google-apps.folder"}' | jq -r '.id')` Enter
Sleep 3s
Type `gws drive files create --json "{\"name\":\"meeting-notes.md\",\"mimeType\":\"text/markdown\",\"parents\":[\"$DEMO\"]}" > /dev/null` Enter
Sleep 2s
Type `gws drive files create --json "{\"name\":\"quarterly-report.csv\",\"mimeType\":\"text/csv\",\"parents\":[\"$DEMO\"]}" > /dev/null` Enter
Sleep 2s
Type `gws drive files create --json "{\"name\":\"project-roadmap.md\",\"mimeType\":\"text/markdown\",\"parents\":[\"$DEMO\"]}" > /dev/null` Enter
Sleep 2s
Type `Q="'$DEMO' in parents"` Enter
Sleep 500ms
Type "clear" Enter
Sleep 1s
Show

# ╔══════════════════════════════════════╗
# ║         ASCII ART INTRO             ║
# ╚══════════════════════════════════════╝

Hide
Type "./scripts/show-art.sh art/intro.txt" Enter
Sleep 1s
Show
Sleep 1s

# ── Scene 1: What is gws? ──
Hide
Type "./scripts/show-art.sh art/scene1.txt" Enter
Sleep 1s
Show
Sleep 4s

# ── Scene 2: Discover all services ──
Hide
Type "./scripts/show-art.sh art/scene2.txt" Enter
Sleep 1s
Show
Sleep 1s

Type "gws --help 2>&1 | head -40" Enter
Sleep 4s

# ── Scene 2b: Inspect Drive resources ──
Hide
Type "./scripts/show-art.sh art/scene2b.txt" Enter
Sleep 1s
Show
Sleep 1s

Type "gws drive --help 2>&1 | head -40" Enter
Sleep 4s

# ── Scene 3: Schema introspection ──
Hide
Type "./scripts/show-art.sh art/scene3.txt" Enter
Sleep 1s
Show
Sleep 1s

Type "gws schema drive.files.list | head -15" Enter
Sleep 3s

# ── Scene 3b: Inspect JSON Schemas ──
Hide
Type "./scripts/show-art.sh art/scene3b.txt" Enter
Sleep 1s
Show
Sleep 1s

Type "gws schema drive.File | head -20" Enter
Sleep 3s

Type "gws schema drive.File --resolve-refs | head -30" Enter
Sleep 5s

# ── Scene 4: List files in a folder ──
Hide
Type "./scripts/show-art.sh art/scene4.txt" Enter
Sleep 1s
Show
Sleep 1s

Type "gws drive files list" 
Type ` --params "{\"q\":\"$Q\",\"fields\":\"files(name,mimeType)\"}"` Enter
Sleep 3s

# ── Scene 5: Upload a file ──
Hide
Type "./scripts/show-art.sh art/scene5.txt" Enter
Sleep 1s
Show
Sleep 1s

Type "echo '# Notes' > /tmp/notes.md" Enter
Sleep 500ms

Type "gws drive files create" 
Type ` --json '{"name":"notes.md","mimeType":"text/markdown"}'` 
Type " --upload /tmp/notes.md" Enter
Sleep 3s

# ── Scene 6: Gmail labels ──
Hide
Type "./scripts/show-art.sh art/scene6.txt" Enter
Sleep 1s
Show
Sleep 1s

Type "JOKE=$(gemini 'tell me a joke')" Enter
Sleep 1s

Type `MSG=$(echo -e "To: justin@example.com\nSubject: joke of the day\n\n$JOKE" | base64 | tr -d '\n' | tr '+/' '-_' | tr -d '=')` Enter

Type "gws gmail users messages send" 
Type ` --params '{"userId":"me"}'` 
Type ` --json "{\"raw\":\"$MSG\"}"` 
Type " | jq ." Enter
Sleep 3s

# ── Scene 7: Calendar event ──
Hide
Type "./scripts/show-art.sh art/scene7.txt" Enter
Sleep 1s
Show
Sleep 1s

Type "gws calendar events insert" 
Type ` --params '{"calendarId":"primary"}'` 
Type ` --json '{"summary":"Ship v1.0 🚀","start":{"dateTime":"2024-06-17T10:00:00-07:00"},"end":{"dateTime":"2024-06-17T10:30:00-07:00"}}'` 
Type " | jq . | head -15" Enter
Sleep 3s

# ── Scene 8: Sheets automation ──
Hide
Type "./scripts/show-art.sh art/scene8.txt" Enter
Sleep 1s
Show
Sleep 1s

Type "gws sheets spreadsheets values append" 
Type ` --params '{"spreadsheetId":"1izmtvgBC4NuxHhABFX6descuB6-SXTm3g7c6LYBngJQ","range":"Sheet1!A1","valueInputOption":"USER_ENTERED"}'` 
Type ` --json '{"values":[["Deploy","v1.0","=NOW()"]]}'` Enter
Sleep 3s

# ── Scene 9: Auto-pagination ──
Hide
Type "./scripts/show-art.sh art/scene9.txt" Enter
Sleep 1s
Show
Sleep 1s

Type "gws drive files list" 
Type " --params '" Sleep 50ms
Type@30ms '{"pageSize":2,"fields":"nextPageToken,files(id)"}' Sleep 50ms
Type "'" 
Type " --page-all" 
Type " | jq -r '.files[]?.id'" Enter
Sleep 6s

# ╔══════════════════════════════════════╗
# ║         OUTRO                        ║
# ╚══════════════════════════════════════╝

Hide
Type "./scripts/show-art.sh art/outro.txt" Enter
Sleep 1s
Show
Sleep 1s

# ── Cleanup (hidden) ──
Hide
Type `gws drive files delete --params "{\"fileId\":\"$DEMO\"}" > /dev/null 2>&1` Enter
Sleep 3s


================================================
FILE: docs/skills.md
================================================
# Skills Index

> Auto-generated by `gws generate-skills`. Do not edit manually.

## Services

Core Google Workspace API skills.

| Skill | Description |
|-------|-------------|
| [gws-shared](../skills/gws-shared/SKILL.md) | gws CLI: Shared patterns for authentication, global flags, and output formatting. |
| [gws-drive](../skills/gws-drive/SKILL.md) | Google Drive: Manage files, folders, and shared drives. |
| [gws-sheets](../skills/gws-sheets/SKILL.md) | Google Sheets: Read and write spreadsheets. |
| [gws-gmail](../skills/gws-gmail/SKILL.md) | Gmail: Send, read, and manage email. |
| [gws-calendar](../skills/gws-calendar/SKILL.md) | Google Calendar: Manage calendars and events. |
| [gws-admin-reports](../skills/gws-admin-reports/SKILL.md) | Google Workspace Admin SDK: Audit logs and usage reports. |
| [gws-docs](../skills/gws-docs/SKILL.md) | Read and write Google Docs. |
| [gws-slides](../skills/gws-slides/SKILL.md) | Google Slides: Read and write presentations. |
| [gws-tasks](../skills/gws-tasks/SKILL.md) | Google Tasks: Manage task lists and tasks. |
| [gws-people](../skills/gws-people/SKILL.md) | Google People: Manage contacts and profiles. |
| [gws-chat](../skills/gws-chat/SKILL.md) | Google Chat: Manage Chat spaces and messages. |
| [gws-classroom](../skills/gws-classroom/SKILL.md) | Google Classroom: Manage classes, rosters, and coursework. |
| [gws-forms](../skills/gws-forms/SKILL.md) | Read and write Google Forms. |
| [gws-keep](../skills/gws-keep/SKILL.md) | Manage Google Keep notes. |
| [gws-meet](../skills/gws-meet/SKILL.md) | Manage Google Meet conferences. |
| [gws-events](../skills/gws-events/SKILL.md) | Subscribe to Google Workspace events. |
| [gws-modelarmor](../skills/gws-modelarmor/SKILL.md) | Google Model Armor: Filter user-generated content for safety. |
| [gws-workflow](../skills/gws-workflow/SKILL.md) | Google Workflow: Cross-service productivity workflows. |

## Helpers

Shortcut commands for common operations.

| Skill | Description |
|-------|-------------|
| [gws-drive-upload](../skills/gws-drive-upload/SKILL.md) | Google Drive: Upload a file with automatic metadata. |
| [gws-sheets-append](../skills/gws-sheets-append/SKILL.md) | Google Sheets: Append a row to a spreadsheet. |
| [gws-sheets-read](../skills/gws-sheets-read/SKILL.md) | Google Sheets: Read values from a spreadsheet. |
| [gws-gmail-send](../skills/gws-gmail-send/SKILL.md) | Gmail: Send an email. |
| [gws-gmail-triage](../skills/gws-gmail-triage/SKILL.md) | Gmail: Show unread inbox summary (sender, subject, date). |
| [gws-gmail-reply](../skills/gws-gmail-reply/SKILL.md) | Gmail: Reply to a message (handles threading automatically). |
| [gws-gmail-reply-all](../skills/gws-gmail-reply-all/SKILL.md) | Gmail: Reply-all to a message (handles threading automatically). |
| [gws-gmail-forward](../skills/gws-gmail-forward/SKILL.md) | Gmail: Forward a message to new recipients. |
| [gws-gmail-read](../skills/gws-gmail-read/SKILL.md) | Gmail: Read a message and extract its body or headers. |
| [gws-gmail-watch](../skills/gws-gmail-watch/SKILL.md) | Gmail: Watch for new emails and stream them as NDJSON. |
| [gws-calendar-insert](../skills/gws-calendar-insert/SKILL.md) | Google Calendar: Create a new event. |
| [gws-calendar-agenda](../skills/gws-calendar-agenda/SKILL.md) | Google Calendar: Show upcoming events across all calendars. |
| [gws-docs-write](../skills/gws-docs-write/SKILL.md) | Google Docs: Append text to a document. |
| [gws-chat-send](../skills/gws-chat-send/SKILL.md) | Google Chat: Send a message to a space. |
| [gws-events-subscribe](../skills/gws-events-subscribe/SKILL.md) | Google Workspace Events: Subscribe to Workspace events and stream them as NDJSON. |
| [gws-events-renew](../skills/gws-events-renew/SKILL.md) | Google Workspace Events: Renew/reactivate Workspace Events subscriptions. |
| [gws-modelarmor-sanitize-prompt](../skills/gws-modelarmor-sanitize-prompt/SKILL.md) | Google Model Armor: Sanitize a user prompt through a Model Armor template. |
| [gws-modelarmor-sanitize-response](../skills/gws-modelarmor-sanitize-response/SKILL.md) | Google Model Armor: Sanitize a model response through a Model Armor template. |
| [gws-modelarmor-create-template](../skills/gws-modelarmor-create-template/SKILL.md) | Google Model Armor: Create a new Model Armor template. |
| [gws-workflow-standup-report](../skills/gws-workflow-standup-report/SKILL.md) | Google Workflow: Today's meetings + open tasks as a standup summary. |
| [gws-workflow-meeting-prep](../skills/gws-workflow-meeting-prep/SKILL.md) | Google Workflow: Prepare for your next meeting: agenda, attendees, and linked docs. |
| [gws-workflow-email-to-task](../skills/gws-workflow-email-to-task/SKILL.md) | Google Workflow: Convert a Gmail message into a Google Tasks entry. |
| [gws-workflow-weekly-digest](../skills/gws-workflow-weekly-digest/SKILL.md) | Google Workflow: Weekly summary: this week's meetings + unread email count. |
| [gws-workflow-file-announce](../skills/gws-workflow-file-announce/SKILL.md) | Google Workflow: Announce a Drive file in a Chat space. |

## Personas

Role-based skill bundles.

| Skill | Description |
|-------|-------------|
| [persona-exec-assistant](../skills/persona-exec-assistant/SKILL.md) | Manage an executive's schedule, inbox, and communications. |
| [persona-project-manager](../skills/persona-project-manager/SKILL.md) | Coordinate projects — track tasks, schedule meetings, and share docs. |
| [persona-hr-coordinator](../skills/persona-hr-coordinator/SKILL.md) | Handle HR workflows — onboarding, announcements, and employee comms. |
| [persona-sales-ops](../skills/persona-sales-ops/SKILL.md) | Manage sales workflows — track deals, schedule calls, client comms. |
| [persona-it-admin](../skills/persona-it-admin/SKILL.md) | Administer IT — monitor security and configure Workspace. |
| [persona-content-creator](../skills/persona-content-creator/SKILL.md) | Create, organize, and distribute content across Workspace. |
| [persona-customer-support](../skills/persona-customer-support/SKILL.md) | Manage customer support — track tickets, respond, escalate issues. |
| [persona-event-coordinator](../skills/persona-event-coordinator/SKILL.md) | Plan and manage events — scheduling, invitations, and logistics. |
| [persona-team-lead](../skills/persona-team-lead/SKILL.md) | Lead a team — run standups, coordinate tasks, and communicate. |
| [persona-researcher](../skills/persona-researcher/SKILL.md) | Organize research — manage references, notes, and collaboration. |

## Recipes

Multi-step task sequences with real commands.

| Skill | Description |
|-------|-------------|
| [recipe-label-and-archive-emails](../skills/recipe-label-and-archive-emails/SKILL.md) | Apply Gmail labels to matching messages and archive them to keep your inbox clean. |
| [recipe-draft-email-from-doc](../skills/recipe-draft-email-from-doc/SKILL.md) | Read content from a Google Doc and use it as the body of a Gmail message. |
| [recipe-organize-drive-folder](../skills/recipe-organize-drive-folder/SKILL.md) | Create a Google Drive folder structure and move files into the right locations. |
| [recipe-share-folder-with-team](../skills/recipe-share-folder-with-team/SKILL.md) | Share a Google Drive folder and all its contents with a list of collaborators. |
| [recipe-email-drive-link](../skills/recipe-email-drive-link/SKILL.md) | Share a Google Drive file and email the link with a message to recipients. |
| [recipe-create-doc-from-template](../skills/recipe-create-doc-from-template/SKILL.md) | Copy a Google Docs template, fill in content, and share with collaborators. |
| [recipe-create-expense-tracker](../skills/recipe-create-expense-tracker/SKILL.md) | Set up a Google Sheets spreadsheet for tracking expenses with headers and initial entries. |
| [recipe-copy-sheet-for-new-month](../skills/recipe-copy-sheet-for-new-month/SKILL.md) | Duplicate a Google Sheets template tab for a new month of tracking. |
| [recipe-block-focus-time](../skills/recipe-block-focus-time/SKILL.md) | Create recurring focus time blocks on Google Calendar to protect deep work hours. |
| [recipe-reschedule-meeting](../skills/recipe-reschedule-meeting/SKILL.md) | Move a Google Calendar event to a new time and automatically notify all attendees. |
| [recipe-create-gmail-filter](../skills/recipe-create-gmail-filter/SKILL.md) | Create a Gmail filter to automatically label, star, or categorize incoming messages. |
| [recipe-schedule-recurring-event](../skills/recipe-schedule-recurring-event/SKILL.md) | Create a recurring Google Calendar event with attendees. |
| [recipe-find-free-time](../skills/recipe-find-free-time/SKILL.md) | Query Google Calendar free/busy status for multiple users to find a meeting slot. |
| [recipe-bulk-download-folder](../skills/recipe-bulk-download-folder/SKILL.md) | List and download all files from a Google Drive folder. |
| [recipe-find-large-files](../skills/recipe-find-large-files/SKILL.md) | Identify large Google Drive files consuming storage quota. |
| [recipe-create-shared-drive](../skills/recipe-create-shared-drive/SKILL.md) | Create a Google Shared Drive and add members with appropriate roles. |
| [recipe-log-deal-update](../skills/recipe-log-deal-update/SKILL.md) | Append a deal status update to a Google Sheets sales tracking spreadsheet. |
| [recipe-collect-form-responses](../skills/recipe-collect-form-responses/SKILL.md) | Retrieve and review responses from a Google Form. |
| [recipe-post-mortem-setup](../skills/recipe-post-mortem-setup/SKILL.md) | Create a Google Docs post-mortem, schedule a Google Calendar review, and notify via Chat. |
| [recipe-create-task-list](../skills/recipe-create-task-list/SKILL.md) | Set up a new Google Tasks list with initial tasks. |
| [recipe-review-overdue-tasks](../skills/recipe-review-overdue-tasks/SKILL.md) | Find Google Tasks that are past due and need attention. |
| [recipe-watch-drive-changes](../skills/recipe-watch-drive-changes/SKILL.md) | Subscribe to change notifications on a Google Drive file or folder. |
| [recipe-create-classroom-course](../skills/recipe-create-classroom-course/SKILL.md) | Create a Google Classroom course and invite students. |
| [recipe-create-meet-space](../skills/recipe-create-meet-space/SKILL.md) | Create a Google Meet meeting space and share the join link. |
| [recipe-review-meet-participants](../skills/recipe-review-meet-participants/SKILL.md) | Review who attended a Google Meet conference and for how long. |
| [recipe-create-presentation](../skills/recipe-create-presentation/SKILL.md) | Create a new Google Slides presentation and add initial slides. |
| [recipe-save-email-attachments](../skills/recipe-save-email-attachments/SKILL.md) | Find Gmail messages with attachments and save them to a Google Drive folder. |
| [recipe-send-team-announcement](../skills/recipe-send-team-announcement/SKILL.md) | Send a team announcement via both Gmail and a Google Chat space. |
| [recipe-create-feedback-form](../skills/recipe-create-feedback-form/SKILL.md) | Create a Google Form for feedback and share it via Gmail. |
| [recipe-sync-contacts-to-sheet](../skills/recipe-sync-contacts-to-sheet/SKILL.md) | Export Google Contacts directory to a Google Sheets spreadsheet. |
| [recipe-share-event-materials](../skills/recipe-share-event-materials/SKILL.md) | Share Google Drive files with all attendees of a Google Calendar event. |
| [recipe-create-vacation-responder](../skills/recipe-create-vacation-responder/SKILL.md) | Enable a Gmail out-of-office auto-reply with a custom message and date range. |
| [recipe-create-events-from-sheet](../skills/recipe-create-events-from-sheet/SKILL.md) | Read event data from a Google Sheets spreadsheet and create Google Calendar entries for each row. |
| [recipe-plan-weekly-schedule](../skills/recipe-plan-weekly-schedule/SKILL.md) | Review your Google Calendar week, identify gaps, and add events to fill them. |
| [recipe-share-doc-and-notify](../skills/recipe-share-doc-and-notify/SKILL.md) | Share a Google Docs document with edit access and email collaborators the link. |
| [recipe-backup-sheet-as-csv](../skills/recipe-backup-sheet-as-csv/SKILL.md) | Export a Google Sheets spreadsheet as a CSV file for local backup or processing. |
| [recipe-save-email-to-doc](../skills/recipe-save-email-to-doc/SKILL.md) | Save a Gmail message body into a Google Doc for archival or reference. |
| [recipe-compare-sheet-tabs](../skills/recipe-compare-sheet-tabs/SKILL.md) | Read data from two tabs in a Google Sheet to compare and identify differences. |
| [recipe-batch-invite-to-event](../skills/recipe-batch-invite-to-event/SKILL.md) | Add a list of attendees to an existing Google Calendar event and send notifications. |
| [recipe-forward-labeled-emails](../skills/recipe-forward-labeled-emails/SKILL.md) | Find Gmail messages with a specific label and forward them to another address. |
| [recipe-generate-report-from-sheet](../skills/recipe-generate-report-from-sheet/SKILL.md) | Read data from a Google Sheet and create a formatted Google Docs report. |



================================================
FILE: flake.nix
================================================
{
  description = "Google Workspace CLI — dynamic command surface from Discovery Service";

  inputs = {
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
    flake-utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, flake-utils }:
    flake-utils.lib.eachDefaultSystem (system:
      let
        pkgs = nixpkgs.legacyPackages.${system};

        # Extract version from Cargo.toml
        cargoToml = builtins.fromTOML (builtins.readFile ./Cargo.toml);
        version = cargoToml.package.version;

        # System dependencies
        # On Linux, keyring often needs libsecret
        # On macOS, it uses Security framework
        linuxDeps = with pkgs; [
          libsecret
        ];

        darwinDeps = with pkgs; [
          libiconv
          apple-sdk
        ];

        gws = pkgs.rustPlatform.buildRustPackage {
          pname = "gws";
          inherit version;

          src = ./.;

          cargoLock = {
            lockFile = ./Cargo.lock;
          };

          nativeBuildInputs = [ pkgs.pkg-config ];
          buildInputs = pkgs.lib.optionals pkgs.stdenv.isLinux linuxDeps
            ++ pkgs.lib.optionals pkgs.stdenv.isDarwin darwinDeps;

          # Tests are disabled by default in buildRustPackage if not specified, 
          # but we'll be explicit. Some tests might require network.
          doCheck = false;

          meta = with pkgs.lib; {
            description = cargoToml.package.description;
            homepage = cargoToml.package.homepage;
            license = licenses.asl20;
            maintainers = [{ name = "Justin Poehnelt"; email = "justin.poehnelt@gmail.com"; }];
            mainProgram = "gws";
          };
        };
      in
      {
        packages.default = gws;
        packages.gws = gws;

        apps.default = flake-utils.lib.mkApp {
          drv = gws;
        };

        devShells.default = pkgs.mkShell {
          inputsFrom = [ gws ];
          buildInputs = with pkgs; [
            rustc
            cargo
            rust-analyzer
            clippy
            rustfmt
          ];
        };
      }
    );
}


================================================
FILE: gemini-extension.json
================================================
{
  "name": "google-workspace-cli",
  "version": "latest",
  "description": "CLI tool for managing Google Workspace resources dynamically using Discovery APIs.",
  "contextFileName": "CONTEXT.md"
}


================================================
FILE: lefthook.yml
================================================
pre-commit:
  parallel: false
  commands:
    fmt:
      glob: "*.rs"
      run: cargo fmt -- --check
    clippy:
      glob: "*.rs"
      run: cargo clippy -- -D warnings

pre-push:
  parallel: true
  commands:
    test:
      glob: "*.rs"
      run: cargo test
    check:
      glob: "*.rs"
      run: cargo check


================================================
FILE: package.json
================================================
{
  "name": "@googleworkspace/cli",
  "version": "0.18.1",
  "private": true,
  "description": "Google Workspace CLI — dynamic command surface from Discovery Service",
  "license": "Apache-2.0",
  "repository": {
    "type": "git",
    "url": "https://github.com/googleworkspace/cli.git"
  },
  "author": {
    "name": "Justin Poehnelt",
    "email": "justin.poehnelt@gmail.com"
  },
  "homepage": "https://github.com/googleworkspace/cli",
  "bugs": {
    "url": "https://github.com/googleworkspace/cli/issues"
  },
  "scripts": {
    "test": "cargo test",
    "prepare": "lefthook install",
    "version-sync": "bash scripts/version-sync.sh",
    "tag-release": "bash scripts/tag-release.sh"
  },
  "publishConfig": {
    "provenance": true,
    "registry": "https://wombat-dressing-room.appspot.com"
  },
  "engines": {
    "node": ">=18"
  },
  "packageManager": "pnpm@10.0.0",
  "keywords": [
    "cli",
    "google-workspace",
    "google",
    "google-api",
    "google-drive",
    "google-gmail",
    "google-sheets",
    "google-calendar",
    "google-docs",
    "google-chat",
    "google-admin",
    "gsuite",
    "discovery-api",
    "ai-agent",
    "agent-skills",
    "automation",
    "oauth2",
    "rust"
  ],
  "devDependencies": {
    "@changesets/cli": "^2.29.8",
    "lefthook": "^2.1.2"
  }
}


================================================
FILE: registry/personas.yaml
================================================
# Persona Packs — Role-based skill bundles for AI agents
#
# Each persona defines a role-based context with:
#   - name: unique id (used as directory name: persona-{name})
#   - title: human-readable name
#   - description: when to use this persona
#   - services: which gws services this persona commonly uses
#   - workflows: which workflow commands are relevant
#   - instructions: step-by-step guidance for agents adopting this role
#   - tips: useful reminders

personas:
  - name: exec-assistant
    title: Executive Assistant
    description: "Manage an executive's schedule, inbox, and communications."
    services: [gmail, calendar, drive, chat]
    workflows: ["+standup-report", "+meeting-prep", "+weekly-digest"]
    instructions:
      - "Start each day with `gws workflow +standup-report` to get the executive's agenda and open tasks."
      - "Before each meeting, run `gws workflow +meeting-prep` to see attendees, description, and linked docs."
      - "Triage the inbox with `gws gmail +triage --max 10` — prioritize emails from direct reports and leadership."
      - "Schedule meetings with `gws calendar +insert` — always check for conflicts first using `gws calendar +agenda`."
      - "Draft replies with `gws gmail +send` — keep tone professional and concise."
    tips:
      - "Always confirm calendar changes with the executive before committing."
      - "Use `--format table` for quick visual scans of agenda and triage output."
      - "Check `gws calendar +agenda --week` on Monday mornings for weekly planning."

  - name: project-manager
    title: Project Manager
    description: "Coordinate projects — track tasks, schedule meetings, and share docs."
    services: [drive, sheets, calendar, gmail, chat]
    workflows: ["+standup-report", "+weekly-digest", "+file-announce"]
    instructions:
      - "Start the week with `gws workflow +weekly-digest` for a snapshot of upcoming meetings and unread items."
      - "Track project status in Sheets using `gws sheets +append` to log updates."
      - "Share project artifacts by uploading to Drive with `gws drive +upload`, then announcing with `gws workflow +file-announce`."
      - "Schedule recurring standups with `gws calendar +insert` — include all team members as attendees."
      - "Send status update emails to stakeholders with `gws gmail +send`."
    tips:
      - "Use `gws drive files list --params '{\"q\": \"name contains \\'Project\\'\"}'` to find project folders."
      - "Pipe triage output through `jq` for filtering by sender or subject."
      - "Use `--dry-run` before any write operations to preview what will happen."

  - name: hr-coordinator
    title: HR Coordinator
    description: "Handle HR workflows — onboarding, announcements, and employee comms."
    services: [gmail, calendar, drive, chat]
    workflows: ["+email-to-task", "+file-announce"]
    instructions:
      - "For new hire onboarding, create calendar events for orientation sessions with `gws calendar +insert`."
      - "Upload onboarding docs to a shared Drive folder with `gws drive +upload`."
      - "Announce new hires in Chat spaces with `gws workflow +file-announce` to share their profile doc."
      - "Convert email requests into tracked tasks with `gws workflow +email-to-task`."
      - "Send bulk announcements with `gws gmail +send` — use clear subject lines."
    tips:
      - "Always use `--sanitize` for PII-sensitive operations."
      - "Create a dedicated 'HR Onboarding' calendar for tracking orientation schedules."

  - name: sales-ops
    title: Sales Operations
    description: "Manage sales workflows — track deals, schedule calls, client comms."
    services: [gmail, calendar, sheets, drive]
    workflows: ["+meeting-prep", "+email-to-task", "+weekly-diges
Download .txt
gitextract_nx_35fca/

├── .agent/
│   ├── skills/
│   │   └── vhs.md
│   └── workflows/
│       └── verify-skills.md
├── .changeset/
│   ├── README.md
│   └── config.json
├── .claude/
│   └── settings.json
├── .gemini/
│   ├── config.yaml
│   └── style_guide.md
├── .github/
│   ├── CODEOWNERS
│   ├── PULL_REQUEST_TEMPLATE.md
│   ├── labeler.yml
│   └── workflows/
│       ├── automation.yml
│       ├── ci.yml
│       ├── cla.yml
│       ├── coverage.yml
│       ├── generate-skills.yml
│       ├── policy.yml
│       ├── publish-skills.yml
│       ├── release-changesets.yml
│       ├── release.yml
│       └── stale.yml
├── .gitignore
├── AGENTS.md
├── CHANGELOG.md
├── CLAUDE.md
├── CONTEXT.md
├── Cargo.toml
├── LICENSE
├── README.md
├── SECURITY.md
├── art/
│   ├── features.txt
│   ├── intro.txt
│   ├── outro.txt
│   ├── qr.txt
│   ├── scene1.txt
│   ├── scene2.txt
│   ├── scene2b.txt
│   ├── scene3.txt
│   ├── scene3b.txt
│   ├── scene4.txt
│   ├── scene5.txt
│   ├── scene6.txt
│   ├── scene7.txt
│   ├── scene8.txt
│   └── scene9.txt
├── dist-workspace.toml
├── docs/
│   ├── CODE_OF_CONDUCT.md
│   ├── CONTRIBUTING.md
│   ├── demo.tape
│   └── skills.md
├── flake.nix
├── gemini-extension.json
├── lefthook.yml
├── package.json
├── registry/
│   ├── personas.yaml
│   └── recipes.yaml
├── scripts/
│   ├── coverage.sh
│   ├── show-art.sh
│   ├── tag-release.sh
│   └── version-sync.sh
├── skills/
│   ├── gws-admin-reports/
│   │   └── SKILL.md
│   ├── gws-calendar/
│   │   └── SKILL.md
│   ├── gws-calendar-agenda/
│   │   └── SKILL.md
│   ├── gws-calendar-insert/
│   │   └── SKILL.md
│   ├── gws-chat/
│   │   └── SKILL.md
│   ├── gws-chat-send/
│   │   └── SKILL.md
│   ├── gws-classroom/
│   │   └── SKILL.md
│   ├── gws-docs/
│   │   └── SKILL.md
│   ├── gws-docs-write/
│   │   └── SKILL.md
│   ├── gws-drive/
│   │   └── SKILL.md
│   ├── gws-drive-upload/
│   │   └── SKILL.md
│   ├── gws-events/
│   │   └── SKILL.md
│   ├── gws-events-renew/
│   │   └── SKILL.md
│   ├── gws-events-subscribe/
│   │   └── SKILL.md
│   ├── gws-forms/
│   │   └── SKILL.md
│   ├── gws-gmail/
│   │   └── SKILL.md
│   ├── gws-gmail-forward/
│   │   └── SKILL.md
│   ├── gws-gmail-read/
│   │   └── SKILL.md
│   ├── gws-gmail-reply/
│   │   └── SKILL.md
│   ├── gws-gmail-reply-all/
│   │   └── SKILL.md
│   ├── gws-gmail-send/
│   │   └── SKILL.md
│   ├── gws-gmail-triage/
│   │   └── SKILL.md
│   ├── gws-gmail-watch/
│   │   └── SKILL.md
│   ├── gws-keep/
│   │   └── SKILL.md
│   ├── gws-meet/
│   │   └── SKILL.md
│   ├── gws-modelarmor/
│   │   └── SKILL.md
│   ├── gws-modelarmor-create-template/
│   │   └── SKILL.md
│   ├── gws-modelarmor-sanitize-prompt/
│   │   └── SKILL.md
│   ├── gws-modelarmor-sanitize-response/
│   │   └── SKILL.md
│   ├── gws-people/
│   │   └── SKILL.md
│   ├── gws-shared/
│   │   └── SKILL.md
│   ├── gws-sheets/
│   │   └── SKILL.md
│   ├── gws-sheets-append/
│   │   └── SKILL.md
│   ├── gws-sheets-read/
│   │   └── SKILL.md
│   ├── gws-slides/
│   │   └── SKILL.md
│   ├── gws-tasks/
│   │   └── SKILL.md
│   ├── gws-workflow/
│   │   └── SKILL.md
│   ├── gws-workflow-email-to-task/
│   │   └── SKILL.md
│   ├── gws-workflow-file-announce/
│   │   └── SKILL.md
│   ├── gws-workflow-meeting-prep/
│   │   └── SKILL.md
│   ├── gws-workflow-standup-report/
│   │   └── SKILL.md
│   ├── gws-workflow-weekly-digest/
│   │   └── SKILL.md
│   ├── persona-content-creator/
│   │   └── SKILL.md
│   ├── persona-customer-support/
│   │   └── SKILL.md
│   ├── persona-event-coordinator/
│   │   └── SKILL.md
│   ├── persona-exec-assistant/
│   │   └── SKILL.md
│   ├── persona-hr-coordinator/
│   │   └── SKILL.md
│   ├── persona-it-admin/
│   │   └── SKILL.md
│   ├── persona-project-manager/
│   │   └── SKILL.md
│   ├── persona-researcher/
│   │   └── SKILL.md
│   ├── persona-sales-ops/
│   │   └── SKILL.md
│   ├── persona-team-lead/
│   │   └── SKILL.md
│   ├── recipe-backup-sheet-as-csv/
│   │   └── SKILL.md
│   ├── recipe-batch-invite-to-event/
│   │   └── SKILL.md
│   ├── recipe-block-focus-time/
│   │   └── SKILL.md
│   ├── recipe-bulk-download-folder/
│   │   └── SKILL.md
│   ├── recipe-collect-form-responses/
│   │   └── SKILL.md
│   ├── recipe-compare-sheet-tabs/
│   │   └── SKILL.md
│   ├── recipe-copy-sheet-for-new-month/
│   │   └── SKILL.md
│   ├── recipe-create-classroom-course/
│   │   └── SKILL.md
│   ├── recipe-create-doc-from-template/
│   │   └── SKILL.md
│   ├── recipe-create-events-from-sheet/
│   │   └── SKILL.md
│   ├── recipe-create-expense-tracker/
│   │   └── SKILL.md
│   ├── recipe-create-feedback-form/
│   │   └── SKILL.md
│   ├── recipe-create-gmail-filter/
│   │   └── SKILL.md
│   ├── recipe-create-meet-space/
│   │   └── SKILL.md
│   ├── recipe-create-presentation/
│   │   └── SKILL.md
│   ├── recipe-create-shared-drive/
│   │   └── SKILL.md
│   ├── recipe-create-task-list/
│   │   └── SKILL.md
│   ├── recipe-create-vacation-responder/
│   │   └── SKILL.md
│   ├── recipe-draft-email-from-doc/
│   │   └── SKILL.md
│   ├── recipe-email-drive-link/
│   │   └── SKILL.md
│   ├── recipe-find-free-time/
│   │   └── SKILL.md
│   ├── recipe-find-large-files/
│   │   └── SKILL.md
│   ├── recipe-forward-labeled-emails/
│   │   └── SKILL.md
│   ├── recipe-generate-report-from-sheet/
│   │   └── SKILL.md
│   ├── recipe-label-and-archive-emails/
│   │   └── SKILL.md
│   ├── recipe-log-deal-update/
│   │   └── SKILL.md
│   ├── recipe-organize-drive-folder/
│   │   └── SKILL.md
│   ├── recipe-plan-weekly-schedule/
│   │   └── SKILL.md
│   ├── recipe-post-mortem-setup/
│   │   └── SKILL.md
│   ├── recipe-reschedule-meeting/
│   │   └── SKILL.md
│   ├── recipe-review-meet-participants/
│   │   └── SKILL.md
│   ├── recipe-review-overdue-tasks/
│   │   └── SKILL.md
│   ├── recipe-save-email-attachments/
│   │   └── SKILL.md
│   ├── recipe-save-email-to-doc/
│   │   └── SKILL.md
│   ├── recipe-schedule-recurring-event/
│   │   └── SKILL.md
│   ├── recipe-send-team-announcement/
│   │   └── SKILL.md
│   ├── recipe-share-doc-and-notify/
│   │   └── SKILL.md
│   ├── recipe-share-event-materials/
│   │   └── SKILL.md
│   ├── recipe-share-folder-with-team/
│   │   └── SKILL.md
│   ├── recipe-sync-contacts-to-sheet/
│   │   └── SKILL.md
│   └── recipe-watch-drive-changes/
│       └── SKILL.md
├── src/
│   ├── auth.rs
│   ├── auth_commands.rs
│   ├── client.rs
│   ├── commands.rs
│   ├── credential_store.rs
│   ├── discovery.rs
│   ├── error.rs
│   ├── executor.rs
│   ├── formatter.rs
│   ├── fs_util.rs
│   ├── generate_skills.rs
│   ├── helpers/
│   │   ├── README.md
│   │   ├── calendar.rs
│   │   ├── chat.rs
│   │   ├── docs.rs
│   │   ├── drive.rs
│   │   ├── events/
│   │   │   ├── mod.rs
│   │   │   ├── renew.rs
│   │   │   └── subscribe.rs
│   │   ├── gmail/
│   │   │   ├── forward.rs
│   │   │   ├── mod.rs
│   │   │   ├── read.rs
│   │   │   ├── reply.rs
│   │   │   ├── send.rs
│   │   │   ├── triage.rs
│   │   │   └── watch.rs
│   │   ├── mod.rs
│   │   ├── modelarmor.rs
│   │   ├── script.rs
│   │   ├── sheets.rs
│   │   └── workflows.rs
│   ├── logging.rs
│   ├── main.rs
│   ├── oauth_config.rs
│   ├── output.rs
│   ├── schema.rs
│   ├── services.rs
│   ├── setup.rs
│   ├── setup_tui.rs
│   ├── text.rs
│   ├── timezone.rs
│   ├── token_storage.rs
│   └── validate.rs
└── templates/
    └── modelarmor/
        └── jailbreak.json
Download .txt
SYMBOL INDEX (1329 symbols across 42 files)

FILE: src/auth.rs
  function get_quota_project (line 33) | pub fn get_quota_project() -> Option<String> {
  function adc_well_known_path (line 65) | fn adc_well_known_path() -> Option<PathBuf> {
  type Credential (line 75) | enum Credential {
  type AccessTokenProvider (line 85) | pub trait AccessTokenProvider: Send + Sync {
    method access_token (line 86) | async fn access_token(&self) -> anyhow::Result<String>;
    method access_token (line 108) | async fn access_token(&self) -> anyhow::Result<String> {
    method access_token (line 138) | async fn access_token(&self) -> anyhow::Result<String> {
  type ScopedTokenProvider (line 94) | pub struct ScopedTokenProvider {
    method new (line 99) | pub fn new(scopes: &[&str]) -> Self {
  function token_provider (line 114) | pub fn token_provider(scopes: &[&str]) -> ScopedTokenProvider {
  type FakeTokenProvider (line 120) | pub struct FakeTokenProvider {
    method new (line 126) | pub fn new(tokens: impl IntoIterator<Item = &'static str>) -> Self {
  function get_token (line 158) | pub async fn get_token(scopes: &[&str]) -> anyhow::Result<String> {
  function get_token_inner (line 176) | async fn get_token_inner(
  function parse_credential_file (line 228) | async fn parse_credential_file(
  function load_credentials_inner (line 256) | async fn load_credentials_inner(
  type EnvVarGuard (line 365) | struct EnvVarGuard {
    method set (line 372) | fn set(name: &str, value: impl AsRef<std::ffi::OsStr>) -> Self {
    method remove (line 382) | fn remove(name: &str) -> Self {
  method drop (line 393) | fn drop(&mut self) {
  function test_load_credentials_no_options (line 403) | async fn test_load_credentials_no_options() {
  function test_load_credentials_adc_env_var_authorized_user (line 426) | async fn test_load_credentials_adc_env_var_authorized_user() {
  function test_load_credentials_adc_env_var_service_account (line 459) | async fn test_load_credentials_adc_env_var_service_account() {
  function test_load_credentials_adc_env_var_missing_file (line 498) | async fn test_load_credentials_adc_env_var_missing_file() {
  function test_load_credentials_env_file_missing (line 519) | async fn test_load_credentials_env_file_missing() {
  function test_load_credentials_env_file_authorized_user (line 531) | async fn test_load_credentials_env_file_authorized_user() {
  function test_load_credentials_env_file_service_account (line 559) | async fn test_load_credentials_env_file_service_account() {
  function test_load_credentials_default_path_authorized_user (line 590) | async fn test_load_credentials_default_path_authorized_user() {
  function test_get_token_from_env_var (line 614) | async fn test_get_token_from_env_var() {
  function test_scoped_token_provider_uses_get_token (line 625) | async fn test_scoped_token_provider_uses_get_token() {
  function test_load_credentials_encrypted_file (line 637) | async fn test_load_credentials_encrypted_file() {
  function test_load_credentials_encrypted_takes_priority_over_default (line 668) | async fn test_load_credentials_encrypted_takes_priority_over_default() {
  function test_load_credentials_corrupt_encrypted_file_is_removed (line 708) | async fn test_load_credentials_corrupt_encrypted_file_is_removed() {
  function test_load_credentials_corrupt_encrypted_falls_through_to_plaintext (line 742) | async fn test_load_credentials_corrupt_encrypted_falls_through_to_plaint...
  function test_get_token_env_var_empty_falls_through (line 781) | async fn test_get_token_env_var_empty_falls_through() {
  function test_get_quota_project_priority_env_var (line 808) | fn test_get_quota_project_priority_env_var() {
  function test_get_quota_project_priority_config (line 819) | fn test_get_quota_project_priority_config() {
  function test_get_quota_project_priority_adc_fallback (line 837) | fn test_get_quota_project_priority_adc_fallback() {
  function test_get_quota_project_reads_adc (line 857) | fn test_get_quota_project_reads_adc() {

FILE: src/auth_commands.rs
  function mask_secret (line 25) | fn mask_secret(s: &str) -> String {
  constant MINIMAL_SCOPES (line 46) | pub const MINIMAL_SCOPES: &[&str] = &[
  constant DEFAULT_SCOPES (line 63) | pub const DEFAULT_SCOPES: &[&str] = MINIMAL_SCOPES;
  constant FULL_SCOPES (line 71) | pub const FULL_SCOPES: &[&str] = &[
  constant READONLY_SCOPES (line 84) | const READONLY_SCOPES: &[&str] = &[
  function config_dir (line 94) | pub fn config_dir() -> PathBuf {
  function plain_credentials_path (line 120) | fn plain_credentials_path() -> PathBuf {
  function token_cache_path (line 127) | fn token_cache_path() -> PathBuf {
  function handle_auth_command (line 132) | pub async fn handle_auth_command(args: &[String]) -> Result<(), GwsError> {
  function run_login (line 174) | pub async fn run_login(args: &[String]) -> Result<(), GwsError> {
  type CliFlowDelegate (line 179) | struct CliFlowDelegate {
    method present_user_url (line 184) | fn present_user_url<'a>(
  function handle_login (line 213) | async fn handle_login(args: &[String]) -> Result<(), GwsError> {
  function fetch_userinfo_email (line 386) | async fn fetch_userinfo_email(access_token: &str) -> Option<String> {
  function handle_export (line 406) | async fn handle_export(unmasked: bool) -> Result<(), GwsError> {
  function resolve_client_credentials (line 441) | fn resolve_client_credentials() -> Result<(String, String, Option<String...
  function resolve_scopes (line 479) | async fn resolve_scopes(
  function scope_matches_service (line 545) | fn scope_matches_service(scope_url: &str, services: &HashSet<String>) ->...
  function map_service_to_scope_prefixes (line 568) | fn map_service_to_scope_prefixes(service: &str) -> Vec<&str> {
  function filter_redundant_restrictive_scopes (line 585) | fn filter_redundant_restrictive_scopes(scopes: Vec<String>) -> Vec<Strin...
  function filter_scopes_by_services (line 613) | fn filter_scopes_by_services(
  function is_subsumed_scope (line 628) | fn is_subsumed_scope(short: &str, all_shorts: &[&str]) -> bool {
  function is_recommended_scope (line 643) | fn is_recommended_scope(
  function run_discovery_scope_picker (line 659) | fn run_discovery_scope_picker(
  function run_simple_scope_picker (line 884) | fn run_simple_scope_picker(services_filter: Option<&HashSet<String>>) ->...
  function handle_status (line 932) | async fn handle_status() -> Result<(), GwsError> {
  function handle_logout (line 1183) | fn handle_logout() -> Result<(), GwsError> {
  function extract_refresh_token (line 1230) | pub fn extract_refresh_token(token_data: &str) -> Option<String> {
  type ScopeEntry (line 1261) | struct ScopeEntry {
  constant SCOPE_ENTRIES (line 1266) | const SCOPE_ENTRIES: &[ScopeEntry] = &[
  function is_app_only_scope (line 1309) | fn is_app_only_scope(url: &str) -> bool {
  function is_workspace_admin_scope (line 1330) | fn is_workspace_admin_scope(url: &str) -> bool {
  function find_unmatched_services (line 1347) | fn find_unmatched_services(scopes: &[String], services: &HashSet<String>...
  function extract_scopes_from_doc (line 1378) | fn extract_scopes_from_doc(
  function fetch_scopes_for_unmatched_services (line 1401) | async fn fetch_scopes_for_unmatched_services(
  function augment_with_dynamic_scopes (line 1431) | async fn augment_with_dynamic_scopes(
  function run_resolve_scopes (line 1454) | fn run_resolve_scopes(args: &[String], project_id: Option<&str>) -> Vec<...
  function run_resolve_scopes_with_services (line 1460) | fn run_resolve_scopes_with_services(
  function resolve_scopes_returns_defaults_when_no_flag (line 1471) | fn resolve_scopes_returns_defaults_when_no_flag() {
  function resolve_scopes_returns_custom_scopes (line 1479) | fn resolve_scopes_returns_custom_scopes() {
  function resolve_scopes_handles_multiple_comma_separated (line 1490) | fn resolve_scopes_handles_multiple_comma_separated() {
  function resolve_scopes_ignores_trailing_flag (line 1503) | fn resolve_scopes_ignores_trailing_flag() {
  function resolve_scopes_readonly_returns_readonly_scopes (line 1511) | fn resolve_scopes_readonly_returns_readonly_scopes() {
  function resolve_scopes_custom_overrides_readonly (line 1524) | fn resolve_scopes_custom_overrides_readonly() {
  function resolve_client_credentials_from_env_vars (line 1538) | fn resolve_client_credentials_from_env_vars() {
  function resolve_client_credentials_missing_env_vars_uses_config (line 1556) | fn resolve_client_credentials_missing_env_vars_uses_config() {
  function config_dir_returns_gws_subdir (line 1577) | fn config_dir_returns_gws_subdir() {
  function config_dir_primary_uses_dot_config (line 1583) | fn config_dir_primary_uses_dot_config() {
  function config_dir_fallback_to_legacy (line 1593) | fn config_dir_fallback_to_legacy() {
  function plain_credentials_path_defaults_to_config_dir (line 1612) | fn plain_credentials_path_defaults_to_config_dir() {
  function plain_credentials_path_respects_env_var (line 1624) | fn plain_credentials_path_respects_env_var() {
  function token_cache_path_is_in_config_dir (line 1639) | fn token_cache_path_is_in_config_dir() {
  function handle_auth_command_empty_args_prints_usage (line 1646) | async fn handle_auth_command_empty_args_prints_usage() {
  function handle_auth_command_help_flag_returns_ok (line 1654) | async fn handle_auth_command_help_flag_returns_ok() {
  function handle_auth_command_help_short_flag_returns_ok (line 1661) | async fn handle_auth_command_help_short_flag_returns_ok() {
  function handle_auth_command_invalid_subcommand (line 1668) | async fn handle_auth_command_invalid_subcommand() {
  function resolve_credentials_fails_without_env_vars_or_config (line 1680) | fn resolve_credentials_fails_without_env_vars_or_config() {
  function resolve_credentials_uses_env_vars_when_present (line 1700) | fn resolve_credentials_uses_env_vars_when_present() {
  function handle_status_succeeds_without_credentials (line 1720) | async fn handle_status_succeeds_without_credentials() {
  function credential_store_save_load_round_trip (line 1728) | fn credential_store_save_load_round_trip() {
  function extract_refresh_token_from_yup_oauth2_format (line 1737) | fn extract_refresh_token_from_yup_oauth2_format() {
  function extract_refresh_token_missing_token (line 1747) | fn extract_refresh_token_missing_token() {
  function extract_refresh_token_empty_array (line 1753) | fn extract_refresh_token_empty_array() {
  function extract_refresh_token_invalid_json (line 1758) | fn extract_refresh_token_invalid_json() {
  function extract_refresh_token_object_format (line 1763) | fn extract_refresh_token_object_format() {
  function is_workspace_admin_scope_apps_alerts (line 1772) | fn is_workspace_admin_scope_apps_alerts() {
  function is_workspace_admin_scope_apps_groups_settings (line 1779) | fn is_workspace_admin_scope_apps_groups_settings() {
  function is_workspace_admin_scope_apps_licensing (line 1786) | fn is_workspace_admin_scope_apps_licensing() {
  function is_workspace_admin_scope_cloud_identity (line 1793) | fn is_workspace_admin_scope_cloud_identity() {
  function is_workspace_admin_scope_ediscovery (line 1806) | fn is_workspace_admin_scope_ediscovery() {
  function is_workspace_admin_scope_directory_readonly (line 1813) | fn is_workspace_admin_scope_directory_readonly() {
  function is_workspace_admin_scope_groups (line 1820) | fn is_workspace_admin_scope_groups() {
  function is_workspace_admin_scope_normal_scopes_not_admin (line 1827) | fn is_workspace_admin_scope_normal_scopes_not_admin() {
  function is_workspace_admin_scope_chat_admin (line 1849) | fn is_workspace_admin_scope_chat_admin() {
  function is_workspace_admin_scope_classroom (line 1865) | fn is_workspace_admin_scope_classroom() {
  function scope_matches_service_exact_match (line 1877) | fn scope_matches_service_exact_match() {
  function scope_matches_service_aliases (line 1886) | fn scope_matches_service_aliases() {
  function scope_matches_service_prefix_match (line 1907) | fn scope_matches_service_prefix_match() {
  function scope_matches_service_no_match (line 1920) | fn scope_matches_service_no_match() {
  function scope_matches_service_cloud_platform_always_matches (line 1929) | fn scope_matches_service_cloud_platform_always_matches() {
  function scope_matches_service_no_partial_name_collision (line 1938) | fn scope_matches_service_no_partial_name_collision() {
  function scope_matches_service_people_contacts (line 1948) | fn scope_matches_service_people_contacts() {
  function scope_matches_service_chat (line 1969) | fn scope_matches_service_chat() {
  function resolve_scopes_with_services_filter (line 1984) | fn resolve_scopes_with_services_filter() {
  function resolve_scopes_services_filter_unknown_service_ignored (line 2002) | fn resolve_scopes_services_filter_unknown_service_ignored() {
  function resolve_scopes_services_takes_priority_with_readonly (line 2011) | fn resolve_scopes_services_takes_priority_with_readonly() {
  function resolve_scopes_services_takes_priority_with_full (line 2027) | fn resolve_scopes_services_takes_priority_with_full() {
  function resolve_scopes_explicit_scopes_bypass_services_filter (line 2043) | fn resolve_scopes_explicit_scopes_bypass_services_filter() {
  function filter_scopes_by_services_none_returns_all (line 2055) | fn filter_scopes_by_services_none_returns_all() {
  function filter_scopes_by_services_empty_set_returns_all (line 2065) | fn filter_scopes_by_services_empty_set_returns_all() {
  function filter_restrictive_removes_metadata_when_broader_present (line 2076) | fn filter_restrictive_removes_metadata_when_broader_present() {
  function filter_restrictive_removes_metadata_when_full_gmail_present (line 2088) | fn filter_restrictive_removes_metadata_when_full_gmail_present() {
  function filter_restrictive_keeps_metadata_when_only_scope (line 2098) | fn filter_restrictive_keeps_metadata_when_only_scope() {
  function mask_secret_long_string (line 2108) | fn mask_secret_long_string() {
  function mask_secret_short_string (line 2114) | fn mask_secret_short_string() {
  function mask_secret_boundary (line 2122) | fn mask_secret_boundary() {
  function find_unmatched_services_identifies_missing (line 2128) | fn find_unmatched_services_identifies_missing() {
  function find_unmatched_services_all_matched (line 2140) | fn find_unmatched_services_all_matched() {
  function make_test_discovery_doc (line 2150) | fn make_test_discovery_doc(scope_urls: &[&str]) -> crate::discovery::Res...
  function extract_scopes_from_doc_filters_app_only (line 2171) | fn extract_scopes_from_doc_filters_app_only() {
  function extract_scopes_from_doc_readonly_filter (line 2190) | fn extract_scopes_from_doc_readonly_filter() {
  function extract_scopes_from_doc_empty_auth (line 2209) | fn extract_scopes_from_doc_empty_auth() {

FILE: src/client.rs
  function build_client (line 3) | pub fn build_client() -> Result<reqwest::Client, crate::error::GwsError> {
  constant MAX_RETRIES (line 22) | const MAX_RETRIES: u32 = 3;
  constant MAX_RETRY_DELAY_SECS (line 25) | const MAX_RETRY_DELAY_SECS: u64 = 60;
  function send_with_retry (line 29) | pub async fn send_with_retry(
  function compute_retry_delay (line 55) | fn compute_retry_delay(header_value: Option<&str>, attempt: u32) -> u64 {
  function build_client_succeeds (line 67) | fn build_client_succeeds() {
  function retry_delay_caps_large_header_value (line 72) | fn retry_delay_caps_large_header_value() {
  function retry_delay_passes_through_small_header_value (line 77) | fn retry_delay_passes_through_small_header_value() {
  function retry_delay_falls_back_to_exponential_on_missing_header (line 82) | fn retry_delay_falls_back_to_exponential_on_missing_header() {
  function retry_delay_falls_back_on_unparseable_header (line 89) | fn retry_delay_falls_back_on_unparseable_header() {
  function retry_delay_caps_at_boundary (line 95) | fn retry_delay_caps_at_boundary() {

FILE: src/commands.rs
  function build_cli (line 20) | pub fn build_cli(doc: &RestDescription) -> Command {
  function build_resource_command (line 75) | fn build_resource_command(name: &str, resource: &RestResource) -> Option...
  function make_doc (line 190) | fn make_doc() -> RestDescription {
  function test_all_commands_always_shown (line 255) | fn test_all_commands_always_shown() {
  function test_sanitize_arg_present (line 270) | fn test_sanitize_arg_present() {

FILE: src/credential_store.rs
  function ensure_key_dir (line 28) | fn ensure_key_dir(path: &std::path::Path) -> std::io::Result<()> {
  function save_key_file_exclusive (line 50) | fn save_key_file_exclusive(path: &std::path::Path, b64_key: &str) -> std...
  function save_key_file (line 71) | fn save_key_file(path: &std::path::Path, b64_key: &str) -> std::io::Resu...
  function read_key_file (line 74) | fn read_key_file(path: &std::path::Path) -> Option<[u8; 32]> {
  function generate_random_key (line 109) | fn generate_random_key() -> [u8; 32] {
  type KeyringProvider (line 116) | trait KeyringProvider {
    method get_password (line 118) | fn get_password(&self) -> Result<String, keyring::Error>;
    method set_password (line 120) | fn set_password(&self, password: &str) -> Result<(), keyring::Error>;
    method get_password (line 133) | fn get_password(&self) -> Result<String, keyring::Error> {
    method set_password (line 140) | fn set_password(&self, password: &str) -> Result<(), keyring::Error> {
    method get_password (line 472) | fn get_password(&self) -> Result<String, keyring::Error> {
    method set_password (line 482) | fn set_password(&self, password: &str) -> Result<(), keyring::Error> {
  type OsKeyring (line 124) | struct OsKeyring(Option<Entry>);
    method new (line 127) | fn new(service: &str, user: &str) -> Self {
  type KeyringBackend (line 154) | enum KeyringBackend {
    method from_env (line 160) | fn from_env() -> Self {
    method as_str (line 178) | fn as_str(&self) -> &'static str {
  function resolve_key (line 196) | fn resolve_key(
  function get_or_create_key (line 289) | fn get_or_create_key() -> anyhow::Result<[u8; 32]> {
  function encrypt (line 321) | pub fn encrypt(plaintext: &[u8]) -> anyhow::Result<Vec<u8>> {
  function decrypt (line 338) | pub fn decrypt(data: &[u8]) -> anyhow::Result<Vec<u8>> {
  function active_backend_name (line 359) | pub fn active_backend_name() -> &'static str {
  function encrypted_credentials_path (line 364) | pub fn encrypted_credentials_path() -> PathBuf {
  function save_encrypted (line 369) | pub fn save_encrypted(json: &str) -> anyhow::Result<PathBuf> {
  function load_encrypted_from_path (line 397) | pub fn load_encrypted_from_path(path: &std::path::Path) -> anyhow::Resul...
  function load_encrypted (line 404) | pub fn load_encrypted() -> anyhow::Result<String> {
  type MockState (line 415) | enum MockState {
  type MockKeyring (line 422) | struct MockKeyring {
    method with_password (line 430) | fn with_password(b64: &str) -> Self {
    method no_entry (line 439) | fn no_entry() -> Self {
    method platform_error (line 448) | fn platform_error() -> Self {
    method with_set_failure (line 457) | fn with_set_failure(mut self) -> Self {
    method with_on_set (line 462) | fn with_on_set<F>(self, callback: F) -> Self
  function write_test_key (line 495) | fn write_test_key(dir: &std::path::Path) -> ([u8; 32], std::path::PathBu...
  function keyring_backend_returns_keyring_key (line 506) | fn keyring_backend_returns_keyring_key() {
  function keyring_backend_creates_file_backup_when_missing (line 517) | fn keyring_backend_creates_file_backup_when_missing() {
  function keyring_backend_syncs_file_when_keyring_differs (line 538) | fn keyring_backend_syncs_file_when_keyring_differs() {
  function keyring_backend_no_entry_reads_file (line 557) | fn keyring_backend_no_entry_reads_file() {
  function keyring_backend_no_entry_no_file_generates_and_saves_both (line 571) | fn keyring_backend_no_entry_no_file_generates_and_saves_both() {
  function keyring_backend_no_entry_no_file_keyring_set_fails (line 584) | fn keyring_backend_no_entry_no_file_keyring_set_fails() {
  function keyring_backend_platform_error_falls_back_to_file (line 596) | fn keyring_backend_platform_error_falls_back_to_file() {
  function keyring_backend_platform_error_no_file_generates (line 605) | fn keyring_backend_platform_error_no_file_generates() {
  function keyring_backend_invalid_keyring_data_uses_file (line 615) | fn keyring_backend_invalid_keyring_data_uses_file() {
  function file_backend_reads_existing_key (line 627) | fn file_backend_reads_existing_key() {
  function file_backend_generates_when_missing (line 636) | fn file_backend_generates_when_missing() {
  function file_backend_skips_keyring_entirely (line 650) | fn file_backend_skips_keyring_entirely() {
  function key_is_stable_across_calls (line 663) | fn key_is_stable_across_calls() {
  function backend_default_is_keyring (line 675) | fn backend_default_is_keyring() {
  function read_key_file_valid (line 683) | fn read_key_file_valid() {
  function read_key_file_missing (line 693) | fn read_key_file_missing() {
  function read_key_file_wrong_length (line 699) | fn read_key_file_wrong_length() {
  function read_key_file_invalid_base64 (line 708) | fn read_key_file_invalid_base64() {
  function get_or_create_key_is_deterministic (line 718) | fn get_or_create_key_is_deterministic() {
  function get_or_create_key_produces_256_bits (line 725) | fn get_or_create_key_produces_256_bits() {
  function encrypt_decrypt_round_trip (line 731) | fn encrypt_decrypt_round_trip() {
  function encrypt_decrypt_empty (line 741) | fn encrypt_decrypt_empty() {
  function decrypt_rejects_short_data (line 749) | fn decrypt_rejects_short_data() {
  function decrypt_rejects_tampered_ciphertext (line 756) | fn decrypt_rejects_tampered_ciphertext() {
  function each_encryption_produces_different_output (line 767) | fn each_encryption_produces_different_output() {
  function save_key_file_exclusive_creates_new_file (line 781) | fn save_key_file_exclusive_creates_new_file() {
  function save_key_file_exclusive_rejects_existing_file (line 789) | fn save_key_file_exclusive_rejects_existing_file() {
  function save_key_file_overwrites_existing (line 802) | fn save_key_file_overwrites_existing() {
  function ensure_key_dir_creates_nested_dirs (line 813) | fn ensure_key_dir_creates_nested_dirs() {
  function backend_from_env_file_lowercase (line 823) | fn backend_from_env_file_lowercase() {
  function backend_from_env_file_uppercase (line 836) | fn backend_from_env_file_uppercase() {
  function backend_from_env_invalid_defaults_to_keyring (line 848) | fn backend_from_env_invalid_defaults_to_keyring() {
  function race_loser_syncs_winner_key_to_keyring (line 861) | fn race_loser_syncs_winner_key_to_keyring() {
  function race_loser_corrupt_file_overwrites (line 884) | fn race_loser_corrupt_file_overwrites() {

FILE: src/discovery.rs
  type RestDescription (line 29) | pub struct RestDescription {
  type AuthDescription (line 48) | pub struct AuthDescription {
  type OAuth2Description (line 53) | pub struct OAuth2Description {
  type ScopeDescription (line 58) | pub struct ScopeDescription {
  type RestResource (line 64) | pub struct RestResource {
  type RestMethod (line 74) | pub struct RestMethod {
  type MediaUpload (line 97) | pub struct MediaUpload {
  type MediaUploadProtocols (line 104) | pub struct MediaUploadProtocols {
  type MediaUploadProtocol (line 110) | pub struct MediaUploadProtocol {
  type SchemaRef (line 117) | pub struct SchemaRef {
  type MethodParameter (line 127) | pub struct MethodParameter {
  type JsonSchema (line 150) | pub struct JsonSchema {
  type JsonSchemaProperty (line 168) | pub struct JsonSchemaProperty {
  function fetch_discovery_document (line 187) | pub async fn fetch_discovery_document(
  function test_deserialize_rest_description (line 261) | fn test_deserialize_rest_description() {
  function test_deserialize_defaults (line 313) | fn test_deserialize_defaults() {

FILE: src/error.rs
  type GwsError (line 19) | pub enum GwsError {
    constant EXIT_CODE_API (line 69) | pub const EXIT_CODE_API: i32 = 1;
    constant EXIT_CODE_AUTH (line 71) | pub const EXIT_CODE_AUTH: i32 = 2;
    constant EXIT_CODE_VALIDATION (line 73) | pub const EXIT_CODE_VALIDATION: i32 = 3;
    constant EXIT_CODE_DISCOVERY (line 75) | pub const EXIT_CODE_DISCOVERY: i32 = 4;
    constant EXIT_CODE_OTHER (line 77) | pub const EXIT_CODE_OTHER: i32 = 5;
    method exit_code (line 89) | pub fn exit_code(&self) -> i32 {
    method to_json (line 99) | pub fn to_json(&self) -> serde_json::Value {
  constant EXIT_CODE_DOCUMENTATION (line 46) | pub const EXIT_CODE_DOCUMENTATION: &[(i32, &str)] = &[
  function error_label (line 154) | fn error_label(err: &GwsError) -> String {
  function print_error_json (line 170) | pub fn print_error_json(err: &GwsError) {
  function test_exit_code_api (line 212) | fn test_exit_code_api() {
  function test_exit_code_auth (line 223) | fn test_exit_code_auth() {
  function test_exit_code_validation (line 231) | fn test_exit_code_validation() {
  function test_exit_code_discovery (line 239) | fn test_exit_code_discovery() {
  function test_exit_code_other (line 247) | fn test_exit_code_other() {
  function test_exit_codes_are_distinct (line 255) | fn test_exit_codes_are_distinct() {
  function test_error_to_json_api (line 273) | fn test_error_to_json_api() {
  function test_error_to_json_validation (line 288) | fn test_error_to_json_validation() {
  function test_error_to_json_auth (line 297) | fn test_error_to_json_auth() {
  function test_error_to_json_discovery (line 306) | fn test_error_to_json_discovery() {
  function test_error_to_json_other (line 315) | fn test_error_to_json_other() {
  function test_error_to_json_access_not_configured_with_url (line 326) | fn test_error_to_json_access_not_configured_with_url() {
  function test_error_to_json_access_not_configured_without_url (line 343) | fn test_error_to_json_access_not_configured_without_url() {
  function test_colorize_respects_no_color_env (line 361) | fn test_colorize_respects_no_color_env() {
  function test_error_label_contains_variant_name (line 371) | fn test_error_label_contains_variant_name() {
  function test_sanitize_for_terminal_strips_control_chars (line 395) | fn test_sanitize_for_terminal_strips_control_chars() {

FILE: src/executor.rs
  type AuthMethod (line 36) | pub enum AuthMethod {
  type UploadSource (line 49) | pub enum UploadSource<'a> {
  type PaginationConfig (line 65) | pub struct PaginationConfig {
  method default (line 75) | fn default() -> Self {
  type ExecutionInput (line 86) | struct ExecutionInput {
  function parse_and_validate_inputs (line 95) | fn parse_and_validate_inputs(
  function build_http_request (line 161) | async fn build_http_request(
  function handle_json_response (line 244) | async fn handle_json_response(
  function handle_binary_response (line 340) | async fn handle_binary_response(
  function execute_method (line 398) | pub async fn execute_method(
  function build_url (line 540) | fn build_url(
  function extract_template_path_parameters (line 662) | fn extract_template_path_parameters(path_template: &str) -> HashSet<&str> {
  function render_path_template (line 685) | fn render_path_template(
  function extract_enable_url (line 739) | pub fn extract_enable_url(message: &str) -> Option<String> {
  function handle_error_response (line 753) | fn handle_error_response<T>(
  function resolve_upload_mime (line 831) | fn resolve_upload_mime(
  function build_multipart_stream (line 867) | fn build_multipart_stream(
  function build_multipart_bytes (line 925) | fn build_multipart_bytes(
  function build_multipart_body (line 963) | fn build_multipart_body(
  function validate_body_against_schema (line 995) | fn validate_body_against_schema(
  function validate_value (line 1013) | fn validate_value(
  function validate_properties (line 1038) | fn validate_properties(
  function validate_property (line 1075) | fn validate_property(
  function get_value_type (line 1142) | fn get_value_type(val: &Value) -> &'static str {
  function mime_to_extension (line 1155) | pub fn mime_to_extension(mime: &str) -> &str {
  function test_pagination_config_default (line 1198) | fn test_pagination_config_default() {
  function test_auth_method_equality (line 1206) | fn test_auth_method_equality() {
  function test_mime_to_extension_more_types (line 1213) | fn test_mime_to_extension_more_types() {
  function test_validate_body_valid (line 1231) | fn test_validate_body_valid() {
  function test_validate_body_unknown_field (line 1260) | fn test_validate_body_unknown_field() {
  function test_validate_body_deep_validation (line 1292) | fn test_validate_body_deep_validation() {
  function test_build_multipart_body (line 1412) | async fn test_build_multipart_body() {
  function test_build_multipart_body_no_metadata (line 1433) | async fn test_build_multipart_body_no_metadata() {
  function test_resolve_upload_mime_explicit_flag (line 1448) | fn test_resolve_upload_mime_explicit_flag() {
  function test_resolve_upload_mime_extension_beats_metadata (line 1455) | fn test_resolve_upload_mime_extension_beats_metadata() {
  function test_resolve_upload_mime_metadata_fallback_for_unknown_extension (line 1465) | fn test_resolve_upload_mime_metadata_fallback_for_unknown_extension() {
  function test_resolve_upload_mime_extension_when_no_metadata (line 1475) | fn test_resolve_upload_mime_extension_when_no_metadata() {
  function test_resolve_upload_mime_fallback (line 1487) | fn test_resolve_upload_mime_fallback() {
  function test_resolve_upload_mime_explicit_enables_import_conversion (line 1493) | fn test_resolve_upload_mime_explicit_enables_import_conversion() {
  function test_build_multipart_bytes_with_metadata (line 1503) | fn test_build_multipart_bytes_with_metadata() {
  function test_build_multipart_bytes_without_metadata (line 1521) | fn test_build_multipart_bytes_without_metadata() {
  function test_build_multipart_stream_content_length (line 1530) | async fn test_build_multipart_stream_content_length() {
  function test_build_multipart_stream_large_file (line 1567) | async fn test_build_multipart_stream_large_file() {
  function test_build_url_basic (line 1606) | fn test_build_url_basic() {
  function test_build_url_substitution (line 1623) | fn test_build_url_substitution() {
  function test_build_url_query_params (line 1641) | fn test_build_url_query_params() {
  function test_build_url_repeated_query_param_expands_array (line 1660) | fn test_build_url_repeated_query_param_expands_array() {
  function test_build_url_encodes_path_parameter_chars (line 1699) | fn test_build_url_encodes_path_parameter_chars() {
  function test_build_url_plus_expansion_preserves_slashes (line 1737) | fn test_build_url_plus_expansion_preserves_slashes() {
  function test_build_url_plus_expansion_rejects_reserved_chars (line 1770) | fn test_build_url_plus_expansion_rejects_reserved_chars() {
  function test_build_url_plus_expansion_rejects_path_traversal (line 1797) | fn test_build_url_plus_expansion_rejects_path_traversal() {
  function test_build_url_upload_endpoint_substitutes_path_params (line 1824) | fn test_build_url_upload_endpoint_substitutes_path_params() {
  function test_build_url_does_not_replace_placeholder_like_values (line 1864) | fn test_build_url_does_not_replace_placeholder_like_values() {
  function test_build_url_errors_for_path_param_not_in_template (line 1886) | fn test_build_url_errors_for_path_param_not_in_template() {
  function test_build_url_flatpath_fallback_on_mismatch (line 1915) | fn test_build_url_flatpath_fallback_on_mismatch() {
  function test_handle_error_response_401 (line 1945) | fn test_handle_error_response_401() {
  function test_handle_error_response_401_with_oauth_does_not_mask_error (line 1959) | fn test_handle_error_response_401_with_oauth_does_not_mask_error() {
  function test_handle_error_response_api_error (line 1997) | fn test_handle_error_response_api_error() {
  function test_execute_method_dry_run (line 2030) | async fn test_execute_method_dry_run() {
  function test_execute_method_missing_path_param (line 2107) | async fn test_execute_method_missing_path_param() {
  function test_handle_error_response_non_json (line 2154) | fn test_handle_error_response_non_json() {
  function test_extract_enable_url_typical_message (line 2177) | fn test_extract_enable_url_typical_message() {
  function test_extract_enable_url_no_url (line 2188) | fn test_extract_enable_url_no_url() {
  function test_extract_enable_url_non_http (line 2194) | fn test_extract_enable_url_non_http() {
  function test_extract_enable_url_trims_trailing_punctuation (line 2200) | fn test_extract_enable_url_trims_trailing_punctuation() {
  function test_handle_error_response_access_not_configured_with_url (line 2210) | fn test_handle_error_response_access_not_configured_with_url() {
  function test_handle_error_response_access_not_configured_errors_array (line 2248) | fn test_handle_error_response_access_not_configured_errors_array() {
  function test_get_value_type_helper (line 2279) | fn test_get_value_type_helper() {
  function test_post_without_body_sets_content_length_zero (line 2290) | async fn test_post_without_body_sets_content_length_zero() {
  function test_post_with_body_does_not_add_content_length_zero (line 2330) | async fn test_post_with_body_does_not_add_content_length_zero() {
  function test_get_does_not_set_content_length_zero (line 2368) | async fn test_get_does_not_set_content_length_zero() {

FILE: src/formatter.rs
  type OutputFormat (line 24) | pub enum OutputFormat {
    method parse (line 42) | pub fn parse(s: &str) -> Result<Self, String> {
    method from_str (line 55) | pub fn from_str(s: &str) -> Self {
  function format_value (line 61) | pub fn format_value(value: &Value, format: &OutputFormat) -> String {
  function format_value_paginated (line 79) | pub fn format_value_paginated(value: &Value, format: &OutputFormat, is_f...
  function extract_items (line 93) | fn extract_items(value: &Value) -> Option<(&str, &Vec<Value>)> {
  function format_table (line 109) | fn format_table(value: &Value) -> String {
  function flatten_object (line 117) | fn flatten_object(obj: &serde_json::Map<String, Value>, prefix: &str) ->...
  function format_table_page (line 142) | fn format_table_page(value: &Value, emit_header: bool) -> String {
  function format_array_as_table (line 164) | fn format_array_as_table(arr: &[Value], emit_header: bool) -> String {
  function format_yaml (line 273) | fn format_yaml(value: &Value) -> String {
  function json_to_yaml (line 277) | fn json_to_yaml(value: &Value, indent: usize) -> String {
  function format_csv (line 335) | fn format_csv(value: &Value) -> String {
  function format_csv_page (line 343) | fn format_csv_page(value: &Value, emit_header: bool) -> String {
  function csv_escape (line 413) | fn csv_escape(s: &str) -> String {
  function value_to_cell (line 421) | fn value_to_cell(value: &Value) -> String {
  function test_output_format_from_str (line 441) | fn test_output_format_from_str() {
  function test_output_format_parse_known (line 451) | fn test_output_format_parse_known() {
  function test_output_format_parse_unknown_returns_err (line 463) | fn test_output_format_parse_unknown_returns_err() {
  function test_format_json (line 470) | fn test_format_json() {
  function test_format_table_array_of_objects (line 478) | fn test_format_table_array_of_objects() {
  function test_format_table_single_object (line 495) | fn test_format_table_single_object() {
  function test_format_table_nested_object_flattened (line 503) | fn test_format_table_nested_object_flattened() {
  function test_format_table_nested_objects_in_array (line 537) | fn test_format_table_nested_objects_in_array() {
  function test_format_table_multibyte_truncation_does_not_panic (line 552) | fn test_format_table_multibyte_truncation_does_not_panic() {
  function test_format_table_multibyte_exact_boundary (line 563) | fn test_format_table_multibyte_exact_boundary() {
  function test_format_csv (line 571) | fn test_format_csv() {
  function test_format_csv_array_of_arrays (line 585) | fn test_format_csv_array_of_arrays() {
  function test_format_csv_flat_scalars (line 602) | fn test_format_csv_flat_scalars() {
  function test_format_csv_flat_scalars_with_escaping (line 614) | fn test_format_csv_flat_scalars_with_escaping() {
  function test_format_csv_escape (line 626) | fn test_format_csv_escape() {
  function test_format_yaml (line 633) | fn test_format_yaml() {
  function test_format_table_empty_array (line 641) | fn test_format_table_empty_array() {
  function test_extract_items (line 649) | fn test_extract_items() {
  function test_extract_items_none (line 657) | fn test_extract_items_none() {
  function test_format_yaml_hash_in_string_is_quoted_not_block (line 665) | fn test_format_yaml_hash_in_string_is_quoted_not_block() {
  function test_format_yaml_colon_in_string_is_quoted (line 682) | fn test_format_yaml_colon_in_string_is_quoted() {
  function test_format_yaml_multiline_still_uses_block (line 693) | fn test_format_yaml_multiline_still_uses_block() {
  function test_format_value_paginated_csv_first_page_has_header (line 706) | fn test_format_value_paginated_csv_first_page_has_header() {
  function test_format_value_paginated_csv_continuation_no_header (line 720) | fn test_format_value_paginated_csv_continuation_no_header() {
  function test_format_value_paginated_table_first_page_has_header (line 737) | fn test_format_value_paginated_table_first_page_has_header() {
  function test_format_value_paginated_table_continuation_no_header (line 752) | fn test_format_value_paginated_table_continuation_no_header() {
  function test_format_value_paginated_yaml_has_document_separator (line 767) | fn test_format_value_paginated_yaml_has_document_separator() {

FILE: src/fs_util.rs
  function atomic_write (line 30) | pub fn atomic_write(path: &Path, data: &[u8]) -> io::Result<()> {
  function atomic_write_async (line 48) | pub async fn atomic_write_async(path: &Path, data: &[u8]) -> io::Result<...
  function test_atomic_write_creates_file (line 115) | fn test_atomic_write_creates_file() {
  function test_atomic_write_overwrites_existing (line 130) | fn test_atomic_write_overwrites_existing() {
  function test_atomic_write_leaves_no_tmp_file (line 139) | fn test_atomic_write_leaves_no_tmp_file() {
  function test_atomic_write_async_creates_file (line 153) | async fn test_atomic_write_async_creates_file() {

FILE: src/generate_skills.rs
  constant PERSONAS_YAML (line 27) | const PERSONAS_YAML: &str = include_str!("../registry/personas.yaml");
  constant RECIPES_YAML (line 28) | const RECIPES_YAML: &str = include_str!("../registry/recipes.yaml");
  constant BLOCKED_METHODS (line 32) | const BLOCKED_METHODS: &[(&str, &str, &str)] = &[
  type PersonaRegistry (line 42) | struct PersonaRegistry {
  type PersonaEntry (line 47) | struct PersonaEntry {
  type RecipeRegistry (line 59) | struct RecipeRegistry {
  type RecipeEntry (line 64) | struct RecipeEntry {
  type SkillIndexEntry (line 74) | struct SkillIndexEntry {
  function handle_generate_skills (line 81) | pub async fn handle_generate_skills(args: &[String]) -> Result<(), GwsEr...
  function parse_output_dir (line 276) | fn parse_output_dir(args: &[String]) -> String {
  function parse_filter (line 288) | fn parse_filter(args: &[String]) -> Option<String> {
  function write_skill (line 299) | fn write_skill(base: &Path, name: &str, content: &str) -> Result<(), Gws...
  function write_skills_index (line 310) | fn write_skills_index(entries: &[SkillIndexEntry]) -> Result<(), GwsErro...
  function is_blocked_method (line 366) | fn is_blocked_method(alias: &str, resource: &str, method: &str) -> bool {
  function render_service_skill (line 372) | fn render_service_skill(
  function render_helper_skill (line 486) | fn render_helper_skill(
  function generate_shared_skill (line 668) | fn generate_shared_skill(base: &Path) -> Result<(), GwsError> {
  function render_persona_skill (line 755) | fn render_persona_skill(persona: &PersonaEntry) -> String {
  function render_recipe_skill (line 826) | fn render_recipe_skill(recipe: &RecipeEntry) -> String {
  function truncate_desc (line 885) | fn truncate_desc(desc: &str) -> String {
  function lookup_method_description (line 902) | fn lookup_method_description(
  function capitalize_first (line 923) | fn capitalize_first(s: &str) -> String {
  function product_name_from_title (line 931) | fn product_name_from_title(title: &str) -> String {
  function service_description (line 952) | fn service_description(product_name: &str, discovery_desc: &str) -> Stri...
  function test_registry_references (line 973) | fn test_registry_references() {
  function test_truncate_desc_short (line 1035) | fn test_truncate_desc_short() {
  function test_truncate_desc_capitalizes (line 1040) | fn test_truncate_desc_capitalizes() {
  function test_truncate_desc_replaces_quotes (line 1045) | fn test_truncate_desc_replaces_quotes() {
  function test_truncate_desc_truncates_long (line 1053) | fn test_truncate_desc_truncates_long() {
  function test_truncate_desc_adds_period (line 1063) | fn test_truncate_desc_adds_period() {
  function test_truncate_desc_preserves_existing_period (line 1068) | fn test_truncate_desc_preserves_existing_period() {
  function test_truncate_desc_ellipsis_no_period (line 1073) | fn test_truncate_desc_ellipsis_no_period() {
  function test_lookup_method_description_found (line 1082) | fn test_lookup_method_description_found() {
  function test_lookup_method_description_missing_resource (line 1114) | fn test_lookup_method_description_missing_resource() {
  function test_lookup_method_description_missing_method (line 1123) | fn test_lookup_method_description_missing_method() {
  function test_lookup_method_description_no_description (line 1138) | fn test_lookup_method_description_no_description() {
  function test_capitalize_first_empty (line 1166) | fn test_capitalize_first_empty() {
  function test_capitalize_first_basic (line 1171) | fn test_capitalize_first_basic() {
  function test_product_name_from_title_strips_api (line 1176) | fn test_product_name_from_title_strips_api() {
  function test_product_name_from_title_no_api_suffix (line 1181) | fn test_product_name_from_title_no_api_suffix() {
  function test_product_name_from_title_adds_google (line 1187) | fn test_product_name_from_title_adds_google() {

FILE: src/helpers/calendar.rs
  type CalendarHelper (line 25) | pub struct CalendarHelper;
  method inject_commands (line 28) | fn inject_commands(
  method handle (line 157) | fn handle<'a>(
  function handle_agenda (line 209) | async fn handle_agenda(matches: &ArgMatches) -> Result<(), GwsError> {
  function build_insert_request (line 422) | fn build_insert_request(
  function make_mock_doc (line 526) | fn make_mock_doc() -> crate::discovery::RestDescription {
  function make_matches_insert (line 538) | fn make_matches_insert(args: &[&str]) -> ArgMatches {
  function test_build_insert_request (line 560) | fn test_build_insert_request() {
  function test_build_insert_request_with_meet (line 580) | fn test_build_insert_request_with_meet() {
  function test_build_insert_request_with_meet_is_idempotent (line 604) | fn test_build_insert_request_with_meet_is_idempotent() {
  function test_build_insert_request_with_meet_idempotency_robust (line 633) | fn test_build_insert_request_with_meet_idempotency_robust() {
  function test_build_insert_request_with_optional_fields (line 714) | fn test_build_insert_request_with_optional_fields() {
  function agenda_day_boundaries_use_account_timezone (line 743) | fn agenda_day_boundaries_use_account_timezone() {

FILE: src/helpers/chat.rs
  type ChatHelper (line 24) | pub struct ChatHelper;
  method inject_commands (line 27) | fn inject_commands(
  method handle (line 62) | fn handle<'a>(
  function build_send_request (line 129) | fn build_send_request(
  type SendConfig (line 163) | pub struct SendConfig {
  function parse_send_args (line 179) | pub fn parse_send_args(matches: &ArgMatches) -> Result<SendConfig, GwsEr...
  function make_mock_doc (line 195) | fn make_mock_doc() -> RestDescription {
  function make_matches_send (line 222) | fn make_matches_send(args: &[&str]) -> ArgMatches {
  function test_build_send_request (line 230) | fn test_build_send_request() {
  function test_parse_send_args (line 244) | fn test_parse_send_args() {
  function test_parse_send_args_rejects_traversal_in_space (line 252) | fn test_parse_send_args_rejects_traversal_in_space() {
  function test_parse_send_args_rejects_query_injection_in_space (line 262) | fn test_parse_send_args_rejects_query_injection_in_space() {
  function test_inject_commands (line 273) | fn test_inject_commands() {

FILE: src/helpers/docs.rs
  type DocsHelper (line 24) | pub struct DocsHelper;
  method inject_commands (line 27) | fn inject_commands(
  method handle (line 62) | fn handle<'a>(
  function build_write_request (line 119) | fn build_write_request(
  function make_mock_doc (line 166) | fn make_mock_doc() -> RestDescription {
  function make_matches_write (line 188) | fn make_matches_write(args: &[&str]) -> ArgMatches {
  function test_build_write_request (line 196) | fn test_build_write_request() {

FILE: src/helpers/drive.rs
  type DriveHelper (line 25) | pub struct DriveHelper;
  method inject_commands (line 28) | fn inject_commands(
  method handle (line 69) | fn handle<'a>(
  function determine_filename (line 133) | fn determine_filename(file_path: &str, name_arg: Option<&str>) -> Result...
  function build_metadata (line 145) | fn build_metadata(filename: &str, parent_id: Option<&str>) -> Value {
  function test_determine_filename_explicit (line 162) | fn test_determine_filename_explicit() {
  function test_determine_filename_from_path (line 170) | fn test_determine_filename_from_path() {
  function test_determine_filename_invalid_path (line 178) | fn test_determine_filename_invalid_path() {
  function test_build_metadata_no_parent (line 184) | fn test_build_metadata_no_parent() {
  function test_build_metadata_with_parent (line 191) | fn test_build_metadata_with_parent() {

FILE: src/helpers/events/mod.rs
  type EventsHelper (line 31) | pub struct EventsHelper;
  constant PUBSUB_SCOPE (line 32) | pub(super) const PUBSUB_SCOPE: &str = "https://www.googleapis.com/auth/p...
  constant WORKSPACE_EVENTS_SCOPE (line 33) | pub(super) const WORKSPACE_EVENTS_SCOPE: &str =
  type ProjectId (line 37) | pub struct ProjectId(pub String);
    method fmt (line 39) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  type SubscriptionName (line 45) | pub struct SubscriptionName(pub String);
    method fmt (line 47) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  method inject_commands (line 53) | fn inject_commands(
  method handle (line 173) | fn handle<'a>(
  function test_inject_commands (line 200) | fn test_inject_commands() {

FILE: src/helpers/events/renew.rs
  type RenewConfig (line 4) | pub struct RenewConfig {
  function parse_renew_args (line 10) | fn parse_renew_args(matches: &ArgMatches) -> Result<RenewConfig, GwsErro...
  function handle_renew (line 28) | pub(super) async fn handle_renew(
  function filter_subscriptions_to_renew (line 110) | fn filter_subscriptions_to_renew(subs: &[Value], now_secs: u64, within_s...
  function parse_duration (line 128) | fn parse_duration(s: &str) -> Result<u64, GwsError> {
  function parse_rfc3339_rough (line 151) | fn parse_rfc3339_rough(s: &str) -> Option<u64> {
  function make_matches_renew (line 161) | fn make_matches_renew(args: &[&str]) -> ArgMatches {
  function test_parse_duration_hours (line 170) | fn test_parse_duration_hours() {
  function test_parse_duration_minutes (line 176) | fn test_parse_duration_minutes() {
  function test_parse_duration_days (line 181) | fn test_parse_duration_days() {
  function test_parse_duration_invalid (line 187) | fn test_parse_duration_invalid() {
  function test_parse_rfc3339_rough (line 193) | fn test_parse_rfc3339_rough() {
  function test_parse_renew_args_name (line 205) | fn test_parse_renew_args_name() {
  function test_parse_renew_args_all (line 213) | fn test_parse_renew_args_all() {
  function test_parse_renew_args_missing (line 222) | fn test_parse_renew_args_missing() {
  function test_filter_subscriptions_to_renew (line 228) | fn test_filter_subscriptions_to_renew() {

FILE: src/helpers/events/subscribe.rs
  type SubscribeConfig (line 9) | pub struct SubscribeConfig {
  function parse_subscribe_args (line 32) | fn parse_subscribe_args(matches: &ArgMatches) -> Result<SubscribeConfig,...
  function validate_subscribe_config (line 83) | fn validate_subscribe_config(config: &SubscribeConfig) -> Result<(), Gws...
  function handle_subscribe (line 105) | pub(super) async fn handle_subscribe(
  function pull_loop (line 315) | async fn pull_loop(
  function process_events_pull_response (line 427) | fn process_events_pull_response(response: &Value) -> (Vec<String>, Vec<V...
  function decode_cloud_event (line 447) | fn decode_cloud_event(pubsub_msg: &Value) -> Value {
  function derive_slug_from_event_types (line 488) | fn derive_slug_from_event_types(event_types: &[&str]) -> String {
  function spawn_subscribe_server (line 554) | async fn spawn_subscribe_server() -> (
  function make_matches_subscribe (line 618) | fn make_matches_subscribe(args: &[&str]) -> ArgMatches {
  function test_parse_subscribe_args_invalid_output_dir (line 638) | fn test_parse_subscribe_args_invalid_output_dir() {
  function test_parse_subscribe_args (line 647) | fn test_parse_subscribe_args() {
  function test_parse_subscribe_args_subscription (line 671) | fn test_parse_subscribe_args_subscription() {
  function test_slug_single_event_type (line 684) | fn test_slug_single_event_type() {
  function test_slug_single_event_type_chat (line 690) | fn test_slug_single_event_type_chat() {
  function test_slug_multiple_event_types_common_prefix (line 696) | fn test_slug_multiple_event_types_common_prefix() {
  function test_slug_non_workspace_prefix (line 706) | fn test_slug_non_workspace_prefix() {
  function test_slug_truncation (line 713) | fn test_slug_truncation() {
  function test_decode_cloud_event (line 721) | fn test_decode_cloud_event() {
  function test_process_events_pull_response (line 744) | fn test_process_events_pull_response() {
  function test_process_events_pull_response_empty (line 799) | fn test_process_events_pull_response_empty() {
  function test_handle_subscribe_validation_missing_target (line 807) | fn test_handle_subscribe_validation_missing_target() {
  function test_handle_subscribe_validation_missing_events (line 820) | fn test_handle_subscribe_validation_missing_events() {
  function test_handle_subscribe_validation_missing_project (line 833) | fn test_handle_subscribe_validation_missing_project() {
  function test_pull_loop_refreshes_pubsub_token_between_requests (line 846) | async fn test_pull_loop_refreshes_pubsub_token_between_requests() {

FILE: src/helpers/gmail/forward.rs
  function handle_forward (line 18) | pub(super) async fn handle_forward(
  type ForwardConfig (line 71) | pub(super) struct ForwardConfig {
  type ForwardEnvelope (line 82) | struct ForwardEnvelope<'a> {
  function build_forward_subject (line 95) | fn build_forward_subject(original_subject: &str) -> String {
  function create_forward_raw_message (line 103) | fn create_forward_raw_message(
  function join_mailboxes (line 129) | fn join_mailboxes(mailboxes: &[Mailbox]) -> String {
  function format_forwarded_message (line 137) | fn format_forwarded_message(original: &OriginalMessage) -> String {
  function format_forwarded_message_html (line 162) | fn format_forwarded_message_html(original: &OriginalMessage) -> String {
  function parse_forward_args (line 200) | fn parse_forward_args(matches: &ArgMatches) -> Result<ForwardConfig, Gws...
  function test_format_forwarded_message (line 227) | fn test_format_forwarded_message() {
  function test_format_forwarded_message_missing_date (line 246) | fn test_format_forwarded_message_missing_date() {
  function test_format_forwarded_message_with_cc (line 263) | fn test_format_forwarded_message_with_cc() {
  function test_build_forward_subject_without_prefix (line 291) | fn test_build_forward_subject_without_prefix() {
  function test_build_forward_subject_with_prefix (line 296) | fn test_build_forward_subject_with_prefix() {
  function test_build_forward_subject_case_insensitive (line 301) | fn test_build_forward_subject_case_insensitive() {
  function test_create_forward_raw_message_without_body (line 306) | fn test_create_forward_raw_message_without_body() {
  function test_create_forward_raw_message_with_all_optional_headers (line 350) | fn test_create_forward_raw_message_with_all_optional_headers() {
  function test_create_forward_raw_message_references_chain (line 400) | fn test_create_forward_raw_message_references_chain() {
  function make_forward_matches (line 444) | fn make_forward_matches(args: &[&str]) -> ArgMatches {
  function test_parse_forward_args (line 468) | fn test_parse_forward_args() {
  function test_parse_forward_args_with_all_options (line 480) | fn test_parse_forward_args_with_all_options() {
  function test_parse_forward_args_html_flag (line 520) | fn test_parse_forward_args_html_flag() {
  function test_parse_forward_args_empty_to_returns_error (line 540) | fn test_parse_forward_args_empty_to_returns_error() {
  function test_format_forwarded_message_html_with_html_body (line 552) | fn test_format_forwarded_message_html_with_html_body() {
  function test_format_forwarded_message_html_fallback_plain_text (line 572) | fn test_format_forwarded_message_html_fallback_plain_text() {
  function test_format_forwarded_message_html_escapes_metadata (line 587) | fn test_format_forwarded_message_html_escapes_metadata() {
  function test_format_forwarded_message_html_conditional_cc (line 608) | fn test_format_forwarded_message_html_conditional_cc() {
  function test_create_forward_raw_message_html_without_body (line 630) | fn test_create_forward_raw_message_html_without_body() {
  function test_create_forward_raw_message_html_plain_text_fallback (line 671) | fn test_create_forward_raw_message_html_plain_text_fallback() {
  function test_create_forward_raw_message_html (line 707) | fn test_create_forward_raw_message_html() {
  function test_create_forward_raw_message_with_attachment (line 746) | fn test_create_forward_raw_message_with_attachment() {

FILE: src/helpers/gmail/mod.rs
  type GmailHelper (line 43) | pub struct GmailHelper;
  constant GMAIL_SCOPE (line 45) | pub(super) const GMAIL_SCOPE: &str = "https://www.googleapis.com/auth/gm...
  constant GMAIL_READONLY_SCOPE (line 46) | pub(super) const GMAIL_READONLY_SCOPE: &str = "https://www.googleapis.co...
  constant PUBSUB_SCOPE (line 47) | pub(super) const PUBSUB_SCOPE: &str = "https://www.googleapis.com/auth/p...
  function sanitize_control_chars (line 56) | fn sanitize_control_chars(s: &str) -> String {
  type Mailbox (line 62) | pub(super) struct Mailbox {
    method parse (line 75) | pub fn parse(raw: &str) -> Self {
    method parse_list (line 102) | pub fn parse_list(raw: &str) -> Vec<Self> {
    method email_lowercase (line 111) | pub fn email_lowercase(&self) -> String {
    method fmt (line 119) | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
  function to_mb_address (line 128) | pub(super) fn to_mb_address(mailbox: &Mailbox) -> MbAddress<'_> {
  function to_mb_address_list (line 133) | pub(super) fn to_mb_address_list(mailboxes: &[Mailbox]) -> MbAddress<'_> {
  function strip_angle_brackets (line 138) | pub(super) fn strip_angle_brackets(id: &str) -> &str {
  type OriginalMessage (line 153) | pub(super) struct OriginalMessage {
    method dry_run_placeholder (line 172) | pub(super) fn dry_run_placeholder(message_id: &str) -> Self {
  type ParsedMessageHeaders (line 190) | struct ParsedMessageHeaders {
  function append_header_value (line 201) | fn append_header_value(existing: &mut String, value: &str) {
  function append_address_list_header_value (line 208) | fn append_address_list_header_value(existing: &mut String, value: &str) {
  function parse_message_headers (line 219) | fn parse_message_headers(headers: &[Value]) -> ParsedMessageHeaders {
  function non_empty_then (line 243) | fn non_empty_then<T>(s: &str, f: impl FnOnce(&str) -> T) -> Option<T> {
  function non_empty_slice (line 252) | pub(super) fn non_empty_slice<T>(s: &[T]) -> Option<&[T]> {
  function parse_original_message (line 260) | fn parse_original_message(msg: &Value) -> Result<OriginalMessage, GwsErr...
  function fetch_message_metadata (line 327) | pub(super) async fn fetch_message_metadata(
  function build_api_error (line 370) | pub(super) fn build_api_error(status: u16, body: &str, context: &str) ->...
  type SendAsIdentity (line 405) | struct SendAsIdentity {
  function fetch_send_as_identities (line 411) | async fn fetch_send_as_identities(
  function parse_send_as_response (line 445) | fn parse_send_as_response(body: &Value) -> Vec<SendAsIdentity> {
  function resolve_sender_from_identities (line 484) | fn resolve_sender_from_identities(
  function resolve_sender (line 526) | pub(super) async fn resolve_sender(
  function fetch_profile_display_name (line 604) | async fn fetch_profile_display_name(
  function parse_profile_display_name (line 634) | fn parse_profile_display_name(body: &Value) -> Option<String> {
  function extract_body_by_mime (line 644) | fn extract_body_by_mime(payload: &Value, target_mime: &str) -> Option<St...
  function extract_plain_text_body (line 688) | fn extract_plain_text_body(payload: &Value) -> Option<String> {
  function extract_html_body (line 692) | fn extract_html_body(payload: &Value) -> Option<String> {
  function resolve_html_body (line 699) | pub(super) fn resolve_html_body(original: &OriginalMessage) -> String {
  function html_escape (line 710) | pub(super) fn html_escape(text: &str) -> String {
  function split_raw_mailbox_list (line 721) | fn split_raw_mailbox_list(header: &str) -> Vec<&str> {
  function format_email_link (line 758) | pub(super) fn format_email_link(email: &str) -> String {
  function format_sender_for_attribution (line 768) | pub(super) fn format_sender_for_attribution(mailbox: &Mailbox) -> String {
  function format_address_list_with_links (line 781) | pub(super) fn format_address_list_with_links(mailboxes: &[Mailbox]) -> S...
  function format_date_for_attribution (line 793) | pub(super) fn format_date_for_attribution(raw_date: &str) -> String {
  function format_forward_from (line 808) | pub(super) fn format_forward_from(mailbox: &Mailbox) -> String {
  type ThreadingHeaders (line 828) | pub(super) struct ThreadingHeaders<'a> {
  function build_references_chain (line 834) | pub(super) fn build_references_chain(original: &OriginalMessage) -> Vec<...
  function set_threading_headers (line 844) | pub(super) fn set_threading_headers<'x>(
  function apply_optional_headers (line 872) | pub(super) fn apply_optional_headers<'x>(
  function finalize_message (line 891) | pub(super) fn finalize_message(
  function parse_optional_trimmed (line 911) | pub(super) fn parse_optional_trimmed(matches: &ArgMatches, name: &str) -...
  function parse_optional_mailboxes (line 920) | pub(super) fn parse_optional_mailboxes(matches: &ArgMatches, name: &str)...
  constant MAX_TOTAL_ATTACHMENT_BYTES (line 930) | const MAX_TOTAL_ATTACHMENT_BYTES: u64 = 25 * 1024 * 1024;
  type Attachment (line 939) | pub(super) struct Attachment {
  function parse_attachments (line 956) | pub(super) fn parse_attachments(matches: &ArgMatches) -> Result<Vec<Atta...
  function resolve_send_method (line 1013) | pub(super) fn resolve_send_method(
  function build_send_metadata (line 1033) | fn build_send_metadata(thread_id: Option<&str>) -> Option<String> {
  function send_raw_email (line 1037) | pub(super) async fn send_raw_email(
  function common_mail_args (line 1096) | fn common_mail_args(cmd: Command) -> Command {
  function common_reply_args (line 1132) | fn common_reply_args(cmd: Command) -> Command {
  method inject_commands (line 1166) | fn inject_commands(
  method handle (line 1503) | fn handle<'a>(
  function extract_header (line 1559) | pub(super) fn extract_header(raw: &str, name: &str) -> Option<String> {
  function strip_qp_soft_breaks (line 1584) | pub(super) fn strip_qp_soft_breaks(raw: &str) -> String {
  function test_to_mb_address_bare_email (line 1591) | fn test_to_mb_address_bare_email() {
  function test_to_mb_address_with_display_name (line 1604) | fn test_to_mb_address_with_display_name() {
  function test_to_mb_address_list_multiple (line 1618) | fn test_to_mb_address_list_multiple() {
  function test_set_threading_headers_output (line 1633) | fn test_set_threading_headers_output() {
  function test_original_message_default (line 1661) | fn test_original_message_default() {
  function test_parse_original_message_minimal (line 1678) | fn test_parse_original_message_minimal() {
  function test_parse_original_message_bare_message_id (line 1709) | fn test_parse_original_message_bare_message_id() {
  function test_parse_original_message_missing_payload (line 1729) | fn test_parse_original_message_missing_payload() {
  function test_parse_original_message_missing_thread_id (line 1740) | fn test_parse_original_message_missing_thread_id() {
  function test_parse_original_message_missing_from (line 1757) | fn test_parse_original_message_missing_from() {
  function test_parse_original_message_missing_message_id (line 1775) | fn test_parse_original_message_missing_message_id() {
  function test_parse_original_message_snippet_fallback (line 1793) | fn test_parse_original_message_snippet_fallback() {
  function test_extract_plain_text_body_simple (line 1815) | fn test_extract_plain_text_body_simple() {
  function test_extract_plain_text_body_multipart (line 1826) | fn test_extract_plain_text_body_multipart() {
  function test_extract_plain_text_body_nested_multipart (line 1847) | fn test_extract_plain_text_body_nested_multipart() {
  function test_extract_plain_text_body_no_text_part (line 1877) | fn test_extract_plain_text_body_no_text_part() {
  function test_inject_commands (line 1886) | fn test_inject_commands() {
  function test_build_send_metadata_with_thread_id (line 1902) | fn test_build_send_metadata_with_thread_id() {
  function test_build_send_metadata_without_thread_id (line 1909) | fn test_build_send_metadata_without_thread_id() {
  function test_append_address_list_header_value (line 1914) | fn test_append_address_list_header_value() {
  function test_parse_original_message_concatenates_repeated_address_and_reference_headers (line 1925) | fn test_parse_original_message_concatenates_repeated_address_and_referen...
  function test_parse_original_message_multipart_alternative (line 1981) | fn test_parse_original_message_multipart_alternative() {
  function test_resolve_send_method_finds_gmail_send_method (line 2014) | fn test_resolve_send_method_finds_gmail_send_method() {
  function test_html_escape (line 2037) | fn test_html_escape() {
  function test_extract_html_body_direct (line 2056) | fn test_extract_html_body_direct() {
  function test_extract_html_body_from_multipart (line 2067) | fn test_extract_html_body_from_multipart() {
  function test_extract_html_body_missing (line 2088) | fn test_extract_html_body_missing() {
  function test_extract_html_body_from_nested_multipart (line 2097) | fn test_extract_html_body_from_nested_multipart() {
  function test_resolve_html_body_uses_html_when_present (line 2127) | fn test_resolve_html_body_uses_html_when_present() {
  function test_resolve_html_body_escapes_plain_text_fallback (line 2137) | fn test_resolve_html_body_escapes_plain_text_fallback() {
  function test_mailbox_parse_bare_email (line 2153) | fn test_mailbox_parse_bare_email() {
  function test_mailbox_parse_with_display_name (line 2160) | fn test_mailbox_parse_with_display_name() {
  function test_mailbox_parse_quoted_display_name (line 2167) | fn test_mailbox_parse_quoted_display_name() {
  function test_mailbox_parse_malformed_no_closing_bracket (line 2174) | fn test_mailbox_parse_malformed_no_closing_bracket() {
  function test_mailbox_parse_empty (line 2181) | fn test_mailbox_parse_empty() {
  function test_mailbox_parse_empty_angle_brackets (line 2188) | fn test_mailbox_parse_empty_angle_brackets() {
  function test_mailbox_parse_strips_crlf_injection_in_email (line 2196) | fn test_mailbox_parse_strips_crlf_injection_in_email() {
  function test_mailbox_parse_strips_crlf_injection_in_angle_bracket_email (line 2204) | fn test_mailbox_parse_strips_crlf_injection_in_angle_bracket_email() {
  function test_mailbox_parse_strips_control_chars_from_name (line 2212) | fn test_mailbox_parse_strips_control_chars_from_name() {
  function test_mailbox_parse_strips_null_bytes_from_email (line 2219) | fn test_mailbox_parse_strips_null_bytes_from_email() {
  function test_mailbox_parse_strips_tab_from_email (line 2225) | fn test_mailbox_parse_strips_tab_from_email() {
  function test_mailbox_parse_non_ascii_display_name (line 2231) | fn test_mailbox_parse_non_ascii_display_name() {
  function test_mailbox_parse_list (line 2249) | fn test_mailbox_parse_list() {
  function test_mailbox_parse_list_with_quoted_comma (line 2258) | fn test_mailbox_parse_list_with_quoted_comma() {
  function test_mailbox_parse_list_filters_empty_emails (line 2267) | fn test_mailbox_parse_list_filters_empty_emails() {
  function test_mailbox_display (line 2291) | fn test_mailbox_display() {
  function test_strip_angle_brackets (line 2306) | fn test_strip_angle_brackets() {
  function test_build_references_chain (line 2316) | fn test_build_references_chain() {
  function test_format_sender_for_attribution (line 2354) | fn test_format_sender_for_attribution() {
  function test_format_email_link_prevents_mailto_injection (line 2376) | fn test_format_email_link_prevents_mailto_injection() {
  function test_format_address_list_with_links (line 2388) | fn test_format_address_list_with_links() {
  function test_format_date_for_attribution (line 2413) | fn test_format_date_for_attribution() {
  function test_format_forward_from (line 2425) | fn test_format_forward_from() {
  function test_split_raw_mailbox_list (line 2441) | fn test_split_raw_mailbox_list() {
  function test_parse_optional_trimmed (line 2469) | fn test_parse_optional_trimmed() {
  function make_attach_matches (line 2503) | fn make_attach_matches(args: &[&str]) -> ArgMatches {
  function test_attachment_single_file (line 2514) | fn test_attachment_single_file() {
  function test_attachment_multiple_files (line 2532) | fn test_attachment_multiple_files() {
  function test_attachment_with_html_body (line 2556) | fn test_attachment_with_html_body() {
  function test_attachment_empty_produces_no_multipart (line 2575) | fn test_attachment_empty_produces_no_multipart() {
  function test_parse_attachments_rejects_control_chars (line 2586) | fn test_parse_attachments_rejects_control_chars() {
  function test_parse_attachments_rejects_directory (line 2593) | fn test_parse_attachments_rejects_directory() {
  function test_parse_attachments_empty_returns_empty_vec (line 2601) | fn test_parse_attachments_empty_returns_empty_vec() {
  function test_parse_attachments_reads_real_file (line 2608) | fn test_parse_attachments_reads_real_file() {
  function test_parse_attachments_nonexistent_file (line 2627) | fn test_parse_attachments_nonexistent_file() {
  function test_parse_attachments_unknown_extension_falls_back_to_octet_stream (line 2638) | fn test_parse_attachments_unknown_extension_falls_back_to_octet_stream() {
  function test_parse_attachments_size_limit_accumulates (line 2654) | fn test_parse_attachments_size_limit_accumulates() {
  function test_parse_attachments_rejects_empty_file (line 2682) | fn test_parse_attachments_rejects_empty_file() {
  function test_parse_send_as_response (line 2700) | fn test_parse_send_as_response() {
  function test_parse_send_as_response_empty (line 2751) | fn test_parse_send_as_response_empty() {
  function test_parse_send_as_response_skips_missing_email (line 2758) | fn test_parse_send_as_response_skips_missing_email() {
  function make_identities (line 2770) | fn make_identities() -> Vec<SendAsIdentity> {
  function test_resolve_sender_no_from_returns_default (line 2790) | fn test_resolve_sender_no_from_returns_default() {
  function test_resolve_sender_bare_email_enriched (line 2800) | fn test_resolve_sender_bare_email_enriched() {
  function test_resolve_sender_bare_email_case_insensitive (line 2810) | fn test_resolve_sender_bare_email_case_insensitive() {
  function test_resolve_sender_bare_email_not_in_list_passes_through (line 2819) | fn test_resolve_sender_bare_email_not_in_list_passes_through() {
  function test_resolve_sender_with_display_name_returns_as_is (line 2829) | fn test_resolve_sender_with_display_name_returns_as_is() {
  function test_resolve_sender_mixed_enriches_only_bare (line 2839) | fn test_resolve_sender_mixed_enriches_only_bare() {
  function test_resolve_sender_no_default_in_list (line 2854) | fn test_resolve_sender_no_default_in_list() {
  function test_resolve_sender_empty_display_name_treated_as_none (line 2867) | fn test_resolve_sender_empty_display_name_treated_as_none() {
  function test_parse_profile_display_name (line 2884) | fn test_parse_profile_display_name() {
  function test_parse_profile_display_name_empty (line 2906) | fn test_parse_profile_display_name_empty() {
  function test_parse_profile_display_name_empty_name (line 2912) | fn test_parse_profile_display_name_empty_name() {
  function test_parse_profile_display_name_no_names_array (line 2920) | fn test_parse_profile_display_name_no_names_array() {
  function test_build_api_error_parses_google_json_format (line 2928) | fn test_build_api_error_parses_google_json_format() {
  function test_build_api_error_falls_back_to_raw_body (line 2949) | fn test_build_api_error_falls_back_to_raw_body() {
  function test_build_api_error_extracts_top_level_reason (line 2967) | fn test_build_api_error_extracts_top_level_reason() {
  function test_build_api_error_access_not_configured_extracts_url (line 2977) | fn test_build_api_error_access_not_configured_extracts_url() {

FILE: src/helpers/gmail/read.rs
  function handle_read (line 19) | pub(super) async fn handle_read(
  function format_mailbox_list (line 96) | fn format_mailbox_list(mailboxes: &[Mailbox]) -> String {
  function test_sanitize_for_terminal (line 111) | fn test_sanitize_for_terminal() {
  function test_format_mailbox_list_empty (line 126) | fn test_format_mailbox_list_empty() {
  function test_format_mailbox_list_single (line 131) | fn test_format_mailbox_list_single() {
  function test_format_mailbox_list_multiple (line 138) | fn test_format_mailbox_list_multiple() {

FILE: src/helpers/gmail/reply.rs
  function handle_reply (line 18) | pub(super) async fn handle_reply(
  type ReplyRecipients (line 123) | struct ReplyRecipients {
  type ReplyEnvelope (line 128) | struct ReplyEnvelope<'a> {
  type ReplyConfig (line 139) | pub(super) struct ReplyConfig {
  function fetch_user_email (line 154) | async fn fetch_user_email(client: &reqwest::Client, token: &str) -> Resu...
  function extract_reply_to_address (line 190) | fn extract_reply_to_address(original: &OriginalMessage) -> Vec<Mailbox> {
  function build_reply_all_recipients (line 197) | fn build_reply_all_recipients(
  function dedup_recipients (line 268) | fn dedup_recipients(
  function collect_excluded_emails (line 294) | fn collect_excluded_emails(
  function build_reply_subject (line 321) | fn build_reply_subject(original_subject: &str) -> String {
  function create_reply_raw_message (line 329) | fn create_reply_raw_message(
  function format_quoted_original (line 351) | fn format_quoted_original(original: &OriginalMessage) -> String {
  function format_quoted_original_html (line 366) | fn format_quoted_original_html(original: &OriginalMessage) -> String {
  function parse_reply_args (line 396) | fn parse_reply_args(matches: &ArgMatches) -> Result<ReplyConfig, GwsErro...
  function test_build_reply_subject_without_prefix (line 432) | fn test_build_reply_subject_without_prefix() {
  function test_build_reply_subject_with_prefix (line 437) | fn test_build_reply_subject_with_prefix() {
  function test_build_reply_subject_case_insensitive (line 442) | fn test_build_reply_subject_case_insensitive() {
  function test_create_reply_raw_message_basic (line 447) | fn test_create_reply_raw_message_basic() {
  function test_create_reply_raw_message_with_all_optional_headers (line 490) | fn test_create_reply_raw_message_with_all_optional_headers() {
  function test_build_reply_all_recipients (line 534) | fn test_build_reply_all_recipients() {
  function test_build_reply_all_with_remove (line 558) | fn test_build_reply_all_with_remove() {
  function test_build_reply_all_remove_primary_returns_empty_to (line 578) | fn test_build_reply_all_remove_primary_returns_empty_to() {
  function test_reply_all_excludes_from_alias_from_cc (line 593) | fn test_reply_all_excludes_from_alias_from_cc() {
  function test_build_reply_all_from_alias_is_self_reply (line 621) | fn test_build_reply_all_from_alias_is_self_reply() {
  function make_reply_matches (line 643) | fn make_reply_matches(args: &[&str]) -> ArgMatches {
  function test_parse_reply_args (line 668) | fn test_parse_reply_args() {
  function test_parse_reply_args_with_all_options (line 680) | fn test_parse_reply_args_with_all_options() {
  function test_parse_reply_args_html_flag (line 729) | fn test_parse_reply_args_html_flag() {
  function test_parse_reply_args_without_remove_defined (line 749) | fn test_parse_reply_args_without_remove_defined() {
  function test_extract_reply_to_address_falls_back_to_from (line 773) | fn test_extract_reply_to_address_falls_back_to_from() {
  function test_extract_reply_to_address_prefers_reply_to (line 785) | fn test_extract_reply_to_address_prefers_reply_to() {
  function test_remove_does_not_match_substring (line 797) | fn test_remove_does_not_match_substring() {
  function test_reply_all_uses_reply_to_for_to (line 816) | fn test_reply_all_uses_reply_to_for_to() {
  function test_sender_with_display_name_excluded_from_cc (line 832) | fn test_sender_with_display_name_excluded_from_cc() {
  function test_remove_with_display_name_format (line 849) | fn test_remove_with_display_name_format() {
  function test_reply_all_with_extra_cc (line 867) | fn test_reply_all_with_extra_cc() {
  function test_reply_all_cc_none_when_all_filtered (line 882) | fn test_reply_all_cc_none_when_all_filtered() {
  function test_case_insensitive_sender_exclusion (line 893) | fn test_case_insensitive_sender_exclusion() {
  function test_reply_all_multi_address_reply_to_deduplicates_cc (line 909) | fn test_reply_all_multi_address_reply_to_deduplicates_cc() {
  function test_reply_all_with_quoted_comma_display_name (line 938) | fn test_reply_all_with_quoted_comma_display_name() {
  function test_remove_with_quoted_comma_display_name (line 951) | fn test_remove_with_quoted_comma_display_name() {
  function test_reply_all_excludes_self_email (line 965) | fn test_reply_all_excludes_self_email() {
  function test_reply_all_excludes_self_case_insensitive (line 983) | fn test_reply_all_excludes_self_case_insensitive() {
  function test_reply_all_deduplicates_cc (line 1001) | fn test_reply_all_deduplicates_cc() {
  function test_reply_all_to_own_message_puts_original_to_in_to (line 1023) | fn test_reply_all_to_own_message_puts_original_to_in_to() {
  function test_reply_all_to_own_message_detected_via_alias (line 1047) | fn test_reply_all_to_own_message_detected_via_alias() {
  function test_reply_all_to_own_message_excludes_self_from_original_to (line 1067) | fn test_reply_all_to_own_message_excludes_self_from_original_to() {
  function test_reply_all_to_own_message_ignores_reply_to (line 1086) | fn test_reply_all_to_own_message_ignores_reply_to() {
  function test_dedup_no_overlap (line 1107) | fn test_dedup_no_overlap() {
  function test_dedup_to_wins_over_cc (line 1118) | fn test_dedup_to_wins_over_cc() {
  function test_dedup_to_wins_over_bcc (line 1131) | fn test_dedup_to_wins_over_bcc() {
  function test_dedup_cc_wins_over_bcc (line 1144) | fn test_dedup_cc_wins_over_bcc() {
  function test_dedup_all_three_overlap (line 1158) | fn test_dedup_all_three_overlap() {
  function test_dedup_case_insensitive (line 1176) | fn test_dedup_case_insensitive() {
  function test_dedup_bcc_fully_overlaps_returns_empty (line 1189) | fn test_dedup_bcc_fully_overlaps_returns_empty() {
  function test_dedup_with_display_names (line 1201) | fn test_dedup_with_display_names() {
  function test_dedup_intro_pattern (line 1215) | fn test_dedup_intro_pattern() {
  function test_dedup_simple_reply_no_cc_bcc (line 1226) | fn test_dedup_simple_reply_no_cc_bcc() {
  function test_format_quoted_original (line 1238) | fn test_format_quoted_original() {
  function test_format_quoted_original_empty_body (line 1253) | fn test_format_quoted_original_empty_body() {
  function test_format_quoted_original_missing_date (line 1266) | fn test_format_quoted_original_missing_date() {
  function test_extra_to_appears_in_raw_message (line 1282) | fn test_extra_to_appears_in_raw_message() {
  function test_intro_pattern_raw_message (line 1321) | fn test_intro_pattern_raw_message() {
  function test_format_quoted_original_html_with_html_body (line 1382) | fn test_format_quoted_original_html_with_html_body() {
  function test_format_quoted_original_html_fallback_plain_text (line 1401) | fn test_format_quoted_original_html_fallback_plain_text() {
  function test_format_quoted_original_html_escapes_metadata (line 1416) | fn test_format_quoted_original_html_escapes_metadata() {
  function test_create_reply_raw_message_html (line 1430) | fn test_create_reply_raw_message_html() {
  function test_create_reply_raw_message_with_attachment (line 1471) | fn test_create_reply_raw_message_with_attachment() {

FILE: src/helpers/gmail/send.rs
  function handle_send (line 18) | pub(super) async fn handle_send(
  type SendConfig (line 47) | pub(super) struct SendConfig {
  function create_send_raw_message (line 58) | fn create_send_raw_message(config: &SendConfig) -> Result<String, GwsErr...
  function parse_send_args (line 73) | fn parse_send_args(matches: &ArgMatches) -> Result<SendConfig, GwsError> {
  function make_matches_send (line 97) | fn make_matches_send(args: &[&str]) -> ArgMatches {
  function test_parse_send_args (line 116) | fn test_parse_send_args() {
  function test_parse_send_args_with_from (line 137) | fn test_parse_send_args_with_from() {
  function test_parse_send_args_with_cc_and_bcc (line 169) | fn test_parse_send_args_with_cc_and_bcc() {
  function test_parse_send_args_html_flag (line 207) | fn test_parse_send_args_html_flag() {
  function test_parse_send_args_empty_to_returns_error (line 236) | fn test_parse_send_args_empty_to_returns_error() {
  function test_send_html_raw_message (line 246) | fn test_send_html_raw_message() {
  function test_send_plain_text_raw_message (line 272) | fn test_send_plain_text_raw_message() {
  function test_send_with_cc_and_bcc (line 294) | fn test_send_with_cc_and_bcc() {
  function test_send_with_from (line 326) | fn test_send_with_from() {
  function test_send_without_from_has_no_from_header (line 348) | fn test_send_without_from_has_no_from_header() {
  function test_send_multiple_to_recipients (line 365) | fn test_send_multiple_to_recipients() {
  function test_send_crlf_injection_in_from_does_not_create_header (line 383) | fn test_send_crlf_injection_in_from_does_not_create_header() {
  function test_send_crlf_injection_in_cc_does_not_create_header (line 410) | fn test_send_crlf_injection_in_cc_does_not_create_header() {
  function test_send_with_attachment_produces_multipart (line 435) | fn test_send_with_attachment_produces_multipart() {

FILE: src/helpers/gmail/triage.rs
  function handle_triage (line 24) | pub async fn handle_triage(matches: &ArgMatches) -> Result<(), GwsError> {
  function no_messages_msg (line 183) | fn no_messages_msg(query: &str) -> String {
  function triage_cmd (line 197) | fn triage_cmd() -> Command {
  function defaults_max_to_20_and_query_to_unread (line 211) | fn defaults_max_to_20_and_query_to_unread() {
  function explicit_max_overrides_default (line 226) | fn explicit_max_overrides_default() {
  function non_numeric_max_falls_back_to_20 (line 238) | fn non_numeric_max_falls_back_to_20() {
  function custom_query_overrides_default (line 250) | fn custom_query_overrides_default() {
  function labels_flag_defaults_to_false (line 262) | fn labels_flag_defaults_to_false() {
  function labels_flag_set_when_passed (line 268) | fn labels_flag_set_when_passed() {
  function format_defaults_to_table_when_absent (line 276) | fn format_defaults_to_table_when_absent() {
  function format_json_when_specified (line 286) | fn format_json_when_specified() {
  function empty_result_message_is_not_json (line 298) | fn empty_result_message_is_not_json() {

FILE: src/helpers/gmail/watch.rs
  constant GMAIL_API_BASE (line 7) | const GMAIL_API_BASE: &str = "https://gmail.googleapis.com/gmail/v1";
  function handle_watch (line 10) | pub(super) async fn handle_watch(
  function watch_pull_loop (line 261) | async fn watch_pull_loop(
  function process_pull_response (line 361) | fn process_pull_response(response: &Value) -> (Vec<String>, u64) {
  function fetch_and_output_messages (line 398) | async fn fetch_and_output_messages(
  function apply_sanitization_result (line 496) | fn apply_sanitization_result(
  function extract_message_ids_from_history (line 526) | fn extract_message_ids_from_history(history_body: &Value) -> Vec<String> {
  type WatchConfig (line 551) | struct WatchConfig {
  type WatchRuntime (line 564) | struct WatchRuntime<'a> {
  function parse_watch_args (line 573) | fn parse_watch_args(matches: &ArgMatches) -> Result<WatchConfig, GwsErro...
  function spawn_watch_server (line 620) | async fn spawn_watch_server() -> (
  function test_extract_message_ids_from_history (line 704) | fn test_extract_message_ids_from_history() {
  function test_extract_message_ids_empty (line 728) | fn test_extract_message_ids_empty() {
  function test_process_pull_response (line 735) | fn test_process_pull_response() {
  function make_matches_watch (line 764) | fn make_matches_watch(args: &[&str]) -> ArgMatches {
  function test_parse_watch_args_invalid_format_rejected_by_clap (line 784) | fn test_parse_watch_args_invalid_format_rejected_by_clap() {
  function test_parse_watch_args_invalid_output_dir (line 802) | fn test_parse_watch_args_invalid_output_dir() {
  function test_parse_watch_args_rejects_traversal_subscription (line 811) | fn test_parse_watch_args_rejects_traversal_subscription() {
  function test_parse_watch_args_full (line 820) | fn test_parse_watch_args_full() {
  function test_parse_watch_args_defaults (line 846) | fn test_parse_watch_args_defaults() {
  function test_parse_watch_args_invalid_numbers (line 859) | fn test_parse_watch_args_invalid_numbers() {
  function test_apply_sanitization_result_block_mode (line 874) | fn test_apply_sanitization_result_block_mode() {
  function test_apply_sanitization_result_warn_mode (line 891) | fn test_apply_sanitization_result_warn_mode() {
  function test_apply_sanitization_result_no_match (line 910) | fn test_apply_sanitization_result_no_match() {
  function test_watch_pull_loop_refreshes_tokens_for_each_request (line 929) | async fn test_watch_pull_loop_refreshes_tokens_for_each_request() {

FILE: src/helpers/mod.rs
  constant PUBSUB_API_BASE (line 34) | pub(crate) const PUBSUB_API_BASE: &str = "https://pubsub.googleapis.com/...
  function shutdown_signal (line 46) | pub(crate) async fn shutdown_signal() {
  type Helper (line 94) | pub trait Helper: Send + Sync {
    method inject_commands (line 96) | fn inject_commands(&self, cmd: Command, doc: &crate::discovery::RestDe...
    method handle (line 101) | fn handle<'a>(
    method helper_only (line 109) | fn helper_only(&self) -> bool {
  function get_helper (line 114) | pub fn get_helper(service: &str) -> Option<Box<dyn Helper>> {

FILE: src/helpers/modelarmor.rs
  type SanitizationResult (line 29) | pub struct SanitizationResult {
  type SanitizeMode (line 42) | pub enum SanitizeMode {
    method from_str (line 73) | pub fn from_str(s: &str) -> Self {
  type SanitizeConfig (line 51) | pub struct SanitizeConfig {
  method default (line 60) | fn default() -> Self {
  type ModelArmorHelper (line 81) | pub struct ModelArmorHelper;
  function regional_base_url (line 86) | fn regional_base_url(location: &str) -> String {
  function extract_location (line 92) | fn extract_location(resource_name: &str) -> Option<&str> {
  method inject_commands (line 103) | fn inject_commands(&self, mut cmd: Command, _doc: &RestDescription) -> C...
  method helper_only (line 218) | fn helper_only(&self) -> bool {
  method handle (line 222) | fn handle<'a>(
  constant CLOUD_PLATFORM_SCOPE (line 246) | pub const CLOUD_PLATFORM_SCOPE: &str = "https://www.googleapis.com/auth/...
  function sanitize_text (line 250) | pub async fn sanitize_text(template: &str, text: &str) -> Result<Sanitiz...
  function model_armor_post (line 283) | async fn model_armor_post(url: &str, body: &str) -> Result<(), GwsError> {
  function handle_sanitize (line 313) | async fn handle_sanitize(
  type CreateTemplateConfig (line 336) | pub struct CreateTemplateConfig {
  function parse_create_template_args (line 343) | fn parse_create_template_args(matches: &ArgMatches) -> Result<CreateTemp...
  function build_create_template_url (line 369) | pub fn build_create_template_url(config: &CreateTemplateConfig) -> String {
  function handle_create_template (line 381) | async fn handle_create_template(matches: &ArgMatches) -> Result<(), GwsE...
  function load_preset_template (line 399) | fn load_preset_template(name: &str) -> Result<String, GwsError> {
  function test_sanitize_config_default (line 441) | fn test_sanitize_config_default() {
  function test_sanitize_config_with_template (line 448) | fn test_sanitize_config_with_template() {
  function test_sanitize_mode_from_str_warn (line 461) | fn test_sanitize_mode_from_str_warn() {
  function test_sanitize_mode_from_str_block (line 468) | fn test_sanitize_mode_from_str_block() {
  function test_sanitize_mode_from_str_unknown_defaults_to_warn (line 475) | fn test_sanitize_mode_from_str_unknown_defaults_to_warn() {
  function test_extract_location_valid (line 482) | fn test_extract_location_valid() {
  function test_extract_location_different_region (line 490) | fn test_extract_location_different_region() {
  function test_extract_location_no_locations (line 498) | fn test_extract_location_no_locations() {
  function test_extract_location_empty (line 503) | fn test_extract_location_empty() {
  function test_extract_location_trailing_locations (line 508) | fn test_extract_location_trailing_locations() {
  function test_regional_base_url (line 514) | fn test_regional_base_url() {
  function test_regional_base_url_different_region (line 522) | fn test_regional_base_url_different_region() {
  function test_cloud_platform_scope_constant (line 530) | fn test_cloud_platform_scope_constant() {
  function test_build_sanitize_request_data (line 538) | fn test_build_sanitize_request_data() {
  function test_parse_sanitize_response_success (line 547) | fn test_parse_sanitize_response_success() {
  function test_parse_sanitize_response_missing_field (line 562) | fn test_parse_sanitize_response_missing_field() {
  function build_sanitize_request_data (line 568) | pub fn build_sanitize_request_data(
  function parse_sanitize_response (line 593) | pub fn parse_sanitize_response(resp_text: &str) -> Result<SanitizationRe...
  function parse_sanitize_args (line 609) | fn parse_sanitize_args(matches: &ArgMatches, data_field: &str) -> Result...
  function make_matches (line 643) | fn make_matches(args: &[&str]) -> ArgMatches {
  function test_parse_sanitize_args_json (line 651) | fn test_parse_sanitize_args_json() {
  function test_parse_sanitize_args_text (line 658) | fn test_parse_sanitize_args_text() {
  function test_build_create_template_url (line 666) | fn test_build_create_template_url() {
  function make_matches_create (line 681) | fn make_matches_create(args: &[&str]) -> ArgMatches {
  function test_parse_create_template_args_json (line 692) | fn test_parse_create_template_args_json() {
  function test_parse_create_template_args_preset (line 712) | fn test_parse_create_template_args_preset() {
  function test_load_preset_template_fallback (line 732) | fn test_load_preset_template_fallback() {
  function test_inject_commands (line 739) | fn test_inject_commands() {
  function test_build_create_template_url_encodes_segments (line 752) | fn test_build_create_template_url_encodes_segments() {
  function test_parse_create_template_args_rejects_traversal (line 766) | fn test_parse_create_template_args_rejects_traversal() {

FILE: src/helpers/script.rs
  type ScriptHelper (line 27) | pub struct ScriptHelper;
  method inject_commands (line 30) | fn inject_commands(
  method handle (line 66) | fn handle<'a>(
  function visit_dirs (line 142) | fn visit_dirs(dir: &Path, files: &mut Vec<serde_json::Value>) -> Result<...
  function process_file (line 157) | fn process_file(path: &Path) -> Result<Option<serde_json::Value>, GwsErr...
  function test_process_file_server_js (line 201) | fn test_process_file_server_js() {
  function test_process_file_html (line 217) | fn test_process_file_html() {
  function test_process_file_appsscript_json (line 229) | fn test_process_file_appsscript_json() {
  function test_process_file_ignored (line 241) | fn test_process_file_ignored() {
  function test_visit_dirs (line 263) | fn test_visit_dirs() {

FILE: src/helpers/sheets.rs
  type SheetsHelper (line 24) | pub struct SheetsHelper;
  method inject_commands (line 27) | fn inject_commands(
  method handle (line 97) | fn handle<'a>(
  function build_append_request (line 200) | fn build_append_request(
  function build_read_request (line 233) | fn build_read_request(
  type AppendConfig (line 262) | pub struct AppendConfig {
  function parse_append_args (line 272) | pub fn parse_append_args(matches: &ArgMatches) -> AppendConfig {
  type ReadConfig (line 299) | pub struct ReadConfig {
  function parse_read_args (line 305) | pub fn parse_read_args(matches: &ArgMatches) -> ReadConfig {
  function make_mock_doc (line 318) | fn make_mock_doc() -> RestDescription {
  function make_matches_append (line 352) | fn make_matches_append(args: &[&str]) -> ArgMatches {
  function make_matches_read (line 360) | fn make_matches_read(args: &[&str]) -> ArgMatches {
  function test_build_append_request (line 368) | fn test_build_append_request() {
  function test_build_read_request (line 384) | fn test_build_read_request() {
  function test_parse_append_args_values (line 398) | fn test_parse_append_args_values() {
  function test_parse_append_args_json_single_row (line 406) | fn test_parse_append_args_json_single_row() {
  function test_parse_append_args_json_multi_row (line 419) | fn test_parse_append_args_json_multi_row() {
  function test_build_append_request_multi_row (line 435) | fn test_build_append_request_multi_row() {
  function test_parse_read_args (line 453) | fn test_parse_read_args() {
  function test_inject_commands (line 461) | fn test_inject_commands() {

FILE: src/helpers/workflows.rs
  type WorkflowHelper (line 27) | pub struct WorkflowHelper;
  method inject_commands (line 30) | fn inject_commands(
  method handle (line 43) | fn handle<'a>(
  method helper_only (line 74) | fn helper_only(&self) -> bool {
  function build_standup_report_cmd (line 83) | fn build_standup_report_cmd() -> Command {
  function build_meeting_prep_cmd (line 105) | fn build_meeting_prep_cmd() -> Command {
  function build_email_to_task_cmd (line 134) | fn build_email_to_task_cmd() -> Command {
  function build_weekly_digest_cmd (line 163) | fn build_weekly_digest_cmd() -> Command {
  function build_file_announce_cmd (line 185) | fn build_file_announce_cmd() -> Command {
  function get_json (line 232) | async fn get_json(
  function format_and_print (line 262) | fn format_and_print(value: &Value, matches: &ArgMatches) {
  function handle_standup_report (line 270) | async fn handle_standup_report(matches: &ArgMatches) -> Result<(), GwsEr...
  function handle_meeting_prep (line 368) | async fn handle_meeting_prep(matches: &ArgMatches) -> Result<(), GwsErro...
  function handle_email_to_task (line 445) | async fn handle_email_to_task(matches: &ArgMatches) -> Result<(), GwsErr...
  function handle_weekly_digest (line 535) | async fn handle_weekly_digest(matches: &ArgMatches) -> Result<(), GwsErr...
  function handle_file_announce (line 620) | async fn handle_file_announce(matches: &ArgMatches) -> Result<(), GwsErr...
  function test_inject_commands (line 704) | fn test_inject_commands() {
  function test_helper_only (line 721) | fn test_helper_only() {
  function test_build_standup_report_cmd (line 728) | fn test_build_standup_report_cmd() {
  function test_build_meeting_prep_cmd (line 734) | fn test_build_meeting_prep_cmd() {
  function test_build_email_to_task_cmd (line 740) | fn test_build_email_to_task_cmd() {
  function test_build_weekly_digest_cmd (line 755) | fn test_build_weekly_digest_cmd() {
  function test_build_file_announce_cmd (line 761) | fn test_build_file_announce_cmd() {

FILE: src/logging.rs
  constant ENV_LOG (line 32) | const ENV_LOG: &str = "GOOGLE_WORKSPACE_CLI_LOG";
  constant ENV_LOG_FILE (line 35) | const ENV_LOG_FILE: &str = "GOOGLE_WORKSPACE_CLI_LOG_FILE";
  function init_logging (line 45) | pub fn init_logging() {
  function test_init_logging_default_no_panic (line 98) | fn test_init_logging_default_no_panic() {
  function test_env_var_names (line 108) | fn test_env_var_names() {

FILE: src/main.rs
  function main (line 49) | async fn main() {
  function run (line 62) | async fn run() -> Result<(), GwsError> {
  function select_scope (line 309) | pub(crate) fn select_scope(scopes: &[String]) -> Option<&str> {
  function parse_pagination_config (line 313) | fn parse_pagination_config(matches: &clap::ArgMatches) -> executor::Pagi...
  function parse_service_and_version (line 321) | pub fn parse_service_and_version(
  function filter_args_for_subcommand (line 348) | pub fn filter_args_for_subcommand(args: &[String], service_name: &str) -...
  function parse_sanitize_config (line 373) | fn parse_sanitize_config(
  function resolve_method_from_matches (line 384) | fn resolve_method_from_matches<'a>(
  function print_usage (line 440) | fn print_usage() {
  function is_help_flag (line 516) | fn is_help_flag(arg: &str) -> bool {
  function is_version_flag (line 520) | fn is_version_flag(arg: &str) -> bool {
  function test_parse_pagination_config_defaults (line 529) | fn test_parse_pagination_config_defaults() {
  function test_parse_pagination_config_custom (line 555) | fn test_parse_pagination_config_custom() {
  function test_parse_sanitize_config_valid (line 588) | fn test_parse_sanitize_config_valid() {
  function test_parse_sanitize_config_no_template (line 598) | fn test_parse_sanitize_config_no_template() {
  function test_is_version_flag (line 606) | fn test_is_version_flag() {
  function test_is_help_flag (line 616) | fn test_is_help_flag() {
  function test_resolve_method_from_matches_basic (line 624) | fn test_resolve_method_from_matches_basic() {
  function test_resolve_method_from_matches_nested (line 653) | fn test_resolve_method_from_matches_nested() {
  function test_filter_args_strips_api_version (line 686) | fn test_filter_args_strips_api_version() {
  function test_filter_args_no_special_flags (line 700) | fn test_filter_args_no_special_flags() {
  function test_select_scope_picks_first (line 714) | fn test_select_scope_picks_first() {
  function test_select_scope_single (line 725) | fn test_select_scope_single() {
  function test_select_scope_empty (line 734) | fn test_select_scope_empty() {

FILE: src/oauth_config.rs
  type InstalledConfig (line 37) | pub struct InstalledConfig {
  type ClientSecretFile (line 51) | pub struct ClientSecretFile {
  function client_config_path (line 56) | pub fn client_config_path() -> PathBuf {
  function save_client_config (line 61) | pub fn save_client_config(
  function load_client_config (line 91) | pub fn load_client_config() -> anyhow::Result<InstalledConfig> {
  function test_save_load_round_trip (line 105) | fn test_save_load_round_trip() {
  function test_parse_google_console_format (line 137) | fn test_parse_google_console_format() {
  function test_parse_missing_optional_fields (line 158) | fn test_parse_missing_optional_fields() {
  function test_parse_invalid_json_fails (line 177) | fn test_parse_invalid_json_fails() {
  function test_parse_missing_client_id_fails (line 184) | fn test_parse_missing_client_id_fails() {
  type EnvGuard (line 198) | struct EnvGuard {
    method new (line 204) | fn new(key: &str, value: &str) -> Self {
  method drop (line 215) | fn drop(&mut self) {
  function test_load_client_config (line 226) | fn test_load_client_config() {

FILE: src/output.rs
  function is_dangerous_unicode (line 32) | pub(crate) fn is_dangerous_unicode(c: char) -> bool {
  function sanitize_for_terminal (line 51) | pub(crate) fn sanitize_for_terminal(text: &str) -> String {
  function reject_dangerous_chars (line 70) | pub(crate) fn reject_dangerous_chars(value: &str, flag_name: &str) -> Re...
  function stderr_supports_color (line 90) | pub(crate) fn stderr_supports_color() -> bool {
  function colorize (line 98) | pub(crate) fn colorize(text: &str, ansi_color: &str) -> String {
  function status (line 111) | pub(crate) fn status(msg: &str) {
  function warn (line 118) | pub(crate) fn warn(msg: &str) {
  function info (line 126) | pub(crate) fn info(msg: &str) {
  function sanitize_strips_ansi_escape_sequences (line 137) | fn sanitize_strips_ansi_escape_sequences() {
  function sanitize_preserves_newlines_and_tabs (line 145) | fn sanitize_preserves_newlines_and_tabs() {
  function sanitize_strips_bell_and_backspace (line 151) | fn sanitize_strips_bell_and_backspace() {
  function sanitize_strips_carriage_return (line 157) | fn sanitize_strips_carriage_return() {
  function sanitize_strips_bidi_overrides (line 163) | fn sanitize_strips_bidi_overrides() {
  function sanitize_strips_zero_width_chars (line 169) | fn sanitize_strips_zero_width_chars() {
  function sanitize_strips_line_separators (line 175) | fn sanitize_strips_line_separators() {
  function sanitize_strips_directional_isolates (line 181) | fn sanitize_strips_directional_isolates() {
  function sanitize_preserves_normal_unicode (line 186) | fn sanitize_preserves_normal_unicode() {
  function reject_clean_string (line 193) | fn reject_clean_string() {
  function reject_tab (line 198) | fn reject_tab() {
  function reject_newline (line 203) | fn reject_newline() {
  function reject_del (line 208) | fn reject_del() {
  function reject_zero_width_space (line 213) | fn reject_zero_width_space() {
  function reject_bom (line 218) | fn reject_bom() {
  function reject_rtl_override (line 223) | fn reject_rtl_override() {
  function reject_line_separator (line 228) | fn reject_line_separator() {
  function reject_paragraph_separator (line 233) | fn reject_paragraph_separator() {
  function reject_zero_width_joiner (line 238) | fn reject_zero_width_joiner() {
  function reject_preserves_normal_unicode (line 243) | fn reject_preserves_normal_unicode() {
  function reject_c1_control_csi (line 250) | fn reject_c1_control_csi() {
  function colorize_returns_text_in_no_color_mode (line 259) | fn colorize_returns_text_in_no_color_mode() {

FILE: src/schema.rs
  function handle_schema_command (line 34) | pub async fn handle_schema_command(path: &str, resolve_refs: bool) -> Re...
  function find_method (line 104) | fn find_method<'a>(
  function build_schema_output (line 146) | fn build_schema_output(doc: &RestDescription, method: &RestMethod) -> Va...
  function param_to_json (line 192) | fn param_to_json(param: &MethodParameter) -> Value {
  function schema_to_json (line 223) | fn schema_to_json(schema: &JsonSchema) -> Value {
  function resolve_schema_refs (line 274) | fn resolve_schema_refs(
  function test_param_to_json (line 326) | fn test_param_to_json() {
  function test_param_to_json_repeated (line 356) | fn test_param_to_json_repeated() {
  function test_schema_to_json_basic (line 370) | fn test_schema_to_json_basic() {
  function test_resolve_schema_refs_basic (line 393) | fn test_resolve_schema_refs_basic() {
  function test_resolve_schema_refs_nested (line 420) | fn test_resolve_schema_refs_nested() {

FILE: src/services.rs
  type ServiceEntry (line 18) | pub struct ServiceEntry {
  constant SERVICES (line 26) | pub const SERVICES: &[ServiceEntry] = &[
  function resolve_service (line 132) | pub fn resolve_service(name: &str) -> Result<(String, String), GwsError> {
  function test_resolve_service_known (line 154) | fn test_resolve_service_known() {
  function test_resolve_service_unknown (line 170) | fn test_resolve_service_unknown() {

FILE: src/setup.rs
  type ApiEntry (line 31) | struct ApiEntry {
  constant WORKSPACE_APIS (line 41) | const WORKSPACE_APIS: &[ApiEntry] = &[
  constant RESTRICTED_SCOPES (line 176) | const RESTRICTED_SCOPES: &[&str] = &[
  constant SENSITIVE_SCOPES (line 198) | const SENSITIVE_SCOPES: &[&str] = &[
  function all_api_ids (line 227) | fn all_api_ids() -> Vec<&'static str> {
  type ScopeClassification (line 232) | pub enum ScopeClassification {
  constant PLATFORM_SCOPE (line 238) | pub const PLATFORM_SCOPE: &str = "https://www.googleapis.com/auth/cloud-...
  type DiscoveredScope (line 242) | pub struct DiscoveredScope {
  function fetch_scopes_for_apis (line 259) | pub async fn fetch_scopes_for_apis(enabled_api_ids: &[String]) -> Vec<Di...
  type SetupOptions (line 374) | pub struct SetupOptions {
  function parse_setup_args (line 381) | pub fn parse_setup_args(args: &[String]) -> SetupOptions {
  function gcloud_bin (line 415) | fn gcloud_bin() -> &'static str {
  function gcloud_cmd (line 425) | fn gcloud_cmd() -> Command {
  function is_gcloud_installed (line 432) | pub fn is_gcloud_installed() -> bool {
  function gcloud_auth_login (line 443) | fn gcloud_auth_login() -> Result<(), GwsError> {
  function get_gcloud_account (line 456) | fn get_gcloud_account() -> Result<Option<String>, GwsError> {
  function list_gcloud_accounts (line 475) | fn list_gcloud_accounts() -> Vec<(String, bool)> {
  function set_gcloud_account (line 499) | fn set_gcloud_account(account: &str) -> Result<(), GwsError> {
  function get_gcloud_project (line 515) | fn get_gcloud_project() -> Result<Option<String>, GwsError> {
  function set_gcloud_project (line 533) | fn set_gcloud_project(project_id: &str) -> Result<(), GwsError> {
  function list_gcloud_projects (line 553) | fn list_gcloud_projects() -> (Vec<(String, String)>, Option<String>) {
  function get_access_token (line 625) | fn get_access_token() -> Result<String, GwsError> {
  function is_tos_precondition_error (line 640) | fn is_tos_precondition_error(gcloud_output: &str) -> bool {
  function is_invalid_project_id_error (line 647) | fn is_invalid_project_id_error(gcloud_output: &str) -> bool {
  function is_project_id_in_use_error (line 653) | fn is_project_id_in_use_error(gcloud_output: &str) -> bool {
  function primary_gcloud_error_line (line 661) | fn primary_gcloud_error_line(gcloud_output: &str) -> Option<String> {
  function format_project_create_failure (line 669) | fn format_project_create_failure(project_id: &str, account: &str, gcloud...
  function enable_apis (line 728) | async fn enable_apis(
  function get_enabled_apis (line 802) | pub fn get_enabled_apis(project_id: &str) -> Vec<String> {
  function configure_consent_screen (line 837) | async fn configure_consent_screen(
  constant STEP_LABELS (line 898) | const STEP_LABELS: [&str; 5] = [
  type SetupStage (line 906) | enum SetupStage {
  type SetupContext (line 916) | struct SetupContext {
    method wiz (line 933) | fn wiz(&mut self, idx: usize, status: StepStatus) {
    method finish_wizard (line 940) | fn finish_wizard(&mut self) {
  function stage_check_gcloud (line 948) | fn stage_check_gcloud(ctx: &mut SetupContext) -> Result<SetupStage, GwsE...
  function stage_account (line 969) | fn stage_account(ctx: &mut SetupContext) -> Result<SetupStage, GwsError> {
  function stage_project (line 1074) | fn stage_project(ctx: &mut SetupContext) -> Result<SetupStage, GwsError> {
  function stage_enable_apis (line 1284) | async fn stage_enable_apis(ctx: &mut SetupContext) -> Result<SetupStage,...
  function manual_oauth_instructions (line 1402) | fn manual_oauth_instructions(project_id: &str) -> String {
  function stage_configure_oauth (line 1458) | async fn stage_configure_oauth(ctx: &mut SetupContext) -> Result<SetupSt...
  function should_offer_login_prompt (line 1568) | fn should_offer_login_prompt(
  function prompt_login_after_setup (line 1577) | fn prompt_login_after_setup() -> Result<bool, GwsError> {
  function run_setup (line 1601) | pub async fn run_setup(args: &[String]) -> Result<(), GwsError> {
  type SetupAction (line 1702) | enum SetupAction {
  function resolve_account_selection (line 1712) | fn resolve_account_selection(items: &[SelectItem]) -> SetupAction {
  function resolve_project_selection (line 1720) | fn resolve_project_selection(items: &[SelectItem]) -> SetupAction {
  function resolve_api_selection (line 1731) | fn resolve_api_selection(items: &[SelectItem]) -> SetupAction {
  function make_items (line 1743) | fn make_items(labels: &[&str]) -> Vec<SelectItem> {
  function simulate_picker (line 1757) | fn simulate_picker(
  function test_workspace_api_ids_not_empty (line 1774) | fn test_workspace_api_ids_not_empty() {
  function test_workspace_api_ids_all_have_googleapis_suffix (line 1779) | fn test_workspace_api_ids_all_have_googleapis_suffix() {
  function test_workspace_api_ids_no_duplicates (line 1790) | fn test_workspace_api_ids_no_duplicates() {
  function test_workspace_api_ids_covers_services (line 1798) | fn test_workspace_api_ids_covers_services() {
  function test_parse_setup_args_empty (line 1828) | fn test_parse_setup_args_empty() {
  function test_parse_setup_args_with_project (line 1836) | fn test_parse_setup_args_with_project() {
  function test_parse_setup_args_with_project_equals (line 1844) | fn test_parse_setup_args_with_project_equals() {
  function test_parse_setup_args_ignores_unknown (line 1852) | fn test_parse_setup_args_ignores_unknown() {
  function test_parse_setup_args_dry_run (line 1860) | fn test_parse_setup_args_dry_run() {
  function test_parse_setup_args_dry_run_with_project (line 1868) | fn test_parse_setup_args_dry_run_with_project() {
  function test_parse_setup_args_login_flag (line 1877) | fn test_parse_setup_args_login_flag() {
  function test_should_offer_login_prompt_default_interactive (line 1886) | fn test_should_offer_login_prompt_default_interactive() {
  function test_should_not_offer_login_prompt_when_login_requested (line 1891) | fn test_should_not_offer_login_prompt_when_login_requested() {
  function test_should_not_offer_login_prompt_non_interactive (line 1896) | fn test_should_not_offer_login_prompt_non_interactive() {
  function test_should_not_offer_login_prompt_dry_run (line 1901) | fn test_should_not_offer_login_prompt_dry_run() {
  function test_format_project_create_failure_tos_guidance (line 1906) | fn test_format_project_create_failure_tos_guidance() {
  function test_format_project_create_failure_invalid_id_guidance (line 1921) | fn test_format_project_create_failure_invalid_id_guidance() {
  function test_format_project_create_failure_in_use_guidance (line 1935) | fn test_format_project_create_failure_in_use_guidance() {
  function test_format_project_create_failure_immutable_guidance (line 1947) | fn test_format_project_create_failure_immutable_guidance() {
  function test_account_select_existing_triggers_set_account (line 1960) | fn test_account_select_existing_triggers_set_account() {
  function test_account_select_login_new_triggers_login (line 1970) | fn test_account_select_login_new_triggers_login() {
  function test_account_select_none_returns_no_selection (line 1980) | fn test_account_select_none_returns_no_selection() {
  function test_project_select_existing (line 1988) | fn test_project_select_existing() {
  function test_project_select_create_new (line 1998) | fn test_project_select_create_new() {
  function test_project_select_enter_manually (line 2008) | fn test_project_select_enter_manually() {
  function test_project_select_none (line 2022) | fn test_project_select_none() {
  function test_api_select_none_enables_nothing (line 2030) | fn test_api_select_none_enables_nothing() {
  function test_api_select_first_enables_one (line 2049) | fn test_api_select_first_enables_one() {
  function test_api_select_all_enables_all (line 2069) | fn test_api_select_all_enables_all() {
  function test_pipeline_select_account_via_keys (line 2090) | fn test_pipeline_select_account_via_keys() {
  function test_pipeline_login_new_via_keys (line 2121) | fn test_pipeline_login_new_via_keys() {
  function test_pipeline_select_project_via_keys (line 2148) | fn test_pipeline_select_project_via_keys() {
  function test_pipeline_select_all_apis_via_keys (line 2192) | fn test_pipeline_select_all_apis_via_keys() {
  function test_pipeline_select_two_apis_via_keys (line 2215) | fn test_pipeline_select_two_apis_via_keys() {
  function test_enable_apis_with_no_apis_to_enable (line 2250) | async fn test_enable_apis_with_no_apis_to_enable() {
  function test_enable_apis_with_invalid_project (line 2260) | async fn test_enable_apis_with_invalid_project() {
  function test_failed_apis_json_structure (line 2275) | fn test_failed_apis_json_structure() {
  function test_failed_apis_json_empty (line 2297) | fn test_failed_apis_json_empty() {
  function gcloud_bin_returns_platform_appropriate_name (line 2308) | fn gcloud_bin_returns_platform_appropriate_name() {

FILE: src/setup_tui.rs
  type SelectItem (line 37) | pub struct SelectItem {
  type PickerResult (line 47) | pub enum PickerResult {
  type InputResult (line 57) | pub enum InputResult {
  function wrap_text (line 67) | pub fn wrap_text(text: &str, max_width: u16) -> Vec<String> {
  type PickerState (line 96) | pub struct PickerState {
    method new (line 137) | pub fn new(title: &str, help_text: &str, items: Vec<SelectItem>, multi...
    method toggle_current (line 150) | fn toggle_current(&mut self) {
    method toggle_all (line 206) | fn toggle_all(&mut self) {
    method next (line 219) | fn next(&mut self) {
    method previous (line 227) | fn previous(&mut self) {
    method selected_count (line 241) | fn selected_count(&self) -> usize {
    method handle_key (line 246) | pub fn handle_key(&mut self, code: KeyCode) -> Option<PickerResult> {
  type InputState (line 105) | pub struct InputState {
    method new (line 111) | pub fn new(title: &str, _help_text: &str, initial: Option<&str>) -> Se...
    method handle_key (line 118) | pub fn handle_key(&mut self, code: KeyCode) -> Option<InputResult> {
  function run_picker (line 315) | pub fn run_picker(
  function run_picker_loop (line 338) | fn run_picker_loop(
  function drain_pending_events (line 456) | fn drain_pending_events() -> std::io::Result<()> {
  type StepStatus (line 467) | pub enum StepStatus {
  type WizardStep (line 476) | struct WizardStep {
  type SetupWizard (line 483) | pub struct SetupWizard {
    method start (line 491) | pub fn start(step_labels: &[&str]) -> std::io::Result<Self> {
    method update_step (line 512) | pub fn update_step(&mut self, idx: usize, status: StepStatus) -> std::...
    method show_message (line 521) | pub fn show_message(&mut self, msg: &str) -> std::io::Result<()> {
    method suspend (line 527) | pub fn suspend(&mut self) -> std::io::Result<()> {
    method resume (line 535) | pub fn resume(&mut self) -> std::io::Result<()> {
    method show_picker (line 543) | pub fn show_picker(
    method show_input (line 607) | pub fn show_input(
    method finish (line 662) | pub fn finish(self) -> std::io::Result<()> {
    method draw_progress (line 669) | fn draw_progress(&mut self) -> std::io::Result<()> {
    method render_steps (line 689) | fn render_steps(
    method render_picker (line 770) | fn render_picker(
    method render_input (line 825) | fn render_input(
  function make_items (line 860) | fn make_items(labels: &[&str]) -> Vec<SelectItem> {
  function run_keys (line 876) | fn run_keys(state: &mut PickerState, keys: &[KeyCode]) -> Option<PickerR...
  function test_picker_state_toggle (line 888) | fn test_picker_state_toggle() {
  function test_picker_state_toggle_all (line 904) | fn test_picker_state_toggle_all() {
  function test_picker_state_navigation (line 916) | fn test_picker_state_navigation() {
  function test_enter_confirms_with_current_state (line 934) | fn test_enter_confirms_with_current_state() {
  function test_esc_cancels (line 948) | fn test_esc_cancels() {
  function test_q_cancels (line 956) | fn test_q_cancels() {
  function test_space_toggle_then_enter (line 964) | fn test_space_toggle_then_enter() {
  function test_select_all_then_deselect_one (line 989) | fn test_select_all_then_deselect_one() {
  function test_vim_navigation_j_k (line 1013) | fn test_vim_navigation_j_k() {
  function test_wrap_around_navigation (line 1040) | fn test_wrap_around_navigation() {
  function test_unknown_key_ignored (line 1063) | fn test_unknown_key_ignored() {
  function test_double_toggle_returns_to_original (line 1073) | fn test_double_toggle_returns_to_original() {
  function test_toggle_all_twice_resets (line 1094) | fn test_toggle_all_twice_resets() {
  function test_preselected_items_preserved (line 1115) | fn test_preselected_items_preserved() {
  function test_input_state_new_empty (line 1135) | fn test_input_state_new_empty() {
  function test_input_state_new_with_initial (line 1141) | fn test_input_state_new_with_initial() {
  function test_input_state_typing (line 1147) | fn test_input_state_typing() {
  function test_input_state_backspace (line 1155) | fn test_input_state_backspace() {
  function test_input_state_backspace_empty (line 1164) | fn test_input_state_backspace_empty() {
  function test_input_state_enter_confirms (line 1172) | fn test_input_state_enter_confirms() {
  function test_input_state_esc_cancels (line 1182) | fn test_input_state_esc_cancels() {
  function test_input_state_up_goes_back (line 1189) | fn test_input_state_up_goes_back() {
  function test_input_state_backtab_goes_back (line 1196) | fn test_input_state_backtab_goes_back() {
  function test_input_state_unknown_key_ignored (line 1203) | fn test_input_state_unknown_key_ignored() {
  function test_input_state_type_backspace_confirm (line 1211) | fn test_input_state_type_backspace_confirm() {
  function test_backspace_goes_back (line 1228) | fn test_backspace_goes_back() {
  function test_left_goes_back (line 1236) | fn test_left_goes_back() {
  function test_h_goes_back (line 1244) | fn test_h_goes_back() {
  function test_selected_count_none (line 1254) | fn test_selected_count_none() {
  function test_selected_count_some (line 1261) | fn test_selected_count_some() {
  function test_selected_count_after_toggle (line 1270) | fn test_selected_count_after_toggle() {
  function test_fixed_item_cannot_be_toggled (line 1284) | fn test_fixed_item_cannot_be_toggled() {
  function test_fixed_item_preserved_during_toggle_all (line 1296) | fn test_fixed_item_preserved_during_toggle_all() {
  function test_single_select_enter_selects_highlighted (line 1321) | fn test_single_select_enter_selects_highlighted() {
  function test_single_select_navigation_auto_selects (line 1346) | fn test_single_select_navigation_auto_selects() {
  function test_single_select_space_selects_current (line 1361) | fn test_single_select_space_selects_current() {
  function test_single_select_a_does_not_toggle_all (line 1374) | fn test_single_select_a_does_not_toggle_all() {
  function test_single_select_up_navigation_auto_selects (line 1386) | fn test_single_select_up_navigation_auto_selects() {
  function test_single_select_k_navigation_auto_selects (line 1397) | fn test_single_select_k_navigation_auto_selects() {
  function test_single_select_j_navigation_auto_selects (line 1407) | fn test_single_select_j_navigation_auto_selects() {
  function make_template_items (line 1417) | fn make_template_items() -> Vec<SelectItem> {
  function test_template_select_applies_scopes (line 1471) | fn test_template_select_applies_scopes() {
  function test_template_deselects_other_templates (line 1492) | fn test_template_deselects_other_templates() {
  function test_toggling_individual_deselects_templates (line 1514) | fn test_toggling_individual_deselects_templates() {
  function test_deselect_template_does_not_change_individual_items (line 1533) | fn test_deselect_template_does_not_change_individual_items() {
  function test_selecting_scope_deselects_readonly_counterpart (line 1555) | fn test_selecting_scope_deselects_readonly_counterpart() {
  function test_selecting_readonly_deselects_write_counterpart (line 1579) | fn test_selecting_readonly_deselects_write_counterpart() {
  function test_deselecting_scope_does_not_affect_counterpart (line 1602) | fn test_deselecting_scope_does_not_affect_counterpart() {
  function test_wrap_text_no_wrapping_needed (line 1632) | fn test_wrap_text_no_wrapping_needed() {
  function test_wrap_text_wraps_long_line (line 1638) | fn test_wrap_text_wraps_long_line() {
  function test_wrap_text_preserves_newlines (line 1644) | fn test_wrap_text_preserves_newlines() {
  function test_wrap_text_empty_lines (line 1650) | fn test_wrap_text_empty_lines() {
  function test_wrap_text_zero_width (line 1656) | fn test_wrap_text_zero_width() {
  function test_wrap_text_single_long_word (line 1662) | fn test_wrap_text_single_long_word() {
  function test_wrap_text_multiple_paragraphs_with_wrapping (line 1669) | fn test_wrap_text_multiple_paragraphs_with_wrapping() {
  function test_picker_starts_at_first_selected_item (line 1677) | fn test_picker_starts_at_first_selected_item() {
  function test_picker_starts_at_zero_when_none_selected (line 1685) | fn test_picker_starts_at_zero_when_none_selected() {
  function test_single_item_toggle (line 1694) | fn test_single_item_toggle() {
  function test_single_item_navigation_wraps (line 1704) | fn test_single_item_navigation_wraps() {
  function test_fixed_item_in_single_select_preserved (line 1714) | fn test_fixed_item_in_single_select_preserved() {

FILE: src/text.rs
  constant CLI_DESCRIPTION_LIMIT (line 16) | pub const CLI_DESCRIPTION_LIMIT: usize = 200;
  constant FRONTMATTER_DESCRIPTION_LIMIT (line 19) | pub const FRONTMATTER_DESCRIPTION_LIMIT: usize = 120;
  constant SKILL_BODY_DESCRIPTION_LIMIT (line 22) | pub const SKILL_BODY_DESCRIPTION_LIMIT: usize = 500;
  function truncate_description (line 35) | pub fn truncate_description(desc: &str, max_chars: usize, strip_links: b...
  function strip_markdown_links (line 76) | fn strip_markdown_links(s: &str) -> String {
  function find_char_from (line 104) | fn find_char_from(chars: &[char], target: char, from: usize) -> Option<u...
  function find_last_sentence_boundary (line 114) | fn find_last_sentence_boundary(prefix: &str) -> Option<usize> {
  function rfind_char_boundary (line 134) | fn rfind_char_boundary(s: &str, target: char) -> Option<usize> {
  function short_desc_unchanged (line 144) | fn short_desc_unchanged() {
  function truncate_at_sentence_boundary (line 150) | fn truncate_at_sentence_boundary() {
  function truncate_at_word_boundary (line 165) | fn truncate_at_word_boundary() {
  function hard_cut_no_spaces (line 175) | fn hard_cut_no_spaces() {
  function strips_markdown_links (line 182) | fn strips_markdown_links() {
  function preserves_links_when_strip_links_false (line 194) | fn preserves_links_when_strip_links_false() {
  function strips_markdown_links_and_truncates (line 202) | fn strips_markdown_links_and_truncates() {
  function multibyte_safe (line 211) | fn multibyte_safe() {
  function empty_and_whitespace (line 218) | fn empty_and_whitespace() {
  function test_strip_markdown_links (line 225) | fn test_strip_markdown_links() {
  function preserves_sentence_ending_at_limit (line 238) | fn preserves_sentence_ending_at_limit() {
  function does_not_cut_url_looking_periods (line 244) | fn does_not_cut_url_looking_periods() {
  function sentence_boundary_at_exact_limit (line 254) | fn sentence_boundary_at_exact_limit() {
  function zero_max_chars (line 262) | fn zero_max_chars() {

FILE: src/timezone.rs
  constant CACHE_FILENAME (line 28) | const CACHE_FILENAME: &str = "account_timezone";
  constant CACHE_TTL_SECS (line 31) | const CACHE_TTL_SECS: u64 = 86400;
  function cache_path (line 34) | fn cache_path() -> PathBuf {
  function invalidate_cache (line 40) | pub fn invalidate_cache() {
  function read_cache (line 50) | fn read_cache() -> Option<Tz> {
  function write_cache (line 64) | fn write_cache(tz_name: &str) {
  function fetch_account_timezone (line 78) | async fn fetch_account_timezone(client: &reqwest::Client, token: &str) -...
  function parse_timezone (line 131) | pub fn parse_timezone(tz_str: &str) -> Result<Tz, GwsError> {
  function resolve_account_timezone (line 144) | pub async fn resolve_account_timezone(
  function start_of_today (line 188) | pub fn start_of_today(tz: Tz) -> Result<chrono::DateTime<Tz>, crate::err...
  function iana_time_zone_fallback (line 207) | fn iana_time_zone_fallback() -> String {
  function parse_valid_iana_timezone (line 219) | fn parse_valid_iana_timezone() {
  function parse_utc_timezone (line 225) | fn parse_utc_timezone() {
  function parse_invalid_timezone_fails (line 231) | fn parse_invalid_timezone_fails() {
  function parse_empty_string_fails (line 240) | fn parse_empty_string_fails() {
  function cache_roundtrip (line 246) | fn cache_roundtrip() {
  function iana_fallback_returns_valid_tz (line 258) | fn iana_fallback_returns_valid_tz() {

FILE: src/token_storage.rs
  type EncryptedTokenStorage (line 25) | pub struct EncryptedTokenStorage {
    method new (line 32) | pub fn new(path: PathBuf) -> Self {
    method load_from_disk (line 39) | async fn load_from_disk(&self) -> HashMap<String, TokenInfo> {
    method save_to_disk (line 80) | async fn save_to_disk(&self, map: &HashMap<String, TokenInfo>) -> anyh...
    method cache_key (line 100) | fn cache_key(scopes: &[&str]) -> String {
  method set (line 110) | async fn set(&self, scopes: &[&str], token: TokenInfo) -> Result<(), Tok...
  method get (line 128) | async fn get(&self, scopes: &[&str]) -> Option<TokenInfo> {
  function test_encrypted_token_storage_new (line 152) | async fn test_encrypted_token_storage_new() {

FILE: src/validate.rs
  function validate_safe_output_dir (line 33) | pub fn validate_safe_output_dir(dir: &str) -> Result<PathBuf, GwsError> {
  function validate_safe_dir_path (line 81) | pub fn validate_safe_dir_path(dir: &str) -> Result<PathBuf, GwsError> {
  function validate_safe_file_path (line 138) | pub fn validate_safe_file_path(path_str: &str, flag_name: &str) -> Resul...
  function normalize_dotdot (line 182) | fn normalize_dotdot(path: &Path) -> PathBuf {
  function normalize_non_existing (line 200) | fn normalize_non_existing(path: &Path) -> Result<PathBuf, GwsError> {
  function encode_path_segment (line 238) | pub fn encode_path_segment(s: &str) -> String {
  function encode_path_preserving_slashes (line 249) | pub fn encode_path_preserving_slashes(s: &str) -> String {
  function validate_resource_name (line 259) | pub fn validate_resource_name(s: &str) -> Result<&str, GwsError> {
  function validate_api_identifier (line 295) | pub fn validate_api_identifier(s: &str) -> Result<&str, GwsError> {
  function test_output_dir_relative_subdir (line 323) | fn test_output_dir_relative_subdir() {
  function test_output_dir_rejects_symlink_traversal (line 342) | fn test_output_dir_rejects_symlink_traversal() {
  function test_output_dir_rejects_traversal (line 371) | fn test_output_dir_rejects_traversal() {
  function test_output_dir_rejects_absolute (line 386) | fn test_output_dir_rejects_absolute() {
  function test_output_dir_rejects_null_bytes (line 391) | fn test_output_dir_rejects_null_bytes() {
  function test_output_dir_rejects_control_chars (line 396) | fn test_output_dir_rejects_control_chars() {
  function test_output_dir_non_existing_subdir (line 402) | fn test_output_dir_non_existing_subdir() {
  function test_dir_path_cwd (line 420) | fn test_dir_path_cwd() {
  function test_dir_path_rejects_traversal (line 426) | fn test_dir_path_rejects_traversal() {
  function test_dir_path_rejects_absolute (line 439) | fn test_dir_path_rejects_absolute() {
  function test_reject_control_chars_clean (line 446) | fn test_reject_control_chars_clean() {
  function test_reject_control_chars_tab (line 451) | fn test_reject_control_chars_tab() {
  function test_reject_control_chars_newline (line 456) | fn test_reject_control_chars_newline() {
  function test_reject_control_chars_del (line 461) | fn test_reject_control_chars_del() {
  function test_encode_path_segment_plain_id (line 468) | fn test_encode_path_segment_plain_id() {
  function test_encode_path_segment_email (line 473) | fn test_encode_path_segment_email() {
  function test_encode_path_segment_query_injection (line 481) | fn test_encode_path_segment_query_injection() {
  function test_encode_path_segment_fragment_injection (line 489) | fn test_encode_path_segment_fragment_injection() {
  function test_encode_path_segment_path_traversal (line 495) | fn test_encode_path_segment_path_traversal() {
  function test_encode_path_segment_unicode (line 503) | fn test_encode_path_segment_unicode() {
  function test_encode_path_segment_spaces (line 510) | fn test_encode_path_segment_spaces() {
  function test_encode_path_segment_already_encoded (line 516) | fn test_encode_path_segment_already_encoded() {
  function test_encode_path_preserving_slashes_hierarchical_name (line 525) | fn test_encode_path_preserving_slashes_hierarchical_name() {
  function test_encode_path_preserving_slashes_escapes_reserved_chars (line 531) | fn test_encode_path_preserving_slashes_escapes_reserved_chars() {
  function test_encode_path_preserving_slashes_spaces_and_unicode (line 537) | fn test_encode_path_preserving_slashes_spaces_and_unicode() {
  function test_validate_resource_name_valid (line 546) | fn test_validate_resource_name_valid() {
  function test_validate_resource_name_traversal (line 554) | fn test_validate_resource_name_traversal() {
  function test_validate_resource_name_control_chars (line 561) | fn test_validate_resource_name_control_chars() {
  function test_validate_resource_name_empty (line 569) | fn test_validate_resource_name_empty() {
  function test_validate_resource_name_query_injection (line 574) | fn test_validate_resource_name_query_injection() {
  function test_validate_resource_name_error_messages_are_clear (line 581) | fn test_validate_resource_name_error_messages_are_clear() {
  function test_validate_resource_name_percent_bypass (line 593) | fn test_validate_resource_name_percent_bypass() {
  function test_reject_control_chars_zero_width_space (line 604) | fn test_reject_control_chars_zero_width_space() {
  function test_reject_control_chars_bom (line 610) | fn test_reject_control_chars_bom() {
  function test_reject_control_chars_rtl_override (line 616) | fn test_reject_control_chars_rtl_override() {
  function test_reject_control_chars_unicode_line_separator (line 622) | fn test_reject_control_chars_unicode_line_separator() {
  function test_reject_control_chars_paragraph_separator (line 628) | fn test_reject_control_chars_paragraph_separator() {
  function test_reject_control_chars_zero_width_joiner (line 634) | fn test_reject_control_chars_zero_width_joiner() {
  function test_reject_control_chars_normal_unicode_ok (line 640) | fn test_reject_control_chars_normal_unicode_ok() {
  function test_output_dir_rejects_zero_width_chars (line 650) | fn test_output_dir_rejects_zero_width_chars() {
  function test_output_dir_rejects_rtl_override (line 656) | fn test_output_dir_rejects_rtl_override() {
  function test_output_dir_rejects_unicode_line_separator (line 661) | fn test_output_dir_rejects_unicode_line_separator() {
  function test_validate_resource_name_zero_width_chars (line 668) | fn test_validate_resource_name_zero_width_chars() {
  function test_validate_resource_name_unicode_line_seps (line 676) | fn test_validate_resource_name_unicode_line_seps() {
  function test_validate_resource_name_rtl_override (line 682) | fn test_validate_resource_name_rtl_override() {
  function test_validate_resource_name_bidi_embedding (line 687) | fn test_validate_resource_name_bidi_embedding() {
  function test_validate_resource_name_homoglyphs_pass_through (line 694) | fn test_validate_resource_name_homoglyphs_pass_through() {
  function test_validate_resource_name_overlong_accepted (line 701) | fn test_validate_resource_name_overlong_accepted() {
  function test_validate_api_identifier_valid (line 710) | fn test_validate_api_identifier_valid() {
  function test_validate_api_identifier_rejects_path_traversal (line 725) | fn test_validate_api_identifier_rejects_path_traversal() {
  function test_validate_api_identifier_rejects_special_chars (line 731) | fn test_validate_api_identifier_rejects_special_chars() {
  function test_validate_api_identifier_empty (line 740) | fn test_validate_api_identifier_empty() {
  function test_file_path_relative_is_ok (line 748) | fn test_file_path_relative_is_ok() {
  function test_file_path_rejects_traversal (line 764) | fn test_file_path_rejects_traversal() {
  function test_file_path_rejects_control_chars (line 782) | fn test_file_path_rejects_control_chars() {
  function test_file_path_rejects_symlink_escape (line 789) | fn test_file_path_rejects_symlink_escape() {
  function test_file_path_rejects_traversal_via_nonexistent_prefix (line 811) | fn test_file_path_rejects_traversal_via_nonexistent_prefix() {
Condensed preview — 196 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,514K chars).
[
  {
    "path": ".agent/skills/vhs.md",
    "chars": 3505,
    "preview": "---\ndescription: Writing and editing VHS `.tape` files for terminal demo GIFs\n---\n\n# VHS Tape Files\n\n[VHS](https://githu"
  },
  {
    "path": ".agent/workflows/verify-skills.md",
    "chars": 2581,
    "preview": "---\ndescription: Verify all skills/*/SKILL.md files against actual CLI output for accuracy\n---\n\n# Verify Skills\n\nEnsure "
  },
  {
    "path": ".changeset/README.md",
    "chars": 510,
    "preview": "# Changesets\n\nHello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that wo"
  },
  {
    "path": ".changeset/config.json",
    "chars": 271,
    "preview": "{\n  \"$schema\": \"https://unpkg.com/@changesets/config@3.1.2/schema.json\",\n  \"changelog\": \"@changesets/cli/changelog\",\n  \""
  },
  {
    "path": ".claude/settings.json",
    "chars": 44,
    "preview": "{\n  \"attribution\": {\n    \"commit\": \"\"\n  }\n}\n"
  },
  {
    "path": ".gemini/config.yaml",
    "chars": 153,
    "preview": "code_review:\n  comment_severity_threshold: HIGH\n  pull_request_opened:\n    help: false\n    summary: true\n    code_review"
  },
  {
    "path": ".gemini/style_guide.md",
    "chars": 2676,
    "preview": "# Code Review Style Guide\n\n## Project Architecture\n\n`gws` is a Rust CLI that dynamically generates commands from Google "
  },
  {
    "path": ".github/CODEOWNERS",
    "chars": 299,
    "preview": "# Codeowners\n\n# Core engine code strictly requires your review\n# Isolates agents to `skills/` or `src/helpers/` unless a"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "chars": 733,
    "preview": "## Description\n\nPlease include a summary of the change and which issue is fixed. If adding a new feature or command, ple"
  },
  {
    "path": ".github/labeler.yml",
    "chars": 1575,
    "preview": "# Labels applied to PRs based on changed files.\n# Used by the actions/labeler action in .github/workflows/automation.yml"
  },
  {
    "path": ".github/workflows/automation.yml",
    "chars": 4703,
    "preview": "# Copyright 2026 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this f"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 12842,
    "preview": "# Copyright 2026 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this f"
  },
  {
    "path": ".github/workflows/cla.yml",
    "chars": 1915,
    "preview": "# Copyright 2026 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this f"
  },
  {
    "path": ".github/workflows/coverage.yml",
    "chars": 1791,
    "preview": "# Copyright 2026 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this f"
  },
  {
    "path": ".github/workflows/generate-skills.yml",
    "chars": 4072,
    "preview": "# Copyright 2026 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this f"
  },
  {
    "path": ".github/workflows/policy.yml",
    "chars": 2698,
    "preview": "# Copyright 2026 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this f"
  },
  {
    "path": ".github/workflows/publish-skills.yml",
    "chars": 1338,
    "preview": "name: Publish OpenClaw Skills\n\non:\n  push:\n    branches: [main]\n    paths:\n      - \"skills/**\"\n      - \".github/workflow"
  },
  {
    "path": ".github/workflows/release-changesets.yml",
    "chars": 2267,
    "preview": "# Copyright 2026 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this f"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 14628,
    "preview": "# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist\n#\n# Copyright 2022-2024, axodotdev\n# SPDX-"
  },
  {
    "path": ".github/workflows/stale.yml",
    "chars": 1291,
    "preview": "# Copyright 2026 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this f"
  },
  {
    "path": ".gitignore",
    "chars": 438,
    "preview": "# Rust\n/target/\n**/*.rs.bk\nbin/\nlcov.info\n\n\n.emails/\n\n# Node\nnode_modules/\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log"
  },
  {
    "path": "AGENTS.md",
    "chars": 10484,
    "preview": "# AGENTS.md\n\n## Project Overview\n\n`gws` is a Rust CLI tool for interacting with Google Workspace APIs. It dynamically ge"
  },
  {
    "path": "CHANGELOG.md",
    "chars": 34203,
    "preview": "# @googleworkspace/cli\n\n## 0.18.1\n\n### Patch Changes\n\n- a87037b: Handle SIGTERM in `gws gmail +watch` and `gws events +s"
  },
  {
    "path": "CLAUDE.md",
    "chars": 110,
    "preview": "When contributing to this repository, you must strictly follow all guidelines outlined in the AGENTS.md file.\n"
  },
  {
    "path": "CONTEXT.md",
    "chars": 2670,
    "preview": "# Google Workspace CLI (`gws`) Context\n\nThe `gws` CLI provides dynamic access to Google Workspace APIs (Drive, Gmail, Ca"
  },
  {
    "path": "Cargo.toml",
    "chars": 2563,
    "preview": "# Copyright 2026 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this f"
  },
  {
    "path": "LICENSE",
    "chars": 11358,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 20053,
    "preview": "<h1 align=\"center\">gws</h1>\n\n**One CLI for all of Google Workspace — built for humans and AI agents.**<br>\nDrive, Gmail,"
  },
  {
    "path": "SECURITY.md",
    "chars": 386,
    "preview": "# Report a security issue\n\nTo report a security issue, please use [https://g.co/vulnz](https://g.co/vulnz). We use\n[http"
  },
  {
    "path": "art/features.txt",
    "chars": 643,
    "preview": "\n        ╔════════════════════════════════════════════════╗\n        ║                                                ║\n "
  },
  {
    "path": "art/intro.txt",
    "chars": 1374,
    "preview": "          \n          ╔════════════════════════════════════════════════╗\n          ║                                     "
  },
  {
    "path": "art/outro.txt",
    "chars": 1374,
    "preview": "          \n          ╔════════════════════════════════════════════════╗\n          ║                                     "
  },
  {
    "path": "art/qr.txt",
    "chars": 10603,
    "preview": "\u001b[47m  \u001b[0m\u001b[47m  \u001b[0m\u001b[47m  \u001b[0m\u001b[47m  \u001b[0m\u001b[47m  \u001b[0m\u001b[47m  \u001b[0m\u001b[47m  \u001b[0m\u001b[47m  \u001b[0m\u001b[47m  \u001b[0m\u001b[47m  \u001b[0m\u001b[47m  \u001b[0"
  },
  {
    "path": "art/scene1.txt",
    "chars": 243,
    "preview": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n 🤖 WHAT IS GWS?\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n "
  },
  {
    "path": "art/scene2.txt",
    "chars": 122,
    "preview": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n 📂 EXPLORE SERVICES\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  },
  {
    "path": "art/scene2b.txt",
    "chars": 119,
    "preview": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n 🔍 INSPECT DRIVE\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
  },
  {
    "path": "art/scene3.txt",
    "chars": 174,
    "preview": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n 🔍 INTROSPECT APIS\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  },
  {
    "path": "art/scene3b.txt",
    "chars": 118,
    "preview": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n 📊 JSON SCHEMAS\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n"
  },
  {
    "path": "art/scene4.txt",
    "chars": 131,
    "preview": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n 🗂️  List files in a folder\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  },
  {
    "path": "art/scene5.txt",
    "chars": 122,
    "preview": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n 📤 Upload to Drive\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  },
  {
    "path": "art/scene6.txt",
    "chars": 120,
    "preview": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n 📧 Send an email\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n\n"
  },
  {
    "path": "art/scene7.txt",
    "chars": 125,
    "preview": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n 📅 Schedule a meeting\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  },
  {
    "path": "art/scene8.txt",
    "chars": 125,
    "preview": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n 📊 Log data to Sheets\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  },
  {
    "path": "art/scene9.txt",
    "chars": 201,
    "preview": "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n ♾️  Paginate all pages\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"
  },
  {
    "path": "dist-workspace.toml",
    "chars": 1851,
    "preview": "# Copyright 2026 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this f"
  },
  {
    "path": "docs/CODE_OF_CONDUCT.md",
    "chars": 4461,
    "preview": "# Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and"
  },
  {
    "path": "docs/CONTRIBUTING.md",
    "chars": 3567,
    "preview": "# How to contribute\n\nWe'd love to accept your patches and contributions to this project.\n\n## Before you begin\n\n### Sign "
  },
  {
    "path": "docs/demo.tape",
    "chars": 4782,
    "preview": "# GWS CLI — README Demo\n# Run: vhs docs/demo.tape\n#\n# All cosmetic sleeps minimized. API sleeps kept for responses.\n# Si"
  },
  {
    "path": "docs/skills.md",
    "chars": 13094,
    "preview": "# Skills Index\n\n> Auto-generated by `gws generate-skills`. Do not edit manually.\n\n## Services\n\nCore Google Workspace API"
  },
  {
    "path": "flake.nix",
    "chars": 2124,
    "preview": "{\n  description = \"Google Workspace CLI — dynamic command surface from Discovery Service\";\n\n  inputs = {\n    nixpkgs.url"
  },
  {
    "path": "gemini-extension.json",
    "chars": 198,
    "preview": "{\n  \"name\": \"google-workspace-cli\",\n  \"version\": \"latest\",\n  \"description\": \"CLI tool for managing Google Workspace reso"
  },
  {
    "path": "lefthook.yml",
    "chars": 316,
    "preview": "pre-commit:\n  parallel: false\n  commands:\n    fmt:\n      glob: \"*.rs\"\n      run: cargo fmt -- --check\n    clippy:\n      "
  },
  {
    "path": "package.json",
    "chars": 1313,
    "preview": "{\n  \"name\": \"@googleworkspace/cli\",\n  \"version\": \"0.18.1\",\n  \"private\": true,\n  \"description\": \"Google Workspace CLI — d"
  },
  {
    "path": "registry/personas.yaml",
    "chars": 9544,
    "preview": "# Persona Packs — Role-based skill bundles for AI agents\n#\n# Each persona defines a role-based context with:\n#   - name:"
  },
  {
    "path": "registry/recipes.yaml",
    "chars": 34769,
    "preview": "# Curated Recipe Registry — Real-world Google Workspace workflows\n#\n# Each recipe defines a reusable multi-step task wit"
  },
  {
    "path": "scripts/coverage.sh",
    "chars": 1155,
    "preview": "#!/bin/bash\n# Copyright 2026 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may no"
  },
  {
    "path": "scripts/show-art.sh",
    "chars": 602,
    "preview": "#!/bin/bash\n# Copyright 2026 Google LLC\n#\n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may no"
  },
  {
    "path": "scripts/tag-release.sh",
    "chars": 513,
    "preview": "#!/usr/bin/env bash\n# Creates and pushes a git tag based on the version in package.json.\n# Idempotent — skips if the tag"
  },
  {
    "path": "scripts/version-sync.sh",
    "chars": 972,
    "preview": "#!/usr/bin/env bash\n# Syncs the version from package.json into Cargo.toml and updates Cargo.lock.\n# Used by changesets/a"
  },
  {
    "path": "skills/gws-admin-reports/SKILL.md",
    "chars": 2335,
    "preview": "---\nname: gws-admin-reports\nversion: 1.0.0\ndescription: \"Google Workspace Admin SDK: Audit logs and usage reports.\"\nmeta"
  },
  {
    "path": "skills/gws-calendar/SKILL.md",
    "chars": 4407,
    "preview": "---\nname: gws-calendar\nversion: 1.0.0\ndescription: \"Google Calendar: Manage calendars and events.\"\nmetadata:\n  openclaw:"
  },
  {
    "path": "skills/gws-calendar-agenda/SKILL.md",
    "chars": 1544,
    "preview": "---\nname: gws-calendar-agenda\nversion: 1.0.0\ndescription: \"Google Calendar: Show upcoming events across all calendars.\"\n"
  },
  {
    "path": "skills/gws-calendar-insert/SKILL.md",
    "chars": 1748,
    "preview": "---\nname: gws-calendar-insert\nversion: 1.0.0\ndescription: \"Google Calendar: Create a new event.\"\nmetadata:\n  openclaw:\n "
  },
  {
    "path": "skills/gws-chat/SKILL.md",
    "chars": 5649,
    "preview": "---\nname: gws-chat\nversion: 1.0.0\ndescription: \"Google Chat: Manage Chat spaces and messages.\"\nmetadata:\n  openclaw:\n   "
  },
  {
    "path": "skills/gws-chat-send/SKILL.md",
    "chars": 1129,
    "preview": "---\nname: gws-chat-send\nversion: 1.0.0\ndescription: \"Google Chat: Send a message to a space.\"\nmetadata:\n  openclaw:\n    "
  },
  {
    "path": "skills/gws-classroom/SKILL.md",
    "chars": 6835,
    "preview": "---\nname: gws-classroom\nversion: 1.0.0\ndescription: \"Google Classroom: Manage classes, rosters, and coursework.\"\nmetadat"
  },
  {
    "path": "skills/gws-docs/SKILL.md",
    "chars": 1549,
    "preview": "---\nname: gws-docs\nversion: 1.0.0\ndescription: \"Read and write Google Docs.\"\nmetadata:\n  openclaw:\n    category: \"produc"
  },
  {
    "path": "skills/gws-docs-write/SKILL.md",
    "chars": 1114,
    "preview": "---\nname: gws-docs-write\nversion: 1.0.0\ndescription: \"Google Docs: Append text to a document.\"\nmetadata:\n  openclaw:\n   "
  },
  {
    "path": "skills/gws-drive/SKILL.md",
    "chars": 13257,
    "preview": "---\nname: gws-drive\nversion: 1.0.0\ndescription: \"Google Drive: Manage files, folders, and shared drives.\"\nmetadata:\n  op"
  },
  {
    "path": "skills/gws-drive-upload/SKILL.md",
    "chars": 1278,
    "preview": "---\nname: gws-drive-upload\nversion: 1.0.0\ndescription: \"Google Drive: Upload a file with automatic metadata.\"\nmetadata:\n"
  },
  {
    "path": "skills/gws-events/SKILL.md",
    "chars": 3486,
    "preview": "---\nname: gws-events\nversion: 1.0.0\ndescription: \"Subscribe to Google Workspace events.\"\nmetadata:\n  openclaw:\n    categ"
  },
  {
    "path": "skills/gws-events-renew/SKILL.md",
    "chars": 1238,
    "preview": "---\nname: gws-events-renew\nversion: 1.0.0\ndescription: \"Google Workspace Events: Renew/reactivate Workspace Events subsc"
  },
  {
    "path": "skills/gws-events-subscribe/SKILL.md",
    "chars": 2026,
    "preview": "---\nname: gws-events-subscribe\nversion: 1.0.0\ndescription: \"Google Workspace Events: Subscribe to Workspace events and s"
  },
  {
    "path": "skills/gws-forms/SKILL.md",
    "chars": 1553,
    "preview": "---\nname: gws-forms\nversion: 1.0.0\ndescription: \"Read and write Google Forms.\"\nmetadata:\n  openclaw:\n    category: \"prod"
  },
  {
    "path": "skills/gws-gmail/SKILL.md",
    "chars": 1991,
    "preview": "---\nname: gws-gmail\nversion: 1.0.0\ndescription: \"Gmail: Send, read, and manage email.\"\nmetadata:\n  openclaw:\n    categor"
  },
  {
    "path": "skills/gws-gmail-forward/SKILL.md",
    "chars": 2350,
    "preview": "---\nname: gws-gmail-forward\nversion: 1.0.0\ndescription: \"Gmail: Forward a message to new recipients.\"\nmetadata:\n  opencl"
  },
  {
    "path": "skills/gws-gmail-read/SKILL.md",
    "chars": 1369,
    "preview": "---\nname: gws-gmail-read\nversion: 1.0.0\ndescription: \"Gmail: Read a message and extract its body or headers.\"\nmetadata:\n"
  },
  {
    "path": "skills/gws-gmail-reply/SKILL.md",
    "chars": 2449,
    "preview": "---\nname: gws-gmail-reply\nversion: 1.0.0\ndescription: \"Gmail: Reply to a message (handles threading automatically).\"\nmet"
  },
  {
    "path": "skills/gws-gmail-reply-all/SKILL.md",
    "chars": 2766,
    "preview": "---\nname: gws-gmail-reply-all\nversion: 1.0.0\ndescription: \"Gmail: Reply-all to a message (handles threading automaticall"
  },
  {
    "path": "skills/gws-gmail-send/SKILL.md",
    "chars": 2354,
    "preview": "---\nname: gws-gmail-send\nversion: 1.0.0\ndescription: \"Gmail: Send an email.\"\nmetadata:\n  openclaw:\n    category: \"produc"
  },
  {
    "path": "skills/gws-gmail-triage/SKILL.md",
    "chars": 1194,
    "preview": "---\nname: gws-gmail-triage\nversion: 1.0.0\ndescription: \"Gmail: Show unread inbox summary (sender, subject, date).\"\nmetad"
  },
  {
    "path": "skills/gws-gmail-watch/SKILL.md",
    "chars": 1895,
    "preview": "---\nname: gws-gmail-watch\nversion: 1.0.0\ndescription: \"Gmail: Watch for new emails and stream them as NDJSON.\"\nmetadata:"
  },
  {
    "path": "skills/gws-keep/SKILL.md",
    "chars": 1750,
    "preview": "---\nname: gws-keep\nversion: 1.0.0\ndescription: \"Manage Google Keep notes.\"\nmetadata:\n  openclaw:\n    category: \"producti"
  },
  {
    "path": "skills/gws-meet/SKILL.md",
    "chars": 1776,
    "preview": "---\nname: gws-meet\nversion: 1.0.0\ndescription: \"Manage Google Meet conferences.\"\nmetadata:\n  openclaw:\n    category: \"pr"
  },
  {
    "path": "skills/gws-modelarmor/SKILL.md",
    "chars": 1207,
    "preview": "---\nname: gws-modelarmor\nversion: 1.0.0\ndescription: \"Google Model Armor: Filter user-generated content for safety.\"\nmet"
  },
  {
    "path": "skills/gws-modelarmor-create-template/SKILL.md",
    "chars": 1658,
    "preview": "---\nname: gws-modelarmor-create-template\nversion: 1.0.0\ndescription: \"Google Model Armor: Create a new Model Armor templ"
  },
  {
    "path": "skills/gws-modelarmor-sanitize-prompt/SKILL.md",
    "chars": 1406,
    "preview": "---\nname: gws-modelarmor-sanitize-prompt\nversion: 1.0.0\ndescription: \"Google Model Armor: Sanitize a user prompt through"
  },
  {
    "path": "skills/gws-modelarmor-sanitize-response/SKILL.md",
    "chars": 1411,
    "preview": "---\nname: gws-modelarmor-sanitize-response\nversion: 1.0.0\ndescription: \"Google Model Armor: Sanitize a model response th"
  },
  {
    "path": "skills/gws-people/SKILL.md",
    "chars": 6413,
    "preview": "---\nname: gws-people\nversion: 1.0.0\ndescription: \"Google People: Manage contacts and profiles.\"\nmetadata:\n  openclaw:\n  "
  },
  {
    "path": "skills/gws-shared/SKILL.md",
    "chars": 2657,
    "preview": "---\nname: gws-shared\nversion: 1.0.0\ndescription: \"gws CLI: Shared patterns for authentication, global flags, and output "
  },
  {
    "path": "skills/gws-sheets/SKILL.md",
    "chars": 2579,
    "preview": "---\nname: gws-sheets\nversion: 1.0.0\ndescription: \"Google Sheets: Read and write spreadsheets.\"\nmetadata:\n  openclaw:\n   "
  },
  {
    "path": "skills/gws-sheets-append/SKILL.md",
    "chars": 1290,
    "preview": "---\nname: gws-sheets-append\nversion: 1.0.0\ndescription: \"Google Sheets: Append a row to a spreadsheet.\"\nmetadata:\n  open"
  },
  {
    "path": "skills/gws-sheets-read/SKILL.md",
    "chars": 1105,
    "preview": "---\nname: gws-sheets-read\nversion: 1.0.0\ndescription: \"Google Sheets: Read values from a spreadsheet.\"\nmetadata:\n  openc"
  },
  {
    "path": "skills/gws-slides/SKILL.md",
    "chars": 1618,
    "preview": "---\nname: gws-slides\nversion: 1.0.0\ndescription: \"Google Slides: Read and write presentations.\"\nmetadata:\n  openclaw:\n  "
  },
  {
    "path": "skills/gws-tasks/SKILL.md",
    "chars": 2978,
    "preview": "---\nname: gws-tasks\nversion: 1.0.0\ndescription: \"Google Tasks: Manage task lists and tasks.\"\nmetadata:\n  openclaw:\n    c"
  },
  {
    "path": "skills/gws-workflow/SKILL.md",
    "chars": 1414,
    "preview": "---\nname: gws-workflow\nversion: 1.0.0\ndescription: \"Google Workflow: Cross-service productivity workflows.\"\nmetadata:\n  "
  },
  {
    "path": "skills/gws-workflow-email-to-task/SKILL.md",
    "chars": 1243,
    "preview": "---\nname: gws-workflow-email-to-task\nversion: 1.0.0\ndescription: \"Google Workflow: Convert a Gmail message into a Google"
  },
  {
    "path": "skills/gws-workflow-file-announce/SKILL.md",
    "chars": 1457,
    "preview": "---\nname: gws-workflow-file-announce\nversion: 1.0.0\ndescription: \"Google Workflow: Announce a Drive file in a Chat space"
  },
  {
    "path": "skills/gws-workflow-meeting-prep/SKILL.md",
    "chars": 1191,
    "preview": "---\nname: gws-workflow-meeting-prep\nversion: 1.0.0\ndescription: \"Google Workflow: Prepare for your next meeting: agenda,"
  },
  {
    "path": "skills/gws-workflow-standup-report/SKILL.md",
    "chars": 1096,
    "preview": "---\nname: gws-workflow-standup-report\nversion: 1.0.0\ndescription: \"Google Workflow: Today's meetings + open tasks as a s"
  },
  {
    "path": "skills/gws-workflow-weekly-digest/SKILL.md",
    "chars": 1113,
    "preview": "---\nname: gws-workflow-weekly-digest\nversion: 1.0.0\ndescription: \"Google Workflow: Weekly summary: this week's meetings "
  },
  {
    "path": "skills/persona-content-creator/SKILL.md",
    "chars": 1184,
    "preview": "---\nname: persona-content-creator\nversion: 1.0.0\ndescription: \"Create, organize, and distribute content across Workspace"
  },
  {
    "path": "skills/persona-customer-support/SKILL.md",
    "chars": 1174,
    "preview": "---\nname: persona-customer-support\nversion: 1.0.0\ndescription: \"Manage customer support — track tickets, respond, escala"
  },
  {
    "path": "skills/persona-event-coordinator/SKILL.md",
    "chars": 1281,
    "preview": "---\nname: persona-event-coordinator\nversion: 1.0.0\ndescription: \"Plan and manage events — scheduling, invitations, and l"
  },
  {
    "path": "skills/persona-exec-assistant/SKILL.md",
    "chars": 1367,
    "preview": "---\nname: persona-exec-assistant\nversion: 1.0.0\ndescription: \"Manage an executive's schedule, inbox, and communications."
  },
  {
    "path": "skills/persona-hr-coordinator/SKILL.md",
    "chars": 1196,
    "preview": "---\nname: persona-hr-coordinator\nversion: 1.0.0\ndescription: \"Handle HR workflows — onboarding, announcements, and emplo"
  },
  {
    "path": "skills/persona-it-admin/SKILL.md",
    "chars": 884,
    "preview": "---\nname: persona-it-admin\nversion: 1.0.0\ndescription: \"Administer IT — monitor security and configure Workspace.\"\nmetad"
  },
  {
    "path": "skills/persona-project-manager/SKILL.md",
    "chars": 1408,
    "preview": "---\nname: persona-project-manager\nversion: 1.0.0\ndescription: \"Coordinate projects — track tasks, schedule meetings, and"
  },
  {
    "path": "skills/persona-researcher/SKILL.md",
    "chars": 1087,
    "preview": "---\nname: persona-researcher\nversion: 1.0.0\ndescription: \"Organize research — manage references, notes, and collaboratio"
  },
  {
    "path": "skills/persona-sales-ops/SKILL.md",
    "chars": 1263,
    "preview": "---\nname: persona-sales-ops\nversion: 1.0.0\ndescription: \"Manage sales workflows — track deals, schedule calls, client co"
  },
  {
    "path": "skills/persona-team-lead/SKILL.md",
    "chars": 1244,
    "preview": "---\nname: persona-team-lead\nversion: 1.0.0\ndescription: \"Lead a team — run standups, coordinate tasks, and communicate.\""
  },
  {
    "path": "skills/recipe-backup-sheet-as-csv/SKILL.md",
    "chars": 823,
    "preview": "---\nname: recipe-backup-sheet-as-csv\nversion: 1.0.0\ndescription: \"Export a Google Sheets spreadsheet as a CSV file for l"
  },
  {
    "path": "skills/recipe-batch-invite-to-event/SKILL.md",
    "chars": 981,
    "preview": "---\nname: recipe-batch-invite-to-event\nversion: 1.0.0\ndescription: \"Add a list of attendees to an existing Google Calend"
  },
  {
    "path": "skills/recipe-block-focus-time/SKILL.md",
    "chars": 972,
    "preview": "---\nname: recipe-block-focus-time\nversion: 1.0.0\ndescription: \"Create recurring focus time blocks on Google Calendar to "
  },
  {
    "path": "skills/recipe-bulk-download-folder/SKILL.md",
    "chars": 800,
    "preview": "---\nname: recipe-bulk-download-folder\nversion: 1.0.0\ndescription: \"List and download all files from a Google Drive folde"
  },
  {
    "path": "skills/recipe-collect-form-responses/SKILL.md",
    "chars": 673,
    "preview": "---\nname: recipe-collect-form-responses\nversion: 1.0.0\ndescription: \"Retrieve and review responses from a Google Form.\"\n"
  },
  {
    "path": "skills/recipe-compare-sheet-tabs/SKILL.md",
    "chars": 713,
    "preview": "---\nname: recipe-compare-sheet-tabs\nversion: 1.0.0\ndescription: \"Read data from two tabs in a Google Sheet to compare an"
  },
  {
    "path": "skills/recipe-copy-sheet-for-new-month/SKILL.md",
    "chars": 987,
    "preview": "---\nname: recipe-copy-sheet-for-new-month\nversion: 1.0.0\ndescription: \"Duplicate a Google Sheets template tab for a new "
  },
  {
    "path": "skills/recipe-create-classroom-course/SKILL.md",
    "chars": 866,
    "preview": "---\nname: recipe-create-classroom-course\nversion: 1.0.0\ndescription: \"Create a Google Classroom course and invite studen"
  },
  {
    "path": "skills/recipe-create-doc-from-template/SKILL.md",
    "chars": 1003,
    "preview": "---\nname: recipe-create-doc-from-template\nversion: 1.0.0\ndescription: \"Copy a Google Docs template, fill in content, and"
  },
  {
    "path": "skills/recipe-create-events-from-sheet/SKILL.md",
    "chars": 870,
    "preview": "---\nname: recipe-create-events-from-sheet\nversion: 1.0.0\ndescription: \"Read event data from a Google Sheets spreadsheet "
  },
  {
    "path": "skills/recipe-create-expense-tracker/SKILL.md",
    "chars": 1150,
    "preview": "---\nname: recipe-create-expense-tracker\nversion: 1.0.0\ndescription: \"Set up a Google Sheets spreadsheet for tracking exp"
  },
  {
    "path": "skills/recipe-create-feedback-form/SKILL.md",
    "chars": 805,
    "preview": "---\nname: recipe-create-feedback-form\nversion: 1.0.0\ndescription: \"Create a Google Form for feedback and share it via Gm"
  },
  {
    "path": "skills/recipe-create-gmail-filter/SKILL.md",
    "chars": 1024,
    "preview": "---\nname: recipe-create-gmail-filter\nversion: 1.0.0\ndescription: \"Create a Gmail filter to automatically label, star, or"
  },
  {
    "path": "skills/recipe-create-meet-space/SKILL.md",
    "chars": 732,
    "preview": "---\nname: recipe-create-meet-space\nversion: 1.0.0\ndescription: \"Create a Google Meet meeting space and share the join li"
  },
  {
    "path": "skills/recipe-create-presentation/SKILL.md",
    "chars": 787,
    "preview": "---\nname: recipe-create-presentation\nversion: 1.0.0\ndescription: \"Create a new Google Slides presentation and add initia"
  },
  {
    "path": "skills/recipe-create-shared-drive/SKILL.md",
    "chars": 899,
    "preview": "---\nname: recipe-create-shared-drive\nversion: 1.0.0\ndescription: \"Create a Google Shared Drive and add members with appr"
  },
  {
    "path": "skills/recipe-create-task-list/SKILL.md",
    "chars": 927,
    "preview": "---\nname: recipe-create-task-list\nversion: 1.0.0\ndescription: \"Set up a new Google Tasks list with initial tasks.\"\nmetad"
  },
  {
    "path": "skills/recipe-create-vacation-responder/SKILL.md",
    "chars": 1055,
    "preview": "---\nname: recipe-create-vacation-responder\nversion: 1.0.0\ndescription: \"Enable a Gmail out-of-office auto-reply with a c"
  },
  {
    "path": "skills/recipe-draft-email-from-doc/SKILL.md",
    "chars": 766,
    "preview": "---\nname: recipe-draft-email-from-doc\nversion: 1.0.0\ndescription: \"Read content from a Google Doc and use it as the body"
  },
  {
    "path": "skills/recipe-email-drive-link/SKILL.md",
    "chars": 941,
    "preview": "---\nname: recipe-email-drive-link\nversion: 1.0.0\ndescription: \"Share a Google Drive file and email the link with a messa"
  },
  {
    "path": "skills/recipe-find-free-time/SKILL.md",
    "chars": 943,
    "preview": "---\nname: recipe-find-free-time\nversion: 1.0.0\ndescription: \"Query Google Calendar free/busy status for multiple users t"
  },
  {
    "path": "skills/recipe-find-large-files/SKILL.md",
    "chars": 685,
    "preview": "---\nname: recipe-find-large-files\nversion: 1.0.0\ndescription: \"Identify large Google Drive files consuming storage quota"
  },
  {
    "path": "skills/recipe-forward-labeled-emails/SKILL.md",
    "chars": 889,
    "preview": "---\nname: recipe-forward-labeled-emails\nversion: 1.0.0\ndescription: \"Find Gmail messages with a specific label and forwa"
  },
  {
    "path": "skills/recipe-generate-report-from-sheet/SKILL.md",
    "chars": 1119,
    "preview": "---\nname: recipe-generate-report-from-sheet\nversion: 1.0.0\ndescription: \"Read data from a Google Sheet and create a form"
  },
  {
    "path": "skills/recipe-label-and-archive-emails/SKILL.md",
    "chars": 944,
    "preview": "---\nname: recipe-label-and-archive-emails\nversion: 1.0.0\ndescription: \"Apply Gmail labels to matching messages and archi"
  },
  {
    "path": "skills/recipe-log-deal-update/SKILL.md",
    "chars": 911,
    "preview": "---\nname: recipe-log-deal-update\nversion: 1.0.0\ndescription: \"Append a deal status update to a Google Sheets sales track"
  },
  {
    "path": "skills/recipe-organize-drive-folder/SKILL.md",
    "chars": 1059,
    "preview": "---\nname: recipe-organize-drive-folder\nversion: 1.0.0\ndescription: \"Create a Google Drive folder structure and move file"
  },
  {
    "path": "skills/recipe-plan-weekly-schedule/SKILL.md",
    "chars": 912,
    "preview": "---\nname: recipe-plan-weekly-schedule\nversion: 1.0.0\ndescription: \"Review your Google Calendar week, identify gaps, and "
  },
  {
    "path": "skills/recipe-post-mortem-setup/SKILL.md",
    "chars": 994,
    "preview": "---\nname: recipe-post-mortem-setup\nversion: 1.0.0\ndescription: \"Create a Google Docs post-mortem, schedule a Google Cale"
  },
  {
    "path": "skills/recipe-reschedule-meeting/SKILL.md",
    "chars": 954,
    "preview": "---\nname: recipe-reschedule-meeting\nversion: 1.0.0\ndescription: \"Move a Google Calendar event to a new time and automati"
  },
  {
    "path": "skills/recipe-review-meet-participants/SKILL.md",
    "chars": 872,
    "preview": "---\nname: recipe-review-meet-participants\nversion: 1.0.0\ndescription: \"Review who attended a Google Meet conference and "
  },
  {
    "path": "skills/recipe-review-overdue-tasks/SKILL.md",
    "chars": 677,
    "preview": "---\nname: recipe-review-overdue-tasks\nversion: 1.0.0\ndescription: \"Find Google Tasks that are past due and need attentio"
  },
  {
    "path": "skills/recipe-save-email-attachments/SKILL.md",
    "chars": 1025,
    "preview": "---\nname: recipe-save-email-attachments\nversion: 1.0.0\ndescription: \"Find Gmail messages with attachments and save them "
  },
  {
    "path": "skills/recipe-save-email-to-doc/SKILL.md",
    "chars": 998,
    "preview": "---\nname: recipe-save-email-to-doc\nversion: 1.0.0\ndescription: \"Save a Gmail message body into a Google Doc for archival"
  },
  {
    "path": "skills/recipe-schedule-recurring-event/SKILL.md",
    "chars": 908,
    "preview": "---\nname: recipe-schedule-recurring-event\nversion: 1.0.0\ndescription: \"Create a recurring Google Calendar event with att"
  },
  {
    "path": "skills/recipe-send-team-announcement/SKILL.md",
    "chars": 766,
    "preview": "---\nname: recipe-send-team-announcement\nversion: 1.0.0\ndescription: \"Send a team announcement via both Gmail and a Googl"
  },
  {
    "path": "skills/recipe-share-doc-and-notify/SKILL.md",
    "chars": 1088,
    "preview": "---\nname: recipe-share-doc-and-notify\nversion: 1.0.0\ndescription: \"Share a Google Docs document with edit access and ema"
  },
  {
    "path": "skills/recipe-share-event-materials/SKILL.md",
    "chars": 905,
    "preview": "---\nname: recipe-share-event-materials\nversion: 1.0.0\ndescription: \"Share Google Drive files with all attendees of a Goo"
  },
  {
    "path": "skills/recipe-share-folder-with-team/SKILL.md",
    "chars": 1097,
    "preview": "---\nname: recipe-share-folder-with-team\nversion: 1.0.0\ndescription: \"Share a Google Drive folder and all its contents wi"
  },
  {
    "path": "skills/recipe-sync-contacts-to-sheet/SKILL.md",
    "chars": 973,
    "preview": "---\nname: recipe-sync-contacts-to-sheet\nversion: 1.0.0\ndescription: \"Export Google Contacts directory to a Google Sheets"
  },
  {
    "path": "skills/recipe-watch-drive-changes/SKILL.md",
    "chars": 907,
    "preview": "---\nname: recipe-watch-drive-changes\nversion: 1.0.0\ndescription: \"Subscribe to change notifications on a Google Drive fi"
  },
  {
    "path": "src/auth.rs",
    "chars": 32750,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/auth_commands.rs",
    "chars": 82550,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/client.rs",
    "chars": 3337,
    "preview": "use reqwest::header::{HeaderMap, HeaderValue};\n\npub fn build_client() -> Result<reqwest::Client, crate::error::GwsError>"
  },
  {
    "path": "src/commands.rs",
    "chars": 9695,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/credential_store.rs",
    "chars": 32550,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/discovery.rs",
    "chars": 10603,
    "preview": "#![allow(dead_code)]\n// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n"
  },
  {
    "path": "src/error.rs",
    "chars": 14173,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/executor.rs",
    "chars": 80506,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/formatter.rs",
    "chars": 26977,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/fs_util.rs",
    "chars": 5214,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/generate_skills.rs",
    "chars": 37676,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/README.md",
    "chars": 3311,
    "preview": "# Helper Modules\n\nThis directory contains \"Helper\" implementations that provide high-value, simplified commands for comp"
  },
  {
    "path": "src/helpers/calendar.rs",
    "chars": 27730,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/chat.rs",
    "chars": 9819,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/docs.rs",
    "chars": 6870,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/drive.rs",
    "chars": 6527,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/events/mod.rs",
    "chars": 7550,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/events/renew.rs",
    "chars": 8173,
    "preview": "use super::*;\n\n#[derive(Debug, PartialEq)]\npub struct RenewConfig {\n    pub name: Option<String>,\n    pub all: bool,\n   "
  },
  {
    "path": "src/helpers/events/subscribe.rs",
    "chars": 31620,
    "preview": "use super::*;\nuse crate::auth::AccessTokenProvider;\nuse crate::helpers::PUBSUB_API_BASE;\nuse crate::output::sanitize_for"
  },
  {
    "path": "src/helpers/gmail/forward.rs",
    "chars": 27844,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/gmail/mod.rs",
    "chars": 109238,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/gmail/read.rs",
    "chars": 4909,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/gmail/reply.rs",
    "chars": 54316,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/gmail/send.rs",
    "chars": 14887,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/gmail/triage.rs",
    "chars": 10388,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/gmail/watch.rs",
    "chars": 36435,
    "preview": "use super::*;\nuse crate::auth::AccessTokenProvider;\nuse crate::helpers::PUBSUB_API_BASE;\nuse crate::output::colorize;\nus"
  },
  {
    "path": "src/helpers/mod.rs",
    "chars": 4760,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/modelarmor.rs",
    "chars": 26908,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/script.rs",
    "chars": 9657,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/sheets.rs",
    "chars": 16872,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/helpers/workflows.rs",
    "chars": 24965,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/logging.rs",
    "chars": 4246,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/main.rs",
    "chars": 26291,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/oauth_config.rs",
    "chars": 9113,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/output.rs",
    "chars": 8808,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/schema.rs",
    "chars": 15758,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/services.rs",
    "chars": 5200,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/setup.rs",
    "chars": 81353,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/setup_tui.rs",
    "chars": 61480,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/text.rs",
    "chars": 10532,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/timezone.rs",
    "chars": 8744,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/token_storage.rs",
    "chars": 5173,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "src/validate.rs",
    "chars": 28814,
    "preview": "// Copyright 2026 Google LLC\n//\n// Licensed under the Apache License, Version 2.0 (the \"License\");\n// you may not use th"
  },
  {
    "path": "templates/modelarmor/jailbreak.json",
    "chars": 151,
    "preview": "{\n  \"filterConfig\": {\n    \"piAndJailbreakFilterSettings\": {\n      \"filterEnforcement\": \"ENABLED\",\n      \"confidenceLevel"
  }
]

About this extraction

This page contains the full source code of the googleworkspace/cli GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 196 files (1.4 MB), approximately 345.7k tokens, and a symbol index with 1329 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!