@iamrommel/google-cli
v0.2.1
Published
Google CLI — Gmail, Calendar, Tasks, Docs, Sheets, Drive, Maps, Cloud Text-to-Speech, YouTube uploads. One binary, per-project config.
Downloads
98
Maintainers
Readme
@iamrommel/google-cli
A single binary, google-cli, for working with Google from the terminal — Gmail, Calendar, Tasks, Docs, Sheets, Drive, Maps, Cloud Text-to-Speech, and YouTube uploads.
Designed for per-project configuration so the same install works across many projects with different OAuth credentials. All commands return JSON, so it's easy to script or call from agents.
Install
npm install -g @iamrommel/google-cliRequires Node.js ≥ 20.
Quick start
# 1. Create a Google Cloud project + OAuth client (one-time, see below)
# 2. Drop client_secret.json into one of the supported locations
# 3. Authorize
google-cli auth setup
# 4. Use it
google-cli gmail send --to "[email protected]" --subject "Hi" --body "Hello"
google-cli calendar list
google-cli drive search "invoice 2026"Google Cloud setup (one-time)
- Go to https://console.cloud.google.com/ and create a project (or use an existing one)
- APIs & Services → Library → enable each of:
- Gmail API
- Google Calendar API
- Google Tasks API
- Google Docs API
- Google Drive API
- Google Sheets API
- For Maps support also enable: Places API, Directions API, Geocoding API
- For
ttsalso enable: Cloud Text-to-Speech API (needs billing on the project) - For
youtubealso enable: YouTube Data API v3
- APIs & Services → Credentials → Create Credentials → OAuth client ID
- Application type: Desktop app
- Download the JSON
- Place the downloaded file as
client_secret.jsonsomewheregoogle-clican find it (see Configuration) - Run
google-cli auth setupand complete the browser flow — tokens are written next toclient_secret.json
Configuration
google-cli resolves credentials in this order (first match wins). Run google-cli auth where to see what got picked up.
| # | Source | How to use |
|---|---|---|
| 1 | ./.google-cli/ in cwd (walks upward, like git) | mkdir .google-cli && cp ~/Downloads/client_secret.json .google-cli/ — highest priority so project-local always wins |
| 2 | Env vars | GOOGLE_CLIENT_SECRET_PATH=/path/to/client_secret.json GOOGLE_TOKENS_PATH=/path/to/tokens.json google-cli ... — or set GOOGLE_CLI_CREDENTIALS_DIR=/path/to/dir to point at a directory containing both |
| 3 | $XDG_CONFIG_HOME/google-cli/ (default ~/.config/google-cli/) | mkdir -p ~/.config/google-cli && cp ~/Downloads/client_secret.json ~/.config/google-cli/ |
| 4 | Default for first-time write | If nothing above exists, auth setup creates ./.google-cli/ in the current working directory |
Per-project OAuth clients are easy: drop a different .google-cli/ in each project root and that project's CLI runs use it.
Env vars reference
| Variable | Purpose |
|---|---|
| GOOGLE_CLI_CREDENTIALS_DIR | Override the entire credentials directory |
| GOOGLE_CLIENT_SECRET_PATH | Override the client_secret.json path |
| GOOGLE_TOKENS_PATH | Override the tokens.json path |
| GOOGLE_CLI_TIMEZONE | Default IANA timezone for calendar events (default UTC) |
| GOOGLE_MAPS_API_KEY | Required for google-cli maps subcommands |
YouTube tokens live beside the others as youtube_tokens.json in the same resolved directory — google-cli auth where prints both paths.
Command reference
All commands return JSON to stdout. Errors return JSON with an error field and exit non-zero.
Auth
google-cli auth setup # Interactive OAuth (opens listener, prints URL, waits)
google-cli auth url # Print the auth URL only (non-blocking)
google-cli auth listen # Wait for callback on localhost:3456
google-cli auth status # Show authentication state
google-cli auth where # Show resolved credentials directory
google-cli auth revoke # Revoke + delete tokensGmail
# Compose, send & reply
google-cli gmail send --to "[email protected]" --subject "Hi" --body "Hello"
google-cli gmail send --to "[email protected]" --cc "[email protected]" --subject "Hi" --body "<p>Hello</p>" --html
google-cli gmail send --to "[email protected]" --subject "Report" --body "See attached" --attach ./a.pdf --attach ./b.png
google-cli gmail reply <messageId> --body "Thanks, will do" # reply in-thread to the sender
google-cli gmail reply <messageId> --body "Sounds good" --reply-all # also include original To/Cc
google-cli gmail reply <messageId> --body "<p>Done</p>" --html --attach ./file.pdf # HTML + attachment(s), repeat --attach
google-cli gmail reply <messageId> --body "Not ready to send yet" --draft # save as an in-thread draft
# Drafts
google-cli gmail draft --to "[email protected]" --subject "Hi" --body "Draft" --cc "[email protected]" --html --attach ./file.pdf
google-cli gmail list-drafts --max 20
google-cli gmail delete-draft <draftId> # permanent
# Read & search
google-cli gmail search "is:unread newer_than:7d" --max 10
google-cli gmail read <messageId>
google-cli gmail attachments <messageId>
google-cli gmail download-attachment <messageId> --attachment-id <id> --output ./file.pdf
# Read state, archive & trash
google-cli gmail mark-read <messageId>
google-cli gmail archive <messageId> # remove from inbox
google-cli gmail trash <messageId> # to Trash (recoverable ~30d)
google-cli gmail trash-by-query "from:[email protected] older_than:1y" --max 500 # bulk, still recoverable
# Labels & filters
google-cli gmail list-labels
google-cli gmail create-label "Mandy/Trading" # "Parent/Child" = nested label
google-cli gmail label-by-query "from:[email protected]" --label "Mandy/Trading" --max 500
google-cli gmail create-filter --query "from:[email protected]" --add-label "Mandy/Trading" --skip-inboxreply vs send — replying in-thread. gmail reply <messageId> keeps the response inside the original Gmail conversation. It reuses the source message's threadId, sets In-Reply-To/References from the original Message-ID, reuses the subject (prefixed with Re: once), and replies to the sender (Reply-To if present, otherwise From). A plain gmail send with a Re: subject starts a new thread instead — that's the bug reply fixes. Flags: --reply-all adds the original To/Cc recipients (minus yourself and the sender); --html and --attach <path> (repeatable) behave exactly as in send; --draft saves an in-thread draft instead of sending.
Calendar
google-cli calendar list
google-cli calendar list --from 2026-04-23T00:00:00Z --to 2026-04-30T23:59:59Z --max 50
google-cli calendar create --title "Meeting" --start 2026-04-25T10:00:00 --end 2026-04-25T11:00:00 --timezone Asia/Manila
google-cli calendar update <eventId> --title "New title"
google-cli calendar delete <eventId>Tasks
google-cli tasks lists
google-cli tasks list
google-cli tasks list --show-completed
google-cli tasks create --title "Buy milk" --due 2026-04-25
google-cli tasks update <taskId> --notes "..."
google-cli tasks complete <taskId>
google-cli tasks delete <taskId>Docs
google-cli docs create --title "Notes" --content "Initial body"
google-cli docs read <docId>
google-cli docs update <docId> --content "Replacement"
google-cli docs update <docId> --content "Appended" --append
google-cli docs list --query "weekly"Sheets
google-cli sheets create --title "Budget" --sheets "Income,Expenses"
google-cli sheets read <id> --range "Sheet1!A1:D10"
google-cli sheets read <id> --range "Sheet1!A1:D10" --formula # return cell formulas (=SUM(...)) instead of computed values
google-cli sheets read <id> --range "Sheet1!A1:D10" --value-render UNFORMATTED_VALUE # raw values (numbers, not display strings)
google-cli sheets update <id> --range "Sheet1!A1:B2" --values '[["Name","Age"],["Rommel",30]]'
google-cli sheets append <id> --range "Sheet1!A:B" --values '[["Mandy",1]]'
google-cli sheets add-chart <id> --title "Sales" --type line --data-range "Sheet1!A1:D10"
google-cli sheets clear <id> --range "Sheet1!A1:Z100"
google-cli sheets merge-cells <id> --range "Sheet1!A1:E1"
google-cli sheets format-cells <id> --range "Sheet1!A1:E1" --bold --bg-color "#6AA84F"
google-cli sheets listDrive
google-cli drive list
google-cli drive search "invoice 2026"
google-cli drive get <fileId>
google-cli drive upload ./report.pdf --parent-id <folderId>
google-cli drive download <fileId> --output ./local.pdf
google-cli drive mkdir "Reports"
google-cli drive share <fileId> --email "[email protected]" --role writer --notify
google-cli drive trash <fileId>
google-cli drive delete <fileId>Maps (uses an API key, not OAuth)
export GOOGLE_MAPS_API_KEY=...
google-cli maps search "coffee near Makati"
google-cli maps directions --from "BGC" --to "Makati Med" --mode driving
google-cli maps geocode "BGC, Taguig"Text-to-Speech
Synthesizes speech with Cloud Text-to-Speech. Unlike Maps this uses OAuth, not an API key — it needs the cloud-platform scope, and the Cloud project must have billing enabled.
# Pick a real voice name from actual output rather than guessing one
google-cli tts list-voices --language-code en-US
# One file. Prints the path AND the duration in seconds.
google-cli tts synthesize --text "Welcome to the app." --out intro.mp3
google-cli tts synthesize --in script.txt --out narration.mp3 --speaking-rate 0.95
cat script.txt | google-cli tts synthesize --out narration.wav --encoding LINEAR16
# Many segments in one pass, each written to <out-dir>/<id>.<ext>
google-cli tts batch --manifest segments.json --out-dir ./narration/ --speaking-rate 0.95segments.json is a JSON array; text (or ssml) is required, everything else is optional and overrides the command-level default:
[
{ "id": "01-download", "text": "Start by installing the app." },
{ "id": "02-signup", "text": "Create an account with your email.", "speakingRate": 0.9 }
]Every result carries durationSeconds, parsed straight from the returned audio — so you can time an ffmpeg slideshow without shelling out to ffprobe:
{
"outDir": "./narration/",
"items": [
{ "index": 0, "id": "01-download", "path": "narration/01-download.mp3",
"bytes": 24192, "durationSeconds": 3.552, "encoding": "MP3",
"voice": "en-US-Neural2-F", "languageCode": "en-US", "characters": 28 }
],
"totalBytes": 24192, "totalCharacters": 28, "totalDurationSeconds": 3.552
}Notes:
- Input is capped at 5000 bytes per request — the API's own limit. Oversized input fails with the byte count rather than being silently split, so segment seams stay where you put them.
--encodingacceptsMP3(default),LINEAR16(WAV),OGG_OPUS,MULAW,ALAW. The output extension is corrected to match.- Duration is measured from MP3 frame headers, the WAV byte-rate, or the Ogg granule position. It is
nullif the audio can't be parsed.
YouTube
YouTube authorizes separately from everything else, into its own youtube_tokens.json:
google-cli auth setup --youtubeThis is not a style choice. Google only shows the channel picker — the only way to reach a Brand Account channel — when the OAuth request carries YouTube scopes and nothing else. Bundled with the Workspace scopes, Google treats the grant as account-level, skips the picker entirely, and silently binds the token to your personal channel. Uploads then land there with no warning. Granting YouTube on its own is what makes the picker appear, so the two grants cannot share a token file.
# Confirm which channel this token uploads to
google-cli youtube list-channels
google-cli youtube upload --file walkthrough.mp4 \
--title "Product walkthrough" \
--description "A short tour." \
--privacy unlisted \
--channel-id UCxxxxxxxxxxxxxxxxxxxxxxPrints the video id and watch URL:
{
"videoId": "dQw4w9WgXcQ",
"url": "https://www.youtube.com/watch?v=dQw4w9WgXcQ",
"title": "Product walkthrough",
"privacyStatus": "unlisted",
"channelId": "UCxxxxxxxxxxxxxxxxxxxxxx",
"channelTitle": "My Channel",
"bytes": 8419234
}The upload lands on whichever channel the token is bound to.
videos.inserthas no target-channel parameter, and most Google accounts also own a personal channel alongside any Brand Accounts. Runyoutube list-channelsfirst, then pass that id as--channel-id— the upload is refused on a mismatch, which is much cheaper than deleting a video from the wrong channel afterwards. To change the target channel, re-rungoogle-cli auth setup --youtubeand pick a different one.
--privacy defaults to unlisted, never public. Uploads are simple (non-resumable), which is fine for files up to a few hundred MB.
Use as a Claude Code skill
A ready-to-use SKILL.md template is shipped at skill-template/SKILL.md. Drop it into your Claude Code project so the agent knows how to call google-cli:
# From your project root
mkdir -p .claude/skills/google
curl -fsSL https://raw.githubusercontent.com/iamrommel/google-cli/main/skill-template/SKILL.md \
-o .claude/skills/google/SKILL.mdOr, if you have the package installed globally, copy from node_modules:
cp "$(npm root -g)/@iamrommel/google-cli/skill-template/SKILL.md" .claude/skills/google/SKILL.mdThe template covers every subcommand with examples and includes the auth recovery flow, so Claude Code (or any agent) can pick up Google Workspace operations with no extra prompting. Edit it freely to fit your project's conventions.
Programmatic use
Everything is also exposed as a library:
import { GoogleAuth, GmailService } from '@iamrommel/google-cli'
const auth = new GoogleAuth()
const client = await auth.getClient()
const gmail = new GmailService(client)
await gmail.send({ to: '[email protected]', subject: 'Hi', body: 'Hello' })Scopes requested
Authorization grants these scopes:
gmail.modify— read, send, modify, trash (no delete)calendartasksdocumentsspreadsheetsdrive— full read/writecontactscloud-platform— required by Cloud Text-to-Speech
google-cli auth setup --youtube grants a separate token (youtube_tokens.json) with only:
youtube.upload— upload videosyoutube.readonly— read the channel list, to confirm the upload target
Upgrading from ≤ 0.1.13 requires re-authorizing. The Workspace scope list is requested as a single block with no incremental-authorization path, so the
cloud-platformscope added in 0.2.0 means re-runninggoogle-cli auth setupand re-approving everything. Existing tokens keep working for the older commands until you do;ttswill fail until you re-consent.youtubeneeds its ownauth setup --youtuberegardless.
License
MIT — see LICENSE.
