openclaw-groupme 0.4.4 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +147 -45
  2. package/channel-plugin-api.ts +3 -0
  3. package/dist/channel-plugin-api.js +3 -0
  4. package/dist/index.js +15 -9
  5. package/dist/runtime-setter-api.js +1 -0
  6. package/dist/secret-contract-api.js +1 -0
  7. package/dist/setup-entry.js +16 -0
  8. package/dist/setup-plugin-api.js +3 -0
  9. package/dist/src/accounts.js +24 -48
  10. package/dist/src/channel.js +63 -29
  11. package/dist/src/config-schema.js +10 -11
  12. package/dist/src/groupme-api.js +9 -5
  13. package/dist/src/inbound.js +18 -10
  14. package/dist/src/monitor.js +25 -27
  15. package/dist/src/normalize.js +6 -0
  16. package/dist/src/onboarding.js +364 -337
  17. package/dist/src/parse.js +4 -14
  18. package/dist/src/policy.js +1 -1
  19. package/dist/src/rate-limit.js +12 -7
  20. package/dist/src/replay-cache.js +0 -3
  21. package/dist/src/secret-contract.js +49 -0
  22. package/dist/src/security.js +17 -34
  23. package/dist/src/send.js +19 -13
  24. package/index.ts +15 -10
  25. package/openclaw.plugin.json +14 -15
  26. package/package.json +43 -9
  27. package/runtime-setter-api.ts +1 -0
  28. package/secret-contract-api.ts +5 -0
  29. package/setup-entry.ts +17 -0
  30. package/setup-plugin-api.ts +3 -0
  31. package/src/accounts.ts +29 -68
  32. package/src/channel.ts +74 -64
  33. package/src/config-schema.ts +10 -11
  34. package/src/groupme-api.ts +21 -5
  35. package/src/history.ts +1 -1
  36. package/src/inbound.ts +45 -75
  37. package/src/monitor.ts +37 -52
  38. package/src/normalize.ts +7 -1
  39. package/src/onboarding.ts +449 -409
  40. package/src/parse.ts +6 -23
  41. package/src/policy.ts +1 -4
  42. package/src/rate-limit.ts +15 -12
  43. package/src/replay-cache.ts +1 -4
  44. package/src/runtime.ts +1 -1
  45. package/src/secret-contract.ts +66 -0
  46. package/src/security.ts +28 -66
  47. package/src/send.ts +32 -38
  48. package/src/types.ts +7 -7
package/README.md CHANGED
@@ -1,6 +1,22 @@
1
1
  # openclaw-groupme
2
2
 
3
- An [OpenClaw](https://github.com/oddrationale/openclaw) channel plugin that brings your AI agent into GroupMe group chats. It hooks into GroupMe's Bot API via webhooks so your agent can receive messages, understand context, and reply — all within the group conversations your team (or friends) are already having. Group chats only; DMs are not supported by the GroupMe Bot API.
3
+ [![npm version](https://img.shields.io/npm/v/openclaw-groupme.svg)](https://www.npmjs.com/package/openclaw-groupme)
4
+ [![npm downloads](https://img.shields.io/npm/dm/openclaw-groupme.svg)](https://www.npmjs.com/package/openclaw-groupme)
5
+ [![CI](https://github.com/oddrationale/openclaw-groupme/actions/workflows/ci.yml/badge.svg)](https://github.com/oddrationale/openclaw-groupme/actions/workflows/ci.yml)
6
+ [![CodeQL](https://github.com/oddrationale/openclaw-groupme/actions/workflows/codeql.yml/badge.svg)](https://github.com/oddrationale/openclaw-groupme/actions/workflows/codeql.yml)
7
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
8
+ [![Node.js](https://img.shields.io/node/v/openclaw-groupme.svg)](https://nodejs.org)
9
+ [![OpenClaw](https://img.shields.io/badge/OpenClaw-%E2%89%A5%202026.6.1-6f42c1.svg)](https://github.com/openclaw/openclaw)
10
+
11
+ An [OpenClaw](https://github.com/openclaw/openclaw) channel plugin that brings your AI agent into GroupMe group chats. It hooks into GroupMe's Bot API via webhooks so your agent can receive messages, understand context, and reply — all within the group conversations your team (or friends) are already having. Group chats only; DMs are not supported by the GroupMe Bot API.
12
+
13
+ ## Requirements
14
+
15
+ - **OpenClaw** `>= 2026.6.1` — this plugin targets the 2026.6.1 plugin SDK.
16
+ - **Node.js** `>= 22.19.0` — matches OpenClaw's runtime.
17
+ - A reachable, public **HTTPS** endpoint for the GroupMe callback (see [Prerequisites](#prerequisites)).
18
+
19
+ > New to OpenClaw? Start with the [OpenClaw docs](https://docs.openclaw.ai) and the [channels overview](https://docs.openclaw.ai/channels).
4
20
 
5
21
  ## Install
6
22
 
@@ -20,7 +36,7 @@ After installing, restart the gateway so it picks up the new plugin:
20
36
  openclaw gateway restart
21
37
  ```
22
38
 
23
- That's it — the plugin is loaded and ready to configure.
39
+ That's it — the plugin is loaded and ready to configure. (It ships compiled, so there's no build step on your end.)
24
40
 
25
41
  ## Prerequisites
26
42
 
@@ -66,31 +82,33 @@ The interactive wizard is the easiest way to get started. It creates a new Group
66
82
  openclaw channels add
67
83
  ```
68
84
 
69
- **2.** When asked "Configure chat channels now?", select **Yes**.
85
+ > You can also reach the same GroupMe wizard from the broader setup flows: `openclaw configure --section channels` or the first-run `openclaw onboard`.
70
86
 
71
- **3.** Use the arrow keys to select **GroupMe** from the channel list.
87
+ **2.** Choose **GroupMe** from the channel list (use the arrow keys to select it).
72
88
 
73
- **4.** Enter a **bot name**. This is the display name your bot will use in the group (e.g., `openclaw`). This name is also used for mention detection.
89
+ **3.** Enter a **bot name**. This is the display name your bot will use in the group (e.g., `openclaw`). This name is also used for mention detection.
74
90
 
75
- **5.** Paste your **GroupMe access token** when prompted.
91
+ **4.** Paste your **GroupMe access token** when prompted.
76
92
 
77
- **6.** The wizard fetches your groups from GroupMe. **Select the group** you want the bot to live in.
93
+ **5.** The wizard fetches your groups from GroupMe. **Select the group** you want the bot to live in.
78
94
 
79
- **7.** Choose whether to **require an @mention**:
95
+ **6.** Choose whether to **require an @mention**:
80
96
  - **Yes** — The bot only responds when someone mentions it by name (e.g., "hey @openclaw, what's the weather?"). This is great for groups with multiple people where you don't want the bot jumping into every conversation.
81
97
  - **No** — The bot responds to every message in the group. Perfect if you're the only human in the group and want a direct chat experience.
82
98
 
83
- **8.** Enter the **public domain** that will host your callback URL. This should be the domain name (or public IP) that can reach your OpenClaw gateway — for example, `bot.example.com`. The wizard generates a secure callback URL with a random path and secret token.
99
+ **7.** Enter the **public domain** that will host your callback URL. This should be the domain name (or public IP) that can reach your OpenClaw gateway — for example, `bot.example.com`. The wizard generates a secure callback URL with a random path and secret token, then registers the bot with GroupMe.
84
100
 
85
- **9.** The wizard registers the bot with GroupMe and writes the config. You'll see a summary of what was created.
101
+ **8.** The wizard writes the config and shows a summary of what was created.
86
102
 
87
- **10.** Restart the gateway:
103
+ **9.** Restart the gateway:
88
104
 
89
105
  ```bash
90
106
  openclaw gateway restart
91
107
  ```
92
108
 
93
- **11.** Send a test message in the GroupMe group. If everything is wired up correctly, your bot should respond. Make sure your reverse proxy or port forwarding is configured to route the callback URL to your gateway.
109
+ **10.** Send a test message in the GroupMe group. If everything is wired up correctly, your bot should respond. Make sure your reverse proxy or port forwarding is configured to route the callback URL to your gateway.
110
+
111
+ > **After the wizard — hardening secrets (recommended for production):** the wizard writes your `accessToken`, `botId`, and `callbackToken` as plaintext literals in `openclaw.json`. That's fine for local testing, but for production you should migrate them to [SecretRefs](#secrets) so no plaintext credentials live in your config. See [Secrets](#secrets) below.
94
112
 
95
113
  ## Setup: Non-Interactive CLI
96
114
 
@@ -102,7 +120,7 @@ If you already have a GroupMe bot created (maybe from the [Bots page](https://de
102
120
  openclaw channels add --channel groupme \
103
121
  --token "YOUR_GROUPME_BOT_ID" \
104
122
  --access-token "YOUR_GROUPME_ACCESS_TOKEN" \
105
- --webhook-url "/groupme/callback?k=YOUR_SECRET"
123
+ --webhook-path "/groupme/callback"
106
124
  ```
107
125
 
108
126
  For named accounts (useful if you're running multiple bots):
@@ -113,26 +131,28 @@ openclaw channels add --channel groupme \
113
131
  --name "Work Bot" \
114
132
  --token "YOUR_GROUPME_BOT_ID" \
115
133
  --access-token "YOUR_GROUPME_ACCESS_TOKEN" \
116
- --webhook-url "/groupme/callback?k=YOUR_SECRET"
134
+ --webhook-path "/groupme/callback"
117
135
  ```
118
136
 
119
137
  | Flag | Maps to config | Description |
120
138
  | ---- | -------------- | ----------- |
121
139
  | `--token` | `botId` | Your GroupMe Bot ID |
122
140
  | `--access-token` | `accessToken` | Your GroupMe access token |
123
- | `--webhook-url` | `callbackUrl` | Full relative webhook URL (path + query token) |
124
- | `--webhook-path` | `callbackUrl` | Alias for `--webhook-url` |
141
+ | `--webhook-url` | `webhookPath` (+ `callbackToken` if a `?k=` is present) | Full webhook URL; the path and `k` token are extracted |
142
+ | `--webhook-path` | `webhookPath` (+ `callbackToken` if a `?k=` is present) | Relative webhook route path |
125
143
  | `--account` | account ID | Named account identifier |
126
144
  | `--name` | `name` | Display name for the account |
127
145
 
128
- > **Note:** The non-interactive CLI does not prompt for `botName`, `groupId`, `requireMention`, or `publicDomain`. You can add these manually in your config file afterward, or set them via environment variables.
146
+ > **Note:** The non-interactive CLI does not prompt for `botName`, `groupId`, `requireMention`, `publicDomain`, or `callbackToken`. Add those manually afterward (a `?k=<token>` on `--webhook-path`/`--webhook-url` is the one exception — it's parsed into `callbackToken`), or use the interactive wizard to generate complete webhook settings.
129
147
 
130
- After adding the channel, make sure the callback URL you gave GroupMe (when you created the bot) matches what you passed to `--webhook-url`. Then restart the gateway:
148
+ After adding the channel, make sure the callback URL you gave GroupMe uses the configured `webhookPath` and `callbackToken`. Then restart the gateway:
131
149
 
132
150
  ```bash
133
151
  openclaw gateway restart
134
152
  ```
135
153
 
154
+ Run `openclaw channels add --help` to see the full list of per-channel flags.
155
+
136
156
  ## Manual Config Example
137
157
 
138
158
  If you prefer editing config files directly, here's what a complete setup looks like:
@@ -147,21 +167,62 @@ If you prefer editing config files directly, here's what a complete setup looks
147
167
  "botId": "YOUR_GROUPME_BOT_ID",
148
168
  "groupId": "YOUR_GROUPME_GROUP_ID",
149
169
  "publicDomain": "bot.example.com",
150
- "callbackUrl": "/groupme/e60b3e59da98950f?k=YOUR_SECRET_TOKEN",
170
+ "webhookPath": "/groupme/e60b3e59da98950f",
171
+ "callbackToken": "YOUR_SECRET_TOKEN",
151
172
  "requireMention": true
152
173
  }
153
174
  }
154
175
  }
155
176
  ```
156
177
 
178
+ ## Multiple Accounts
179
+
180
+ Each GroupMe bot is bound to a single group, so running the bot in more than one group means configuring more than one account. Top-level fields act as the `default` account, and additional accounts live under `accounts.<id>`. Named accounts inherit any fields you don't override.
181
+
182
+ ```json
183
+ {
184
+ "channels": {
185
+ "groupme": {
186
+ "enabled": true,
187
+ "defaultAccount": "family",
188
+ "botName": "openclaw",
189
+ "accounts": {
190
+ "family": {
191
+ "name": "Family Bot",
192
+ "accessToken": "FAMILY_ACCESS_TOKEN",
193
+ "botId": "FAMILY_BOT_ID",
194
+ "groupId": "FAMILY_GROUP_ID",
195
+ "publicDomain": "bot.example.com",
196
+ "webhookPath": "/groupme/family-a1b2c3",
197
+ "callbackToken": "FAMILY_SECRET_TOKEN",
198
+ "requireMention": false
199
+ },
200
+ "work": {
201
+ "name": "Work Bot",
202
+ "accessToken": "WORK_ACCESS_TOKEN",
203
+ "botId": "WORK_BOT_ID",
204
+ "groupId": "WORK_GROUP_ID",
205
+ "publicDomain": "bot.example.com",
206
+ "webhookPath": "/groupme/work-d4e5f6",
207
+ "callbackToken": "WORK_SECRET_TOKEN",
208
+ "requireMention": true
209
+ }
210
+ }
211
+ }
212
+ }
213
+ }
214
+ ```
215
+
216
+ Each account needs its **own** `webhookPath` and `callbackToken`, since each registered bot has its own callback URL.
217
+
157
218
  ## Reconfiguring an Existing Setup
158
219
 
159
- If GroupMe is already configured and you run `openclaw configure` again, you'll get a menu of targeted actions instead of repeating the full wizard:
220
+ If GroupMe is already configured and you run `openclaw configure --section channels` (or `openclaw channels add`) again, you'll get a menu of targeted actions instead of repeating the full wizard:
160
221
 
161
222
  - **Skip** — leave everything as-is
162
223
  - **Rotate access token** — replace the stored access token (validates the new token by fetching your groups)
163
224
  - **Change group** — pick a different GroupMe group and optionally register a new bot in it
164
- - **Regenerate callback URL** — create a new random callback path and secret token (you'll need to update the bot's callback URL in GroupMe afterward)
225
+ - **Regenerate webhook settings** — create a new random webhook path and callback token (you'll need to update the bot's callback URL in GroupMe afterward)
165
226
  - **Toggle requireMention** — flip mention-required mode on or off
166
227
  - **Update public domain** — change the public domain used for callback URLs
167
228
  - **Full re-setup** — start from scratch with the interactive wizard
@@ -231,7 +292,7 @@ The plugin ships with some security defaults out of the box. Replay protection a
231
292
  Every incoming webhook request goes through this gauntlet before your agent ever sees it:
232
293
 
233
294
  1. **Method check** — Only `POST` requests are accepted (405 for everything else)
234
- 2. **Callback token auth** — The `k` query parameter in the callback URL is verified using timing-safe comparison
295
+ 2. **Callback token auth** — The `k` query parameter in the callback URL is verified against `callbackToken` using timing-safe comparison
235
296
  3. **Proxy validation** — If configured, validates trusted proxy headers, allowed hosts, and HTTPS protocol
236
297
  4. **Body parsing** — 64KB size limit, 15-second timeout
237
298
  5. **Payload parsing** — Extracts the GroupMe callback data and filters out bot messages, system messages, and empty messages
@@ -239,6 +300,8 @@ Every incoming webhook request goes through this gauntlet before your agent ever
239
300
  7. **Replay protection** — SHA-256 keyed deduplication with a sliding TTL window
240
301
  8. **Rate limiting** — Per-IP, per-sender, and global concurrency caps
241
302
 
303
+ Accepted requests get an immediate `200 ok` and the message is processed asynchronously, so GroupMe never waits on your agent.
304
+
242
305
  ### Outbound Media Security
243
306
 
244
307
  When the bot sends image replies, outbound media fetches are hardened with:
@@ -296,6 +359,8 @@ You only need a `security` block if you want to override the defaults. Just incl
296
359
  | `security.media.requestTimeoutMs` | number | `10000` | Timeout for outbound media fetch requests (ms) |
297
360
  | `security.media.allowedMimePrefixes` | string[] | `["image/"]` | Allowed MIME type prefixes for outbound media |
298
361
 
362
+ > **Inbound vs. outbound media limits:** the `security.media.*` settings above govern **outbound** media — images the bot downloads from a URL and re-uploads to GroupMe. The separate top-level `mediaMaxMb` field governs **inbound** media the OpenClaw runtime fetches from GroupMe callbacks. They are independent knobs.
363
+
299
364
  #### Logging
300
365
 
301
366
  | Field | Type | Default | Description |
@@ -321,47 +386,78 @@ Include a `proxy` block to enable trusted-proxy validation. This is useful when
321
386
  | `security.proxy.requireHttpsProto` | boolean | `false` | Require the effective protocol to be HTTPS |
322
387
  | `security.proxy.rejectStatus` | number | `403` | HTTP status for proxy-policy rejections (`400`, `403`, or `404`) |
323
388
 
324
- ## Environment Variables
389
+ ## Secrets
390
+
391
+ `botId`, `accessToken`, and `callbackToken` are OpenClaw secret inputs. You can store literal values, but **for production the recommended approach is SecretRefs** so no plaintext credentials live in your config — this matches OpenClaw's [secrets guidance](https://docs.openclaw.ai/gateway/secrets) and how official channels like Slack and Discord document setup. Plaintext is fully supported and fine for quick local testing.
325
392
 
326
- For the default account, you can use environment variables instead of (or alongside) config file values. This is handy for CI/CD, Docker deployments, or anywhere you'd rather not put secrets in a config file.
393
+ The plugin does **not** read environment variables on its own. The `GROUPME_BOT_ID`, `GROUPME_ACCESS_TOKEN`, and `GROUPME_CALLBACK_TOKEN` names it declares (as `channelEnvVars`) are what OpenClaw surfaces as **env-backed SecretRefs** and for setup tooling — to actually use them, reference them from config with a SecretRef (shown below), or let the setup wizard write the config for you. Setting those env vars alone, without a SecretRef in config, is not enough to configure the channel.
327
394
 
328
- | Variable | Maps to config | Description |
329
- | -------- | -------------- | ----------- |
330
- | `GROUPME_BOT_ID` | `botId` | GroupMe Bot ID |
331
- | `GROUPME_ACCESS_TOKEN` | `accessToken` | GroupMe access token |
332
- | `GROUPME_BOT_NAME` | `botName` | Bot display name |
333
- | `GROUPME_GROUP_ID` | `groupId` | GroupMe group ID |
334
- | `GROUPME_PUBLIC_DOMAIN` | `publicDomain` | Public domain for the callback URL |
335
- | `GROUPME_CALLBACK_URL` | `callbackUrl` | Relative webhook URL (path + query token) |
395
+ ```json
396
+ {
397
+ "channels": {
398
+ "groupme": {
399
+ "botId": { "source": "env", "provider": "default", "id": "GROUPME_BOT_ID" },
400
+ "accessToken": { "source": "env", "provider": "default", "id": "GROUPME_ACCESS_TOKEN" },
401
+ "callbackToken": { "source": "env", "provider": "default", "id": "GROUPME_CALLBACK_TOKEN" },
402
+ "groupId": "YOUR_GROUPME_GROUP_ID",
403
+ "webhookPath": "/groupme/callback"
404
+ }
405
+ }
406
+ }
407
+ ```
336
408
 
337
- If both a config value and an environment variable are set, the **config value wins**. Environment variables only apply to the default account — named accounts must be configured in the config file.
409
+ SecretRefs also support `file`- and `exec`-backed providers, and work per account (under `accounts.<id>`), not just the default account.
410
+
411
+ ### Migrating wizard-written plaintext to SecretRefs
412
+
413
+ The setup wizard writes plaintext credentials. To convert them to SecretRefs (and scrub the plaintext residue), use OpenClaw's secrets workflow — GroupMe's `botId`, `accessToken`, and `callbackToken` are registered targets, so they show up in the planner automatically:
414
+
415
+ ```bash
416
+ openclaw secrets configure # plan the migration (interactive)
417
+ openclaw secrets apply --from <plan> # write SecretRefs and scrub plaintext
418
+ openclaw secrets audit --check # verify no plaintext residue remains
419
+ openclaw secrets reload # re-resolve refs into the runtime snapshot
420
+ ```
338
421
 
339
422
  ## Config Reference
340
423
 
341
424
  | Field | Type | Default | Description |
342
425
  | ----- | ---- | ------- | ----------- |
343
- | `botId` | string | | GroupMe Bot ID |
344
- | `accessToken` | string | — | GroupMe access token (required for image uploads and the interactive wizard) |
426
+ | `enabled` | boolean | `true` | Whether the GroupMe channel/account is active; only an explicit `false` disables it. An account still needs a `botId` before it will actually run. |
427
+ | `name` | string | — | Display name for the account |
428
+ | `botId` | secret input | — | GroupMe Bot ID |
429
+ | `accessToken` | secret input | — | GroupMe access token (required for image uploads and the interactive wizard) |
430
+ | `callbackToken` | secret input | — | Secret expected in the inbound `k` query parameter |
345
431
  | `botName` | string | — | Bot display name, used for mention detection |
346
432
  | `groupId` | string | — | Expected GroupMe `group_id` for inbound binding |
347
433
  | `publicDomain` | string | — | Public domain where the gateway is reachable (e.g., `bot.example.com`) |
348
- | `callbackUrl` | string | `/groupme` | Relative webhook URL including query token |
434
+ | `webhookPath` | string | `/groupme` | Relative webhook route path |
349
435
  | `requireMention` | boolean | `true` | Only respond when mentioned by name |
350
- | `historyLimit` | number | `20` | Max buffered messages per group (when `requireMention: true`) |
436
+ | `historyLimit` | number | `20` | Max buffered messages per group (when `requireMention: true`); `0` disables buffering |
351
437
  | `mentionPatterns` | string[] | — | Custom regex patterns for mention detection |
352
438
  | `allowFrom` | array | — | Sender allowlist (`"*"` allows everyone) |
353
- | `textChunkLimit` | number | `1000` | Max characters per outbound text chunk |
439
+ | `textChunkLimit` | number | `1000` | Max characters per outbound text chunk (capped at GroupMe's 1000-char limit) |
440
+ | `responsePrefix` | string | — | Text prepended to each outbound reply |
441
+ | `blockStreaming` | boolean | unset (OpenClaw default) | Override block streaming for this channel. When unset, OpenClaw's dispatcher default applies; set `true` to stream completed assistant blocks as separate messages, or `false` to send a single final reply |
442
+ | `blockStreamingCoalesce` | object | — | Fine-tunes how streamed blocks are coalesced (see OpenClaw docs) |
443
+ | `markdown` | object | — | Markdown rendering overrides for outbound messages |
444
+ | `mediaMaxMb` | number | — | Max size (MB) for **inbound** media the OpenClaw runtime fetches from GroupMe. Distinct from `security.media.maxDownloadBytes`, which caps **outbound** media the bot downloads before re-uploading. |
354
445
  | `security` | object | — | Security overrides (see [Security](#security) section above) |
446
+ | `accounts` | object | — | Named accounts (`accounts.<id>`), each accepting the fields above |
447
+ | `defaultAccount` | string | — | Which named account is the default for outbound routing |
355
448
 
356
- ## Callback URL Format
449
+ ## Webhook URL Format
357
450
 
358
- The `callbackUrl` stores the full relative webhook URL (path + secret query token):
451
+ The plugin stores the route path and secret separately:
359
452
 
360
- ```
361
- /groupme/e60b3e59da98950f?k=775c9958da544c73e6d97c04f884957caa174c8570889bbaa0900d6253f20bbc
453
+ ```json
454
+ {
455
+ "webhookPath": "/groupme/e60b3e59da98950f",
456
+ "callbackToken": "775c9958da544c73e6d97c04f884957caa174c8570889bbaa0900d6253f20bbc"
457
+ }
362
458
  ```
363
459
 
364
- The full URL you register with GroupMe is your public domain + this path:
460
+ The full URL you register with GroupMe is your public domain plus the path and `k` token:
365
461
 
366
462
  ```
367
463
  https://bot.example.com/groupme/e60b3e59da98950f?k=775c9958da544c73e6d97c04f884957caa174c8570889bbaa0900d6253f20bbc
@@ -390,14 +486,14 @@ GroupMe bots are intentionally limited compared to full user accounts. These con
390
486
  ## Troubleshooting
391
487
 
392
488
  - **Bot doesn't respond:**
393
- - Is your webhook URL public, HTTPS, and matching the `callbackUrl` in config?
489
+ - Is your webhook URL public, HTTPS, and matching `webhookPath` plus `callbackToken`?
394
490
  - Does `groupId` match the actual GroupMe group ID?
395
491
  - Is `botId` correct?
396
492
  - If `requireMention: true`, are you mentioning the bot by name?
397
493
  - Check `allowFrom` if you have a sender allowlist configured
398
494
 
399
495
  - **Webhook returns 404 or 403:**
400
- - Verify the `k` token in the URL matches what's in `callbackUrl`
496
+ - Verify the `k` token in the URL matches `callbackToken`
401
497
  - Check `groupId` binding
402
498
  - If using proxy validation, check your `security.proxy` settings
403
499
 
@@ -425,6 +521,12 @@ openclaw channels logs --channel groupme
425
521
  openclaw channels status --probe
426
522
  ```
427
523
 
524
+ ## Resources
525
+
526
+ - [OpenClaw documentation](https://docs.openclaw.ai) · [Channels](https://docs.openclaw.ai/channels) · [Plugins](https://docs.openclaw.ai/plugins)
527
+ - [GroupMe developer portal](https://dev.groupme.com) · [Bots](https://dev.groupme.com/bots)
528
+ - [Contributing guide](CONTRIBUTING.md) · [Security policy](SECURITY.md) · [Changelog](CHANGELOG.md)
529
+
428
530
  ## License
429
531
 
430
532
  [MIT](LICENSE)
@@ -0,0 +1,3 @@
1
+ // Keep channel entry imports narrow so bootstrap/discovery paths do not pull
2
+ // setup-only GroupMe surfaces into lightweight channel plugin loads.
3
+ export { groupmePlugin } from "./src/channel.js";
@@ -0,0 +1,3 @@
1
+ // Keep channel entry imports narrow so bootstrap/discovery paths do not pull
2
+ // setup-only GroupMe surfaces into lightweight channel plugin loads.
3
+ export { groupmePlugin } from "./src/channel.js";
package/dist/index.js CHANGED
@@ -1,14 +1,20 @@
1
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
2
- import { groupmePlugin } from "./src/channel.js";
3
- import { setGroupMeRuntime } from "./src/runtime.js";
4
- const plugin = {
1
+ import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
2
+ const plugin = defineBundledChannelEntry({
5
3
  id: "groupme",
6
4
  name: "GroupMe",
7
5
  description: "GroupMe channel plugin",
8
- configSchema: emptyPluginConfigSchema(),
9
- register(api) {
10
- setGroupMeRuntime(api.runtime);
11
- api.registerChannel({ plugin: groupmePlugin });
6
+ importMetaUrl: import.meta.url,
7
+ plugin: {
8
+ specifier: "./channel-plugin-api.js",
9
+ exportName: "groupmePlugin",
12
10
  },
13
- };
11
+ secrets: {
12
+ specifier: "./secret-contract-api.js",
13
+ exportName: "channelSecrets",
14
+ },
15
+ runtime: {
16
+ specifier: "./runtime-setter-api.js",
17
+ exportName: "setGroupMeRuntime",
18
+ },
19
+ });
14
20
  export default plugin;
@@ -0,0 +1 @@
1
+ export { setGroupMeRuntime } from "./src/runtime.js";
@@ -0,0 +1 @@
1
+ export { channelSecrets, collectRuntimeConfigAssignments, secretTargetRegistryEntries, } from "./src/secret-contract.js";
@@ -0,0 +1,16 @@
1
+ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entry-contract";
2
+ export default defineBundledChannelSetupEntry({
3
+ importMetaUrl: import.meta.url,
4
+ plugin: {
5
+ specifier: "./setup-plugin-api.js",
6
+ exportName: "groupmeSetupPlugin",
7
+ },
8
+ secrets: {
9
+ specifier: "./secret-contract-api.js",
10
+ exportName: "channelSecrets",
11
+ },
12
+ runtime: {
13
+ specifier: "./runtime-setter-api.js",
14
+ exportName: "setGroupMeRuntime",
15
+ },
16
+ });
@@ -0,0 +1,3 @@
1
+ // Keep setup entry imports narrow while reusing the same channel plugin setup
2
+ // adapter until GroupMe grows a separate setup-only surface.
3
+ export { groupmePlugin as groupmeSetupPlugin } from "./src/channel.js";
@@ -1,10 +1,4 @@
1
- import { DEFAULT_ACCOUNT_ID, normalizeAccountId, } from "openclaw/plugin-sdk/account-id";
2
- const ENV_BOT_ID = "GROUPME_BOT_ID";
3
- const ENV_ACCESS_TOKEN = "GROUPME_ACCESS_TOKEN";
4
- const ENV_BOT_NAME = "GROUPME_BOT_NAME";
5
- const ENV_CALLBACK_URL = "GROUPME_CALLBACK_URL";
6
- const ENV_GROUP_ID = "GROUPME_GROUP_ID";
7
- const ENV_PUBLIC_DOMAIN = "GROUPME_PUBLIC_DOMAIN";
1
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
8
2
  export function readTrimmed(value) {
9
3
  if (typeof value !== "string") {
10
4
  return undefined;
@@ -12,6 +6,15 @@ export function readTrimmed(value) {
12
6
  const trimmed = value.trim();
13
7
  return trimmed || undefined;
14
8
  }
9
+ export function hasSecretInput(value) {
10
+ if (typeof value === "string") {
11
+ return Boolean(value.trim());
12
+ }
13
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
14
+ }
15
+ function trimSecretInput(value) {
16
+ return typeof value === "string" ? readTrimmed(value) : value;
17
+ }
15
18
  function listConfiguredAccountIds(cfg) {
16
19
  const accounts = cfg.channels?.groupme?.accounts;
17
20
  if (!accounts || typeof accounts !== "object") {
@@ -40,9 +43,7 @@ function resolveAccountConfig(cfg, accountId) {
40
43
  function mergeAccountConfig(cfg, accountId) {
41
44
  const raw = (cfg.channels?.groupme ?? {});
42
45
  const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
43
- const account = accountId === DEFAULT_ACCOUNT_ID
44
- ? {}
45
- : (resolveAccountConfig(cfg, accountId) ?? {});
46
+ const account = accountId === DEFAULT_ACCOUNT_ID ? {} : (resolveAccountConfig(cfg, accountId) ?? {});
46
47
  return {
47
48
  ...base,
48
49
  ...account,
@@ -50,10 +51,7 @@ function mergeAccountConfig(cfg, accountId) {
50
51
  }
51
52
  export function listGroupMeAccountIds(cfg) {
52
53
  const sorted = listConfiguredAccountIds(cfg).toSorted((a, b) => a.localeCompare(b));
53
- return [
54
- DEFAULT_ACCOUNT_ID,
55
- ...sorted.filter((id) => id !== DEFAULT_ACCOUNT_ID),
56
- ];
54
+ return [DEFAULT_ACCOUNT_ID, ...sorted.filter((id) => id !== DEFAULT_ACCOUNT_ID)];
57
55
  }
58
56
  export function resolveDefaultGroupMeAccountId(cfg) {
59
57
  const configuredDefault = readTrimmed(cfg.channels?.groupme?.defaultAccount);
@@ -64,54 +62,32 @@ export function resolveDefaultGroupMeAccountId(cfg) {
64
62
  }
65
63
  export function resolveGroupMeAccount(params) {
66
64
  const normalizedRequested = normalizeAccountId(params.accountId);
67
- const accountId = normalizedRequested ||
68
- resolveDefaultGroupMeAccountId(params.cfg) ||
69
- DEFAULT_ACCOUNT_ID;
65
+ const accountId = normalizedRequested || resolveDefaultGroupMeAccountId(params.cfg) || DEFAULT_ACCOUNT_ID;
70
66
  const merged = mergeAccountConfig(params.cfg, accountId);
71
67
  const baseEnabled = params.cfg.channels?.groupme?.enabled !== false;
72
68
  const accountEnabled = merged.enabled !== false;
73
69
  const enabled = baseEnabled && accountEnabled;
74
- const isDefaultAccount = accountId === DEFAULT_ACCOUNT_ID;
75
- const botId = readTrimmed(merged.botId) ||
76
- (isDefaultAccount ? readTrimmed(process.env[ENV_BOT_ID]) : undefined) ||
77
- "";
78
- const accessToken = readTrimmed(merged.accessToken) ||
79
- (isDefaultAccount
80
- ? readTrimmed(process.env[ENV_ACCESS_TOKEN])
81
- : undefined) ||
82
- "";
83
- const botName = readTrimmed(merged.botName) ||
84
- (isDefaultAccount ? readTrimmed(process.env[ENV_BOT_NAME]) : undefined) ||
85
- undefined;
86
- const groupId = readTrimmed(merged.groupId) ||
87
- (isDefaultAccount
88
- ? readTrimmed(process.env[ENV_GROUP_ID])
89
- : undefined) ||
90
- undefined;
91
- const callbackUrl = readTrimmed(merged.callbackUrl) ||
92
- (isDefaultAccount
93
- ? readTrimmed(process.env[ENV_CALLBACK_URL])
94
- : undefined) ||
95
- undefined;
96
- const publicDomain = readTrimmed(merged.publicDomain) ||
97
- (isDefaultAccount
98
- ? readTrimmed(process.env[ENV_PUBLIC_DOMAIN])
99
- : undefined) ||
100
- undefined;
70
+ const botId = readTrimmed(merged.botId) ?? "";
71
+ const accessToken = readTrimmed(merged.accessToken) ?? "";
72
+ const botName = readTrimmed(merged.botName);
73
+ const groupId = readTrimmed(merged.groupId);
74
+ const webhookPath = readTrimmed(merged.webhookPath);
75
+ const publicDomain = readTrimmed(merged.publicDomain);
101
76
  const config = {
102
77
  ...merged,
103
- botId,
104
- accessToken,
78
+ botId: trimSecretInput(merged.botId),
79
+ accessToken: trimSecretInput(merged.accessToken),
80
+ callbackToken: trimSecretInput(merged.callbackToken),
105
81
  botName,
106
82
  groupId,
107
83
  publicDomain,
108
- callbackUrl,
84
+ webhookPath,
109
85
  };
110
86
  return {
111
87
  accountId,
112
88
  name: readTrimmed(merged.name),
113
89
  enabled,
114
- configured: Boolean(botId),
90
+ configured: hasSecretInput(merged.botId),
115
91
  botId,
116
92
  accessToken,
117
93
  config,