feat(googlechat): add Google Chat provider

Add Google Chat as the 8th messaging platform, using Pub/Sub for inbound
messages and the Google Chat API for outbound. This enables messaging
with Clawbot via Google Workspace's Google Chat.

Features:
- Pub/Sub-based message monitoring (firewall-friendly)
- Direct message and space support
- Thread support
- Media via cards
- DM/space policy configuration
- Multi-account support

Requires:
- Google Workspace account (not @gmail.com)
- Google Cloud project with Chat API and Pub/Sub enabled
- Service account credentials

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Justin Massa 2026-01-11 19:14:21 -06:00
parent 74526645eb
commit 8bf4401bd8
15 changed files with 1249 additions and 7 deletions

View File

@ -127,6 +127,7 @@
"dependencies": {
"@buape/carbon": "0.0.0-beta-20260110172854",
"@clack/prompts": "^0.11.0",
"@google-cloud/pubsub": "^5.2.1",
"@grammyjs/runner": "^2.0.3",
"@grammyjs/transformer-throttler": "^1.2.1",
"@homebridge/ciao": "^1.3.4",
@ -153,6 +154,7 @@
"dotenv": "^17.2.3",
"express": "^5.2.1",
"file-type": "^21.3.0",
"googleapis": "^170.0.0",
"grammy": "^1.39.2",
"jiti": "^2.6.1",
"json5": "^2.2.3",

360
pnpm-lock.yaml generated
View File

@ -22,6 +22,9 @@ importers:
'@clack/prompts':
specifier: ^0.11.0
version: 0.11.0
'@google-cloud/pubsub':
specifier: ^5.2.1
version: 5.2.1
'@grammyjs/runner':
specifier: ^2.0.3
version: 2.0.3(grammy@1.39.2)
@ -100,6 +103,9 @@ importers:
file-type:
specifier: ^21.3.0
version: 21.3.0
googleapis:
specifier: ^170.0.0
version: 170.0.0
grammy:
specifier: ^1.39.2
version: 1.39.2
@ -220,7 +226,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^4.0.16
version: 4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
wireit:
specifier: ^0.14.12
version: 0.14.12
@ -251,7 +257,7 @@ importers:
version: 5.9.3
vitest:
specifier: 4.0.16
version: 4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
version: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
packages:
@ -566,6 +572,26 @@ packages:
'@glideapps/ts-necessities@2.2.3':
resolution: {integrity: sha512-gXi0awOZLHk3TbW55GZLCPP6O+y/b5X1pBXKBVckFONSwF1z1E5ND2BGJsghQFah+pW7pkkyFb2VhUQI2qhL5w==}
'@google-cloud/paginator@6.0.0':
resolution: {integrity: sha512-g5nmMnzC+94kBxOKkLGpK1ikvolTFCC3s2qtE4F+1EuArcJ7HHC23RDQVt3Ra3CqpUYZ+oXNKZ8n5Cn5yug8DA==}
engines: {node: '>=18'}
'@google-cloud/precise-date@5.0.0':
resolution: {integrity: sha512-9h0Gvw92EvPdE8AK8AgZPbMnH5ftDyPtKm7/KUfcJVaPEPjwGDsJd1QV0H8esBDV4II41R/2lDWH1epBqIoKUw==}
engines: {node: '>=18'}
'@google-cloud/projectify@5.0.0':
resolution: {integrity: sha512-XXQLaIcLrOAMWvRrzz+mlUGtN6vlVNja3XQbMqRi/V7XJTAVwib3VcKd7oRwyZPkp7rBVlHGcaqdyGRrcnkhlA==}
engines: {node: '>=18'}
'@google-cloud/promisify@5.0.0':
resolution: {integrity: sha512-N8qS6dlORGHwk7WjGXKOSsLjIjNINCPicsOX6gyyLiYk7mq3MtII96NZ9N2ahwA2vnkLmZODOIH9rlNniYWvCQ==}
engines: {node: '>=18'}
'@google-cloud/pubsub@5.2.1':
resolution: {integrity: sha512-AGuH/dN8059pLg1A4vMLFQfZEuIwLy1Ykmh8b4XmBM6eT93mAgmrV2y2JMq6dRWTIaBpYGFA7NjRviNL/W/fmQ==}
engines: {node: '>=18'}
'@google/genai@1.34.0':
resolution: {integrity: sha512-vu53UMPvjmb7PGzlYu6Tzxso8Dfhn+a7eQFaS2uNemVtDZKwzSpJ5+ikqBbXplF7RGB1STcVDqCkPvquiwb2sw==}
engines: {node: '>=20.0.0'}
@ -590,6 +616,15 @@ packages:
'@grammyjs/types@3.23.0':
resolution: {integrity: sha512-D3jQ4UWERPsyR3op/YFudMMIPNTU47vy7L51uO9/73tMELmjO/+LX5N36/Y0CG5IQfIsz43MxiHI5rgsK0/k+g==}
'@grpc/grpc-js@1.14.3':
resolution: {integrity: sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==}
engines: {node: '>=12.10.0'}
'@grpc/proto-loader@0.8.0':
resolution: {integrity: sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==}
engines: {node: '>=6'}
hasBin: true
'@hapi/boom@9.1.4':
resolution: {integrity: sha512-Ls1oH8jaN1vNsqcaHVYJrKmgMcKsC1wcp8bujvXrHaAqD2iDYq3HoOwsxwo09Cuda5R5nC0o0IxlrlTuvPuzSw==}
@ -765,6 +800,9 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@js-sdsl/ordered-map@4.4.2':
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
'@keyv/bigmap@1.3.0':
resolution: {integrity: sha512-KT01GjzV6AQD5+IYrcpoYLkCu1Jod3nau1Z7EsEuViO3TZGRacSbO9MfHmbJ1WaOXFtWLxPVj169cn2WNKPkIg==}
engines: {node: '>= 18'}
@ -900,6 +938,24 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@opentelemetry/api@1.9.0':
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
engines: {node: '>=8.0.0'}
'@opentelemetry/core@1.30.1':
resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==}
engines: {node: '>=14'}
peerDependencies:
'@opentelemetry/api': '>=1.0.0 <1.10.0'
'@opentelemetry/semantic-conventions@1.28.0':
resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==}
engines: {node: '>=14'}
'@opentelemetry/semantic-conventions@1.34.0':
resolution: {integrity: sha512-aKcOkyrorBGlajjRdVoJWHTxfxO1vCNHLJVlSDaRHDIdjU+pX8IYQPvPDkYiujKLbRnWU+1TBwEt0QRgSm4SGA==}
engines: {node: '>=14'}
'@oxc-project/types@0.107.0':
resolution: {integrity: sha512-QFDRbYfV2LVx8tyqtyiah3jQPUj1mK2+RYwxyFWyGoys6XJnwTdlzO6rdNNHOPorHAu5Uo34oWRKcvNpbJarmQ==}
@ -1278,6 +1334,10 @@ packages:
'@tokenizer/token@0.3.0':
resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==}
'@tootallnate/once@2.0.0':
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
engines: {node: '>= 10'}
'@tybys/wasm-util@0.10.1':
resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==}
@ -1487,6 +1547,10 @@ packages:
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
engines: {node: '>= 0.6'}
agent-base@6.0.2:
resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
engines: {node: '>= 6.0.0'}
agent-base@7.1.4:
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
engines: {node: '>= 14'}
@ -1538,6 +1602,10 @@ packages:
aria-query@5.3.0:
resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==}
arrify@2.0.1:
resolution: {integrity: sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==}
engines: {node: '>=8'}
assertion-error@2.0.1:
resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==}
engines: {node: '>=12'}
@ -1669,6 +1737,10 @@ packages:
cliui@7.0.4:
resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==}
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
clsx@2.1.1:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'}
@ -1790,6 +1862,9 @@ packages:
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
engines: {node: '>= 0.4'}
duplexify@4.1.3:
resolution: {integrity: sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==}
eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@ -1809,6 +1884,9 @@ packages:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
end-of-stream@1.4.5:
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
entities@4.5.0:
resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==}
engines: {node: '>=0.12'}
@ -2000,10 +2078,22 @@ packages:
resolution: {integrity: sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w==}
engines: {node: '>=18'}
google-gax@5.0.6:
resolution: {integrity: sha512-1kGbqVQBZPAAu4+/R1XxPQKP0ydbNYoLAr4l0ZO2bMV0kLyLW4I1gAk++qBLWt7DPORTzmWRMsCZe86gDjShJA==}
engines: {node: '>=18'}
google-logging-utils@1.1.3:
resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==}
engines: {node: '>=14'}
googleapis-common@8.0.1:
resolution: {integrity: sha512-eCzNACUXPb1PW5l0ULTzMHaL/ltPRADoPgjBlT8jWsTbxkCp6siv+qKJ/1ldaybCthGwsYFYallF7u9AkU4L+A==}
engines: {node: '>=18.0.0'}
googleapis@170.0.0:
resolution: {integrity: sha512-UJz71WZ3ubMr4NhkEU+CFTS0CMrrXq+ltrFnAQo8Llf9M3cy0AIfKLyFQdUJyhqIpJ4jPW4SRCcBBntrLQ72/A==}
engines: {node: '>=18'}
gopd@1.2.0:
resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
engines: {node: '>= 0.4'}
@ -2039,6 +2129,10 @@ packages:
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
engines: {node: '>= 0.4'}
heap-js@2.7.1:
resolution: {integrity: sha512-EQfezRg0NCZGNlhlDR3Evrw1FVL2G3LhU7EgPoxufQKruNBSYA8MiRPHeWbU+36o+Fhel0wMwM+sLEiBAlNLJA==}
engines: {node: '>=10.0.0'}
highlight.js@10.7.3:
resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==}
@ -2063,10 +2157,18 @@ packages:
resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==}
engines: {node: '>= 0.8'}
http-proxy-agent@5.0.0:
resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==}
engines: {node: '>= 6'}
http-proxy-agent@7.0.2:
resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==}
engines: {node: '>= 14'}
https-proxy-agent@5.0.1:
resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==}
engines: {node: '>= 6'}
https-proxy-agent@7.0.6:
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
engines: {node: '>= 14'}
@ -2114,6 +2216,9 @@ packages:
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
is-stream-ended@0.1.4:
resolution: {integrity: sha512-xj0XPvmr7bQFTvirqnFr50o0hQIh6ZItDqloxt5aJrR4NQsYeSsyFQERYGCAzfindAcnKjINnwEEgLx4IqVzQw==}
is-stream@2.0.1:
resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==}
engines: {node: '>=8'}
@ -2296,6 +2401,9 @@ packages:
lit@3.3.2:
resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==}
lodash.camelcase@4.3.0:
resolution: {integrity: sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==}
lodash.clonedeep@4.5.0:
resolution: {integrity: sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==}
@ -2320,6 +2428,9 @@ packages:
lodash.once@4.1.1:
resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==}
lodash.snakecase@4.1.1:
resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==}
lodash@4.17.21:
resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
@ -2492,6 +2603,10 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
object-hash@3.0.0:
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
engines: {node: '>= 6'}
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@ -2553,6 +2668,10 @@ packages:
oxlint-tsgolint:
optional: true
p-defer@3.0.0:
resolution: {integrity: sha512-ugZxsxmtTln604yeYd29EGrNhazN2lywetzpKhfmQjW/VJmhpDmWbiX+h0zL8V91R0UXkhb3KtPmyq9PZw3aYw==}
engines: {node: '>=8'}
p-finally@1.0.0:
resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==}
engines: {node: '>=4'}
@ -2701,6 +2820,10 @@ packages:
proper-lockfile@4.1.2:
resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==}
proto3-json-serializer@3.0.4:
resolution: {integrity: sha512-E1sbAYg3aEbXrq0n1ojJkRHQJGE1kaE/O6GLA94y8rnJBfgvOPTOd1b9hOceQK1FFZI9qMh1vBERCyO2ifubcw==}
engines: {node: '>=18'}
protobufjs@6.8.8:
resolution: {integrity: sha512-AAmHtD5pXgZfi7GMpllpO3q1Xw1OYldr+dMUlAnffGTAhqkg72WdmSY71uKBF/JuyiKs8psYbtKrhi0ASCD8qw==}
hasBin: true
@ -2758,6 +2881,10 @@ packages:
readable-stream@2.3.8:
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
readable-stream@3.6.2:
resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==}
engines: {node: '>= 6'}
readable-stream@4.5.2:
resolution: {integrity: sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@ -2785,6 +2912,10 @@ packages:
resolve-pkg-maps@1.0.0:
resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
retry-request@8.0.2:
resolution: {integrity: sha512-JzFPAfklk1kjR1w76f0QOIhoDkNkSqW8wYKT08n9yysTmZfB+RQ2QoXoTAeOi1HD9ZipTyTAZg3c4pM/jeqgSw==}
engines: {node: '>=18'}
retry@0.12.0:
resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==}
engines: {node: '>= 4'}
@ -2934,6 +3065,12 @@ packages:
std-env@3.10.0:
resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==}
stream-events@1.0.5:
resolution: {integrity: sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==}
stream-shift@1.0.3:
resolution: {integrity: sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==}
string-width@4.2.3:
resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==}
engines: {node: '>=8'}
@ -2960,6 +3097,9 @@ packages:
resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==}
engines: {node: '>=18'}
stubs@3.0.0:
resolution: {integrity: sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==}
supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
engines: {node: '>=8'}
@ -2980,6 +3120,10 @@ packages:
tailwindcss@4.1.17:
resolution: {integrity: sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==}
teeny-request@10.1.0:
resolution: {integrity: sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==}
engines: {node: '>=18'}
thenify-all@1.6.0:
resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==}
engines: {node: '>=0.8'}
@ -3085,6 +3229,9 @@ packages:
urijs@1.19.11:
resolution: {integrity: sha512-HXgFDgDommxn5/bIv0cnQZsPhHDA90NPHD6+c/v21U5+Sx5hoP8+dP9IZXBU1gIfvdRfhG8cel9QNPeionfcCQ==}
url-template@2.0.8:
resolution: {integrity: sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==}
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@ -3256,10 +3403,18 @@ packages:
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
engines: {node: '>=10'}
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
yargs@16.2.0:
resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==}
engines: {node: '>=10'}
yargs@17.7.2:
resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==}
engines: {node: '>=12'}
zod-to-json-schema@3.25.1:
resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==}
peerDependencies:
@ -3538,6 +3693,36 @@ snapshots:
'@glideapps/ts-necessities@2.2.3': {}
'@google-cloud/paginator@6.0.0':
dependencies:
extend: 3.0.2
'@google-cloud/precise-date@5.0.0': {}
'@google-cloud/projectify@5.0.0': {}
'@google-cloud/promisify@5.0.0': {}
'@google-cloud/pubsub@5.2.1':
dependencies:
'@google-cloud/paginator': 6.0.0
'@google-cloud/precise-date': 5.0.0
'@google-cloud/projectify': 5.0.0
'@google-cloud/promisify': 5.0.0
'@opentelemetry/api': 1.9.0
'@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0)
'@opentelemetry/semantic-conventions': 1.34.0
arrify: 2.0.1
extend: 3.0.2
google-auth-library: 10.5.0
google-gax: 5.0.6
heap-js: 2.7.1
is-stream-ended: 0.1.4
lodash.snakecase: 4.1.1
p-defer: 3.0.0
transitivePeerDependencies:
- supports-color
'@google/genai@1.34.0':
dependencies:
google-auth-library: 10.5.0
@ -3559,6 +3744,18 @@ snapshots:
'@grammyjs/types@3.23.0': {}
'@grpc/grpc-js@1.14.3':
dependencies:
'@grpc/proto-loader': 0.8.0
'@js-sdsl/ordered-map': 4.4.2
'@grpc/proto-loader@0.8.0':
dependencies:
lodash.camelcase: 4.3.0
long: 5.3.2
protobufjs: 7.5.4
yargs: 17.7.2
'@hapi/boom@9.1.4':
dependencies:
'@hapi/hoek': 9.3.0
@ -3699,6 +3896,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@js-sdsl/ordered-map@4.4.2': {}
'@keyv/bigmap@1.3.0(keyv@5.5.5)':
dependencies:
hashery: 1.4.0
@ -3902,6 +4101,17 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@opentelemetry/api@1.9.0': {}
'@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)':
dependencies:
'@opentelemetry/api': 1.9.0
'@opentelemetry/semantic-conventions': 1.28.0
'@opentelemetry/semantic-conventions@1.28.0': {}
'@opentelemetry/semantic-conventions@1.34.0': {}
'@oxc-project/types@0.107.0': {}
'@oxlint-tsgolint/darwin-arm64@0.11.0':
@ -4199,6 +4409,8 @@ snapshots:
'@tokenizer/token@0.3.0': {}
'@tootallnate/once@2.0.0': {}
'@tybys/wasm-util@0.10.1':
dependencies:
tslib: 2.8.1
@ -4345,7 +4557,7 @@ snapshots:
'@vitest/mocker': 4.0.16(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
playwright: 1.57.0
tinyrainbow: 3.0.3
vitest: 4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- bufferutil
- msw
@ -4357,7 +4569,7 @@ snapshots:
'@testing-library/dom': 10.4.1
'@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1)
'@vitest/browser': 4.0.16(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)
vitest: 4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
transitivePeerDependencies:
- bufferutil
- msw
@ -4374,7 +4586,7 @@ snapshots:
pngjs: 7.0.0
sirv: 3.0.2
tinyrainbow: 3.0.3
vitest: 4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
ws: 8.19.0
transitivePeerDependencies:
- bufferutil
@ -4395,7 +4607,7 @@ snapshots:
obug: 2.1.1
std-env: 3.10.0
tinyrainbow: 3.0.3
vitest: 4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
vitest: 4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
optionalDependencies:
'@vitest/browser': 4.0.16(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)
transitivePeerDependencies:
@ -4501,6 +4713,12 @@ snapshots:
mime-types: 3.0.2
negotiator: 1.0.0
agent-base@6.0.2:
dependencies:
debug: 4.4.3
transitivePeerDependencies:
- supports-color
agent-base@7.1.4: {}
ajv-formats@3.0.1(ajv@8.17.1):
@ -4543,6 +4761,8 @@ snapshots:
dequal: 2.0.3
optional: true
arrify@2.0.1: {}
assertion-error@2.0.1: {}
ast-v8-to-istanbul@0.3.10:
@ -4709,6 +4929,12 @@ snapshots:
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
cliui@8.0.1:
dependencies:
string-width: 4.2.3
strip-ansi: 6.0.1
wrap-ansi: 7.0.0
clsx@2.1.1: {}
codec-parser@2.5.0:
@ -4798,6 +5024,13 @@ snapshots:
es-errors: 1.3.0
gopd: 1.2.0
duplexify@4.1.3:
dependencies:
end-of-stream: 1.4.5
inherits: 2.0.4
readable-stream: 3.6.2
stream-shift: 1.0.3
eastasianwidth@0.2.0: {}
ecdsa-sig-formatter@1.0.11:
@ -4812,6 +5045,10 @@ snapshots:
encodeurl@2.0.0: {}
end-of-stream@1.4.5:
dependencies:
once: 1.4.0
entities@4.5.0: {}
es-define-property@1.0.1: {}
@ -5072,8 +5309,41 @@ snapshots:
transitivePeerDependencies:
- supports-color
google-gax@5.0.6:
dependencies:
'@grpc/grpc-js': 1.14.3
'@grpc/proto-loader': 0.8.0
duplexify: 4.1.3
google-auth-library: 10.5.0
google-logging-utils: 1.1.3
node-fetch: 3.3.2
object-hash: 3.0.0
proto3-json-serializer: 3.0.4
protobufjs: 7.5.4
retry-request: 8.0.2
rimraf: 5.0.10
transitivePeerDependencies:
- supports-color
google-logging-utils@1.1.3: {}
googleapis-common@8.0.1:
dependencies:
extend: 3.0.2
gaxios: 7.1.3
google-auth-library: 10.5.0
qs: 6.14.1
url-template: 2.0.8
transitivePeerDependencies:
- supports-color
googleapis@170.0.0:
dependencies:
google-auth-library: 10.5.0
googleapis-common: 8.0.1
transitivePeerDependencies:
- supports-color
gopd@1.2.0: {}
graceful-fs@4.2.11: {}
@ -5111,6 +5381,8 @@ snapshots:
dependencies:
function-bind: 1.1.2
heap-js@2.7.1: {}
highlight.js@10.7.3: {}
highlight.js@11.11.1: {}
@ -5132,6 +5404,14 @@ snapshots:
statuses: 2.0.2
toidentifier: 1.0.1
http-proxy-agent@5.0.0:
dependencies:
'@tootallnate/once': 2.0.0
agent-base: 6.0.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.4
@ -5139,6 +5419,13 @@ snapshots:
transitivePeerDependencies:
- supports-color
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
debug: 4.4.3
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.6:
dependencies:
agent-base: 7.1.4
@ -5176,6 +5463,8 @@ snapshots:
is-promise@4.0.0: {}
is-stream-ended@0.1.4: {}
is-stream@2.0.1: {}
is-url@1.2.4: {}
@ -5367,6 +5656,8 @@ snapshots:
lit-element: 4.2.2
lit-html: 3.3.2
lodash.camelcase@4.3.0: {}
lodash.clonedeep@4.5.0: {}
lodash.includes@4.3.0: {}
@ -5383,6 +5674,8 @@ snapshots:
lodash.once@4.1.1: {}
lodash.snakecase@4.1.1: {}
lodash@4.17.21: {}
long@4.0.0: {}
@ -5529,6 +5822,8 @@ snapshots:
object-assign@4.1.1: {}
object-hash@3.0.0: {}
object-inspect@1.13.4: {}
object-path@0.11.8: {}
@ -5590,6 +5885,8 @@ snapshots:
'@oxlint/win32-x64': 1.38.0
oxlint-tsgolint: 0.11.0
p-defer@3.0.0: {}
p-finally@1.0.0: {}
p-queue@6.6.2:
@ -5717,6 +6014,10 @@ snapshots:
retry: 0.12.0
signal-exit: 3.0.7
proto3-json-serializer@3.0.4:
dependencies:
protobufjs: 7.5.4
protobufjs@6.8.8:
dependencies:
'@protobufjs/aspromise': 1.1.2
@ -5817,6 +6118,12 @@ snapshots:
string_decoder: 1.1.1
util-deprecate: 1.0.2
readable-stream@3.6.2:
dependencies:
inherits: 2.0.4
string_decoder: 1.3.0
util-deprecate: 1.0.2
readable-stream@4.5.2:
dependencies:
abort-controller: 3.0.0
@ -5839,6 +6146,13 @@ snapshots:
resolve-pkg-maps@1.0.0: {}
retry-request@8.0.2:
dependencies:
extend: 3.0.2
teeny-request: 10.1.0
transitivePeerDependencies:
- supports-color
retry@0.12.0: {}
retry@0.13.1: {}
@ -6061,6 +6375,12 @@ snapshots:
std-env@3.10.0: {}
stream-events@1.0.5:
dependencies:
stubs: 3.0.0
stream-shift@1.0.3: {}
string-width@4.2.3:
dependencies:
emoji-regex: 8.0.0
@ -6093,6 +6413,8 @@ snapshots:
dependencies:
'@tokenizer/token': 0.3.0
stubs@3.0.0: {}
supports-color@7.2.0:
dependencies:
has-flag: 4.0.0
@ -6107,6 +6429,15 @@ snapshots:
tailwindcss@4.1.17: {}
teeny-request@10.1.0:
dependencies:
http-proxy-agent: 5.0.0
https-proxy-agent: 5.0.1
node-fetch: 3.3.2
stream-events: 1.0.5
transitivePeerDependencies:
- supports-color
thenify-all@1.6.0:
dependencies:
thenify: 3.3.1
@ -6197,6 +6528,8 @@ snapshots:
urijs@1.19.11: {}
url-template@2.0.8: {}
util-deprecate@1.0.2: {}
uuid@11.1.0: {}
@ -6221,7 +6554,7 @@ snapshots:
tsx: 4.21.0
yaml: 2.8.2
vitest@4.0.16(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2):
vitest@4.0.16(@opentelemetry/api@1.9.0)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.16)(@vitest/browser-preview@4.0.16)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2):
dependencies:
'@vitest/expect': 4.0.16
'@vitest/mocker': 4.0.16(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))
@ -6244,6 +6577,7 @@ snapshots:
vite: 7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@opentelemetry/api': 1.9.0
'@types/node': 25.0.3
'@vitest/browser-playwright': 4.0.16(playwright@1.57.0)(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)
'@vitest/browser-preview': 4.0.16(vite@7.3.1(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(tsx@4.21.0)(yaml@2.8.2))(vitest@4.0.16)
@ -6317,6 +6651,8 @@ snapshots:
yargs-parser@20.2.9: {}
yargs-parser@21.1.1: {}
yargs@16.2.0:
dependencies:
cliui: 7.0.4
@ -6327,6 +6663,16 @@ snapshots:
y18n: 5.0.8
yargs-parser: 20.2.9
yargs@17.7.2:
dependencies:
cliui: 8.0.1
escalade: 3.2.0
get-caller-file: 2.0.5
require-directory: 2.1.1
string-width: 4.2.3
y18n: 5.0.8
yargs-parser: 21.1.1
zod-to-json-schema@3.25.1(zod@3.25.76):
dependencies:
zod: 3.25.76

View File

@ -746,6 +746,40 @@ export type MSTeamsTeamConfig = {
channels?: Record<string, MSTeamsChannelConfig>;
};
export type GoogleChatAccountConfig = {
/** Optional display name for this account (used in CLI/UI lists). */
name?: string;
/** If false, do not start this Google Chat account. Default: true. */
enabled?: boolean;
/** Google Cloud Project ID. */
projectId?: string;
/** Pub/Sub subscription name (full path: projects/.../subscriptions/...). */
subscriptionName?: string;
/** Path to service account credentials JSON file. */
credentialsPath?: string;
/** Direct message access policy (default: pairing). */
dmPolicy?: DmPolicy;
/** Allowlist for DM senders (email addresses). */
allowFrom?: string[];
/** Group/space access policy (default: disabled). */
spacePolicy?: GroupPolicy;
/** Allowlist for spaces (space IDs). */
allowSpaces?: string[];
/** Max space messages to keep as history context (0 disables). */
historyLimit?: number;
/** Max DM turns to keep as history context. */
dmHistoryLimit?: number;
/** Outbound text chunk size (chars). Default: 4000. */
textChunkLimit?: number;
/** Outbound message prefix. */
messagePrefix?: string;
};
export type GoogleChatConfig = {
/** Optional per-account Google Chat configuration (multi-account). */
accounts?: Record<string, GoogleChatAccountConfig>;
} & GoogleChatAccountConfig;
export type MSTeamsConfig = {
/** If false, do not start the MS Teams provider. Default: true. */
enabled?: boolean;
@ -1655,6 +1689,7 @@ export type ClawdbotConfig = {
signal?: SignalConfig;
imessage?: IMessageConfig;
msteams?: MSTeamsConfig;
googlechat?: GoogleChatConfig;
cron?: CronConfig;
hooks?: HooksConfig;
bridge?: BridgeConfig;

View File

@ -0,0 +1,89 @@
import type { ClawdbotConfig } from "../config/config.js";
import {
DEFAULT_ACCOUNT_ID,
normalizeAccountId,
} from "../routing/session-key.js";
import type { GoogleChatAccountConfig } from "./types.js";
export type ResolvedGoogleChatAccount = {
accountId: string;
enabled: boolean;
name?: string;
projectId: string;
subscriptionName: string;
credentialsPath?: string;
config: GoogleChatAccountConfig;
};
function listConfiguredAccountIds(cfg: ClawdbotConfig): string[] {
const googlechat = (cfg as { googlechat?: { accounts?: Record<string, unknown> } }).googlechat;
const accounts = googlechat?.accounts;
if (!accounts || typeof accounts !== "object") return [];
return Object.keys(accounts).filter(Boolean);
}
export function listGoogleChatAccountIds(cfg: ClawdbotConfig): string[] {
const ids = listConfiguredAccountIds(cfg);
if (ids.length === 0) return [DEFAULT_ACCOUNT_ID];
return ids.sort((a, b) => a.localeCompare(b));
}
export function resolveDefaultGoogleChatAccountId(cfg: ClawdbotConfig): string {
const ids = listGoogleChatAccountIds(cfg);
if (ids.includes(DEFAULT_ACCOUNT_ID)) return DEFAULT_ACCOUNT_ID;
return ids[0] ?? DEFAULT_ACCOUNT_ID;
}
function resolveAccountConfig(
cfg: ClawdbotConfig,
accountId: string,
): GoogleChatAccountConfig | undefined {
const googlechat = (cfg as { googlechat?: { accounts?: Record<string, GoogleChatAccountConfig> } }).googlechat;
const accounts = googlechat?.accounts;
if (!accounts || typeof accounts !== "object") return undefined;
return accounts[accountId];
}
function mergeGoogleChatAccountConfig(
cfg: ClawdbotConfig,
accountId: string,
): GoogleChatAccountConfig {
const googlechat = (cfg as { googlechat?: GoogleChatAccountConfig & { accounts?: unknown } }).googlechat ?? {};
const { accounts: _ignored, ...base } = googlechat;
const account = resolveAccountConfig(cfg, accountId) ?? {};
return { ...base, ...account };
}
export function resolveGoogleChatAccount(params: {
cfg: ClawdbotConfig;
accountId?: string | null;
}): ResolvedGoogleChatAccount {
const googlechat = (params.cfg as { googlechat?: { enabled?: boolean } }).googlechat;
const baseEnabled = googlechat?.enabled !== false;
const resolve = (accountId: string) => {
const merged = mergeGoogleChatAccountConfig(params.cfg, accountId);
const accountEnabled = merged.enabled !== false;
const enabled = baseEnabled && accountEnabled;
return {
accountId,
enabled,
name: merged.name?.trim() || undefined,
projectId: merged.projectId ?? "",
subscriptionName: merged.subscriptionName ?? "",
credentialsPath: merged.credentialsPath,
config: merged,
} satisfies ResolvedGoogleChatAccount;
};
const normalized = normalizeAccountId(params.accountId);
return resolve(normalized);
}
export function listEnabledGoogleChatAccounts(
cfg: ClawdbotConfig,
): ResolvedGoogleChatAccount[] {
return listGoogleChatAccountIds(cfg)
.map((accountId) => resolveGoogleChatAccount({ cfg, accountId }))
.filter((account) => account.enabled);
}

184
src/googlechat/monitor.ts Normal file
View File

@ -0,0 +1,184 @@
import { PubSub, type Message } from "@google-cloud/pubsub";
import type { ClawdbotConfig } from "../config/config.js";
import type { RuntimeEnv } from "../runtime.js";
import type { ResolvedGoogleChatAccount } from "./accounts.js";
import type { GoogleChatEvent, GoogleChatMessage } from "./types.js";
export type GoogleChatMonitorOptions = {
account: ResolvedGoogleChatAccount;
config: ClawdbotConfig;
runtime: RuntimeEnv;
abortSignal: AbortSignal;
};
export type NormalizedGoogleChatMessage = {
provider: "googlechat";
accountId: string;
messageId: string;
timestamp: number;
sender: {
id: string;
name: string;
email?: string;
};
chat: {
id: string;
name: string;
type: "dm" | "group";
};
thread?: {
id: string;
};
content: {
text: string;
};
raw: GoogleChatEvent;
};
function normalizeMessage(
event: GoogleChatEvent,
accountId: string,
): NormalizedGoogleChatMessage | null {
if (event.type !== "MESSAGE" || !event.message) {
return null;
}
const msg = event.message;
// Skip messages from bots
if (msg.sender.type === "BOT") {
return null;
}
const spaceId = msg.space.name.replace("spaces/", "");
const isDM = msg.space.type === "DM";
return {
provider: "googlechat",
accountId,
messageId: msg.name,
timestamp: new Date(msg.createTime).getTime(),
sender: {
id: msg.sender.name.replace("users/", ""),
name: msg.sender.displayName,
email: msg.sender.email,
},
chat: {
id: spaceId,
name: msg.space.displayName || spaceId,
type: isDM ? "dm" : "group",
},
thread: msg.thread
? {
id: msg.thread.name,
}
: undefined,
content: {
text: msg.argumentText || msg.text || "",
},
raw: event,
};
}
function checkMessagePolicy(
message: NormalizedGoogleChatMessage,
account: ResolvedGoogleChatAccount,
): boolean {
const policy =
message.chat.type === "dm"
? account.config.dmPolicy ?? "pairing"
: account.config.spacePolicy ?? "disabled";
switch (policy) {
case "disabled":
return false;
case "open":
return true;
case "allowlist": {
if (message.chat.type === "dm") {
const allowFrom = account.config.allowFrom ?? [];
return allowFrom.includes(message.sender.email ?? "");
}
const allowSpaces = account.config.allowSpaces ?? [];
return allowSpaces.includes(message.chat.id);
}
case "pairing":
// For pairing mode, we'd need to check the pairing store
// For now, treat as allowlist-based
return false;
default:
return false;
}
}
export async function monitorGoogleChatProvider(
options: GoogleChatMonitorOptions,
): Promise<() => void> {
const { account, abortSignal } = options;
if (!account.projectId || !account.subscriptionName) {
throw new Error(
`Google Chat account ${account.accountId} missing projectId or subscriptionName`,
);
}
const pubsub = new PubSub({
projectId: account.projectId,
keyFilename: account.credentialsPath,
});
const subscription = pubsub.subscription(account.subscriptionName);
const messageHandler = async (message: Message) => {
try {
const eventData = JSON.parse(message.data.toString()) as GoogleChatEvent;
const normalized = normalizeMessage(eventData, account.accountId);
if (normalized) {
const allowed = checkMessagePolicy(normalized, account);
if (allowed) {
// Route to agent - this will be wired up by the gateway
// For now, we just acknowledge the message
console.log(
`[googlechat:${account.accountId}] Received message from ${normalized.sender.name}`,
);
}
}
message.ack();
} catch (error) {
console.error(
`[googlechat:${account.accountId}] Error processing message:`,
error,
);
message.nack();
}
};
const errorHandler = (error: Error) => {
console.error(`[googlechat:${account.accountId}] Subscription error:`, error);
};
subscription.on("message", messageHandler);
subscription.on("error", errorHandler);
// Handle abort signal
const cleanup = () => {
subscription.removeListener("message", messageHandler);
subscription.removeListener("error", errorHandler);
console.log(`[googlechat:${account.accountId}] Monitor stopped`);
};
if (abortSignal.aborted) {
cleanup();
} else {
abortSignal.addEventListener("abort", cleanup, { once: true });
}
console.log(
`[googlechat:${account.accountId}] Listening on subscription: ${account.subscriptionName}`,
);
return cleanup;
}

69
src/googlechat/probe.ts Normal file
View File

@ -0,0 +1,69 @@
import { PubSub } from "@google-cloud/pubsub";
import type { ResolvedGoogleChatAccount } from "./accounts.js";
export type GoogleChatProbeResult = {
ok: boolean;
status: string;
projectId?: string;
subscriptionName?: string;
topic?: string;
error?: string;
};
export async function probeGoogleChat(
account: ResolvedGoogleChatAccount,
timeoutMs = 5000,
): Promise<GoogleChatProbeResult> {
if (!account.projectId || !account.subscriptionName) {
return {
ok: false,
status: "Not configured",
error: "Missing projectId or subscriptionName",
};
}
try {
const pubsub = new PubSub({
projectId: account.projectId,
keyFilename: account.credentialsPath,
});
const subscription = pubsub.subscription(account.subscriptionName);
// Use a timeout for the exists check
const existsPromise = subscription.exists();
const timeoutPromise = new Promise<[boolean]>((_, reject) =>
setTimeout(() => reject(new Error("Timeout")), timeoutMs),
);
const [exists] = await Promise.race([existsPromise, timeoutPromise]);
if (!exists) {
return {
ok: false,
status: "Subscription not found",
projectId: account.projectId,
subscriptionName: account.subscriptionName,
};
}
const [metadata] = await subscription.getMetadata();
return {
ok: true,
status: "Connected",
projectId: account.projectId,
subscriptionName: account.subscriptionName,
topic: metadata.topic ?? undefined,
};
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
return {
ok: false,
status: "Connection failed",
projectId: account.projectId,
subscriptionName: account.subscriptionName,
error: errorMessage,
};
}
}

171
src/googlechat/send.ts Normal file
View File

@ -0,0 +1,171 @@
import { google, type chat_v1 } from "googleapis";
import type { ResolvedGoogleChatAccount } from "./accounts.js";
let chatClients: Map<string, chat_v1.Chat> = new Map();
async function getChatClient(account: ResolvedGoogleChatAccount): Promise<chat_v1.Chat> {
const cacheKey = `${account.accountId}:${account.credentialsPath ?? "default"}`;
const cached = chatClients.get(cacheKey);
if (cached) return cached;
const auth = new google.auth.GoogleAuth({
keyFile: account.credentialsPath,
scopes: ["https://www.googleapis.com/auth/chat.bot"],
});
const client = google.chat({
version: "v1",
auth,
});
chatClients.set(cacheKey, client);
return client;
}
export type SendGoogleChatResult = {
messageId: string;
spaceName: string;
};
export async function sendGoogleChatText(
to: string,
text: string,
options: {
account: ResolvedGoogleChatAccount;
threadKey?: string;
replyToId?: string;
},
): Promise<SendGoogleChatResult> {
const client = await getChatClient(options.account);
const spaceName = to.startsWith("spaces/") ? to : `spaces/${to}`;
const prefix = options.account.config.messagePrefix;
const formattedText = prefix ? `${prefix} ${text}` : text;
const requestBody: chat_v1.Schema$Message = {
text: formattedText,
};
if (options.threadKey) {
requestBody.thread = { name: options.threadKey };
}
const response = await client.spaces.messages.create({
parent: spaceName,
requestBody,
});
return {
messageId: response.data.name ?? "",
spaceName,
};
}
export async function sendGoogleChatCard(
to: string,
card: chat_v1.Schema$CardWithId,
options: {
account: ResolvedGoogleChatAccount;
threadKey?: string;
text?: string;
},
): Promise<SendGoogleChatResult> {
const client = await getChatClient(options.account);
const spaceName = to.startsWith("spaces/") ? to : `spaces/${to}`;
const requestBody: chat_v1.Schema$Message = {
cardsV2: [card],
};
if (options.text) {
requestBody.text = options.text;
}
if (options.threadKey) {
requestBody.thread = { name: options.threadKey };
}
const response = await client.spaces.messages.create({
parent: spaceName,
requestBody,
});
return {
messageId: response.data.name ?? "",
spaceName,
};
}
export async function sendGoogleChatMedia(
to: string,
mediaUrl: string,
options: {
account: ResolvedGoogleChatAccount;
caption?: string;
threadKey?: string;
},
): Promise<SendGoogleChatResult> {
// Google Chat doesn't support direct media upload via API for bots
// Send as a card with image
const card: chat_v1.Schema$CardWithId = {
cardId: `media-${Date.now()}`,
card: {
sections: [
{
widgets: [
{
image: {
imageUrl: mediaUrl,
altText: options.caption ?? "Image",
},
},
],
},
],
},
};
if (options.caption) {
card.card!.sections![0].widgets!.unshift({
textParagraph: { text: options.caption },
});
}
return sendGoogleChatCard(to, card, {
account: options.account,
threadKey: options.threadKey,
});
}
/**
* Chunk text for Google Chat (4096 char limit).
*/
export function chunkGoogleChatText(text: string, maxLength = 4000): string[] {
if (text.length <= maxLength) return [text];
const chunks: string[] = [];
let remaining = text;
while (remaining.length > 0) {
if (remaining.length <= maxLength) {
chunks.push(remaining);
break;
}
// Find last newline or space within limit
let splitAt = remaining.lastIndexOf("\n", maxLength);
if (splitAt === -1 || splitAt < maxLength / 2) {
splitAt = remaining.lastIndexOf(" ", maxLength);
}
if (splitAt === -1 || splitAt < maxLength / 2) {
splitAt = maxLength;
}
chunks.push(remaining.slice(0, splitAt));
remaining = remaining.slice(splitAt).trimStart();
}
return chunks;
}

79
src/googlechat/types.ts Normal file
View File

@ -0,0 +1,79 @@
import type { DmPolicy, GroupPolicy } from "../config/types.js";
export type GoogleChatAccountConfig = {
/** Optional display name for this account (used in CLI/UI lists). */
name?: string;
/** If false, do not start this Google Chat account. Default: true. */
enabled?: boolean;
/** Google Cloud Project ID. */
projectId?: string;
/** Pub/Sub subscription name (full path: projects/.../subscriptions/...). */
subscriptionName?: string;
/** Path to service account credentials JSON file. */
credentialsPath?: string;
/** Direct message access policy (default: pairing). */
dmPolicy?: DmPolicy;
/** Allowlist for DM senders (email addresses). */
allowFrom?: string[];
/** Group/space access policy (default: disabled). */
spacePolicy?: GroupPolicy;
/** Allowlist for spaces (space IDs). */
allowSpaces?: string[];
/** Max space messages to keep as history context (0 disables). */
historyLimit?: number;
/** Max DM turns to keep as history context. */
dmHistoryLimit?: number;
/** Outbound text chunk size (chars). Default: 4000. */
textChunkLimit?: number;
/** Outbound message prefix. */
messagePrefix?: string;
};
export type GoogleChatConfig = {
/** Optional per-account Google Chat configuration (multi-account). */
accounts?: Record<string, GoogleChatAccountConfig>;
} & GoogleChatAccountConfig;
export type GoogleChatMessage = {
name: string;
sender: {
name: string;
displayName: string;
email?: string;
type: "HUMAN" | "BOT";
};
createTime: string;
text?: string;
space: {
name: string;
type: "DM" | "ROOM" | "SPACE";
displayName?: string;
};
thread?: {
name: string;
};
argumentText?: string;
slashCommand?: {
commandId: string;
};
};
export type GoogleChatEvent = {
type:
| "MESSAGE"
| "ADDED_TO_SPACE"
| "REMOVED_FROM_SPACE"
| "CARD_CLICKED";
eventTime: string;
message?: GoogleChatMessage;
user?: {
name: string;
displayName: string;
email?: string;
};
space?: {
name: string;
type: string;
displayName?: string;
};
};

View File

@ -1,5 +1,6 @@
import type { ClawdbotConfig } from "../config/config.js";
import { resolveDiscordAccount } from "../discord/accounts.js";
import { resolveGoogleChatAccount } from "../googlechat/accounts.js";
import { resolveIMessageAccount } from "../imessage/accounts.js";
import { resolveSignalAccount } from "../signal/accounts.js";
import { resolveSlackAccount } from "../slack/accounts.js";
@ -281,6 +282,26 @@ const DOCKS: Record<ProviderId, ProviderDock> = {
formatAllowFrom: ({ allowFrom }) => formatLower(allowFrom),
},
},
googlechat: {
id: "googlechat",
capabilities: {
chatTypes: ["direct", "group", "thread"],
threads: true,
media: true,
},
outbound: { textChunkLimit: 4000 },
config: {
resolveAllowFrom: ({ cfg, accountId }) =>
resolveGoogleChatAccount({ cfg, accountId }).config.allowFrom ?? [],
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => String(entry).trim().toLowerCase())
.filter(Boolean),
},
threading: {
resolveReplyToMode: () => "first",
},
},
};
export function listProviderDocks(): ProviderDock[] {

View File

@ -0,0 +1,191 @@
import { chunkMarkdownText } from "../../auto-reply/chunk.js";
import type { ClawdbotConfig } from "../../config/config.js";
import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js";
import {
listGoogleChatAccountIds,
type ResolvedGoogleChatAccount,
resolveDefaultGoogleChatAccountId,
resolveGoogleChatAccount,
} from "../../googlechat/accounts.js";
import { probeGoogleChat } from "../../googlechat/probe.js";
import {
chunkGoogleChatText,
sendGoogleChatMedia,
sendGoogleChatText,
} from "../../googlechat/send.js";
import { getChatProviderMeta } from "../registry.js";
import {
deleteAccountFromConfigSection,
setAccountEnabledInConfigSection,
} from "./config-helpers.js";
import { formatPairingApproveHint } from "./helpers.js";
import type { ProviderPlugin } from "./types.js";
const meta = getChatProviderMeta("googlechat");
export const googlechatPlugin: ProviderPlugin<ResolvedGoogleChatAccount> = {
id: "googlechat",
meta: {
...meta,
quickstartAllowFrom: true,
},
pairing: {
idLabel: "email",
normalizeAllowEntry: (entry) => entry.toLowerCase().trim(),
},
capabilities: {
chatTypes: ["direct", "group", "thread"],
threads: true,
media: true,
},
reload: { configPrefixes: ["googlechat"] },
config: {
listAccountIds: (cfg) => listGoogleChatAccountIds(cfg),
resolveAccount: (cfg, accountId) =>
resolveGoogleChatAccount({ cfg, accountId }),
defaultAccountId: (cfg) => resolveDefaultGoogleChatAccountId(cfg),
setAccountEnabled: ({ cfg, accountId, enabled }) =>
setAccountEnabledInConfigSection({
cfg,
sectionKey: "googlechat",
accountId,
enabled,
allowTopLevel: true,
}),
deleteAccount: ({ cfg, accountId }) =>
deleteAccountFromConfigSection({
cfg,
sectionKey: "googlechat",
accountId,
clearBaseFields: ["projectId", "subscriptionName", "credentialsPath", "name"],
}),
isConfigured: (account) =>
Boolean(account.projectId?.trim() && account.subscriptionName?.trim()),
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: Boolean(
account.projectId?.trim() && account.subscriptionName?.trim(),
),
}),
resolveAllowFrom: ({ cfg, accountId }) =>
resolveGoogleChatAccount({ cfg, accountId }).config.allowFrom ?? [],
formatAllowFrom: ({ allowFrom }) =>
allowFrom
.map((entry) => String(entry).trim().toLowerCase())
.filter(Boolean),
},
security: {
resolveDmPolicy: ({ cfg, accountId, account }) => {
const googlechat = (cfg as { googlechat?: { accounts?: Record<string, unknown> } }).googlechat;
const resolvedAccountId =
accountId ?? account.accountId ?? DEFAULT_ACCOUNT_ID;
const useAccountPath = Boolean(googlechat?.accounts?.[resolvedAccountId]);
const basePath = useAccountPath
? `googlechat.accounts.${resolvedAccountId}.`
: "googlechat.";
return {
policy: account.config.dmPolicy ?? "pairing",
allowFrom: account.config.allowFrom ?? [],
policyPath: `${basePath}dmPolicy`,
allowFromPath: basePath,
approveHint: formatPairingApproveHint("googlechat"),
normalizeEntry: (raw) => raw.toLowerCase().trim(),
};
},
},
threading: {
resolveReplyToMode: () => "first",
},
outbound: {
deliveryMode: "direct",
chunker: chunkGoogleChatText,
textChunkLimit: 4000,
resolveTarget: ({ to }) => {
const trimmed = to?.trim();
if (!trimmed) {
return {
ok: false,
error: new Error(
"Delivering to Google Chat requires --to <spaceId>",
),
};
}
return { ok: true, to: trimmed };
},
sendText: async ({ to, text, accountId, cfg, replyToId }) => {
const account = resolveGoogleChatAccount({ cfg, accountId });
const result = await sendGoogleChatText(to, text, {
account,
threadKey: replyToId ?? undefined,
});
return { provider: "googlechat", ...result };
},
sendMedia: async ({ to, text, mediaUrl, accountId, cfg, replyToId }) => {
const account = resolveGoogleChatAccount({ cfg, accountId });
const result = await sendGoogleChatMedia(to, mediaUrl ?? "", {
account,
caption: text,
threadKey: replyToId ?? undefined,
});
return { provider: "googlechat", ...result };
},
},
status: {
defaultRuntime: {
accountId: DEFAULT_ACCOUNT_ID,
running: false,
lastStartAt: null,
lastStopAt: null,
lastError: null,
},
buildProviderSummary: ({ snapshot }) => ({
configured: snapshot.configured ?? false,
running: snapshot.running ?? false,
lastStartAt: snapshot.lastStartAt ?? null,
lastStopAt: snapshot.lastStopAt ?? null,
lastError: snapshot.lastError ?? null,
probe: snapshot.probe,
lastProbeAt: snapshot.lastProbeAt ?? null,
}),
probeAccount: async ({ account, timeoutMs }) =>
probeGoogleChat(account, timeoutMs),
buildAccountSnapshot: ({ account, runtime, probe }) => {
const configured = Boolean(
account.projectId?.trim() && account.subscriptionName?.trim(),
);
return {
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured,
running: runtime?.running ?? false,
lastStartAt: runtime?.lastStartAt ?? null,
lastStopAt: runtime?.lastStopAt ?? null,
lastError: runtime?.lastError ?? null,
probe,
lastInboundAt: runtime?.lastInboundAt ?? null,
lastOutboundAt: runtime?.lastOutboundAt ?? null,
};
},
},
gateway: {
startAccount: async (ctx) => {
const account = ctx.account;
ctx.log?.info(`[${account.accountId}] starting Google Chat provider`);
// Lazy import to avoid circular deps
const { monitorGoogleChatProvider } = await import(
"../../googlechat/monitor.js"
);
return monitorGoogleChatProvider({
account,
config: ctx.cfg,
runtime: ctx.runtime,
abortSignal: ctx.abortSignal,
});
},
},
};

View File

@ -4,6 +4,7 @@ import {
normalizeChatProviderId,
} from "../registry.js";
import { discordPlugin } from "./discord.js";
import { googlechatPlugin } from "./googlechat.js";
import { imessagePlugin } from "./imessage.js";
import { msteamsPlugin } from "./msteams.js";
import { signalPlugin } from "./signal.js";
@ -31,6 +32,7 @@ function resolveProviders(): ProviderPlugin[] {
signalPlugin,
imessagePlugin,
msteamsPlugin,
googlechatPlugin,
];
}
@ -57,6 +59,7 @@ export function normalizeProviderId(raw?: string | null): ProviderId | null {
export {
discordPlugin,
googlechatPlugin,
imessagePlugin,
msteamsPlugin,
signalPlugin,

View File

@ -14,6 +14,7 @@ const LOADERS: Record<ProviderId, PluginLoader> = {
signal: async () => (await import("./signal.js")).signalPlugin,
imessage: async () => (await import("./imessage.js")).imessagePlugin,
msteams: async () => (await import("./msteams.js")).msteamsPlugin,
googlechat: async () => (await import("./googlechat.js")).googlechatPlugin,
};
const cache = new Map<ProviderId, ProviderPlugin>();

View File

@ -0,0 +1,40 @@
import { resolveGoogleChatAccount } from "../../../googlechat/accounts.js";
import {
chunkGoogleChatText,
sendGoogleChatMedia,
sendGoogleChatText,
} from "../../../googlechat/send.js";
import type { ProviderOutboundAdapter } from "../types.js";
export const googlechatOutbound: ProviderOutboundAdapter = {
deliveryMode: "direct",
chunker: chunkGoogleChatText,
textChunkLimit: 4000,
resolveTarget: ({ to }) => {
const trimmed = to?.trim();
if (!trimmed) {
return {
ok: false,
error: new Error("Delivering to Google Chat requires --to <spaceId>"),
};
}
return { ok: true, to: trimmed };
},
sendText: async ({ to, text, accountId, cfg, replyToId }) => {
const account = resolveGoogleChatAccount({ cfg, accountId });
const result = await sendGoogleChatText(to, text, {
account,
threadKey: replyToId ?? undefined,
});
return { provider: "googlechat", ...result };
},
sendMedia: async ({ to, text, mediaUrl, accountId, cfg, replyToId }) => {
const account = resolveGoogleChatAccount({ cfg, accountId });
const result = await sendGoogleChatMedia(to, mediaUrl ?? "", {
account,
caption: text,
threadKey: replyToId ?? undefined,
});
return { provider: "googlechat", ...result };
},
};

View File

@ -15,6 +15,7 @@ const LOADERS: Record<ProviderId, OutboundLoader> = {
signal: async () => (await import("./signal.js")).signalOutbound,
imessage: async () => (await import("./imessage.js")).imessageOutbound,
msteams: async () => (await import("./msteams.js")).msteamsOutbound,
googlechat: async () => (await import("./googlechat.js")).googlechatOutbound,
};
const cache = new Map<ProviderId, ProviderOutboundAdapter>();

View File

@ -8,6 +8,7 @@ export const CHAT_PROVIDER_ORDER = [
"signal",
"imessage",
"msteams",
"googlechat",
] as const;
export type ChatProviderId = (typeof CHAT_PROVIDER_ORDER)[number];
@ -94,11 +95,20 @@ const CHAT_PROVIDER_META: Record<ChatProviderId, ChatProviderMeta> = {
docsLabel: "msteams",
blurb: "supported (Bot Framework).",
},
googlechat: {
id: "googlechat",
label: "Google Chat",
selectionLabel: "Google Chat (Pub/Sub)",
docsPath: "/googlechat",
docsLabel: "googlechat",
blurb: "Google Workspace only; uses Pub/Sub for firewall-friendly operation.",
},
};
export const CHAT_PROVIDER_ALIASES: Record<string, ChatProviderId> = {
imsg: "imessage",
teams: "msteams",
gchat: "googlechat",
};
const normalizeProviderKey = (raw?: string | null): string | undefined => {