@openclaw/twitch 2026.5.7-beta.1 → 2026.5.9-beta.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/dist/api.js CHANGED
@@ -1,3 +1,3 @@
1
- import { t as twitchPlugin } from "./plugin-BQX9GiIn.js";
1
+ import { t as twitchPlugin } from "./plugin-CoIt3s7P.js";
2
2
  import { n as setTwitchRuntime } from "./runtime-QZ5I3GlI.js";
3
3
  export { setTwitchRuntime, twitchPlugin };
@@ -1,2 +1,2 @@
1
- import { t as twitchPlugin } from "./plugin-BQX9GiIn.js";
1
+ import { t as twitchPlugin } from "./plugin-CoIt3s7P.js";
2
2
  export { twitchPlugin };
@@ -1,4 +1,4 @@
1
- import { i as normalizeToken, o as resolveTwitchToken } from "./twitch-CklAMZL5.js";
1
+ import { i as normalizeToken, o as resolveTwitchToken } from "./twitch-D0A1Bjui.js";
2
2
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
3
3
  import { RefreshingAuthProvider, StaticAuthProvider } from "@twurple/auth";
4
4
  import { ChatClient, LogLevel } from "@twurple/chat";
@@ -1,8 +1,7 @@
1
- import { i as getOrCreateClientManager, n as stripMarkdownForTwitch } from "./markdown-MRdI1sR7.js";
1
+ import { i as getOrCreateClientManager, n as stripMarkdownForTwitch } from "./markdown-CLNO0I2v.js";
2
2
  import { t as getTwitchRuntime } from "./runtime-QZ5I3GlI.js";
3
3
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
4
4
  import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
5
- import { createChannelReplyPipeline } from "openclaw/plugin-sdk/channel-reply-pipeline";
6
5
  //#region extensions/twitch/src/access-control.ts
7
6
  /**
8
7
  * Check if a Twitch message should be allowed based on account configuration
@@ -117,12 +116,6 @@ function extractMentions(message) {
117
116
  //#endregion
118
117
  //#region extensions/twitch/src/monitor.ts
119
118
  /**
120
- * Twitch message monitor - processes incoming messages and routes to agents.
121
- *
122
- * This monitor connects to the Twitch client manager, processes incoming messages,
123
- * resolves agent routes, and handles replies.
124
- */
125
- /**
126
119
  * Process an incoming Twitch message and dispatch to agent.
127
120
  */
128
121
  async function processTwitchMessage(params) {
@@ -203,12 +196,6 @@ async function processTwitchMessage(params) {
203
196
  channel: "twitch",
204
197
  accountId
205
198
  });
206
- const { onModelSelected, ...replyPipeline } = createChannelReplyPipeline({
207
- cfg,
208
- agentId: route.agentId,
209
- channel: "twitch",
210
- accountId
211
- });
212
199
  return {
213
200
  cfg,
214
201
  channel: "twitch",
@@ -220,24 +207,26 @@ async function processTwitchMessage(params) {
220
207
  recordInboundSession: core.channel.session.recordInboundSession,
221
208
  dispatchReplyWithBufferedBlockDispatcher: core.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
222
209
  delivery: {
210
+ durable: () => ({ to: `twitch:channel:${message.channel}` }),
223
211
  deliver: async (payload) => {
224
- await deliverTwitchReply({
212
+ return await deliverTwitchReply({
225
213
  payload,
226
214
  channel: message.channel,
227
215
  account,
228
216
  accountId,
229
217
  config,
230
218
  tableMode,
231
- runtime,
232
- statusSink
219
+ runtime
233
220
  });
234
221
  },
222
+ onDelivered: (_payload, _info, result) => {
223
+ if (result?.visibleReplySent !== false) statusSink?.({ lastOutboundAt: Date.now() });
224
+ },
235
225
  onError: (err, info) => {
236
226
  runtime.error?.(`Twitch ${info.kind} reply failed: ${String(err)}`);
237
227
  }
238
228
  },
239
- dispatcherOptions: replyPipeline,
240
- replyOptions: { onModelSelected },
229
+ replyPipeline: {},
241
230
  record: { onRecordError: (err) => {
242
231
  runtime.error?.(`Failed updating session meta: ${String(err)}`);
243
232
  } }
@@ -250,7 +239,7 @@ async function processTwitchMessage(params) {
250
239
  * Deliver a reply to Twitch chat.
251
240
  */
252
241
  async function deliverTwitchReply(params) {
253
- const { payload, channel, account, accountId, config, runtime, statusSink } = params;
242
+ const { payload, channel, account, accountId, config, runtime } = params;
254
243
  try {
255
244
  const client = await getOrCreateClientManager(accountId, {
256
245
  info: (msg) => runtime.log?.(msg),
@@ -260,17 +249,18 @@ async function deliverTwitchReply(params) {
260
249
  }).getClient(account, config, accountId);
261
250
  if (!client) {
262
251
  runtime.error?.(`No client available for sending reply`);
263
- return;
252
+ return { visibleReplySent: false };
264
253
  }
265
254
  if (!payload.text) {
266
255
  runtime.error?.(`No text to send in reply payload`);
267
- return;
256
+ return { visibleReplySent: false };
268
257
  }
269
258
  const textToSend = stripMarkdownForTwitch(payload.text);
270
259
  await client.say(channel, textToSend);
271
- statusSink?.({ lastOutboundAt: Date.now() });
260
+ return { visibleReplySent: true };
272
261
  } catch (err) {
273
262
  runtime.error?.(`Failed to send reply: ${String(err)}`);
263
+ return { visibleReplySent: false };
274
264
  }
275
265
  }
276
266
  /**
@@ -1,6 +1,6 @@
1
- import { a as normalizeTwitchChannel, i as normalizeToken, n as isAccountConfigured, o as resolveTwitchToken, r as missingTargetError, t as generateMessageId } from "./twitch-CklAMZL5.js";
2
- import { a as getAccountConfig, c as resolveTwitchAccountContext, i as DEFAULT_ACCOUNT_ID, l as resolveTwitchSnapshotAccountId, o as listAccountIds, r as twitchSetupWizard, s as resolveDefaultTwitchAccountId, t as twitchSetupAdapter } from "./setup-surface-yArVgckI.js";
3
- import { a as removeClientManager, n as stripMarkdownForTwitch, r as getClientManager, t as chunkTextForTwitch } from "./markdown-MRdI1sR7.js";
1
+ import { a as normalizeTwitchChannel, i as normalizeToken, n as isAccountConfigured, o as resolveTwitchToken, r as missingTargetError, t as generateMessageId } from "./twitch-D0A1Bjui.js";
2
+ import { a as getAccountConfig, c as resolveTwitchAccountContext, i as DEFAULT_ACCOUNT_ID, l as resolveTwitchSnapshotAccountId, o as listAccountIds, r as twitchSetupWizard, s as resolveDefaultTwitchAccountId, t as twitchSetupAdapter } from "./setup-surface-CDhELE-4.js";
3
+ import { a as removeClientManager, n as stripMarkdownForTwitch, r as getClientManager, t as chunkTextForTwitch } from "./markdown-CLNO0I2v.js";
4
4
  import { describeAccountSnapshot } from "openclaw/plugin-sdk/account-helpers";
5
5
  import { buildChannelConfigSchema } from "openclaw/plugin-sdk/channel-config-schema";
6
6
  import { createChatChannelPlugin } from "openclaw/plugin-sdk/channel-core";
@@ -8,14 +8,33 @@ import { createLoggedPairingApprovalNotifier, createPairingPrefixStripper } from
8
8
  import { buildPassiveProbedChannelStatusSummary } from "openclaw/plugin-sdk/extension-shared";
9
9
  import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState } from "openclaw/plugin-sdk/status-helpers";
10
10
  import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
11
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
12
+ import { createMessageReceiptFromOutboundResults, defineChannelMessageAdapter } from "openclaw/plugin-sdk/channel-message";
11
13
  import { StaticAuthProvider } from "@twurple/auth";
12
14
  import { ChatClient } from "@twurple/chat";
13
15
  import { MarkdownConfigSchema } from "openclaw/plugin-sdk/channel-config-primitives";
14
16
  import { z } from "openclaw/plugin-sdk/zod";
15
17
  import { ApiClient } from "@twurple/api";
16
- import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
17
18
  //#region extensions/twitch/src/send.ts
18
19
  /**
20
+ * Twitch message sending functions with dependency injection support.
21
+ *
22
+ * These functions are the primary interface for sending messages to Twitch.
23
+ * They support dependency injection via the `deps` parameter for testability.
24
+ */
25
+ function createTwitchSendReceipt(params) {
26
+ const messageId = params.messageId.trim();
27
+ const conversationId = params.channel?.trim();
28
+ return createMessageReceiptFromOutboundResults({
29
+ results: params.visible === true && messageId && messageId !== "skipped" ? [{
30
+ channel: "twitch",
31
+ messageId,
32
+ ...conversationId ? { conversationId } : {}
33
+ }] : [],
34
+ kind: "text"
35
+ });
36
+ }
37
+ /**
19
38
  * Internal send function used by the outbound adapter.
20
39
  *
21
40
  * This function has access to the full OpenClaw config and handles
@@ -44,47 +63,93 @@ async function sendMessageTwitchInternal(channel, text, cfg, accountId, stripMar
44
63
  if (!account) return {
45
64
  ok: false,
46
65
  messageId: generateMessageId(),
66
+ receipt: createTwitchSendReceipt({
67
+ messageId: "",
68
+ channel,
69
+ visible: false
70
+ }),
47
71
  error: `Account not found: ${accountId ?? "(default)"}. Available accounts: ${availableAccountIds.join(", ") || "none"}`
48
72
  };
49
73
  if (!configured) return {
50
74
  ok: false,
51
75
  messageId: generateMessageId(),
76
+ receipt: createTwitchSendReceipt({
77
+ messageId: "",
78
+ channel,
79
+ visible: false
80
+ }),
52
81
  error: `Account ${resolvedAccountId} is not properly configured. Required: username, clientId, and token (config or env for default account).`
53
82
  };
54
83
  const normalizedChannel = channel || account.channel;
55
84
  if (!normalizedChannel) return {
56
85
  ok: false,
57
86
  messageId: generateMessageId(),
87
+ receipt: createTwitchSendReceipt({
88
+ messageId: "",
89
+ channel: normalizedChannel,
90
+ visible: false
91
+ }),
58
92
  error: "No channel specified and no default channel in account config"
59
93
  };
94
+ const deliveryChannel = normalizeTwitchChannel(normalizedChannel);
60
95
  const cleanedText = stripMarkdown ? stripMarkdownForTwitch(text) : text;
61
96
  if (!cleanedText) return {
62
97
  ok: true,
63
- messageId: "skipped"
98
+ messageId: "skipped",
99
+ receipt: createTwitchSendReceipt({
100
+ messageId: "skipped",
101
+ channel: deliveryChannel,
102
+ visible: false
103
+ })
64
104
  };
65
105
  const clientManager = getClientManager(resolvedAccountId);
66
106
  if (!clientManager) return {
67
107
  ok: false,
68
108
  messageId: generateMessageId(),
109
+ receipt: createTwitchSendReceipt({
110
+ messageId: "",
111
+ channel: deliveryChannel,
112
+ visible: false
113
+ }),
69
114
  error: `Client manager not found for account: ${resolvedAccountId}. Please start the Twitch gateway first.`
70
115
  };
71
116
  try {
72
- const result = await clientManager.sendMessage(account, normalizeTwitchChannel(normalizedChannel), cleanedText, cfg, resolvedAccountId);
73
- if (!result.ok) return {
74
- ok: false,
75
- messageId: result.messageId ?? generateMessageId(),
76
- error: result.error ?? "Send failed"
77
- };
117
+ const result = await clientManager.sendMessage(account, deliveryChannel, cleanedText, cfg, resolvedAccountId);
118
+ if (!result.ok) {
119
+ const messageId = result.messageId ?? generateMessageId();
120
+ return {
121
+ ok: false,
122
+ messageId,
123
+ receipt: createTwitchSendReceipt({
124
+ messageId,
125
+ channel: deliveryChannel,
126
+ visible: false
127
+ }),
128
+ error: result.error ?? "Send failed"
129
+ };
130
+ }
131
+ const messageId = result.messageId ?? generateMessageId();
78
132
  return {
79
133
  ok: true,
80
- messageId: result.messageId ?? generateMessageId()
134
+ messageId,
135
+ receipt: createTwitchSendReceipt({
136
+ messageId,
137
+ channel: deliveryChannel,
138
+ visible: true
139
+ })
81
140
  };
82
141
  } catch (error) {
83
142
  const errorMsg = formatErrorMessage(error);
143
+ const messageId = generateMessageId();
84
144
  logger.error(`Failed to send message: ${errorMsg}`);
85
145
  return {
86
146
  ok: false,
87
- messageId: generateMessageId(),
147
+ messageId,
148
+ receipt: createTwitchSendReceipt({
149
+ messageId,
150
+ channel: deliveryChannel,
151
+ visible: false
152
+ }),
88
153
  error: errorMsg
89
154
  };
90
155
  }
@@ -106,6 +171,11 @@ async function sendMessageTwitchInternal(channel, text, cfg, accountId, stripMar
106
171
  const twitchOutbound = {
107
172
  /** Direct delivery mode - messages are sent immediately */
108
173
  deliveryMode: "direct",
174
+ deliveryCapabilities: { durableFinal: {
175
+ text: true,
176
+ media: true,
177
+ messageSendingHooks: true
178
+ } },
109
179
  /** Twitch chat message limit is 500 characters */
110
180
  textChunkLimit: 500,
111
181
  /** Word-boundary chunker with markdown stripping */
@@ -184,6 +254,7 @@ const twitchOutbound = {
184
254
  return {
185
255
  channel: "twitch",
186
256
  messageId: result.messageId,
257
+ receipt: result.receipt,
187
258
  timestamp: Date.now()
188
259
  };
189
260
  },
@@ -216,6 +287,37 @@ const twitchOutbound = {
216
287
  });
217
288
  }
218
289
  };
290
+ function toTwitchMessageSendResult(result, kind) {
291
+ const receipt = result.receipt ?? createMessageReceiptFromOutboundResults({
292
+ results: result.messageId ? [{
293
+ channel: "twitch",
294
+ messageId: result.messageId
295
+ }] : [],
296
+ kind
297
+ });
298
+ return {
299
+ messageId: result.messageId || receipt.primaryPlatformMessageId,
300
+ receipt
301
+ };
302
+ }
303
+ const twitchMessageAdapter = defineChannelMessageAdapter({
304
+ id: "twitch",
305
+ durableFinal: { capabilities: {
306
+ text: true,
307
+ media: true,
308
+ messageSendingHooks: true
309
+ } },
310
+ send: {
311
+ text: async (ctx) => {
312
+ if (!twitchOutbound.sendText) throw new Error("Twitch text sending is not available.");
313
+ return toTwitchMessageSendResult(await twitchOutbound.sendText(ctx), "text");
314
+ },
315
+ media: async (ctx) => {
316
+ if (!twitchOutbound.sendMedia) throw new Error("Twitch media sending is not available.");
317
+ return toTwitchMessageSendResult(await twitchOutbound.sendMedia(ctx), "media");
318
+ }
319
+ }
320
+ });
219
321
  //#endregion
220
322
  //#region extensions/twitch/src/actions.ts
221
323
  /**
@@ -779,6 +881,7 @@ const twitchPlugin = createChatChannelPlugin({
779
881
  setup: twitchSetupAdapter,
780
882
  setupWizard: twitchSetupWizard,
781
883
  capabilities: { chatTypes: ["group"] },
884
+ message: twitchMessageAdapter,
782
885
  configSchema: buildChannelConfigSchema(TwitchConfigSchema),
783
886
  config: {
784
887
  listAccountIds: (cfg) => listAccountIds(cfg),
@@ -851,7 +954,7 @@ const twitchPlugin = createChatChannelPlugin({
851
954
  lastError: null
852
955
  });
853
956
  ctx.log?.info(`Starting Twitch connection for ${account.username}`);
854
- const { monitorTwitchProvider } = await import("./monitor-DS0YTAPB.js");
957
+ const { monitorTwitchProvider } = await import("./monitor-DmMYPZvJ.js");
855
958
  await monitorTwitchProvider({
856
959
  account,
857
960
  accountId,
@@ -1,2 +1,2 @@
1
- import { n as twitchSetupPlugin } from "./setup-surface-yArVgckI.js";
1
+ import { n as twitchSetupPlugin } from "./setup-surface-CDhELE-4.js";
2
2
  export { twitchSetupPlugin };
@@ -1,4 +1,4 @@
1
- import { n as isAccountConfigured, o as resolveTwitchToken } from "./twitch-CklAMZL5.js";
1
+ import { n as isAccountConfigured, o as resolveTwitchToken } from "./twitch-D0A1Bjui.js";
2
2
  import { listCombinedAccountIds, normalizeAccountId, resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution";
3
3
  import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
4
4
  import { getChatChannelMeta } from "openclaw/plugin-sdk/core";
@@ -1,5 +1,6 @@
1
1
  import { DEFAULT_ACCOUNT_ID, normalizeAccountId, resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution";
2
2
  import { randomUUID } from "node:crypto";
3
+ import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
3
4
  //#region extensions/twitch/src/token.ts
4
5
  /**
5
6
  * Twitch access token resolution with environment variable support.
@@ -61,9 +62,6 @@ function resolveTwitchToken(cfg, opts = {}) {
61
62
  /**
62
63
  * Twitch-specific utility functions
63
64
  */
64
- function normalizeLowercaseStringOrEmpty(value) {
65
- return typeof value === "string" ? value.trim().toLowerCase() : "";
66
- }
67
65
  /**
68
66
  * Normalize Twitch channel names.
69
67
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/twitch",
3
- "version": "2026.5.7-beta.1",
3
+ "version": "2026.5.9-beta.1",
4
4
  "description": "OpenClaw Twitch channel plugin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -26,10 +26,10 @@
26
26
  "minHostVersion": ">=2026.4.10"
27
27
  },
28
28
  "compat": {
29
- "pluginApi": ">=2026.5.7-beta.1"
29
+ "pluginApi": ">=2026.5.9-beta.1"
30
30
  },
31
31
  "build": {
32
- "openclawVersion": "2026.5.7-beta.1"
32
+ "openclawVersion": "2026.5.9-beta.1"
33
33
  },
34
34
  "channel": {
35
35
  "id": "twitch",
@@ -56,7 +56,7 @@
56
56
  "README.md"
57
57
  ],
58
58
  "peerDependencies": {
59
- "openclaw": ">=2026.5.7-beta.1"
59
+ "openclaw": ">=2026.5.9-beta.1"
60
60
  },
61
61
  "peerDependenciesMeta": {
62
62
  "openclaw": {