P0 fixes: - agent register: upsert by agent_id (no duplicate rows) - daemon poll-once: extract _gmail_poll_cycle, run synchronously - garc_core.py: suppress urllib3/googleapiclient DeprecationWarnings P1 fixes: - OAuth: detect RefreshError → delete stale token → re-auth flow - OAuth: scope coverage check before returning valid creds - ingress: add stale-reset subcommand (reset in_progress > N min) - sheets: trim-sheet / clean-all — deleteDimension for empty rows - approval gate: send Gmail notification to GARC_APPROVAL_EMAIL P2 additions: - Google Chat: garc-chat-helper.py + garc send chat subcommands - Service Account: garc auth service-account verify + DWD support - Audit log: Sheets audit tab + garc audit list + bin/garc async hook - garc auth revoke: POST /revoke + delete token file - kg: pagination fix, shell injection fix, garc-kg-query.py - docs: _doc_insert_text / append_doc / garc drive append-doc P3 additions: - Multi-tenant: lib/profile.sh (list/use/add/show/remove/current) bin/garc: auto-load profile config.env and token.json - Google Forms pipeline: garc-forms-helper.py + lib/forms.sh garc forms list/responses/watch - systemd: _daemon_install_service OS-detect → launchd or systemd units - Python version gate (>=3.10) in bin/garc + pyproject.toml - garc doctor command for environment diagnostics Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
384 lines
14 KiB
Bash
Executable file
384 lines
14 KiB
Bash
Executable file
#!/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 <<EOF
|
|
GARC v${VERSION} — Google Workspace Agent Runtime CLI
|
|
|
|
Usage: garc <command> [subcommand] [options]
|
|
|
|
━━━ Core ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
init Initialize GARC workspace (config + dirs)
|
|
setup [all|check|sheets|drive] Provision GWS resources automatically
|
|
bootstrap [--agent <id>] Load disclosure chain from Google Drive
|
|
status Show config and connection health
|
|
|
|
━━━ Gmail ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
gmail send --to <email> --subject <text> --body <text> [--cc] [--html]
|
|
gmail reply --thread-id <id> --to <email> --body <text>
|
|
gmail search <query> [--max N] [--body]
|
|
gmail read <message_id>
|
|
gmail inbox [--max N] [--unread]
|
|
gmail draft --to <email> --subject <text> --body <text>
|
|
gmail labels
|
|
gmail profile
|
|
send "<msg>" --to <email> Shorthand for gmail send
|
|
|
|
━━━ Google Calendar ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
calendar today Events for today
|
|
calendar week Events for this week
|
|
calendar list [--days N] [--query <text>]
|
|
calendar create --summary <text> --start <dt> --end <dt> [--attendees ...]
|
|
calendar update <event_id> [--summary ...] [--start ...] [--end ...]
|
|
calendar delete <event_id>
|
|
calendar get <event_id>
|
|
calendar freebusy --start <date> --end <date> --emails email1 [...]
|
|
calendar quick-add "<natural language>"
|
|
calendar calendars List all accessible calendars
|
|
|
|
━━━ Google Drive ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
drive list [--folder-id <id>] [--query <name>]
|
|
drive search <query> [--type doc|sheet|slide|folder|pdf]
|
|
drive info <file_id>
|
|
drive download --file-id <id> | --folder-id + --filename [--output <path>]
|
|
drive upload <local_path> [--folder-id <id>] [--convert]
|
|
drive create-folder <name> [--parent-id <id>]
|
|
drive create-doc <name> [--folder-id <id>] [--content <text>]
|
|
drive share <file_id> --email <email> [--role reader|writer]
|
|
drive move <file_id> --to <folder_id>
|
|
drive delete <file_id> [--permanent]
|
|
|
|
━━━ Google Sheets ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
sheets info [--sheets-id <id>]
|
|
sheets read --range <A1:Z100> [--format table|json]
|
|
sheets write --range <A1> --values '[[...]]'
|
|
sheets append --sheet <name> --values '[...]'
|
|
sheets search --sheet <name> --query <text> [--format json]
|
|
sheets clear --range <range>
|
|
|
|
━━━ Memory ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
memory pull Sync Sheets memory → local cache
|
|
memory push "<entry>" Save entry to Sheets memory
|
|
memory search <query> Search memory entries
|
|
|
|
━━━ Tasks ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
task list [--list <id>] [--completed] [--format json]
|
|
task show <task_id>
|
|
task create "<title>" [--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
|