Commit Graph

278 Commits

Author SHA1 Message Date
Justin Poehnelt 6ccbb42650 fix: auto-install binary on run if missing (#654)
* fix: auto-install binary on run if missing

pnpm skips postinstall when the package is already cached/installed.
This commit ensures run.js will auto-trigger install.js if the
binary is missing, fixing the 'gws binary not found' error.

* chore: add changeset for pnpm binary fix

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-31 11:47:43 -06:00
Justin Poehnelt 5d24ac225b ci: add cargo-audit workflow for dependency vulnerability scanning (#649)
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-31 11:42:06 -06:00
Justin Poehnelt 158f93afd7 fix: verify SHA256 checksum in npm postinstall script (#650)
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-31 11:41:25 -06:00
Justin Poehnelt b422e5d22f ci: pin cross-rs to v0.2.5 in release workflow (#651)
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-31 11:40:47 -06:00
googleworkspace-bot f4ee658360 chore: release versions (#647) v0.22.4 2026-03-31 11:20:03 -06:00
Justin Poehnelt 86c08cfc0d fix: remove cargo-dist, use native fetch for npm installer (#646)
* chore: remove cargo-dist, use native fetch for npm installer

* fix: harden npm scripts — spawnSync, signal handling, binary-exists check

* fix: address PR review comments

- Add null body guard for fetch response
- Fix PowerShell Expand-Archive path quoting vulnerability
- Sanitize error output to prevent ANSI escape injection
- Add proxy support limitation note
- Fix upgrade bug: use .version marker so npm update downloads new binary
- Downgrade changeset from minor to patch (chore, not feature)
- Update AGENTS.md: remove stale cargo-dist reference from labels

* fix: use flat archives for consistent tar/zip extraction

Both tar.gz and zip archives now contain files at root (no nested
directory). Removes --strip-components 1 from install.js since it is
no longer needed. This makes extraction consistent across platforms.

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-31 11:17:57 -06:00
googleworkspace-bot c7c6646d7a chore: release versions (#629) v0.22.3 2026-03-26 12:45:06 -06:00
googleworkspace-bot ec7f56bc39 chore: regenerate skills from Discovery API (#630)
Co-authored-by: jpoehnelt <3392975+jpoehnelt@users.noreply.github.com>
2026-03-26 12:40:24 -06:00
Justin Poehnelt 80bd150f76 feat(auth): use strict OS keychain integration on macOS and Windows (#631)
* fix(auth): implement platform-aware keyring storage

* chore: regenerate skills [skip ci]

* fix(auth): address PR comments on error handling and TOCTOU vulnerabilities

* fix(test): isolate config directory to prevent race conditions during cargo test

* refactor(auth): deduplicate key generation logic in credential store

* fix(test): execute credential tests serially to prevent environment pollution

* fix(auth): sanitize os keyring error strings and handle missing file fallbacks

This commit removes the inadvertently included skills, uses output::sanitize_for_terminal to protect the user from escape sequence injection via keyring errors, and ensures removal of the fallback file properly alerts the user when an io error occurs.

* chore: regenerate skills [skip ci]

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
2026-03-26 12:40:07 -06:00
jpoehnelt-bot 674d53a65a fix(ci): add setup-uv step to Lint Skills job
The Lint Skills job uses uvx to run agentskills validate, but uv
is not pre-installed on GitHub Actions runners. Add astral-sh/setup-uv
action and a skills path filter to the changes detection.
2026-03-26 09:59:40 -06:00
jpoehnelt-bot c7c42f6d07 fix: register script service and fix test path validation 2026-03-26 09:59:14 -06:00
googleworkspace-bot a63b070a51 chore: release versions (#628) v0.22.2 2026-03-26 09:56:34 -06:00
femto a52d297cdf feat: add HTTP proxy support via environment variables (#622)
* feat: add HTTP proxy support via environment variables

When http_proxy/https_proxy/all_proxy environment variables are set,
use reqwest (which natively supports proxy) for token refresh instead
of yup-oauth2's hyper-based client (which doesn't support proxy).

This enables gws to work in environments that require HTTP proxy to
access Google APIs (e.g., users in China).

Changes:
- Cargo.toml: Enable reqwest's default features including proxy support
- src/auth.rs: Add proxy-aware token refresh using reqwest as fallback

Fixes #422

* feat: add proxy support for auth login flow

When proxy env vars are set, use a custom OAuth flow with reqwest
for token exchange instead of yup-oauth2's hyper-based client.

Changes to auth_commands.rs:
- Add login_with_proxy_support() for proxy-aware OAuth login
- Add exchange_code_with_reqwest() for token exchange via reqwest
- Detect proxy env vars and choose appropriate flow

* fix: address proxy auth review feedback

* fix: reuse shared reqwest client for auth flows
2026-03-25 17:31:22 +00:00
googleworkspace-bot 45ca45bfd4 chore: release versions (#624) v0.22.1 2026-03-25 11:31:07 -06:00
Yannik Tausch b45cdf862a ci: add skills-ref lint step for SKILL.md validation (#626) 2026-03-25 09:44:54 -06:00
googleworkspace-bot 6a45832ea5 chore: regenerate skills from Discovery API (#621)
Co-authored-by: jpoehnelt <3392975+jpoehnelt@users.noreply.github.com>
2026-03-24 23:07:45 -06:00
Justin Poehnelt 19ddc25935 fix: move data files into CLI crate for crates.io publishing (#620)
* fix: move registry/ and templates/ into CLI crate for cargo publish

* chore: regenerate skills [skip ci]

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
2026-03-24 16:35:41 -06:00
googleworkspace-bot 9198386a3d chore: release versions (#619) v0.22.0 2026-03-24 16:17:11 -06:00
Malo Bourgon 0850c48b68 feat(gmail): add --draft flag to +send, +reply, +reply-all, +forward (#571)
When --draft is set, calls users.drafts.create instead of
users.messages.send. Message construction is identical; only the
API method and metadata wrapper change. Threaded drafts (replies
and forwards) preserve threadId in the draft metadata.
2026-03-24 16:01:55 -06:00
googleworkspace-bot 2aec0ba5a3 chore: release versions (#618)
* chore: release versions

* chore: regenerate skills [skip ci]

---------

Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
2026-03-24 15:51:18 -06:00
Justin Poehnelt c4448b98a6 chore: add changeset for crates.io publishing (#617)
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-24 15:32:19 -06:00
googleworkspace-bot 186e88caa0 chore: release versions (#616) 2026-03-24 15:01:28 -06:00
Justin Poehnelt ea0849a657 fix: version sync workspace (#615) 2026-03-24 14:53:39 -06:00
googleworkspace-bot eacd436694 chore: release versions (#614) v0.21.0 2026-03-24 14:44:26 -06:00
Justin Poehnelt 029e5def2b feat: extract google-workspace library crate (cargo workspace) (#613)
* feat: extract google-workspace library crate and restructure as cargo workspace

- Create crates/google-workspace/ with public modules: discovery, error, services, validate, client
- Move CLI binary to crates/cli/ (package: google-workspace-cli, binary: gws)
- Root Cargo.toml is now workspace-only
- Binary modules use thin re-exports from library (zero behavioral changes)
- Discovery fetch_discovery_document accepts cache_dir parameter for library consumers
- Break output.rs / error.rs circular dep by moving char detection to library validate module
- Update CI workflows for workspace (--workspace flags, path filters)
- Update dist-workspace.toml and policy.yml for new crate locations

Closes #386

* refactor: rename crates/cli to crates/google-workspace-cli

* chore: sync CLI version to 0.20.1 and regenerate skills

* chore: update labeler paths for workspace crate layout

* fix: update flake.nix to read version from CLI crate Cargo.toml

* fix: use tokio::fs for non-blocking I/O in library discovery cache

* docs: update AGENTS.md source layout for workspace structure

* docs: add crate-level READMEs for crates.io

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-24 14:40:03 -06:00
googleworkspace-bot 3ac16ee595 chore: release versions (#606)
* chore: release versions

* chore: regenerate skills [skip ci]

---------

Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
2026-03-24 13:28:51 -06:00
Justin Poehnelt 2ddb46e39f test(gmail): add regression tests for RFC 2822 display name quoting (#610)
The original bug from #513 (display names with commas/parens causing
'Invalid To header') was already fixed by the mail-builder migration
in #482. This adds explicit regression tests to prevent regressions.

Closes #513

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-24 13:25:54 -06:00
Justin Poehnelt b8fd3d966f fix(client): add 10s connect timeout to prevent initial hangs (#608)
* fix(client): add 10s connect timeout to prevent initial hangs

* fix(client): retry on transient connect/timeout errors

* chore: regenerate skills [skip ci]

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
2026-03-24 12:49:23 -06:00
googleworkspace-bot 75a7121713 chore: sync skills with Discovery API (#607)
* chore: regenerate skills from Discovery API

* chore: regenerate skills [skip ci]

---------

Co-authored-by: jpoehnelt <3392975+jpoehnelt@users.noreply.github.com>
Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
2026-03-24 12:40:27 -06:00
Yannik Tausch 2bfcca97ca feat: move version from top-level SKILL.md frontmatter to metadata and track CLI version (#601) 2026-03-24 11:59:55 -06:00
Justin Poehnelt 203acc7bcf fix(ci): skip publish-skills on fork PRs where secrets are unavailable (#605)
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
v0.20.0
2026-03-24 11:59:37 -06:00
googleworkspace-bot 3e1e0e11f3 chore: release versions (#603)
* chore: release versions

* chore: regenerate skills [skip ci]

---------

Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
2026-03-24 11:50:23 -06:00
Justin Poehnelt 4663021b4a fix(ci): skip publish-skills on fork PRs (#604)
* fix(ci): skip publish-skills on fork PRs where secrets are unavailable

* chore: regenerate skills [skip ci]

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
2026-03-24 11:50:06 -06:00
Malo Bourgon e782dd70b5 feat(gmail): forward original attachments and preserve inline images (#589)
Include original message attachments on +forward by default, matching
Gmail web behavior. Add --no-original-attachments flag to opt out
(skips file attachments but preserves inline images in HTML mode).

Preserve cid: inline images in HTML mode for both +forward and
+reply/+reply-all by building the correct multipart/related MIME
structure via mail-builder's MimePart API. Gmail's API rewrites
Content-Disposition: inline to attachment in multipart/mixed, so
explicit multipart/related is required.

In plain-text mode, inline images are not included for both forward
and reply, matching Gmail web behavior.

Key implementation details:
- Single-pass MIME payload walker replaces separate text/html extractors
- OriginalPart metadata type with lazy attachment data fetching
- Part classification uses Content-Disposition to distinguish regular
  attachments from inline images (some clients set Content-ID on both)
- Content-ID and content_type sanitized against CRLF header injection
- Size preflight before downloading original attachments
- Remote filename sanitization (not rejection) for sender-controlled names
- Walker does not recurse into hydratable parts (e.g., message/rfc822)
2026-03-24 11:32:22 -06:00
Justin Poehnelt 477f5d90db docs: add helper command guidelines and anti-patterns (#598)
* docs: add helper command guidelines and anti-patterns

* Apply suggestions from code review

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
v0.19.0
2026-03-23 15:27:03 -06:00
googleworkspace-bot 834fa7d25a chore: release versions (#590)
* chore: release versions

* chore: regenerate skills [skip ci]

---------

Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
2026-03-23 14:49:28 -06:00
github-actions[bot] e82c02c5a2 style: cargo fmt 2026-03-23 20:38:28 +00:00
JoeVenner 8a749c2c11 feat: add --dry-run support to events helper commands (#457)
Add dry-run mode to gws events +renew and gws events +subscribe commands.
When --dry-run is specified, the commands print what actions would be
taken without making any API calls. This allows agents to simulate
requests and learn without reaching the server.
2026-03-23 14:38:17 -06:00
Adem Akdoğan f157208449 fix: use block-style YAML sequences in SKILL.md frontmatter (#580)
Replace flow sequences (bins: ["gws"], skills: [...]) with block-style
sequences in all generated SKILL.md frontmatter templates.

Flow sequences are valid YAML but rejected by strictyaml, which the
Agent Skills reference implementation (agentskills validate) uses to
parse frontmatter. This caused all 93 generated skills to fail
validation.

Also adds unit tests verifying that service, shared, persona, and
recipe skill templates produce block-style sequences only.

Fixes #521
2026-03-23 14:31:57 -06:00
Justin Poehnelt a078945f2a refactor: replace manual arg parsing in auth commands with clap (#594)
* refactor: replace manual arg parsing in auth commands with clap

- Add ScopeMode enum (Default, Readonly, Full, Custom) for type-safe
  scope selection
- Build auth_command() with clap subcommands: login, setup, status,
  export, logout
- Replace manual --help check and match dispatch in handle_auth_command
  with clap subcommand routing
- Replace two-pass manual parsing in handle_login (services extraction)
  and resolve_scopes (scopes/readonly/full flags) with single clap parse
- Add --unmasked flag to export subcommand via clap
- Preserve run_login(&[]) public API for setup.rs compatibility
- Update all 63 tests to use ScopeMode-based API

* refactor: extract parse_login_args helper to deduplicate scope/services parsing

* fix: make --readonly, --full, --scopes mutually exclusive via clap conflicts

* fix: filter empty strings from custom scopes parsing

* refactor: extract build_login_subcommand to eliminate fragile .expect() lookup

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-23 14:25:42 -06:00
Justin Poehnelt d341de29a2 fix(setup): handle --help/-h before launching setup wizard (#592)
* fix(setup): handle --help/-h before launching setup wizard

Check for --help/-h at the top of run_setup() before any work that
requires gcloud. Previously, running 'gws auth setup --help' would
launch the full setup flow.

Fixes #280

* refactor: extract SETUP_USAGE into const per review

* refactor: replace manual setup arg parsing with clap

Use clap::Command for gws auth setup argument parsing instead of manual
while-loop parser. This gives us:
- Automatic --help/-h handling (no manual check needed)
- Proper error messages for unknown flags
- Consistent with other clap usage in the codebase
- Help text stays in sync with actual arguments

* fix: distinguish --help (clean exit) from invalid flags (error)

Change parse_setup_args return type from Result<SetupOptions, ()> to
Result<Option<SetupOptions>, GwsError> so that:
- Ok(Some(opts)) = successful parse
- Ok(None) = --help/--version displayed (clean exit, code 0)
- Err(GwsError) = invalid flags (error exit, non-zero code)

* fix: propagate e.print() IO errors instead of silently ignoring

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-23 13:08:28 -06:00
Justin Poehnelt d6794019f8 fix(auth): prevent mask_secret panic on multi-byte UTF-8 secrets (#591)
Use char-based indexing instead of byte-offset slicing in mask_secret()
to prevent panics when secrets contain multi-byte UTF-8 characters
(accented letters, emoji, CJK, etc.).

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-23 12:00:16 -06:00
Justin Poehnelt 01fb4f297c chore: fix package name in changeset 2026-03-23 11:25:35 -06:00
Sidharth Rajmohan b4d5e26424 fix(auth): propagate error when token directory cannot be created (#542)
* fix(auth): propagate errors when token directory creation/permissions fail

Previously, failures to create the token directory or set its permissions
were silently ignored using 'let _ = ...'. This could lead to confusing
errors later or security issues if permissions were left insecure.

Now properly propagates errors from:
- tokio::fs::create_dir_all()
- std::fs::set_permissions() (on Unix)

Also sanitizes the path in error messages to prevent terminal escape
sequence injection, aligned with codebase security practices.

* fix(auth): use spawn_blocking for set_permissions to avoid blocking async runtime

Following the reviewer suggestion, std::fs::set_permissions is now executed
via tokio::task::spawn_blocking to avoid potentially blocking the async
runtime thread, which can cause performance issues or deadlocks under load.

* fix(auth): use tokio::fs::set_permissions instead of spawn_blocking

Simplifies the code by using Tokio's native async set_permissions,
removing the need for manual thread spawning.
2026-03-23 11:25:03 -06:00
googleworkspace-bot e9970db26f chore: release versions (#553) v0.18.1 2026-03-18 14:56:16 -06:00
Justin Poehnelt d65578190e ci: Add CI validation for changeset package names and update style guide with new sections and AGENTS.md reference. (#552) 2026-03-18 14:42:44 -06:00
Justin Poehnelt a87037b337 feat: handle SIGTERM in +watch and +subscribe for clean shutdown (#550)
* feat: handle SIGTERM in +watch and +subscribe for clean shutdown

Add shared shutdown_signal() helper that merges SIGINT and SIGTERM
into a single future. Replace tokio::signal::ctrl_c() in both watch
and subscribe pull loops so they exit cleanly under Kubernetes,
Docker, and systemd.

On non-Unix platforms, only SIGINT (Ctrl+C) is handled.

* fix: register SIGTERM handler once via persistent background task

Use OnceLock + tokio::sync::Notify so the signal handler stays active
for the process lifetime. Eliminates the race window between loop
iterations where a SIGTERM would bypass the handler.

* fix: graceful fallback when SIGTERM registration fails

Replace expect() with match: if signal(SIGTERM) fails, log a warning
and fall back to SIGINT-only. Prevents silent task death that would
hang all shutdown_signal() callers indefinitely.

* fix: prevent spurious shutdown from ignored ctrl_c errors

Use Ok(_) pattern matching in select! branches and expect() for
standalone ctrl_c().await calls. Previously .ok() silently swallowed
errors, causing notify_waiters() to fire immediately.

* fix: handle ctrl_c error in select! to avoid losing SIGINT branch

Bind the full Result from ctrl_c() and expect() on it instead of
pattern matching Ok(_), which silently dropped the branch on Err.

---------

Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
2026-03-18 14:42:11 -06:00
googleworkspace-bot 23ffc33cf2 chore: release versions (#547) v0.18.0 2026-03-18 09:48:41 -06:00
Malo Bourgon 908cf7356f feat(gmail): auto-populate From display name + fix reply-all to own message (#530)
* feat(gmail): auto-populate From with display name from send-as settings

Fetch the user's send-as identities via /users/me/settings/sendAs to set
the From header with a display name in all mail helpers, matching Gmail
web client behavior. The gmail.modify scope already covers this endpoint.

Three resolution cases:
- No --from: use the default send-as identity (display name + email)
- --from with bare email: enrich from send-as list if the alias exists
- --from with display name: use as-is, skip the API call

For Workspace accounts where the primary address inherits its display
name from the organization directory (sendAs returns empty displayName),
falls back to the People API to fetch the profile name. This requires
the userinfo.profile scope, which is now included in the identity scopes
that auth login always requests. Degrades gracefully with a tip if the
scope hasn't been granted yet.

Introduces build_api_error, a shared helper that parses Google API JSON
error responses (extracting message, reason, and enable URL), matching
the executor's handle_error_response pattern. Used by all four Gmail API
functions. All error messages printed to stderr are sanitized via
sanitize_for_terminal.

In +send, auth uses the discovery doc scopes rather than hardcoding
gmail.modify, preserving compatibility with narrower send-only OAuth
setups. In reply-all, the profile endpoint is always called for
self-email dedup since the primary address may differ from the send-as
alias.

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

When replying-all to a message you sent, the original sender (you) was
excluded from To, leaving it empty and producing an error. Gmail web
handles this by using the original To recipients as reply targets.

Detect self-reply by checking if the original From matches the user's
primary email or send-as alias, then swap the candidate logic:
- Self-reply: To = original To, CC = original CC
- Normal reply: To = Reply-To or From, CC = original To + CC
2026-03-18 09:47:18 -06:00
Malo Bourgon 1e90380de3 fix(gmail): remove duplicate attachment arg on +send (#532)
The +send subcommand defined its own "attachment" arg in addition to the
"attach" arg already provided by common_mail_args. Since parse_attachments
reads "attach", the duplicate "attachment" arg was dead — +send --attachment
was silently accepted by clap but the value was never read.
2026-03-18 09:45:42 -06:00