google-workspace-cli 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,270 @@
1
+ Metadata-Version: 2.4
2
+ Name: google-workspace-cli
3
+ Version: 0.1.0
4
+ Summary: Google Workspace CLI for Claude Code — Calendar, Gmail, Drive with multi-account OAuth support
5
+ Project-URL: Homepage, https://github.com/JFK/gw-cli
6
+ Project-URL: Repository, https://github.com/JFK/gw-cli
7
+ Project-URL: Issues, https://github.com/JFK/gw-cli/issues
8
+ Author-email: Fumikazu Kiyota <fumikazu.kiyota@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: calendar,claude-code,cli,gmail,google-drive,google-workspace,multi-account,oauth2
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Communications :: Email
20
+ Classifier: Topic :: Office/Business :: Scheduling
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: click>=8.3
23
+ Requires-Dist: google-api-python-client>=2.194
24
+ Requires-Dist: google-auth-oauthlib>=1.3
25
+ Requires-Dist: google-auth>=2.49
26
+ Requires-Dist: rich>=15.0
27
+ Requires-Dist: tzdata>=2024.1; platform_system == 'Windows'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # gw-cli
31
+
32
+ [![PyPI version](https://img.shields.io/pypi/v/google-workspace-cli.svg)](https://pypi.org/project/google-workspace-cli/)
33
+ [![Python versions](https://img.shields.io/pypi/pyversions/google-workspace-cli.svg)](https://pypi.org/project/google-workspace-cli/)
34
+ [![License: MIT](https://img.shields.io/pypi/l/google-workspace-cli.svg)](https://github.com/JFK/gw-cli/blob/master/LICENSE)
35
+ [![CI](https://github.com/JFK/gw-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/JFK/gw-cli/actions/workflows/ci.yml)
36
+
37
+ > **Google Workspace CLI for Claude Code — manage Calendar, Gmail, and Drive across multiple Google accounts.**
38
+
39
+ `gw-cli` is a [Claude Code](https://claude.com/claude-code) plugin that brings Google Workspace into your terminal. Register multiple OAuth accounts, switch between them instantly, and let Claude Code operate your calendar, email, and files through natural language.
40
+
41
+ ## 60-second quickstart
42
+
43
+ ```text
44
+ # In any Claude Code session:
45
+ /plugin marketplace add JFK/gw-cli
46
+ /plugin install gw-cli
47
+
48
+ # Setup (one-time):
49
+ gw auth login --credentials ./credentials.json
50
+
51
+ # Then use naturally:
52
+ "Show me my unread emails"
53
+ "Schedule a meeting tomorrow at 10am with Meet"
54
+ "What's on my calendar this week?"
55
+ ```
56
+
57
+ ## Features
58
+
59
+ - **Multi-account support** — Register multiple Google accounts (personal, work, client projects) and switch between them instantly
60
+ - **Calendar** — List, create, update, delete events. Google Meet setup. Free/busy check across accounts
61
+ - **Gmail** — Search, read, send, reply. Label management. Mark read/unread. Attachment download and save to Drive
62
+ - **Drive** — List, search, upload, create Google Docs/Sheets/Slides. Share/unshare files
63
+ - **Claude Code integration** — All commands support `--json` output for seamless skill parsing
64
+ - **Periodic monitoring** — Use with `/loop` for unread email checks, calendar reminders
65
+ - **WSL2 compatible** — Automatic fallback to manual OAuth flow when localhost callback is unreachable
66
+
67
+ ## Required dependencies
68
+
69
+ | Tool | Required | Why |
70
+ |---|---|---|
71
+ | Python 3.10+ | yes | Runtime |
72
+ | `pip` | yes | Package installation |
73
+ | `gcloud` CLI | recommended | GCP project setup and API enablement |
74
+ | [Claude Code](https://claude.com/claude-code) | recommended | Plugin integration and natural language interface |
75
+
76
+ ## Install
77
+
78
+ ### As a Claude Code plugin (recommended)
79
+
80
+ ```text
81
+ /plugin marketplace add JFK/gw-cli
82
+ /plugin install gw-cli
83
+ ```
84
+
85
+ The plugin auto-installs the Python CLI and registers 7 skills:
86
+
87
+ | Skill | Description |
88
+ |---|---|
89
+ | `/gw-setup` | Guided first-time Google Cloud setup |
90
+ | `/gw-auth` | Account management (login, switch, list, remove) |
91
+ | `/gw-calendar` | Calendar operations |
92
+ | `/gw-mail` | Gmail operations |
93
+ | `/gw-drive` | Drive operations |
94
+ | `/gw-workflow` | Cross-service workflows (email attachment to Drive, etc.) |
95
+ | `/gw-loop` | Periodic monitoring setup |
96
+
97
+ ### Standalone CLI
98
+
99
+ ```bash
100
+ pip install google-workspace-cli
101
+ ```
102
+
103
+ > Published on PyPI as **`google-workspace-cli`** (the `gw-cli` name was already taken). The installed command is still `gw`.
104
+
105
+ Latest from source:
106
+
107
+ ```bash
108
+ pip install git+https://github.com/JFK/gw-cli.git
109
+ ```
110
+
111
+ Or clone and install locally:
112
+
113
+ ```bash
114
+ git clone https://github.com/JFK/gw-cli.git
115
+ cd gw-cli
116
+ pip install -e .
117
+ ```
118
+
119
+ ## Setup
120
+
121
+ ### 1. Create GCP Project
122
+
123
+ ```bash
124
+ gcloud projects create gw-cli-$(date +%Y%m%d) --name="gw-cli"
125
+ gcloud config set project gw-cli-YYYYMMDD
126
+ ```
127
+
128
+ ### 2. Enable APIs
129
+
130
+ ```bash
131
+ gcloud services enable calendar-json.googleapis.com gmail.googleapis.com drive.googleapis.com --project=PROJECT_ID
132
+ ```
133
+
134
+ ### 3. OAuth Consent Screen
135
+
136
+ Open in browser: `https://console.cloud.google.com/apis/credentials/consent?project=PROJECT_ID`
137
+
138
+ - User Type: **External**
139
+ - App name: `gw-cli`
140
+ - Add your email as a **test user** (required, otherwise login fails with 403)
141
+
142
+ ### 4. Create OAuth Client
143
+
144
+ Open in browser: `https://console.cloud.google.com/apis/credentials/oauthclient?project=PROJECT_ID`
145
+
146
+ - Application type: **Desktop app**
147
+ - Download the JSON file
148
+
149
+ ### 5. Login
150
+
151
+ ```bash
152
+ gw auth login --credentials ./credentials.json
153
+ ```
154
+
155
+ On WSL2/remote environments, the tool automatically falls back to manual URL paste mode when the localhost callback is unreachable.
156
+
157
+ ### 6. Verify
158
+
159
+ ```bash
160
+ gw auth status
161
+ gw cal list --days 3
162
+ gw mail list --query "is:unread" --limit 5
163
+ ```
164
+
165
+ ## Usage
166
+
167
+ ### Available commands
168
+
169
+ ```
170
+ gw auth login, list, status, switch, remove
171
+ gw cal list, get, create, update, delete, free
172
+ gw mail list, read, send, reply, labels, label, mark, attachments, download, to-drive
173
+ gw drive list, upload, create, share, unshare
174
+ gw config show, set
175
+ ```
176
+
177
+ ### Calendar
178
+
179
+ ```bash
180
+ gw cal list --days 7
181
+ gw cal create --title "Meeting" --start "2026-04-17 10:00" --end "2026-04-17 11:00" --meet
182
+ gw cal create --title "1on1" --start "2026-04-18 14:00" --end "2026-04-18 14:30" --meet --attendee colleague@example.com
183
+ gw cal free --date 2026-04-17
184
+ gw cal delete EVENT_ID
185
+ ```
186
+
187
+ ### Gmail
188
+
189
+ ```bash
190
+ gw mail list --query "is:unread"
191
+ gw mail read MESSAGE_ID
192
+ gw mail send --to "user@example.com" --subject "Hello" --body "Hi there"
193
+ gw mail reply MESSAGE_ID --body "Thanks!"
194
+ gw mail mark MESSAGE_ID --read
195
+ gw mail attachments MESSAGE_ID
196
+ gw mail to-drive MESSAGE_ID --attachment-id ATT_ID --folder FOLDER_ID
197
+ ```
198
+
199
+ ### Drive
200
+
201
+ ```bash
202
+ gw drive list
203
+ gw drive list --query "name contains 'report'"
204
+ gw drive upload ./file.pdf
205
+ gw drive create --type doc --title "New Document"
206
+ gw drive share FILE_ID --email user@example.com --role writer
207
+ gw drive unshare FILE_ID --email user@example.com
208
+ ```
209
+
210
+ ### Multi-account
211
+
212
+ ```bash
213
+ gw auth login --credentials ./work-credentials.json # Add another account
214
+ gw auth list # Show all accounts
215
+ gw auth switch work@company.com # Switch active account
216
+ gw mail list # Uses active account
217
+ gw mail list --account personal@gmail.com # Override for one command
218
+ ```
219
+
220
+ ### Config
221
+
222
+ ```bash
223
+ gw config show
224
+ gw config set defaults.calendar.days 14
225
+ gw config set defaults.mail.limit 50
226
+ ```
227
+
228
+ ## Output
229
+
230
+ All commands support `--json` for machine-readable output:
231
+
232
+ ```bash
233
+ gw cal list --json
234
+ gw mail list --query "is:unread" --json
235
+ ```
236
+
237
+ Human-readable output always shows the active account:
238
+
239
+ ```
240
+ [fumikazu.kiyota@gmail.com]
241
+ evt1 | Team standup | 2026-04-17T10:00:00+09:00 | ...
242
+ ```
243
+
244
+ ## Periodic Monitoring
245
+
246
+ Use with the `/loop` skill for continuous monitoring:
247
+
248
+ ```
249
+ /loop 5m gw mail list --query "is:unread" --json
250
+ /loop 30m gw cal list --days 1 --json
251
+ ```
252
+
253
+ ## Config storage
254
+
255
+ Config stored at `~/.config/google-workspace-cli/config.json`.
256
+
257
+ Credentials and tokens are stored per account:
258
+
259
+ ```
260
+ ~/.config/google-workspace-cli/
261
+ ├── config.json
262
+ ├── credentials/
263
+ │ └── user@gmail.com.json
264
+ └── tokens/
265
+ └── user@gmail.com.json
266
+ ```
267
+
268
+ ## License
269
+
270
+ MIT
@@ -0,0 +1,13 @@
1
+ gw/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
2
+ gw/auth.py,sha256=tovl1N7FNcg1SJWqe3FfOO3vBus81O8k6iXoaRIQYIg,7287
3
+ gw/calendar.py,sha256=5RIXyiasVRqIfDWhiJzg5XDo5NA0Ey4E7bGHQ-Ab1RQ,8823
4
+ gw/cli.py,sha256=at8JikU302isNawNgxzC0azCKrHrIzRECyC4Ee0gzpk,3424
5
+ gw/config.py,sha256=IjKG6eILcRxgwUEVGWzjNSNqx5LZOjm_7fXpENIN0Ko,4226
6
+ gw/drive.py,sha256=INdU6NZ8RdMs0DKg9QMb0AD4iyQKSCKhzPruVET6cwU,6387
7
+ gw/gmail.py,sha256=BPUuyxnlyNTBzOI2UMtkWRozQ96duZA6fy6ll0_GHUs,13299
8
+ gw/output.py,sha256=c0RYrnvgIXftMuwTFZnRdYTBLMkAPHUFKyANvSRsB1Y,833
9
+ google_workspace_cli-0.1.0.dist-info/METADATA,sha256=eCy8gE4Nv_0vxdJ58yOgEUjTsw53XPyw3X5SMQQ0r1Q,8151
10
+ google_workspace_cli-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
11
+ google_workspace_cli-0.1.0.dist-info/entry_points.txt,sha256=TZgF92FFR2flxwiXWddhth8EI3AxYdIMdEPnBHQgkZ4,34
12
+ google_workspace_cli-0.1.0.dist-info/licenses/LICENSE,sha256=ESYyLizI0WWtxMeS7rGVcX3ivMezm-HOd5WdeOh-9oU,1056
13
+ google_workspace_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ gw = gw.cli:cli
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
gw/__init__.py ADDED
@@ -0,0 +1 @@
1
+ __version__ = "0.1.0"
gw/auth.py ADDED
@@ -0,0 +1,199 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shutil
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ import click
9
+ from google_auth_oauthlib.flow import InstalledAppFlow
10
+ from googleapiclient.discovery import build
11
+
12
+ from gw.config import DEFAULT_CONFIG_DIR, GwConfig
13
+
14
+ SCOPES = [
15
+ "https://www.googleapis.com/auth/calendar",
16
+ "https://www.googleapis.com/auth/gmail.modify",
17
+ "https://www.googleapis.com/auth/gmail.send",
18
+ "https://www.googleapis.com/auth/drive",
19
+ ]
20
+
21
+
22
+ def _can_auto_open_browser() -> bool:
23
+ """Whether auto-opening a browser is likely to reach the user.
24
+
25
+ On WSL2 and headless hosts the OS default URL handler is usually wrong (e.g. a
26
+ file manager) or absent, so an auto-opened consent page never appears. In those
27
+ cases the caller should skip the auto-open and let the operator open the printed
28
+ URL in any browser that can reach this host's localhost (a Windows browser
29
+ reaches WSL2 localhost). On native desktops (macOS / Windows / Linux with a
30
+ display) auto-open is the convenient one-step path.
31
+ """
32
+ if os.environ.get("WSL_DISTRO_NAME"):
33
+ return False
34
+ try:
35
+ with open("/proc/version", encoding="utf-8") as fh:
36
+ if "microsoft" in fh.read().lower():
37
+ return False
38
+ except OSError:
39
+ pass
40
+ if sys.platform.startswith("linux") and not (os.environ.get("DISPLAY") or os.environ.get("WAYLAND_DISPLAY")):
41
+ return False
42
+ return True
43
+
44
+
45
+ def _get_user_email(credentials) -> str:
46
+ """Fetch the authenticated user's email via Gmail API."""
47
+ service = build("gmail", "v1", credentials=credentials)
48
+ profile = service.users().getProfile(userId="me").execute()
49
+ return profile["emailAddress"]
50
+
51
+
52
+ def _resolve_config_dir(ctx: click.Context, config_dir: Path | None) -> Path:
53
+ """Resolve config_dir from subcommand option, parent context, or default."""
54
+ if config_dir is not None:
55
+ return config_dir
56
+ return ctx.obj.get("config_dir", DEFAULT_CONFIG_DIR)
57
+
58
+
59
+ _config_dir_option = click.option(
60
+ "--config-dir",
61
+ type=click.Path(path_type=Path),
62
+ default=None,
63
+ hidden=True,
64
+ help="Config directory (default: ~/.config/google-workspace-cli)",
65
+ )
66
+
67
+
68
+ @click.group()
69
+ @click.pass_context
70
+ def auth(ctx: click.Context) -> None:
71
+ """Manage Google accounts."""
72
+ ctx.ensure_object(dict)
73
+
74
+
75
+ @auth.command()
76
+ @click.option("--credentials", required=True, type=click.Path(exists=True, path_type=Path))
77
+ @_config_dir_option
78
+ @click.pass_context
79
+ def login(ctx: click.Context, credentials: Path, config_dir: Path | None) -> None:
80
+ """Add a Google account via OAuth login."""
81
+ resolved_dir = _resolve_config_dir(ctx, config_dir)
82
+ cfg = GwConfig(resolved_dir)
83
+
84
+ flow = InstalledAppFlow.from_client_secrets_file(str(credentials), SCOPES)
85
+
86
+ try:
87
+ # Loopback flow. Always print the consent URL explicitly (don't rely on
88
+ # the library's default prompt message) and keep the local server
89
+ # listening long enough for the operator to authorize. Only auto-open a
90
+ # browser where one is likely to reach the user: on WSL2/headless the OS
91
+ # default URL handler is often wrong (e.g. a file manager), so we skip the
92
+ # auto-open and let the operator open the printed URL in any browser that
93
+ # can reach this host's localhost (a Windows browser reaches WSL2 localhost).
94
+ flow.run_local_server(
95
+ port=0,
96
+ open_browser=_can_auto_open_browser(),
97
+ authorization_prompt_message=("\nOpen this URL in your browser to authorize:\n\n{url}\n"),
98
+ timeout_seconds=300,
99
+ )
100
+ except Exception:
101
+ # Last-resort manual paste (requires an interactive terminal for the prompt).
102
+ auth_url, _ = flow.authorization_url(prompt="consent")
103
+ click.echo(f"\nOpen this URL in your browser:\n\n{auth_url}\n")
104
+ click.echo("After authorizing, you will be redirected to a localhost URL.")
105
+ click.echo("Copy the FULL URL from the browser address bar and paste it here:\n")
106
+ redirect_url = click.prompt("Redirect URL")
107
+ flow.fetch_token(authorization_response=redirect_url)
108
+
109
+ creds = flow.credentials
110
+ email = _get_user_email(creds)
111
+
112
+ dest_creds = cfg.credentials_dir / f"{email}.json"
113
+ cfg.credentials_dir.mkdir(parents=True, exist_ok=True)
114
+ shutil.copy2(credentials, dest_creds)
115
+ os.chmod(dest_creds, 0o600)
116
+
117
+ cfg.tokens_dir.mkdir(parents=True, exist_ok=True)
118
+ token_path = cfg.tokens_dir / f"{email}.json"
119
+ token_path.write_text(creds.to_json())
120
+ os.chmod(token_path, 0o600)
121
+
122
+ cfg.add_account(email, f"credentials/{email}.json")
123
+ click.echo(f"Logged in as {email}")
124
+
125
+
126
+ @auth.command("list")
127
+ @_config_dir_option
128
+ @click.pass_context
129
+ def list_accounts(ctx: click.Context, config_dir: Path | None) -> None:
130
+ """List registered accounts."""
131
+ resolved_dir = _resolve_config_dir(ctx, config_dir)
132
+ cfg = GwConfig(resolved_dir)
133
+ if not cfg.accounts:
134
+ click.echo("No accounts registered. Run 'gw auth login' to add one.")
135
+ return
136
+ for acct in cfg.accounts:
137
+ marker = " *" if acct["email"] == cfg.active_account else ""
138
+ click.echo(f" {acct['email']}{marker}")
139
+
140
+
141
+ @auth.command()
142
+ @_config_dir_option
143
+ @click.pass_context
144
+ def status(ctx: click.Context, config_dir: Path | None) -> None:
145
+ """Show the active account."""
146
+ resolved_dir = _resolve_config_dir(ctx, config_dir)
147
+ cfg = GwConfig(resolved_dir)
148
+ if cfg.active_account:
149
+ click.echo(f"Active account: {cfg.active_account}")
150
+ else:
151
+ click.echo("No active account. Run 'gw auth login' to add one.")
152
+
153
+
154
+ @auth.command()
155
+ @click.argument("email")
156
+ @_config_dir_option
157
+ @click.pass_context
158
+ def switch(ctx: click.Context, email: str, config_dir: Path | None) -> None:
159
+ """Switch the active account."""
160
+ resolved_dir = _resolve_config_dir(ctx, config_dir)
161
+ cfg = GwConfig(resolved_dir)
162
+ cfg.switch_account(email)
163
+ click.echo(f"Switched to {email}")
164
+
165
+
166
+ @auth.command()
167
+ @click.argument("email")
168
+ @_config_dir_option
169
+ @click.pass_context
170
+ def remove(ctx: click.Context, email: str, config_dir: Path | None) -> None:
171
+ """Remove a registered account."""
172
+ resolved_dir = _resolve_config_dir(ctx, config_dir)
173
+ cfg = GwConfig(resolved_dir)
174
+ cfg.remove_account(email)
175
+ click.echo(f"Removed {email}")
176
+
177
+
178
+ def build_service(cfg: GwConfig, email: str, api_name: str, api_version: str):
179
+ """Build an authenticated Google API service for the given account."""
180
+ from google.auth.transport.requests import Request
181
+ from google.oauth2.credentials import Credentials
182
+
183
+ token_path = cfg.get_token_path(email)
184
+ if not token_path.exists():
185
+ raise RuntimeError(f"No token found for {email}. Run 'gw auth login --credentials <path>' first.")
186
+
187
+ creds = Credentials.from_authorized_user_file(str(token_path), SCOPES)
188
+
189
+ if creds.expired and creds.refresh_token:
190
+ try:
191
+ creds.refresh(Request())
192
+ token_path.write_text(creds.to_json())
193
+ os.chmod(token_path, 0o600)
194
+ except Exception as exc:
195
+ raise RuntimeError(
196
+ f"Token refresh failed for {email}. Run 'gw auth login --credentials <path>' to re-authenticate."
197
+ ) from exc
198
+
199
+ return build(api_name, api_version, credentials=creds)