npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

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-cli

Requires 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)

  1. Go to https://console.cloud.google.com/ and create a project (or use an existing one)
  2. 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 tts also enable: Cloud Text-to-Speech API (needs billing on the project)
    • For youtube also enable: YouTube Data API v3
  3. APIs & Services → Credentials → Create Credentials → OAuth client ID
    • Application type: Desktop app
    • Download the JSON
  4. Place the downloaded file as client_secret.json somewhere google-cli can find it (see Configuration)
  5. Run google-cli auth setup and complete the browser flow — tokens are written next to client_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 tokens

Gmail

# 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-inbox

reply 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 list

Drive

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.95

segments.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.
  • --encoding accepts MP3 (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 null if the audio can't be parsed.

YouTube

YouTube authorizes separately from everything else, into its own youtube_tokens.json:

google-cli auth setup --youtube

This 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 UCxxxxxxxxxxxxxxxxxxxxxx

Prints 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.insert has no target-channel parameter, and most Google accounts also own a personal channel alongside any Brand Accounts. Run youtube list-channels first, 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-run google-cli auth setup --youtube and 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.md

Or, 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.md

The 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)
  • calendar
  • tasks
  • documents
  • spreadsheets
  • drive — full read/write
  • contacts
  • cloud-platform — required by Cloud Text-to-Speech

google-cli auth setup --youtube grants a separate token (youtube_tokens.json) with only:

  • youtube.upload — upload videos
  • youtube.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-platform scope added in 0.2.0 means re-running google-cli auth setup and re-approving everything. Existing tokens keep working for the older commands until you do; tts will fail until you re-consent. youtube needs its own auth setup --youtube regardless.

License

MIT — see LICENSE.