openclaw-groupme 0.0.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.
package/README.md ADDED
@@ -0,0 +1,121 @@
1
+ # openclaw-groupme
2
+
3
+ GroupMe channel plugin for OpenClaw (GroupMe Bot API, group chats only).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ openclaw plugins install openclaw-groupme
9
+ ```
10
+
11
+ Restart the gateway after installing the plugin.
12
+
13
+ ## What this plugin needs
14
+
15
+ 1. A GroupMe bot (from https://dev.groupme.com/bots)
16
+ 2. A public HTTPS URL that can reach your OpenClaw gateway webhook endpoint
17
+ 3. Your GroupMe `bot_id` (required)
18
+ 4. Your GroupMe `access token` (recommended, required for image uploads)
19
+
20
+ ## Step-by-step setup
21
+
22
+ 1. Install plugin:
23
+
24
+ ```bash
25
+ openclaw plugins install openclaw-groupme
26
+ ```
27
+
28
+ 2. Create a GroupMe bot:
29
+ - Go to https://dev.groupme.com/bots
30
+ - Create/select a bot for your target group
31
+ - Copy the bot's `bot_id`
32
+ - Copy your GroupMe `access token`
33
+
34
+ 3. Configure OpenClaw:
35
+ - Option A (interactive):
36
+
37
+ ```bash
38
+ openclaw channels add --channel groupme
39
+ ```
40
+
41
+ - Option B (manual config): add this under your OpenClaw config:
42
+
43
+ ```json5
44
+ {
45
+ channels: {
46
+ groupme: {
47
+ enabled: true,
48
+ botId: "YOUR_GROUPME_BOT_ID",
49
+ accessToken: "YOUR_GROUPME_ACCESS_TOKEN",
50
+ botName: "openclaw",
51
+ callbackPath: "/groupme",
52
+ requireMention: true
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ 4. Point GroupMe to your webhook:
59
+ - Callback URL format: `https://<your-public-domain><callbackPath>`
60
+ - Example: `https://bot.example.com/groupme`
61
+ - Set this URL in the GroupMe bot settings
62
+
63
+ 5. Restart OpenClaw gateway:
64
+
65
+ ```bash
66
+ openclaw gateway restart
67
+ ```
68
+
69
+ 6. Verify channel status:
70
+
71
+ ```bash
72
+ openclaw channels status --probe
73
+ ```
74
+
75
+ 7. Send a test message in the GroupMe group:
76
+ - With default settings, the bot only responds when mentioned (`requireMention: true`)
77
+ - Mention either `@<botName>` or a configured mention pattern
78
+
79
+ ## Config reference (common fields)
80
+
81
+ - `botId` (string, required): GroupMe Bot ID
82
+ - `accessToken` (string): needed for image upload / media replies
83
+ - `botName` (string): mention fallback name used by mention detection
84
+ - `callbackPath` (string, default `/groupme`): webhook route path
85
+ - `requireMention` (boolean, default `true`): require mention before responding
86
+ - `mentionPatterns` (string[]): custom regex patterns that count as a mention
87
+ - `allowFrom` (array of string/number): sender allowlist (`"*"` to allow all)
88
+ - `textChunkLimit` (number): max outbound text chunk size (capped at 1000)
89
+
90
+ ## Environment variables (default account fallback)
91
+
92
+ For the default account only, these env vars are supported:
93
+
94
+ - `GROUPME_BOT_ID`
95
+ - `GROUPME_ACCESS_TOKEN`
96
+ - `GROUPME_BOT_NAME`
97
+ - `GROUPME_CALLBACK_PATH`
98
+
99
+ If both config and env are set, config values take precedence.
100
+
101
+ ## Notes and limitations
102
+
103
+ - Group chats only (no DM channel mode)
104
+ - Inbound bot/system messages are ignored
105
+ - GroupMe message text limit is 1000 chars per chunk
106
+ - Media replies require `accessToken` so OpenClaw can upload images to GroupMe
107
+
108
+ ## Troubleshooting
109
+
110
+ - Bot does not respond:
111
+ - Confirm webhook URL is public + HTTPS and matches `callbackPath`
112
+ - Confirm `botId` is correct
113
+ - If `requireMention: true`, mention the bot in the message
114
+ - Check `allowFrom` (if set)
115
+ - Image replies fail:
116
+ - Ensure `accessToken` is configured
117
+ - Check runtime logs:
118
+
119
+ ```bash
120
+ openclaw channels logs --channel groupme
121
+ ```
package/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ import type { ChannelPlugin, OpenClawPluginApi } from "openclaw/plugin-sdk";
2
+ import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
3
+ import { groupmePlugin } from "./src/channel.js";
4
+ import { setGroupMeRuntime } from "./src/runtime.js";
5
+
6
+ const plugin = {
7
+ id: "groupme",
8
+ name: "GroupMe",
9
+ description: "GroupMe channel plugin",
10
+ configSchema: emptyPluginConfigSchema(),
11
+ register(api: OpenClawPluginApi) {
12
+ setGroupMeRuntime(api.runtime);
13
+ api.registerChannel({ plugin: groupmePlugin as ChannelPlugin });
14
+ },
15
+ };
16
+
17
+ export default plugin;
@@ -0,0 +1,9 @@
1
+ {
2
+ "id": "groupme",
3
+ "channels": ["groupme"],
4
+ "configSchema": {
5
+ "type": "object",
6
+ "additionalProperties": false,
7
+ "properties": {}
8
+ }
9
+ }
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "openclaw-groupme",
3
+ "version": "0.0.1",
4
+ "description": "OpenClaw GroupMe channel plugin",
5
+ "type": "module",
6
+ "dependencies": {
7
+ "zod": "^4.3.6"
8
+ },
9
+ "devDependencies": {
10
+ "openclaw": "workspace:*"
11
+ },
12
+ "openclaw": {
13
+ "extensions": [
14
+ "./index.ts"
15
+ ],
16
+ "channel": {
17
+ "id": "groupme",
18
+ "label": "GroupMe",
19
+ "selectionLabel": "GroupMe (Bot API)",
20
+ "docsPath": "/channels/groupme",
21
+ "docsLabel": "groupme",
22
+ "blurb": "GroupMe bot webhook integration (group chats only).",
23
+ "order": 95,
24
+ "quickstartAllowFrom": true
25
+ },
26
+ "install": {
27
+ "npmSpec": "@openclaw/groupme",
28
+ "localPath": "extensions/groupme",
29
+ "defaultChoice": "npm"
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,139 @@
1
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "openclaw/plugin-sdk/account-id";
2
+ import type {
3
+ CoreConfig,
4
+ GroupMeAccountConfig,
5
+ GroupMeConfig,
6
+ ResolvedGroupMeAccount,
7
+ } from "./types.js";
8
+
9
+ const ENV_BOT_ID = "GROUPME_BOT_ID";
10
+ const ENV_ACCESS_TOKEN = "GROUPME_ACCESS_TOKEN";
11
+ const ENV_BOT_NAME = "GROUPME_BOT_NAME";
12
+ const ENV_CALLBACK_PATH = "GROUPME_CALLBACK_PATH";
13
+
14
+ function readTrimmed(value: unknown): string | undefined {
15
+ if (typeof value !== "string") {
16
+ return undefined;
17
+ }
18
+ const trimmed = value.trim();
19
+ return trimmed || undefined;
20
+ }
21
+
22
+ function listConfiguredAccountIds(cfg: CoreConfig): string[] {
23
+ const accounts = cfg.channels?.groupme?.accounts;
24
+ if (!accounts || typeof accounts !== "object") {
25
+ return [];
26
+ }
27
+
28
+ const ids = new Set<string>();
29
+ for (const key of Object.keys(accounts)) {
30
+ const normalized = normalizeAccountId(key);
31
+ if (normalized) {
32
+ ids.add(normalized);
33
+ }
34
+ }
35
+
36
+ return [...ids];
37
+ }
38
+
39
+ function resolveAccountConfig(
40
+ cfg: CoreConfig,
41
+ accountId: string,
42
+ ): GroupMeAccountConfig | undefined {
43
+ const accounts = cfg.channels?.groupme?.accounts;
44
+ if (!accounts || typeof accounts !== "object") {
45
+ return undefined;
46
+ }
47
+
48
+ if (Object.hasOwn(accounts, accountId)) {
49
+ return accounts[accountId];
50
+ }
51
+
52
+ const hit = Object.keys(accounts).find((key) => normalizeAccountId(key) === accountId);
53
+ return hit ? accounts[hit] : undefined;
54
+ }
55
+
56
+ function mergeAccountConfig(cfg: CoreConfig, accountId: string): GroupMeAccountConfig {
57
+ const raw = (cfg.channels?.groupme ?? {}) as GroupMeConfig;
58
+ const { accounts: _ignored, defaultAccount: _ignored2, ...base } = raw;
59
+ const account =
60
+ accountId === DEFAULT_ACCOUNT_ID ? {} : (resolveAccountConfig(cfg, accountId) ?? {});
61
+
62
+ return {
63
+ ...base,
64
+ ...account,
65
+ };
66
+ }
67
+
68
+ export function listGroupMeAccountIds(cfg: CoreConfig): string[] {
69
+ const ids = new Set<string>([DEFAULT_ACCOUNT_ID]);
70
+ for (const id of listConfiguredAccountIds(cfg)) {
71
+ ids.add(id);
72
+ }
73
+
74
+ const ordered = [...ids].toSorted((a, b) => a.localeCompare(b));
75
+ if (ordered[0] !== DEFAULT_ACCOUNT_ID) {
76
+ ordered.unshift(DEFAULT_ACCOUNT_ID);
77
+ return Array.from(new Set(ordered));
78
+ }
79
+ return ordered;
80
+ }
81
+
82
+ export function resolveDefaultGroupMeAccountId(cfg: CoreConfig): string {
83
+ const configuredDefault = readTrimmed(cfg.channels?.groupme?.defaultAccount);
84
+ if (configuredDefault) {
85
+ return normalizeAccountId(configuredDefault);
86
+ }
87
+
88
+ return DEFAULT_ACCOUNT_ID;
89
+ }
90
+
91
+ export function resolveGroupMeAccount(params: {
92
+ cfg: CoreConfig;
93
+ accountId?: string | null;
94
+ }): ResolvedGroupMeAccount {
95
+ const normalizedRequested = normalizeAccountId(params.accountId);
96
+ const accountId =
97
+ normalizedRequested || resolveDefaultGroupMeAccountId(params.cfg) || DEFAULT_ACCOUNT_ID;
98
+
99
+ const merged = mergeAccountConfig(params.cfg, accountId);
100
+ const baseEnabled = params.cfg.channels?.groupme?.enabled !== false;
101
+ const accountEnabled = merged.enabled !== false;
102
+ const enabled = baseEnabled && accountEnabled;
103
+
104
+ const isDefaultAccount = accountId === DEFAULT_ACCOUNT_ID;
105
+ const botId =
106
+ readTrimmed(merged.botId) ||
107
+ (isDefaultAccount ? readTrimmed(process.env[ENV_BOT_ID]) : undefined) ||
108
+ "";
109
+ const accessToken =
110
+ readTrimmed(merged.accessToken) ||
111
+ (isDefaultAccount ? readTrimmed(process.env[ENV_ACCESS_TOKEN]) : undefined) ||
112
+ "";
113
+ const botName =
114
+ readTrimmed(merged.botName) ||
115
+ (isDefaultAccount ? readTrimmed(process.env[ENV_BOT_NAME]) : undefined) ||
116
+ undefined;
117
+ const callbackPath =
118
+ readTrimmed(merged.callbackPath) ||
119
+ (isDefaultAccount ? readTrimmed(process.env[ENV_CALLBACK_PATH]) : undefined) ||
120
+ undefined;
121
+
122
+ const config: GroupMeAccountConfig = {
123
+ ...merged,
124
+ botId,
125
+ accessToken,
126
+ botName,
127
+ callbackPath,
128
+ };
129
+
130
+ return {
131
+ accountId,
132
+ name: readTrimmed(merged.name),
133
+ enabled,
134
+ configured: Boolean(botId),
135
+ botId,
136
+ accessToken,
137
+ config,
138
+ };
139
+ }
package/src/channel.ts ADDED
@@ -0,0 +1,294 @@
1
+ import {
2
+ buildChannelConfigSchema,
3
+ DEFAULT_ACCOUNT_ID,
4
+ deleteAccountFromConfigSection,
5
+ missingTargetError,
6
+ registerPluginHttpRoute,
7
+ setAccountEnabledInConfigSection,
8
+ type ChannelPlugin,
9
+ } from "openclaw/plugin-sdk";
10
+ import type { CoreConfig, GroupMeProbe, ResolvedGroupMeAccount } from "./types.js";
11
+ import {
12
+ listGroupMeAccountIds,
13
+ resolveDefaultGroupMeAccountId,
14
+ resolveGroupMeAccount,
15
+ } from "./accounts.js";
16
+ import { GroupMeConfigSchema } from "./config-schema.js";
17
+ import { createGroupMeWebhookHandler } from "./monitor.js";
18
+ import {
19
+ normalizeGroupMeAllowEntry,
20
+ normalizeGroupMeTarget,
21
+ looksLikeGroupMeTargetId,
22
+ } from "./normalize.js";
23
+ import { groupmeOnboardingAdapter } from "./onboarding.js";
24
+ import { getGroupMeRuntime } from "./runtime.js";
25
+ import { GROUPME_MAX_TEXT_LENGTH, sendGroupMeMedia, sendGroupMeText } from "./send.js";
26
+
27
+ const CHANNEL_ID = "groupme" as const;
28
+
29
+ const meta = {
30
+ id: CHANNEL_ID,
31
+ label: "GroupMe",
32
+ selectionLabel: "GroupMe (Bot API)",
33
+ docsPath: "/channels/groupme",
34
+ docsLabel: "groupme",
35
+ blurb: "GroupMe bot webhook integration (group chats only).",
36
+ aliases: ["gm"],
37
+ order: 95,
38
+ quickstartAllowFrom: true,
39
+ };
40
+
41
+ export const groupmePlugin: ChannelPlugin<ResolvedGroupMeAccount, GroupMeProbe> = {
42
+ id: CHANNEL_ID,
43
+ meta,
44
+ onboarding: groupmeOnboardingAdapter,
45
+ capabilities: {
46
+ chatTypes: ["group"],
47
+ media: true,
48
+ blockStreaming: true,
49
+ },
50
+ reload: { configPrefixes: ["channels.groupme"] },
51
+ configSchema: buildChannelConfigSchema(GroupMeConfigSchema),
52
+ config: {
53
+ listAccountIds: (cfg) => listGroupMeAccountIds(cfg as CoreConfig),
54
+ resolveAccount: (cfg, accountId) =>
55
+ resolveGroupMeAccount({ cfg: cfg as CoreConfig, accountId }),
56
+ defaultAccountId: (cfg) => resolveDefaultGroupMeAccountId(cfg as CoreConfig),
57
+ setAccountEnabled: ({ cfg, accountId, enabled }) =>
58
+ setAccountEnabledInConfigSection({
59
+ cfg: cfg as CoreConfig,
60
+ sectionKey: CHANNEL_ID,
61
+ accountId,
62
+ enabled,
63
+ allowTopLevel: true,
64
+ }),
65
+ deleteAccount: ({ cfg, accountId }) =>
66
+ deleteAccountFromConfigSection({
67
+ cfg: cfg as CoreConfig,
68
+ sectionKey: CHANNEL_ID,
69
+ accountId,
70
+ clearBaseFields: [
71
+ "name",
72
+ "botId",
73
+ "accessToken",
74
+ "botName",
75
+ "callbackPath",
76
+ "mentionPatterns",
77
+ "requireMention",
78
+ "allowFrom",
79
+ "textChunkLimit",
80
+ "responsePrefix",
81
+ ],
82
+ }),
83
+ isConfigured: (account) => account.configured,
84
+ describeAccount: (account) => ({
85
+ accountId: account.accountId,
86
+ name: account.name,
87
+ enabled: account.enabled,
88
+ configured: account.configured,
89
+ botId: account.botId ? "***" : "",
90
+ callbackPath: account.config.callbackPath,
91
+ }),
92
+ resolveAllowFrom: ({ cfg, accountId }) =>
93
+ (resolveGroupMeAccount({ cfg: cfg as CoreConfig, accountId }).config.allowFrom ?? []).map(
94
+ (entry) => String(entry),
95
+ ),
96
+ formatAllowFrom: ({ allowFrom }) =>
97
+ allowFrom
98
+ .map((entry) => normalizeGroupMeAllowEntry(String(entry)))
99
+ .filter((entry): entry is string => Boolean(entry)),
100
+ },
101
+ groups: {
102
+ resolveRequireMention: ({ cfg, accountId }) => {
103
+ const account = resolveGroupMeAccount({
104
+ cfg: cfg as CoreConfig,
105
+ accountId,
106
+ });
107
+ return account.config.requireMention ?? true;
108
+ },
109
+ },
110
+ outbound: {
111
+ deliveryMode: "direct",
112
+ chunker: (text, limit) => getGroupMeRuntime().channel.text.chunkMarkdownText(text, limit),
113
+ chunkerMode: "markdown",
114
+ textChunkLimit: GROUPME_MAX_TEXT_LENGTH,
115
+ resolveTarget: ({ to }) => {
116
+ const normalized = normalizeGroupMeTarget(to?.trim() ?? "");
117
+ if (!normalized) {
118
+ return {
119
+ ok: false,
120
+ error: missingTargetError("GroupMe", "<group-id>"),
121
+ };
122
+ }
123
+
124
+ return {
125
+ ok: true,
126
+ to: normalized,
127
+ };
128
+ },
129
+ sendText: async ({ cfg, to, text, accountId }) => {
130
+ const result = await sendGroupMeText({
131
+ cfg: cfg as CoreConfig,
132
+ to,
133
+ text,
134
+ accountId,
135
+ });
136
+ return {
137
+ channel: CHANNEL_ID,
138
+ messageId: result.messageId,
139
+ timestamp: result.timestamp,
140
+ };
141
+ },
142
+ sendMedia: async ({ cfg, to, text, mediaUrl, accountId }) => {
143
+ if (!mediaUrl?.trim()) {
144
+ throw new Error("GroupMe media send requires a mediaUrl");
145
+ }
146
+
147
+ const result = await sendGroupMeMedia({
148
+ cfg: cfg as CoreConfig,
149
+ to,
150
+ text,
151
+ mediaUrl,
152
+ accountId,
153
+ });
154
+ return {
155
+ channel: CHANNEL_ID,
156
+ messageId: result.messageId,
157
+ timestamp: result.timestamp,
158
+ };
159
+ },
160
+ },
161
+ messaging: {
162
+ normalizeTarget: normalizeGroupMeTarget,
163
+ targetResolver: {
164
+ looksLikeId: (raw) => looksLikeGroupMeTargetId(raw),
165
+ hint: "<group-id>",
166
+ },
167
+ },
168
+ resolver: {
169
+ resolveTargets: async ({ inputs, kind }) => {
170
+ return inputs.map((input) => {
171
+ const normalized = normalizeGroupMeTarget(input);
172
+ if (!normalized) {
173
+ return {
174
+ input,
175
+ resolved: false,
176
+ note: "empty target",
177
+ };
178
+ }
179
+
180
+ return {
181
+ input,
182
+ resolved: true,
183
+ id: normalized,
184
+ name: normalized,
185
+ note: kind === "user" ? "GroupMe bots are group-only" : undefined,
186
+ };
187
+ });
188
+ },
189
+ },
190
+ directory: {
191
+ self: async () => null,
192
+ listPeers: async ({ cfg, accountId, query, limit }) => {
193
+ const account = resolveGroupMeAccount({
194
+ cfg: cfg as CoreConfig,
195
+ accountId,
196
+ });
197
+ const q = query?.trim().toLowerCase() ?? "";
198
+ return (account.config.allowFrom ?? [])
199
+ .map((entry) => normalizeGroupMeAllowEntry(String(entry)))
200
+ .filter((entry): entry is string => Boolean(entry) && entry !== "*")
201
+ .filter((entry) => (q ? entry.toLowerCase().includes(q) : true))
202
+ .slice(0, limit && limit > 0 ? limit : undefined)
203
+ .map((id) => ({ kind: "user", id }) as const);
204
+ },
205
+ listGroups: async () => [],
206
+ },
207
+ status: {
208
+ defaultRuntime: {
209
+ accountId: DEFAULT_ACCOUNT_ID,
210
+ running: false,
211
+ lastStartAt: null,
212
+ lastStopAt: null,
213
+ lastError: null,
214
+ },
215
+ buildChannelSummary: ({ snapshot }) => ({
216
+ configured: snapshot.configured ?? false,
217
+ running: snapshot.running ?? false,
218
+ callbackPath: snapshot.webhookPath ?? null,
219
+ lastStartAt: snapshot.lastStartAt ?? null,
220
+ lastStopAt: snapshot.lastStopAt ?? null,
221
+ lastInboundAt: snapshot.lastInboundAt ?? null,
222
+ lastOutboundAt: snapshot.lastOutboundAt ?? null,
223
+ lastError: snapshot.lastError ?? null,
224
+ }),
225
+ buildAccountSnapshot: ({ account, runtime }) => ({
226
+ accountId: account.accountId,
227
+ name: account.name,
228
+ enabled: account.enabled,
229
+ configured: account.configured,
230
+ botId: account.botId ? "***" : "",
231
+ tokenSource: account.accessToken ? "configured" : "none",
232
+ webhookPath: account.config.callbackPath ?? "/groupme",
233
+ running: runtime?.running ?? false,
234
+ lastStartAt: runtime?.lastStartAt ?? null,
235
+ lastStopAt: runtime?.lastStopAt ?? null,
236
+ lastInboundAt: runtime?.lastInboundAt ?? null,
237
+ lastOutboundAt: runtime?.lastOutboundAt ?? null,
238
+ lastError: runtime?.lastError ?? null,
239
+ mode: "webhook",
240
+ }),
241
+ },
242
+ gateway: {
243
+ startAccount: async (ctx) => {
244
+ const account = ctx.account;
245
+ if (!account.configured) {
246
+ throw new Error(
247
+ `GroupMe is not configured for account "${account.accountId}" (missing botId).`,
248
+ );
249
+ }
250
+
251
+ const callbackPath = account.config.callbackPath?.trim() || "/groupme";
252
+ const unregister = registerPluginHttpRoute({
253
+ path: callbackPath,
254
+ fallbackPath: "/groupme",
255
+ handler: createGroupMeWebhookHandler({
256
+ account,
257
+ config: ctx.cfg as CoreConfig,
258
+ runtime: ctx.runtime,
259
+ statusSink: (patch) => ctx.setStatus({ accountId: account.accountId, ...patch }),
260
+ }),
261
+ pluginId: CHANNEL_ID,
262
+ accountId: account.accountId,
263
+ log: (message) => ctx.log?.info(message),
264
+ });
265
+
266
+ ctx.setStatus({
267
+ accountId: account.accountId,
268
+ running: true,
269
+ mode: "webhook",
270
+ webhookPath: callbackPath,
271
+ lastStartAt: Date.now(),
272
+ lastError: null,
273
+ });
274
+
275
+ ctx.log?.info(`[${account.accountId}] GroupMe webhook listening on ${callbackPath}`);
276
+
277
+ if (ctx.abortSignal.aborted) {
278
+ unregister();
279
+ return;
280
+ }
281
+
282
+ await new Promise<void>((resolve) => {
283
+ ctx.abortSignal.addEventListener(
284
+ "abort",
285
+ () => {
286
+ unregister();
287
+ resolve();
288
+ },
289
+ { once: true },
290
+ );
291
+ });
292
+ },
293
+ },
294
+ };
@@ -0,0 +1,29 @@
1
+ import { BlockStreamingCoalesceSchema, MarkdownConfigSchema } from "openclaw/plugin-sdk";
2
+ import { z } from "zod";
3
+
4
+ const allowFromEntry = z.union([z.string(), z.number()]);
5
+
6
+ export const GroupMeAccountSchemaBase = z
7
+ .object({
8
+ name: z.string().optional(),
9
+ enabled: z.boolean().optional(),
10
+ botId: z.string().optional(),
11
+ accessToken: z.string().optional(),
12
+ botName: z.string().optional(),
13
+ callbackPath: z.string().optional(),
14
+ mentionPatterns: z.array(z.string()).optional(),
15
+ requireMention: z.boolean().optional().default(true),
16
+ allowFrom: z.array(allowFromEntry).optional(),
17
+ markdown: MarkdownConfigSchema,
18
+ textChunkLimit: z.number().int().positive().optional(),
19
+ blockStreaming: z.boolean().optional(),
20
+ blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
21
+ responsePrefix: z.string().optional(),
22
+ mediaMaxMb: z.number().positive().optional(),
23
+ })
24
+ .strict();
25
+
26
+ export const GroupMeConfigSchema = GroupMeAccountSchemaBase.extend({
27
+ accounts: z.record(z.string(), GroupMeAccountSchemaBase.optional()).optional(),
28
+ defaultAccount: z.string().optional(),
29
+ }).strict();