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>
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)
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.
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
* 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>
* 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>
* 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.
* 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>
* 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
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.
* refactor: consolidate output hygiene into output.rs
Introduce src/output.rs that consolidates:
- sanitize_for_terminal (upgraded: now also strips bidi overrides,
zero-width chars, directional isolates, line/paragraph separators)
- reject_dangerous_chars (renamed from reject_control_chars)
- is_dangerous_unicode predicate
- colorize + stderr_supports_color (NO_COLOR + TTY detection)
- status/warn/info stderr helpers (auto-sanitize)
Migrate existing callers via re-exports from error.rs and validate.rs.
Fix watch.rs:
- Sanitize raw API error body in eprintln (was high-risk injection vector)
- Replace 3 inline ANSI escape codes with colorize() for NO_COLOR support
Fix triage.rs:
- Sanitize user --query string in no_messages_msg output
* refactor: remove re-export indirection, import directly from output
Update all 10 caller files to import sanitize_for_terminal directly
from crate::output instead of going through crate::error re-exports.
Remove pub(crate) re-exports from error.rs and validate.rs.
* fix: use char::is_control() in reject_dangerous_chars for C1 coverage
Address PR review: the manual (c as u32) < 0x20 check missed
C1 control characters (U+0080-U+009F), including CSI (U+009B) which
can inject terminal escape sequences. Using char::is_control() covers
both C0 and C1 ranges.
Add test for CSI rejection.
* fix: validate ansi_color in colorize() to prevent injection
Defense-in-depth: only emit ANSI escape codes when ansi_color
contains exclusively ASCII digits. Falls back to plain text if
an invalid color code is passed.
---------
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
* refactor(gmail): replace hand-rolled email construction with mail-builder
Replace custom MessageBuilder, RFC 2047 encoding, header sanitization,
and address encoding (including #482) with the mail-builder crate
(Stalwart Labs, 0 runtime deps). Each command builds a
mail_builder::MessageBuilder directly.
Introduce structured types throughout:
- Mailbox type (parsed display name + email) replaces raw string passing
- sanitize_control_chars strips ASCII control characters (CRLF, null,
tab, etc.) at the parse boundary — defense-in-depth for mail-builder's
structured header types, superseding sanitize_header_value,
sanitize_component, and encode_address_header from #482
- OriginalMessage fields use Option<T> instead of empty-string sentinels
- parse_original_message returns Result with validation (threadId, From,
Message-ID)
- Pre-parsed Config types (SendConfig, ForwardConfig, ReplyConfig) with
Vec<Mailbox> — parse at the boundary, not downstream
- parse_forward_args and parse_send_args return Result with --to
validation, consistent with parse_reply_args
- parse_optional_mailboxes helper normalizes Some(vec![]) to None for
optional address fields (--cc, --bcc, --from)
- Envelope types borrow from Config + OriginalMessage with lifetimes
- Message IDs stored bare (no angle brackets), parsed once at boundary
- References stored as Vec<String> instead of space-separated string
- ThreadingHeaders bundles In-Reply-To + References with debug_assert
for bare-ID convention
- Shared CLI arg builders (common_mail_args, common_reply_args)
eliminate duplicated --cc/--bcc/--html/--dry-run definitions
Additional improvements:
- finalize_message returns Result instead of panicking via .expect()
- Mailbox::parse_list filters empty-email entries (trailing comma edge
case)
- format_email_link percent-encodes mailto hrefs to prevent parameter
injection
- Forward date handling: omits Date line when absent instead of showing
empty "Date: "
- Dry-run auth: log skipped auth as diagnostic instead of silently
discarding errors
- Restore --html tips in after_help strings (gmail_quote CSS, cid:
image warnings, HTML fragment advice) lost in release PR #434
- Update execute_method call for upload_content_type parameter (#429)
Delete: MessageBuilder, encode_header_value, sanitize_header_value,
encode_address_header, sanitize_component, extract_email,
extract_display_name, split_mailbox_list, build_references.
* feat(gmail): add --from flag to +send for send-as alias support
Consistent with +reply, +reply-all, and +forward which already support
--from. Uses the same parse_optional_mailboxes path and
apply_optional_headers plumbing.
* fix: quote display names with RFC 2822 special characters in +reply
When replying to emails from corporate senders with display names like
"Anderson, Rich (CORP)" <email@adp.com>, the +reply command fails with
"Invalid To header" (400) from the Gmail API.
The root cause: encode_address_header() strips quotes from the display
name via extract_display_name(), then reconstructs the address without
re-quoting. When the display name contains RFC 2822 special characters
(commas, parentheses), the unquoted form is ambiguous — commas split
it into multiple malformed mailboxes and parentheses are interpreted
as RFC 2822 comments.
Fix: re-quote the display name when it contains any RFC 2822 special
characters, using a single-pass character iterator that preserves
already-escaped sequences and escapes bare quotes/backslashes.
Fixes#512
* feat(gmail): add --attachment flag, +read helper, and mail-builder migration
Consolidates PRs #491, #513, #517, and #502 into a single rollup:
- Migrate message construction to mail-builder crate (RFC-compliant MIME)
- Add --from flag to +send for send-as alias support
- Add --attachment flag to +send with MIME auto-detection and path validation
- Add +read helper for extracting message body/headers (text, HTML, JSON)
- Serialize support for OriginalMessage and Mailbox types
- Display name quoting handled natively by mail-builder
* chore: regenerate skills [skip ci]
* fix: use validate_safe_file_path for attachment path validation
Addresses Gemini review: validate_safe_dir_path hardcodes '--dir' in
error messages. validate_safe_file_path accepts the flag name, so errors
now correctly reference '--attachment'.
* refactor: make OriginalMessage.thread_id optional
The Gmail API does not guarantee threadId on all message resources
(e.g. drafts). Making it Option<String> prevents parse failures on
valid messages and avoids requiring thread_id in helpers like +read
that don't use it.
* fix: use canonicalized path for attachment file operations (TOCTOU)
validate_safe_file_path returns a canonicalized PathBuf. Use it for
exists/is_file checks and downstream file reads instead of the original
un-resolved path to prevent time-of-check/time-of-use races.
* feat(gmail): add --attach flag for file attachments
Add -a/--attach to +send, +reply, +reply-all, and +forward. Can be
specified multiple times for multiple attachments. MIME type is auto-
detected via mime_guess2. Closes#247.
Send via the Gmail API upload endpoint (multipart/related with
message/rfc822 media type) instead of base64-encoding into a JSON raw
field. This raises the size limit from ~5MB (metadata-only endpoint) to
35MB (upload endpoint, per discovery document).
Introduce UploadSource enum in the executor to consolidate upload_path,
upload_content_type, and upload_bytes into a single type-safe parameter.
File and Bytes variants make the two upload strategies (from disk vs.
from memory) mutually exclusive by construction.
Validates attachment paths (control characters, regular file, non-empty)
and total size (25MB raw limit, accounting for base64 expansion of
attachments within the MIME message against the 35MB API limit). Size
check uses actual bytes read to avoid TOCTOU race.
* chore: update changeset and fix integration with malob's attachment impl
Update changeset to reflect combined work. Fix thread_id type mismatches
in new tests from cherry-pick. Fix upload_path scope in main.rs. Make
reject_control_chars pub(crate) for attachment validation.
Co-authored-by: Malo Bourgon <mbourgon@gmail.com>
* chore: regenerate skills [skip ci]
* fix: restore MIME sanitization and terminal escape protection in executor
Restore two security features accidentally lost during the UploadSource
refactor:
1. resolve_upload_mime: restructure from early-returns to collect-then-
sanitize pattern — strips control chars from user-supplied MIME types
to prevent CRLF header injection.
2. Model Armor error path: restore sanitize_for_terminal on error messages
to prevent terminal escape sequence injection from API responses.
Co-authored-by: Malo Bourgon <mbourgon@gmail.com>
* chore: remove duplicate changeset from cherry-pick
gmail-attach-flag.md duplicated content already in gmail-helpers-rollup.md.
Both were marked minor, which would cause a double version bump.
* fix: add path traversal protection to attachment validation
Replace reject_control_chars with validate_safe_file_path in
parse_attachments. All file operations (metadata, read, filename
extraction, MIME detection) now use the canonicalized path, preventing
path traversal attacks (e.g. ../../.ssh/id_rsa) and closing TOCTOU gaps.
Update tests to use CWD-relative temp directories (tempdir_in("."))
since validate_safe_file_path rejects paths outside the working directory.
Co-authored-by: Malo Bourgon <mbourgon@gmail.com>
* refactor: deduplicate terminal sanitizer in read.rs
Replace the local sanitize_terminal_output function with the existing
crate::error::sanitize_for_terminal via import alias. This eliminates
code duplication and provides consistent sanitization across the codebase.
The crate-wide sanitizer also correctly strips CR (carriage return) which
can be abused for terminal overwrite attacks.
---------
Co-authored-by: Malo Bourgon <mbourgon@gmail.com>
Co-authored-by: Rich Anderson <richanderson00@gmail.com>
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
* fix: stderr/output hygiene rollup — diagnostics to stderr, colored labels, auth propagation
Component 1 (PR #485): Route triage 'no messages' and modelarmor error
bodies to stderr so stdout stays machine-readable.
Component 2 (PR #466): Add colored error[variant]: labels to stderr
on TTY, respecting NO_COLOR. Replace emoji hint with colorized text.
Component 3 (PR #446): Propagate auth errors as GwsError::Auth in
calendar, chat, docs, drive, script, sheets helpers instead of
silently proceeding unauthenticated. dry-run bypass preserved.
* fix: deduplicate accessNotConfigured stderr output
Use if/else so that accessNotConfigured errors get the specialized
hint guidance instead of redundantly printing both the generic summary
and the hint. Non-accessNotConfigured Api errors and all other variants
still get the generic error[variant]: summary line.
* test: remove misleading model_armor_post error format test
model_armor_post function. A proper integration test would require
HTTP mocking (e.g. mockito/wiremock) which is out of scope for this PR.
* refactor: deduplicate error printing else branches
Use early return in accessNotConfigured branch so the generic
eprintln! only appears once, eliminating the duplicated else blocks.
* security: sanitize error messages before printing to stderr
Add sanitize_for_terminal() to strip control characters (ANSI escape
sequences, bell, backspace, etc.) from error messages before printing
to stderr, preventing terminal escape injection from API responses.
Newlines and tabs are preserved for readability.
The function is pub(crate) so it can be reused by other modules that
print untrusted content to stderr.
* fix: sanitize all stderr error output across codebase
Apply sanitize_for_terminal() to all 16 remaining eprintln sites
that print unsanitized error strings to stderr. This prevents
terminal escape sequence injection through error messages.
Files updated:
- workflows.rs (4 sites)
- watch.rs (2 sites)
- gmail/mod.rs (3 sites)
- executor.rs (1 site)
- subscribe.rs (1 site)
- token_storage.rs (2 sites)
- credential_store.rs (2 sites)
- setup.rs (1 site)
- generate_skills.rs (1 site)
Also fixes clippy: map_err -> inspect_err where closure only logs.
---------
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
* fix(security): cap Retry-After sleep, sanitize upload mimeType, and validate --upload/--output paths
- Cap Retry-After header at 60s to prevent hostile servers from hanging the CLI
- Extract compute_retry_delay() with saturating_pow for safe exponential backoff
- Sanitize mimeType by stripping control characters to prevent MIME header injection
- Add validate_safe_file_path() for --upload and --output path validation
- Gate --upload/--output through path validation in main.rs before any I/O
Consolidates security fixes from PRs #448 and #447.
* chore: regenerate skills [skip ci]
* fix: resolve mimeType sanitization bypass and document TOCTOU caveat
- Restructure resolve_upload_mime() using or_else chain so all code paths
go through control-char stripping (early returns were bypassing it)
- Document TOCTOU limitation in validate_safe_file_path() as known caveat
* fix: use canonicalized paths for I/O and normalize .. in non-existent suffix
Address review comments:
- main.rs: use canonicalized path from validate_safe_file_path for I/O
instead of discarding it (closes TOCTOU gap)
- validate.rs: add normalize_dotdot() to resolve .. components in
non-existent suffix (prevents traversal via doesnt_exist/../../etc/passwd)
- Add regression test for non-existent prefix traversal bypass
---------
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
Co-authored-by: googleworkspace-bot <googleworkspace-bot@users.noreply.github.com>
* fix(validate): reject dangerous Unicode characters in input validation
Extend reject_control_chars() and validate_resource_name() to reject
zero-width chars (U+200B, U+200C, U+200D, U+FEFF), bidi overrides
(U+202A-U+202E), Unicode line/paragraph separators (U+2028, U+2029),
and directional isolates (U+2066-U+2069). These multi-byte codepoints
were silently passing the previous ASCII-range byte check, creating
a potential injection vector when the CLI is driven by LLM agents.
Adds 20 new tests covering all rejected categories plus documented
intentional pass-throughs (homoglyphs, overlong names).
* refactor(validate): replace slice const with is_rejected_unicode() fn using matches!
Switch from a REJECTED_UNICODE_CHARS &[char] constant + .contains() (O(M)
linear scan per character) to an is_rejected_unicode(c: char) -> bool helper
that uses the matches! macro with char ranges. This gives O(1) per character
and reads more clearly at call sites via .any(is_rejected_unicode).
* perf(validate): combine ASCII and Unicode checks into a single pass
Address review feedback: replace the two-iteration approach (one byte
scan + one char scan) in reject_control_chars with a single char loop,
and merge the separate is_control / is_rejected_unicode guards in
validate_resource_name into one any() call. Avoids iterating the input
string twice, closing the O(N*M) concern raised by the reviewer.
* feat: support google meet video conferencing in calendar +insert (#461, #419)
* feat: add unit tests for google meet and align dependencies
* chore: add changeset for google meet support
* style: cargo fmt
* chore: address PR feedback - restore ratatui 0.30.0 and clarify help text
* test: use robust assertions for google meet insert
* feat: make Google Meet requestId deterministic for idempotency
* fix: restore dependencies and make Google Meet requestId seed more robust
* fix: use JSON serialization for robust Google Meet requestId seed
* fix: improve error handling for Google Meet requestId seed serialization
* fix: ensure idempotency key seed structure matches request body
* fix: align seed_payload attendees structure with actual request body
* docs: fix Environment Variables table formatting in README
Remove blank line that was breaking the markdown table into two separate tables.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
* chore: trigger CLA recheck
---------
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
- Use tempfile::NamedTempFile for synchronous atomic_write to ensure 0600 permissions at creation.
- Use tokio::fs::OpenOptions with mode(0o600) for asynchronous atomic_write_async.
- Remove redundant set_permissions calls in oauth_config.rs and credential_store.rs.
- Ensure tempfile is a regular dependency (was dev-dependency).
- Add tests to verify file permissions on Unix systems.
Fixes#401
Add encode_address_header() that parses mailbox lists, RFC 2047
encodes only the display-name portion of non-ASCII addresses, and
leaves email addresses untouched. Applied to all 4 address headers
(To, From, Cc, Bcc) in MessageBuilder::build().
Previously, only Subject got RFC 2047 encoding while address headers
only got CRLF sanitization, causing mojibake for non-ASCII names.
Supersedes #405, #458, #469. Closes#404.
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
Replace machine-local chrono::Local and UTC epoch math with the
authenticated user's Google account timezone (Calendar Settings API).
- Add chrono-tz dependency for IANA timezone parsing
- New src/timezone.rs: resolve timezone with priority:
--timezone flag > 24h cache > Calendar API > local fallback
- calendar.rs: add --timezone/--tz flag to +agenda
- workflows.rs: fix +standup-report, +weekly-digest, +meeting-prep
- auth_commands.rs: invalidate timezone cache on logout
- Update README.md and AGENTS.md with timezone docs
Supersedes #369 and #462.
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
* fix: stream multipart uploads to avoid OOM on large files
Replace buffered file read + build_multipart_body in build_http_request
with streaming build_multipart_stream using tokio_util::io::ReaderStream.
Memory usage drops from O(file_size) to O(64 KB) regardless of upload size.
Content-Length is pre-computed from file metadata so Google APIs still
receive the correct header without buffering.
Fixes#244
* refactor: improve error messages per review feedback
- Metadata error now says 'Failed to get metadata' instead of misleading
'Failed to read upload file'
- File::open error in stream now includes the file path for easier debugging
* test: add Drive upload smoketest to CI
Uploads a small text file, verifies the response has a file ID,
then cleans up by deleting it. Validates the streaming multipart
upload path end-to-end against real Google APIs.
* fix(ci): use drive +upload helper for upload smoketest
The upload is via the +upload helper command, not files create --upload.
Also pipe stderr through tee so errors are visible in CI logs.
* revert: remove Drive upload smoketest (insufficient CI scopes)
---------
Co-authored-by: jpoehnelt-bot <jpoehnelt-bot@users.noreply.github.com>
The multipart upload media Content-Type is now resolved independently
from the metadata mimeType, enabling Drive import conversions (e.g.
Markdown → Google Docs) to work automatically.
Priority order for the media MIME type:
1. --upload-content-type flag (explicit override)
2. File extension inference (best guess for what the bytes are)
3. Metadata mimeType (backward-compat fallback)
4. application/octet-stream
Previously the metadata mimeType was reused for the media part, which
meant uploading `notes.md` with mimeType set to
`application/vnd.google-apps.document` would incorrectly label the
bytes as a Google Doc instead of text/markdown.
Made-with: Cursor