#!/usr/bin/env python3 """Google Workspace API CLI for Hermes Agent. Thin wrapper that delegates to gws (googleworkspace/cli) via gws_bridge.py. Maintains the same CLI interface for backward compatibility with Hermes skills. Usage: python google_api.py gmail search "is:unread" [--max 10] python google_api.py gmail get MESSAGE_ID python google_api.py gmail send --to user@example.com --subject "Hi" --body "Hello" python google_api.py gmail reply MESSAGE_ID --body "Thanks" python google_api.py calendar list [--start DATE] [--end DATE] [--calendar primary] python google_api.py calendar create --summary "Meeting" --start DATETIME --end DATETIME python google_api.py calendar delete EVENT_ID python google_api.py drive search "budget report" [--max 10] python google_api.py contacts list [--max 20] python google_api.py sheets get SHEET_ID RANGE python google_api.py sheets update SHEET_ID RANGE --values '[[...]]' python google_api.py sheets append SHEET_ID RANGE --values '[[...]]' python google_api.py docs get DOC_ID """ import argparse import json import os import subprocess import sys from pathlib import Path BRIDGE = Path(__file__).parent / "gws_bridge.py" PYTHON = sys.executable def gws(*args: str) -> None: """Call gws via the bridge and exit with its return code.""" result = subprocess.run( [PYTHON, str(BRIDGE)] + list(args), env={**os.environ, "HERMES_HOME": os.environ.get("HERMES_HOME", str(Path.home() / ".hermes"))}, ) sys.exit(result.returncode) # -- Gmail -- def gmail_search(args): cmd = ["gmail", "+triage", "--query", args.query, "--max", str(args.max), "--format", "json"] gws(*cmd) def gmail_get(args): gws("gmail", "+read", "--id", args.message_id, "--headers", "--format", "json") def gmail_send(args): cmd = ["gmail", "+send", "--to", args.to, "--subject", args.subject, "--body", args.body, "--format", "json"] if args.cc: cmd += ["--cc", args.cc] if args.html: cmd.append("--html") gws(*cmd) def gmail_reply(args): gws("gmail", "+reply", "--message-id", args.message_id, "--body", args.body, "--format", "json") def gmail_labels(args): gws("gmail", "users", "labels", "list", "--params", json.dumps({"userId": "me"}), "--format", "json") def gmail_modify(args): body = {} if args.add_labels: body["addLabelIds"] = args.add_labels.split(",") if args.remove_labels: body["removeLabelIds"] = args.remove_labels.split(",") gws( "gmail", "users", "messages", "modify", "--params", json.dumps({"userId": "me", "id": args.message_id}), "--json", json.dumps(body), "--format", "json", ) # -- Calendar -- def calendar_list(args): if args.start or args.end: # Specific date range — use raw Calendar API for precise timeMin/timeMax from datetime import datetime, timedelta, timezone as tz now = datetime.now(tz.utc) time_min = args.start or now.isoformat() time_max = args.end or (now + timedelta(days=7)).isoformat() gws( "calendar", "events", "list", "--params", json.dumps({ "calendarId": args.calendar, "timeMin": time_min, "timeMax": time_max, "maxResults": args.max, "singleEvents": True, "orderBy": "startTime", }), "--format", "json", ) else: # No date range — use +agenda helper (defaults to 7 days) cmd = ["calendar", "+agenda", "--days", "7", "--format", "json"] if args.calendar != "primary": cmd += ["--calendar", args.calendar] gws(*cmd) def calendar_create(args): cmd = [ "calendar", "+insert", "--summary", args.summary, "--start", args.start, "--end", args.end, "--format", "json", ] if args.location: cmd += ["--location", args.location] if args.description: cmd += ["--description", args.description] if args.attendees: for email in args.attendees.split(","): cmd += ["--attendee", email.strip()] if args.calendar != "primary": cmd += ["--calendar", args.calendar] gws(*cmd) def calendar_delete(args): gws( "calendar", "events", "delete", "--params", json.dumps({"calendarId": args.calendar, "eventId": args.event_id}), "--format", "json", ) # -- Drive -- def drive_search(args): query = args.query if args.raw_query else f"fullText contains '{args.query}'" gws( "drive", "files", "list", "--params", json.dumps({ "q": query, "pageSize": args.max, "fields": "files(id,name,mimeType,modifiedTime,webViewLink)", }), "--format", "json", ) # -- Contacts -- def contacts_list(args): gws( "people", "people", "connections", "list", "--params", json.dumps({ "resourceName": "people/me", "pageSize": args.max, "personFields": "names,emailAddresses,phoneNumbers", }), "--format", "json", ) # -- Sheets -- def sheets_get(args): gws( "sheets", "+read", "--spreadsheet", args.sheet_id, "--range", args.range, "--format", "json", ) def sheets_update(args): values = json.loads(args.values) gws( "sheets", "spreadsheets", "values", "update", "--params", json.dumps({ "spreadsheetId": args.sheet_id, "range": args.range, "valueInputOption": "USER_ENTERED", }), "--json", json.dumps({"values": values}), "--format", "json", ) def sheets_append(args): values = json.loads(args.values) gws( "sheets", "+append", "--spreadsheet", args.sheet_id, "--json-values", json.dumps(values), "--format", "json", ) # -- Docs -- def docs_get(args): gws( "docs", "documents", "get", "--params", json.dumps({"documentId": args.doc_id}), "--format", "json", ) # -- CLI parser (backward-compatible interface) -- def main(): parser = argparse.ArgumentParser(description="Google Workspace API for Hermes Agent (gws backend)") sub = parser.add_subparsers(dest="service", required=True) # --- Gmail --- gmail = sub.add_parser("gmail") gmail_sub = gmail.add_subparsers(dest="action", required=True) p = gmail_sub.add_parser("search") p.add_argument("query", help="Gmail search query (e.g. 'is:unread')") p.add_argument("--max", type=int, default=10) p.set_defaults(func=gmail_search) p = gmail_sub.add_parser("get") p.add_argument("message_id") p.set_defaults(func=gmail_get) p = gmail_sub.add_parser("send") p.add_argument("--to", required=True) p.add_argument("--subject", required=True) p.add_argument("--body", required=True) p.add_argument("--cc", default="") p.add_argument("--html", action="store_true", help="Send body as HTML") p.add_argument("--thread-id", default="", help="Thread ID (unused with gws, kept for compat)") p.set_defaults(func=gmail_send) p = gmail_sub.add_parser("reply") p.add_argument("message_id", help="Message ID to reply to") p.add_argument("--body", required=True) p.set_defaults(func=gmail_reply) p = gmail_sub.add_parser("labels") p.set_defaults(func=gmail_labels) p = gmail_sub.add_parser("modify") p.add_argument("message_id") p.add_argument("--add-labels", default="", help="Comma-separated label IDs to add") p.add_argument("--remove-labels", default="", help="Comma-separated label IDs to remove") p.set_defaults(func=gmail_modify) # --- Calendar --- cal = sub.add_parser("calendar") cal_sub = cal.add_subparsers(dest="action", required=True) p = cal_sub.add_parser("list") p.add_argument("--start", default="", help="Start time (ISO 8601)") p.add_argument("--end", default="", help="End time (ISO 8601)") p.add_argument("--max", type=int, default=25) p.add_argument("--calendar", default="primary") p.set_defaults(func=calendar_list) p = cal_sub.add_parser("create") p.add_argument("--summary", required=True) p.add_argument("--start", required=True, help="Start (ISO 8601 with timezone)") p.add_argument("--end", required=True, help="End (ISO 8601 with timezone)") p.add_argument("--location", default="") p.add_argument("--description", default="") p.add_argument("--attendees", default="", help="Comma-separated email addresses") p.add_argument("--calendar", default="primary") p.set_defaults(func=calendar_create) p = cal_sub.add_parser("delete") p.add_argument("event_id") p.add_argument("--calendar", default="primary") p.set_defaults(func=calendar_delete) # --- Drive --- drv = sub.add_parser("drive") drv_sub = drv.add_subparsers(dest="action", required=True) p = drv_sub.add_parser("search") p.add_argument("query") p.add_argument("--max", type=int, default=10) p.add_argument("--raw-query", action="store_true", help="Use query as raw Drive API query") p.set_defaults(func=drive_search) # --- Contacts --- con = sub.add_parser("contacts") con_sub = con.add_subparsers(dest="action", required=True) p = con_sub.add_parser("list") p.add_argument("--max", type=int, default=50) p.set_defaults(func=contacts_list) # --- Sheets --- sh = sub.add_parser("sheets") sh_sub = sh.add_subparsers(dest="action", required=True) p = sh_sub.add_parser("get") p.add_argument("sheet_id") p.add_argument("range") p.set_defaults(func=sheets_get) p = sh_sub.add_parser("update") p.add_argument("sheet_id") p.add_argument("range") p.add_argument("--values", required=True, help="JSON array of arrays") p.set_defaults(func=sheets_update) p = sh_sub.add_parser("append") p.add_argument("sheet_id") p.add_argument("range") p.add_argument("--values", required=True, help="JSON array of arrays") p.set_defaults(func=sheets_append) # --- Docs --- docs = sub.add_parser("docs") docs_sub = docs.add_subparsers(dest="action", required=True) p = docs_sub.add_parser("get") p.add_argument("doc_id") p.set_defaults(func=docs_get) args = parser.parse_args() args.func(args) if __name__ == "__main__": main()