← Commit history

Publish 0.5.0

John Lauer ·a07a42de74 ·4mo ago ·parent 51b8b4b
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"   ]-}+}\ No newline at end of file
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]"+  } }\ No newline at end of file
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.