#!/usr/bin/env bash # GARC — Google Workspace Agent Runtime CLI # Main entrypoint set -euo pipefail GARC_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" # ── Python version gate ──────────────────────────────────────────────────── if ! python3 -c "import sys; assert sys.version_info >= (3,10), f'Python 3.10+ required, got {sys.version}'" 2>/dev/null; then _PY_VER=$(python3 --version 2>&1) echo "❌ GARC requires Python 3.10 or higher." >&2 echo " Detected: ${_PY_VER}" >&2 echo " Install Python 3.10+: https://www.python.org/downloads/" >&2 exit 1 fi GARC_LIB="${GARC_DIR}/lib" GARC_CONFIG="${HOME}/.garc" GARC_CONFIG_ENV="${GARC_CONFIG}/config.env" # Load base config if present if [[ -f "${GARC_CONFIG_ENV}" ]]; then # shellcheck source=/dev/null source "${GARC_CONFIG_ENV}" fi # Load profile-specific config (overrides base config) GARC_PROFILE="${GARC_PROFILE:-}" if [[ -n "${GARC_PROFILE}" ]]; then _PROFILE_ENV="${GARC_CONFIG}/profiles/${GARC_PROFILE}/config.env" if [[ -f "${_PROFILE_ENV}" ]]; then # shellcheck source=/dev/null source "${_PROFILE_ENV}" fi # Use profile-specific token if not already overridden _PROFILE_TOKEN="${GARC_CONFIG}/profiles/${GARC_PROFILE}/token.json" if [[ -f "${_PROFILE_TOKEN}" && -z "${GARC_TOKEN_FILE:-}" ]]; then export GARC_TOKEN_FILE="${_PROFILE_TOKEN}" fi fi # Defaults GARC_CACHE_DIR="${GARC_CACHE_DIR:-${GARC_CONFIG}/cache}" GARC_CACHE_TTL="${GARC_CACHE_TTL:-300}" GARC_DEFAULT_AGENT="${GARC_DEFAULT_AGENT:-main}" VERSION="0.1.0" usage() { cat < [subcommand] [options] ━━━ Core ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ init Initialize GARC workspace (config + dirs) setup [all|check|sheets|drive] Provision GWS resources automatically bootstrap [--agent ] Load disclosure chain from Google Drive status Show config and connection health ━━━ Gmail ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ gmail send --to --subject --body [--cc] [--html] gmail reply --thread-id --to --body gmail search [--max N] [--body] gmail read gmail inbox [--max N] [--unread] gmail draft --to --subject --body gmail labels gmail profile send "" --to Shorthand for gmail send ━━━ Google Calendar ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ calendar today Events for today calendar week Events for this week calendar list [--days N] [--query ] calendar create --summary --start
--end
[--attendees ...] calendar update [--summary ...] [--start ...] [--end ...] calendar delete calendar get calendar freebusy --start --end --emails email1 [...] calendar quick-add "" calendar calendars List all accessible calendars ━━━ Google Drive ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ drive list [--folder-id ] [--query ] drive search [--type doc|sheet|slide|folder|pdf] drive info drive download --file-id | --folder-id + --filename [--output ] drive upload [--folder-id ] [--convert] drive create-folder [--parent-id ] drive create-doc [--folder-id ] [--content ] drive share --email [--role reader|writer] drive move --to drive delete [--permanent] ━━━ Google Sheets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ sheets info [--sheets-id ] sheets read --range [--format table|json] sheets write --range --values '[[...]]' sheets append --sheet --values '[...]' sheets search --sheet --query [--format json] sheets clear --range ━━━ Memory ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ memory pull Sync Sheets memory → local cache memory push "" Save entry to Sheets memory memory search Search memory entries ━━━ Tasks ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ task list [--list ] [--completed] [--format json] task show task create "" [--due YYYY-MM-DD] [--notes <text>] [--list <id>] task update <task_id> [--title] [--due] [--notes] task done <task_id> Mark task complete task delete <task_id> task clear-completed Remove all completed tasks task tasklists List all task lists ━━━ People & Contacts ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ people search <query> Search personal contacts people directory <query> Search GWS org directory people list [--max N] people show <contact_id> people create --name <name> [--email] [--phone] [--company] [--title] people update <contact_id> [--name] [--email] ... people delete <contact_id> people lookup <name> Quick name → email lookup ━━━ Permission & Approval ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ auth suggest "<task>" Infer minimum OAuth scopes auth check [--profile <p>] Verify current token scopes auth login [--profile <p>] Launch OAuth2 flow auth status Show token info approve gate <task_type> Check execution gate approve list List pending approvals approve create "<task>" Create approval request approve act <id> --action approve|reject ━━━ Agents & Queue ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ agent list List registered agents agent register [--file] Register from agents.yaml agent show <id> Show agent details ingress enqueue --text "<msg>" [--source gmail|manual] [--sender <email>] ingress list [--status pending|done|failed|all] ingress next [--agent <id>] ingress run-once [--agent <id>] [--dry-run] → outputs Claude prompt ingress execute-stub --queue-id <id> → show execution plan ingress context --queue-id <id> → full Claude-readable bundle ingress delegate --queue-id <id> --to <agent> ingress handoff --queue-id <id> ingress approve/resume --queue-id <id> ingress done/fail --queue-id <id> [--note <text>] ingress verify --queue-id <id> ingress stats ━━━ Google Forms (Response Pipeline) ━━━━━━━━━━━━━━━━━━━━━━━━━━ forms list List accessible Google Forms forms responses <id> List form responses forms watch <id> --agent Poll form and auto-enqueue new responses ━━━ Daemon (Gmail Poller) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ daemon start [--interval <sec>] [--agent <id>] daemon stop daemon status daemon restart daemon poll-once Single poll cycle (foreground) daemon logs [--follow] daemon install Install macOS launchd service ━━━ Knowledge Graph ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ kg build Build KG from Drive Docs kg query "<concept>" Search knowledge graph kg show <doc_id> Show doc + links ━━━ Profiles (Multi-tenant) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ profile list List all tenant profiles profile use <name> Activate a profile (eval output) profile add <name> Create a new profile profile show [<name>] Show profile config profile remove <name> Delete a profile profile current Show active profile ━━━ System ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ heartbeat Log system state to Sheets audit list [--agent <id>] [--since YYYY-MM-DD] View audit log doctor Check Python version and dependencies Options: --help, -h Show this help --version, -v Show version --debug Enable debug output --dry-run Preview without executing --confirm Auto-confirm preview-gated operations Config: ~/.garc/config.env | Cache: ~/.garc/cache/ Docs: docs/google-cloud-setup.md | Quickstart: docs/quickstart.md EOF } # Parse global flags DEBUG=false DRY_RUN=false while [[ $# -gt 0 ]]; do case "$1" in --debug) DEBUG=true; shift ;; --dry-run) DRY_RUN=true; shift ;; --help|-h) usage; exit 0 ;; --version|-v) echo "garc ${VERSION}"; exit 0 ;; *) break ;; esac done export DEBUG DRY_RUN GARC_DIR GARC_LIB GARC_CONFIG GARC_CACHE_DIR GARC_CACHE_TTL # ── Non-blocking audit log ───────────────────────────────────────────────── # Fires in background so it never blocks or fails the main command. _garc_audit_log() { local cmd="$1" local args_str="$2" local result="${3:-ok}" local sheets_id="${GARC_SHEETS_ID:-}" local agent_id="${GARC_DEFAULT_AGENT:-}" [[ -z "${sheets_id}" ]] && return 0 [[ "${cmd}" == "audit" ]] && return 0 # don't audit audit itself ( python3 "${GARC_DIR}/scripts/garc-sheets-helper.py" audit-append \ --sheets-id "${sheets_id}" \ --agent-id "${agent_id}" \ --cmd "${cmd}" \ --args "${args_str}" \ --result "${result}" \ --user "${USER:-}" \ --timestamp "$(date -u +"%Y-%m-%dT%H:%M:%SZ")" \ 2>/dev/null ) & } COMMAND="${1:-help}" shift || true case "${COMMAND}" in init) source "${GARC_LIB}/bootstrap.sh" garc_init "$@" ;; setup) python3 "${GARC_DIR}/scripts/garc-setup.py" "${1:-all}" "${@:2}" ;; bootstrap) source "${GARC_LIB}/bootstrap.sh" garc_bootstrap "$@" ;; status) source "${GARC_LIB}/bootstrap.sh" garc_status "$@" ;; memory) source "${GARC_LIB}/memory.sh" garc_memory "$@" ;; gmail) source "${GARC_LIB}/gmail.sh" garc_gmail "$@" ;; send) # Shorthand: garc send "<msg>" --to <email> source "${GARC_LIB}/send.sh" garc_send "$@" ;; calendar|cal) source "${GARC_LIB}/calendar.sh" garc_calendar "$@" ;; drive) source "${GARC_LIB}/drive.sh" garc_drive "$@" ;; sheets) source "${GARC_LIB}/sheets.sh" garc_sheets "$@" ;; task) source "${GARC_LIB}/task.sh" garc_task "$@" ;; people|contacts) source "${GARC_LIB}/people.sh" garc_people "$@" ;; approve) source "${GARC_LIB}/approve.sh" garc_approve "$@" ;; agent) source "${GARC_LIB}/agent.sh" garc_agent "$@" ;; auth) source "${GARC_LIB}/auth.sh" garc_auth "$@" ;; heartbeat) source "${GARC_LIB}/heartbeat.sh" garc_heartbeat "$@" ;; kg) source "${GARC_LIB}/kg.sh" garc_kg "$@" ;; ingress) source "${GARC_LIB}/ingress.sh" garc_ingress "$@" ;; daemon) source "${GARC_LIB}/daemon.sh" garc_daemon "$@" ;; audit) source "${GARC_LIB}/audit.sh" _garc_audit_log "audit" "$*" garc_audit "$@" ;; profile) source "${GARC_LIB}/profile.sh" garc_profile "$@" ;; forms) source "${GARC_LIB}/forms.sh" garc_forms "$@" ;; doctor) python3 - <<'PY' import sys, importlib, subprocess print("GARC Doctor — Environment Check") print("─" * 40) # Python version pv = sys.version_info status = "✅" if pv >= (3, 10) else "❌" print(f"{status} Python {pv.major}.{pv.minor}.{pv.micro} (required: >=3.10,<3.13)") # Required packages required = [ ("googleapiclient", "google-api-python-client"), ("google.auth", "google-auth"), ("google_auth_oauthlib", "google-auth-oauthlib"), ("httplib2", "google-auth-httplib2"), ("requests", "requests"), ("yaml", "pyyaml"), ("dateutil", "python-dateutil"), ("rich", "rich"), ] print() print("Dependencies:") all_ok = True for module, pkg in required: try: importlib.import_module(module) print(f" ✅ {pkg}") except ImportError: print(f" ❌ {pkg} → pip install {pkg}") all_ok = False print() if all_ok: print("✅ All checks passed.") else: print("⚠️ Some packages missing. Run: pip install -r requirements.txt") sys.exit(1) PY ;; help|--help|-h) usage ;; *) echo "garc: unknown command '${COMMAND}'" >&2 echo "Run 'garc --help' for usage." >&2 exit 1 ;; esac