app
Adom Google
Public Made by Adomby adom
Drive your whole Google Workspace from the terminal, run by your AI. Read Gmail, write Google Docs, build Google Sheets and Google Slides, search Google Drive, manage Google Calendar and Google Tasks, and post to Google Chat, plus any Google API and a zero-setup personal Gmail.
← Commit history
Publish 0.5.0
5 files changed
+204−39
README.md+12−3@@ -170,11 +170,16 @@ adom-google api -X POST "https://tasks.googleapis.com/tasks/v1/lists/@default/ta -d '{"title":"Order solder stencil"}' ``` -**๐ฌ Chat** โ *"Tell the Hardware space the boards shipped."*+**๐ฌ Chat** โ *"Message Colby that the boards shipped."* โ posts as **you** (no webhook prefix): ```bash-adom-google api -X POST "https://chat.googleapis.com/v1/spaces/<spaceId>/messages" \- -d '{"text":"Rev C boards shipped ๐"}'+adom-google chat spaces # find your spaces / space ids+adom-google chat send --to [email protected] "Rev C boards shipped ๐" # email auto-resolves the DM+adom-google chat send --to spaces/AAQAxxxx "Standup in 5" # or a space id+adom-google chat send --to spaces/AAQAxxxx @release-notes.md # @file / '-' = stdin+adom-google chat read --from [email protected] --limit 10 # recent messages ```+> `chat send` posts as your **own Google identity** (clean, no prefix). That's different from+> **`adom-gchat`**, which posts via an **org webhook** attributed as the bot ("on behalf of โฆ"). **๐ Drive** โ *"Find every Gerber zip, then upload my new one."* ```bash@@ -233,6 +238,10 @@ adom-google contacts list | search <query> # Gmail (read) โ attachment bytes + .ics invites the connectors can't reach adom-google gmail attachments <messageId> | read <messageId> <attId> | ics <messageId> +# Google Chat โ post/read as YOURSELF (no webhook "(on behalf of)" prefix)+adom-google chat spaces | dm <email> | read --from <space|email>+adom-google chat send --to <space-id|email> "<text>|-|@file" [--thread <key>]+ adom-google status # what's authorized (never prints secrets) ```
bin/adom-google+130@@ -466,6 +466,122 @@ def cmd_contacts_search(a): print(f" resourceName: {p.get('resourceName','')}") +# ---------------------------------------------------------------- Google Chat (as the user)+# Posts as the SIGNED-IN user (their own Google identity) โ no attribution prefix. This is+# distinct from adom-gchat, which posts via an org webhook and prefixes "(on behalf of โฆ)".+CHAT_API = "https://chat.googleapis.com/v1"+++def _chat(cfg, path, method="GET", body=None, params=None, allow_404=False):+ url = path if path.startswith("http") else f"{CHAT_API}/{path.lstrip('/')}"+ headers = {"Authorization": f"Bearer {_access_token(cfg)}"}+ data = None+ if body is not None:+ data = json.dumps(body)+ headers["Content-Type"] = "application/json"+ r = requests.request(method, url, headers=headers, params=params, data=data, timeout=60)+ if r.status_code in (400, 404) and allow_404:+ return {} # caller prints a friendly "no DM/space" hint+ if r.status_code >= 400:+ sys.exit(f"Chat API error ({r.status_code}) {method} {url}:\n{r.text}")+ return r.json() if r.content else {}+++def _find_dm(cfg, email):+ """Resolve the 1:1 DM space with a person by email โ the Space dict (name, spaceUri)."""+ return _chat(cfg, "spaces:findDirectMessage", params={"name": f"users/{email}"}, allow_404=True)+++def _space_name(v):+ """Normalize a space id to a 'spaces/XXX' resource name (caller handles emails)."""+ return v if v.startswith("spaces/") else f"spaces/{v}"+++def _msg_text(text):+ """Message body source: '-' (stdin), '@file', or a literal string."""+ if text == "-":+ return sys.stdin.read()+ if text.startswith("@"):+ return open(text[1:], encoding="utf-8").read()+ return text+++def cmd_chat_spaces(a):+ cfg = _load()+ spaces, page = [], None+ while True:+ params = {"pageSize": 1000}+ if page:+ params["pageToken"] = page+ j = _chat(cfg, "spaces", params=params)+ spaces += j.get("spaces", [])+ page = j.get("nextPageToken")+ if not page:+ break+ if a.json:+ print(json.dumps(spaces, indent=2, ensure_ascii=False))+ return+ for s in spaces:+ print(f"{s.get('name')} ยท {s.get('spaceType','?')} ยท {s.get('displayName') or '(direct message)'}")+ sys.stderr.write(f"({len(spaces)} spaces โ pass a spaces/XXX id OR an email to chat send/read)\n")+++def cmd_chat_dm(a):+ cfg = _load()+ sp = _find_dm(cfg, a.email)+ name = sp.get("name")+ if not name:+ sys.exit(f"no DM/space found for {a.email} โ run 'adom-google chat spaces', or check the email")+ if a.json:+ print(json.dumps(sp, indent=2, ensure_ascii=False))+ return+ print(name + (f" ยท {sp.get('spaceUri')}" if sp.get("spaceUri") else ""))+++def cmd_chat_send(a):+ cfg = _load()+ if "@" in a.to:+ sp = _find_dm(cfg, a.to)+ space = sp.get("name")+ if not space:+ sys.exit(f"no DM/space found for {a.to} โ run 'adom-google chat spaces', or check the email")+ sys.stderr.write(f"(auto-resolved DM {space} for {a.to})\n")+ label = a.to+ else:+ space = _space_name(a.to)+ sp = _chat(cfg, space) # validate + fetch spaceUri / displayName+ label = sp.get("displayName") or space+ body = {"text": _msg_text(a.text)}+ params = None+ if a.thread:+ body["thread"] = {"threadKey": a.thread}+ params = {"messageReplyOption": "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD"}+ msg = _chat(cfg, f"{space}/messages", method="POST", body=body, params=params)+ uri = sp.get("spaceUri", "")+ who = (msg.get("sender") or {}).get("displayName") or "you"+ print(msg.get("name", "") + (f" ยท {uri}" if uri else ""))+ sys.stderr.write(f"โ posted to {label} as {who}" + (f" ยท {uri}" if uri else "") + "\n")+++def cmd_chat_read(a):+ cfg = _load()+ if "@" in a.frm:+ sp = _find_dm(cfg, a.frm)+ space = sp.get("name")+ if not space:+ sys.exit(f"no DM/space found for {a.frm} โ run 'adom-google chat spaces', or check the email")+ else:+ space = _space_name(a.frm)+ j = _chat(cfg, f"{space}/messages", params={"pageSize": a.limit, "orderBy": "createTime desc"})+ msgs = j.get("messages", [])+ if a.json:+ print(json.dumps(msgs, indent=2, ensure_ascii=False))+ return+ for m in reversed(msgs): # show oldest โ newest of the recent batch+ who = (m.get("sender") or {}).get("displayName") or (m.get("sender") or {}).get("name", "?")+ print(f"{who}: {m.get('text','')}")++ # ---------------------------------------------------------------- generic API passthrough def cmd_api(a): """Sign ANY Google REST/Discovery call with the stored token โ the 'whole board'.@@ -812,6 +928,14 @@ CONTACTS (Google People API; read + write): adom-google contacts list [--limit N] [--json] adom-google contacts search <query> [-v] [--json] +CHAT (Google Chat โ posts as YOU, your own identity, no attribution prefix;+ adom-gchat is the other one โ it posts via an org webhook with "(on behalf of)"):+ adom-google chat spaces [--json] list your spaces (id ยท type ยท name)+ adom-google chat dm <email> [--json] resolve the 1:1 DM space with a person+ adom-google chat send --to <space-id|email> "<text>" post a message (email auto-resolves DM)+ "<text>" may be '-' (stdin) or '@file'; --thread <key> replies in a thread+ adom-google chat read --from <space-id|email> [--limit N] [--json] recent messages+ API (generic passthrough โ hit ANY Google API with your token; the "whole board"): adom-google api <url> [-X METHOD] [-d '<json>'|@file] [-q k=v ...] [--raw] e.g. adom-google api https://sheets.googleapis.com/v4/spreadsheets/<id>/values/A1:C5@@ -857,6 +981,12 @@ def main(): x = cs.add_parser("list"); x.add_argument("--limit", type=int, default=50); x.add_argument("--json", action="store_true"); x.set_defaults(fn=cmd_contacts_list) x = cs.add_parser("search"); x.add_argument("query"); x.add_argument("-v", "--verbose", action="store_true"); x.add_argument("--json", action="store_true"); x.set_defaults(fn=cmd_contacts_search) + ch = sub.add_parser("chat"); chx = ch.add_subparsers(dest="sub")+ x = chx.add_parser("spaces"); x.add_argument("--json", action="store_true"); x.set_defaults(fn=cmd_chat_spaces)+ x = chx.add_parser("dm"); x.add_argument("email"); x.add_argument("--json", action="store_true"); x.set_defaults(fn=cmd_chat_dm)+ x = chx.add_parser("send"); x.add_argument("--to", required=True, help="spaces/XXX id OR an email (auto-resolves the DM)"); x.add_argument("text", help="message text, or '-' for stdin, or '@file'"); x.add_argument("--thread", help="threadKey for a threaded reply"); x.set_defaults(fn=cmd_chat_send)+ x = chx.add_parser("read"); x.add_argument("--from", dest="frm", required=True, help="spaces/XXX id OR an email"); x.add_argument("--limit", type=int, default=20); x.add_argument("--json", action="store_true"); x.set_defaults(fn=cmd_chat_read)+ # Generic passthrough โ hit ANY Google API with the stored token. x = sub.add_parser("api") x.add_argument("url", help="full URL or host/path (https:// assumed)")
package.json+3−3@@ -1,9 +1,9 @@ { "slug": "adom-google",- "version": "0.4.1",+ "version": "0.5.0", "type": "app", "license": "MIT",- "description": "Drive your whole Google Workspace from the terminal โ Gmail, Contacts, Drive, Sheets, Docs, Slides, Calendar, Tasks, Google Chat, plus a generic `api` passthrough to hit ANY Google API. Open source, ships with NO OAuth provider baked in: point it at your own token-broker gateway (`provider set`), buy a managed one, or โ Adom staff โ install the private adom-google-adom provider. One-click OAuth, no client secret on your machine. First run asks safe mode (no delete) vs full mode (incl. delete).",+ "description": "Drive your whole Google Workspace from the terminal \u2014 Gmail, Contacts, Drive, Sheets, Docs, Slides, Calendar, Tasks, Google Chat, plus a generic `api` passthrough to hit ANY Google API. Open source, ships with NO OAuth provider baked in: point it at your own token-broker gateway (`provider set`), buy a managed one, or \u2014 Adom staff \u2014 install the private adom-google-adom provider. One-click OAuth, no client secret on your machine. First run asks safe mode (no delete) vs full mode (incl. delete).", "keywords": [ "adom-google", "google-workspace",@@ -50,4 +50,4 @@ "uninstall.sh", "package.json" ]-}+}
page.json+45−32@@ -1,22 +1,22 @@ {- "schema_version": 1,- "type": "app", "slug": "adom-google",- "title": "adom-google",- "brief": "Drive your whole Google Workspace from the terminal โ Gmail, Drive, Sheets, Docs, Slides, Calendar, Tasks, Chat + any Google API.",- "version": "0.4.1",- "visibility": "public",- "install": {- "binary_name": "adom-google",- "install_dir": "",- "install_hint": "",- "version_cmd": ""- },- "author": {- "id": "6738b00a4dc87b8adbb0a5a3",- "name": "John Lauer",- "email": "[email protected]"- },+ "version": "0.5.0",+ "type": "app",+ "license": "MIT",+ "description": "Drive your whole Google Workspace from the terminal โ Gmail, Contacts, Drive, Sheets, Docs, Slides, Calendar, Tasks, Google Chat, plus a generic `api` passthrough to hit ANY Google API. Open source, ships with NO OAuth provider baked in: point it at your own token-broker gateway (`provider set`), buy a managed one, or โ Adom staff โ install the private adom-google-adom provider. One-click OAuth, no client secret on your machine. First run asks safe mode (no delete) vs full mode (incl. delete).",+ "keywords": [+ "adom-google",+ "google-workspace",+ "oauth",+ "gmail",+ "google-sheets",+ "google-docs",+ "google-slides",+ "google-calendar",+ "google-chat",+ "cli",+ "ai-agent"+ ], "tags": [ "google-workspace", "oauth",@@ -26,21 +26,34 @@ "open-source", "mit" ],- "license": "MIT",- "hero": {- "path": "docs/hero.png",- "type": "image"+ "install": {+ "binary_name": "adom-google" },- "sample_prompts": [],- "discovery_triggers": [],- "discovery_pitch": null,- "metadata": {- "hero_path": "docs/hero.png",- "hero_type": "image"+ "dependencies": {},+ "scripts": {+ "install": "./install.sh",+ "uninstall": "./uninstall.sh" },- "created_at": "2026-06-16T22:28:58.933Z",- "updated_at": "2026-06-17T15:55:18.514Z",- "skills": [],- "hero_path": "docs/hero.png",- "hero_type": "image"+ "files": [+ "bin/adom-google",+ "bin/adom-gmail",+ "skills/adom-google/SKILL.md",+ "README.md",+ "LICENSE",+ "MIGRATION.md",+ "provider.example.json",+ "docs/hero.png",+ "docs/enable-apis.png",+ "docs/chat-reads-email.png",+ "screenshots/hero.png",+ "install.sh",+ "uninstall.sh",+ "package.json"+ ],+ "visibility": "public",+ "org": "adom",+ "author": {+ "name": "John Lauer",+ "email": "[email protected]"+ } }
skills/adom-google/SKILL.md+14−1@@ -1,6 +1,6 @@ --- name: adom-google-description: Programmatic read+write access to a user's WHOLE Google Workspace โ Gmail, Contacts, Drive, Sheets, Docs, Slides, Calendar, Tasks, Google Chat โ plus a generic `api` passthrough to hit ANY Google API. CLI + OAuth only (no browser automation). Public/open-source; used daily by the Adom team. Auth is the account-specific part: Adom employees use it out of the box; other orgs either self-host their own OAuth gateway or email [email protected] for a managed OAuth container (~$5/mo). On FIRST-RUN SETUP you MUST ask the user to pick safe mode (no delete) vs full mode (incl. delete) before authorizing. Use when setting up/installing adom-google, authorizing Google, reading Gmail attachments/.ics, creating/listing/searching Contacts, editing Sheets/Docs/Slides, Drive files, Calendar events, posting to Google Chat, or any Google Workspace automation. Trigger words โ adom-google, install adom-google, set up adom-google, configure adom-google, google workspace cli, edit google sheet/doc/slides, drive upload, calendar event, post to google chat, gmail attachment, google contacts, people api, safe mode vs full mode, google oauth gateway broker, google api passthrough, self-host oauth, managed oauth container.+description: Programmatic read+write access to a user's WHOLE Google Workspace โ Gmail, Contacts, Drive, Sheets, Docs, Slides, Calendar, Tasks, Google Chat โ plus a generic `api` passthrough to hit ANY Google API. CLI + OAuth only (no browser automation). Public/open-source; used daily by the Adom team. Auth is the account-specific part: Adom employees use it out of the box; other orgs either self-host their own OAuth gateway or email [email protected] for a managed OAuth container (~$5/mo). On FIRST-RUN SETUP you MUST ask the user to pick safe mode (no delete) vs full mode (incl. delete) before authorizing. Use when setting up/installing adom-google, authorizing Google, reading Gmail attachments/.ics, creating/listing/searching Contacts, editing Sheets/Docs/Slides, Drive files, Calendar events, posting to Google Chat, or any Google Workspace automation. Trigger words โ adom-google, install adom-google, set up adom-google, configure adom-google, google workspace cli, edit google sheet/doc/slides, drive upload, calendar event, post to google chat, post to chat as me, message colby, send a chat, dm <person>, gmail attachment, google contacts, people api, safe mode vs full mode, google oauth gateway broker, google api passthrough, self-host oauth, managed oauth container. --- # adom-google@@ -152,9 +152,22 @@ adom-google contacts create --name "First Last" [--org O] [--title T] \ adom-google contacts list [--limit N] [--json] adom-google contacts search <query> [-v] [--json] +# Google Chat โ posts as the SIGNED-IN USER (their own identity, NO attribution prefix).+# This is distinct from `adom-gchat`, which posts via an org webhook with "(on behalf of โฆ)".+adom-google chat spaces [--json] # list your spaces (id ยท type ยท name)+adom-google chat dm <email> [--json] # resolve the 1:1 DM space with a person+adom-google chat send --to <space-id|email> "<text>" # post; email auto-resolves the DM+# "<text>" may be '-' (stdin) or '@file'; add --thread <key> to reply in a thread+adom-google chat read --from <space-id|email> [--limit N] [--json] # recent messages+ adom-google status # mode (safe/full) + what's authorized (never prints secrets) ``` +**`chat` vs `adom-gchat`:** use `adom-google chat send` to post as **yourself** (your Google+identity, clean โ no prefix). Use `adom-gchat` only when you want an **org webhook** post+attributed as the bot ("Kel (on behalf of john)"). The `chat.messages` + `chat.spaces.readonly`+scopes are in both safe and full mode, so this works out of the box once authorized.+ We use Google Contacts to store **service identities** (portal/account metadata for an API vendor), e.g. the JPMorgan Payments developer portal. Keep secrets (client secrets, portal passwords) in a password manager โ **never** in a contact's notes.