@openclaw/twitch 2026.5.2-beta.2 → 2026.5.3-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.
Files changed (54) hide show
  1. package/dist/api.js +3 -0
  2. package/dist/channel-plugin-api.js +2 -0
  3. package/dist/index.js +18 -0
  4. package/dist/markdown-MRdI1sR7.js +306 -0
  5. package/dist/monitor-DS0YTAPB.js +333 -0
  6. package/dist/plugin-BQX9GiIn.js +878 -0
  7. package/dist/runtime-QZ5I3GlI.js +8 -0
  8. package/dist/runtime-api.js +1 -0
  9. package/dist/setup-entry.js +11 -0
  10. package/dist/setup-plugin-api.js +2 -0
  11. package/dist/setup-surface-yArVgckI.js +400 -0
  12. package/dist/twitch-CklAMZL5.js +131 -0
  13. package/package.json +20 -3
  14. package/api.ts +0 -21
  15. package/channel-plugin-api.ts +0 -1
  16. package/index.test.ts +0 -13
  17. package/index.ts +0 -16
  18. package/runtime-api.ts +0 -22
  19. package/setup-entry.ts +0 -9
  20. package/setup-plugin-api.ts +0 -3
  21. package/src/access-control.test.ts +0 -388
  22. package/src/access-control.ts +0 -173
  23. package/src/actions.test.ts +0 -74
  24. package/src/actions.ts +0 -175
  25. package/src/client-manager-registry.ts +0 -87
  26. package/src/config-schema.test.ts +0 -46
  27. package/src/config-schema.ts +0 -88
  28. package/src/config.test.ts +0 -233
  29. package/src/config.ts +0 -177
  30. package/src/monitor.ts +0 -314
  31. package/src/outbound.test.ts +0 -468
  32. package/src/outbound.ts +0 -186
  33. package/src/plugin.test.ts +0 -77
  34. package/src/plugin.ts +0 -208
  35. package/src/probe.test.ts +0 -196
  36. package/src/probe.ts +0 -130
  37. package/src/resolver.ts +0 -139
  38. package/src/runtime.ts +0 -9
  39. package/src/send.test.ts +0 -309
  40. package/src/send.ts +0 -139
  41. package/src/setup-surface.test.ts +0 -511
  42. package/src/setup-surface.ts +0 -520
  43. package/src/status.test.ts +0 -237
  44. package/src/status.ts +0 -179
  45. package/src/test-fixtures.ts +0 -30
  46. package/src/token.test.ts +0 -192
  47. package/src/token.ts +0 -93
  48. package/src/twitch-client.test.ts +0 -582
  49. package/src/twitch-client.ts +0 -276
  50. package/src/types.ts +0 -104
  51. package/src/utils/markdown.ts +0 -98
  52. package/src/utils/twitch.ts +0 -84
  53. package/test/setup.ts +0 -7
  54. package/tsconfig.json +0 -16
@@ -0,0 +1,8 @@
1
+ import { createPluginRuntimeStore } from "openclaw/plugin-sdk/runtime-store";
2
+ //#region extensions/twitch/src/runtime.ts
3
+ const { setRuntime: setTwitchRuntime, getRuntime: getTwitchRuntime } = createPluginRuntimeStore({
4
+ pluginId: "twitch",
5
+ errorMessage: "Twitch runtime not initialized"
6
+ });
7
+ //#endregion
8
+ export { setTwitchRuntime as n, getTwitchRuntime as t };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,11 @@
1
+ import { defineBundledChannelSetupEntry } from "openclaw/plugin-sdk/channel-entry-contract";
2
+ //#region extensions/twitch/setup-entry.ts
3
+ var setup_entry_default = defineBundledChannelSetupEntry({
4
+ importMetaUrl: import.meta.url,
5
+ plugin: {
6
+ specifier: "./setup-plugin-api.js",
7
+ exportName: "twitchSetupPlugin"
8
+ }
9
+ });
10
+ //#endregion
11
+ export { setup_entry_default as default };
@@ -0,0 +1,2 @@
1
+ import { n as twitchSetupPlugin } from "./setup-surface-yArVgckI.js";
2
+ export { twitchSetupPlugin };
@@ -0,0 +1,400 @@
1
+ import { n as isAccountConfigured, o as resolveTwitchToken } from "./twitch-CklAMZL5.js";
2
+ import { listCombinedAccountIds, normalizeAccountId, resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution";
3
+ import { normalizeOptionalAccountId } from "openclaw/plugin-sdk/account-id";
4
+ import { getChatChannelMeta } from "openclaw/plugin-sdk/core";
5
+ import { formatDocsLink, normalizeAccountId as normalizeAccountId$1 } from "openclaw/plugin-sdk/setup";
6
+ //#region extensions/twitch/src/config.ts
7
+ /**
8
+ * Default account ID for Twitch
9
+ */
10
+ const DEFAULT_ACCOUNT_ID$1 = "default";
11
+ /**
12
+ * Get account config from core config
13
+ *
14
+ * Handles two patterns:
15
+ * 1. Simplified single-account: base-level properties create implicit "default" account
16
+ * 2. Multi-account: explicit accounts object
17
+ *
18
+ * For "default" account, base-level properties take precedence over accounts.default
19
+ * For other accounts, only the accounts object is checked
20
+ */
21
+ function getAccountConfig(coreConfig, accountId) {
22
+ if (!coreConfig || typeof coreConfig !== "object") return null;
23
+ const cfg = coreConfig;
24
+ const normalizedAccountId = normalizeAccountId(accountId);
25
+ const twitchRaw = cfg.channels?.twitch;
26
+ const accounts = twitchRaw?.accounts;
27
+ if (normalizedAccountId === "default") {
28
+ const accountFromAccounts = resolveNormalizedAccountEntry(accounts, DEFAULT_ACCOUNT_ID$1, normalizeAccountId);
29
+ const baseLevel = {
30
+ username: typeof twitchRaw?.username === "string" ? twitchRaw.username : void 0,
31
+ accessToken: typeof twitchRaw?.accessToken === "string" ? twitchRaw.accessToken : void 0,
32
+ clientId: typeof twitchRaw?.clientId === "string" ? twitchRaw.clientId : void 0,
33
+ channel: typeof twitchRaw?.channel === "string" ? twitchRaw.channel : void 0,
34
+ enabled: typeof twitchRaw?.enabled === "boolean" ? twitchRaw.enabled : void 0,
35
+ allowFrom: Array.isArray(twitchRaw?.allowFrom) ? twitchRaw.allowFrom : void 0,
36
+ allowedRoles: Array.isArray(twitchRaw?.allowedRoles) ? twitchRaw.allowedRoles : void 0,
37
+ requireMention: typeof twitchRaw?.requireMention === "boolean" ? twitchRaw.requireMention : void 0,
38
+ clientSecret: typeof twitchRaw?.clientSecret === "string" ? twitchRaw.clientSecret : void 0,
39
+ refreshToken: typeof twitchRaw?.refreshToken === "string" ? twitchRaw.refreshToken : void 0,
40
+ expiresIn: typeof twitchRaw?.expiresIn === "number" ? twitchRaw.expiresIn : void 0,
41
+ obtainmentTimestamp: typeof twitchRaw?.obtainmentTimestamp === "number" ? twitchRaw.obtainmentTimestamp : void 0
42
+ };
43
+ const merged = {
44
+ ...accountFromAccounts,
45
+ ...baseLevel
46
+ };
47
+ if (merged.username) return merged;
48
+ if (accountFromAccounts) return accountFromAccounts;
49
+ return null;
50
+ }
51
+ const account = resolveNormalizedAccountEntry(accounts, normalizedAccountId, normalizeAccountId);
52
+ if (!account) return null;
53
+ return account;
54
+ }
55
+ /**
56
+ * List all configured account IDs
57
+ *
58
+ * Includes both explicit accounts and implicit "default" from base-level config
59
+ */
60
+ function listAccountIds(cfg) {
61
+ const twitchRaw = cfg.channels?.twitch;
62
+ const accountMap = twitchRaw?.accounts;
63
+ const hasBaseLevelConfig = twitchRaw && (typeof twitchRaw.username === "string" || typeof twitchRaw.accessToken === "string" || typeof twitchRaw.channel === "string");
64
+ return listCombinedAccountIds({
65
+ configuredAccountIds: Object.keys(accountMap ?? {}).map((accountId) => normalizeAccountId(accountId)),
66
+ implicitAccountId: hasBaseLevelConfig ? DEFAULT_ACCOUNT_ID$1 : void 0
67
+ });
68
+ }
69
+ function resolveDefaultTwitchAccountId(cfg) {
70
+ const preferredRaw = typeof cfg.channels?.twitch?.defaultAccount === "string" ? cfg.channels.twitch.defaultAccount.trim() : "";
71
+ const preferred = preferredRaw ? normalizeAccountId(preferredRaw) : "";
72
+ const ids = listAccountIds(cfg);
73
+ if (preferred && ids.includes(preferred)) return preferred;
74
+ if (ids.includes("default")) return DEFAULT_ACCOUNT_ID$1;
75
+ return ids[0] ?? "default";
76
+ }
77
+ function resolveTwitchAccountContext(cfg, accountId) {
78
+ const resolvedAccountId = accountId?.trim() ? normalizeAccountId(accountId) : resolveDefaultTwitchAccountId(cfg);
79
+ const account = getAccountConfig(cfg, resolvedAccountId);
80
+ const tokenResolution = resolveTwitchToken(cfg, { accountId: resolvedAccountId });
81
+ return {
82
+ accountId: resolvedAccountId,
83
+ account,
84
+ tokenResolution,
85
+ configured: account ? isAccountConfigured(account, tokenResolution.token) : false,
86
+ availableAccountIds: listAccountIds(cfg)
87
+ };
88
+ }
89
+ function resolveTwitchSnapshotAccountId(cfg, account) {
90
+ const accountMap = (cfg.channels?.twitch)?.accounts ?? {};
91
+ return Object.entries(accountMap).find(([, value]) => value === account)?.[0] ?? "default";
92
+ }
93
+ //#endregion
94
+ //#region extensions/twitch/src/setup-surface.ts
95
+ /**
96
+ * Twitch setup wizard surface for CLI setup.
97
+ */
98
+ const channel = "twitch";
99
+ const INVALID_ACCOUNT_ID_MESSAGE = "Invalid Twitch account id";
100
+ function normalizeRequestedSetupAccountId(accountId) {
101
+ const normalized = normalizeOptionalAccountId(accountId);
102
+ if (!normalized) throw new Error(INVALID_ACCOUNT_ID_MESSAGE);
103
+ return normalized;
104
+ }
105
+ function resolveSetupAccountId(cfg, requestedAccountId) {
106
+ const requested = requestedAccountId?.trim();
107
+ if (requested) return normalizeRequestedSetupAccountId(requested);
108
+ const preferred = cfg.channels?.twitch?.defaultAccount?.trim();
109
+ return preferred ? normalizeAccountId$1(preferred) : resolveDefaultTwitchAccountId(cfg);
110
+ }
111
+ function setTwitchAccount(cfg, account, accountId = resolveSetupAccountId(cfg)) {
112
+ const resolvedAccountId = accountId.trim() ? normalizeRequestedSetupAccountId(accountId) : resolveSetupAccountId(cfg);
113
+ const existing = getAccountConfig(cfg, resolvedAccountId);
114
+ const merged = {
115
+ username: account.username ?? existing?.username ?? "",
116
+ accessToken: account.accessToken ?? existing?.accessToken ?? "",
117
+ clientId: account.clientId ?? existing?.clientId ?? "",
118
+ channel: account.channel ?? existing?.channel ?? "",
119
+ enabled: account.enabled ?? existing?.enabled ?? true,
120
+ allowFrom: account.allowFrom ?? existing?.allowFrom,
121
+ allowedRoles: account.allowedRoles ?? existing?.allowedRoles,
122
+ requireMention: account.requireMention ?? existing?.requireMention,
123
+ clientSecret: account.clientSecret ?? existing?.clientSecret,
124
+ refreshToken: account.refreshToken ?? existing?.refreshToken,
125
+ expiresIn: account.expiresIn ?? existing?.expiresIn,
126
+ obtainmentTimestamp: account.obtainmentTimestamp ?? existing?.obtainmentTimestamp
127
+ };
128
+ return {
129
+ ...cfg,
130
+ channels: {
131
+ ...cfg.channels,
132
+ twitch: {
133
+ ...cfg.channels?.twitch,
134
+ enabled: true,
135
+ accounts: {
136
+ ...(cfg.channels?.twitch)?.accounts,
137
+ [resolvedAccountId]: merged
138
+ }
139
+ }
140
+ }
141
+ };
142
+ }
143
+ async function noteTwitchSetupHelp(prompter) {
144
+ await prompter.note([
145
+ "Twitch requires a bot account with OAuth token.",
146
+ "1. Create a Twitch application at https://dev.twitch.tv/console",
147
+ "2. Generate a token with scopes: chat:read and chat:write",
148
+ " Use https://twitchtokengenerator.com/ or https://twitchapps.com/tmi/",
149
+ "3. Copy the token (starts with 'oauth:') and Client ID",
150
+ "Env vars supported: OPENCLAW_TWITCH_ACCESS_TOKEN",
151
+ `Docs: ${formatDocsLink("/channels/twitch", "channels/twitch")}`
152
+ ].join("\n"), "Twitch setup");
153
+ }
154
+ async function promptToken(prompter, account, envToken) {
155
+ const existingToken = account?.accessToken ?? "";
156
+ if (existingToken && !envToken) {
157
+ if (await prompter.confirm({
158
+ message: "Access token already configured. Keep it?",
159
+ initialValue: true
160
+ })) return existingToken;
161
+ }
162
+ return (await prompter.text({
163
+ message: "Twitch OAuth token (oauth:...)",
164
+ initialValue: envToken ?? "",
165
+ validate: (value) => {
166
+ const raw = value?.trim() ?? "";
167
+ if (!raw) return "Required";
168
+ if (!raw.startsWith("oauth:")) return "Token should start with 'oauth:'";
169
+ }
170
+ })).trim();
171
+ }
172
+ async function promptUsername(prompter, account) {
173
+ return (await prompter.text({
174
+ message: "Twitch bot username",
175
+ initialValue: account?.username ?? "",
176
+ validate: (value) => value?.trim() ? void 0 : "Required"
177
+ })).trim();
178
+ }
179
+ async function promptClientId(prompter, account) {
180
+ return (await prompter.text({
181
+ message: "Twitch Client ID",
182
+ initialValue: account?.clientId ?? "",
183
+ validate: (value) => value?.trim() ? void 0 : "Required"
184
+ })).trim();
185
+ }
186
+ async function promptChannelName(prompter, account) {
187
+ return (await prompter.text({
188
+ message: "Channel to join",
189
+ initialValue: account?.channel ?? "",
190
+ validate: (value) => value?.trim() ? void 0 : "Required"
191
+ })).trim();
192
+ }
193
+ async function promptRefreshTokenSetup(prompter, account) {
194
+ if (!await prompter.confirm({
195
+ message: "Enable automatic token refresh (requires client secret and refresh token)?",
196
+ initialValue: Boolean(account?.clientSecret && account?.refreshToken)
197
+ })) return {};
198
+ return {
199
+ clientSecret: (await prompter.text({
200
+ message: "Twitch Client Secret (for token refresh)",
201
+ initialValue: account?.clientSecret ?? "",
202
+ validate: (value) => value?.trim() ? void 0 : "Required"
203
+ })).trim() || void 0,
204
+ refreshToken: (await prompter.text({
205
+ message: "Twitch Refresh Token",
206
+ initialValue: account?.refreshToken ?? "",
207
+ validate: (value) => value?.trim() ? void 0 : "Required"
208
+ })).trim() || void 0
209
+ };
210
+ }
211
+ async function configureWithEnvToken(cfg, prompter, account, envToken, forceAllowFrom, dmPolicy, accountId = resolveSetupAccountId(cfg)) {
212
+ const resolvedAccountId = accountId.trim() ? normalizeRequestedSetupAccountId(accountId) : resolveSetupAccountId(cfg);
213
+ if (resolvedAccountId !== "default") return null;
214
+ if (!await prompter.confirm({
215
+ message: "Twitch env var OPENCLAW_TWITCH_ACCESS_TOKEN detected. Use env token?",
216
+ initialValue: true
217
+ })) return null;
218
+ const cfgWithAccount = setTwitchAccount(cfg, {
219
+ username: await promptUsername(prompter, account),
220
+ clientId: await promptClientId(prompter, account),
221
+ accessToken: "",
222
+ enabled: true
223
+ }, resolvedAccountId);
224
+ if (forceAllowFrom && dmPolicy.promptAllowFrom) return { cfg: await dmPolicy.promptAllowFrom({
225
+ cfg: cfgWithAccount,
226
+ prompter,
227
+ accountId: resolvedAccountId
228
+ }) };
229
+ return { cfg: cfgWithAccount };
230
+ }
231
+ function setTwitchAccessControl(cfg, allowedRoles, requireMention, accountId) {
232
+ const resolvedAccountId = resolveSetupAccountId(cfg, accountId);
233
+ const account = getAccountConfig(cfg, resolvedAccountId);
234
+ if (!account) return cfg;
235
+ return setTwitchAccount(cfg, {
236
+ ...account,
237
+ allowedRoles,
238
+ requireMention
239
+ }, resolvedAccountId);
240
+ }
241
+ function resolveTwitchGroupPolicy(cfg, accountId) {
242
+ const account = getAccountConfig(cfg, resolveSetupAccountId(cfg, accountId));
243
+ if (account?.allowedRoles?.includes("all")) return "open";
244
+ if (account?.allowedRoles?.includes("moderator")) return "allowlist";
245
+ return "disabled";
246
+ }
247
+ function setTwitchGroupPolicy(cfg, policy, accountId) {
248
+ return setTwitchAccessControl(cfg, policy === "open" ? ["all"] : policy === "allowlist" ? ["moderator", "vip"] : [], true, accountId);
249
+ }
250
+ const twitchDmPolicy = {
251
+ label: "Twitch",
252
+ channel,
253
+ policyKey: "channels.twitch.accounts.default.allowedRoles",
254
+ allowFromKey: "channels.twitch.accounts.default.allowFrom",
255
+ resolveConfigKeys: (cfg, accountId) => {
256
+ const resolvedAccountId = resolveSetupAccountId(cfg, accountId);
257
+ return {
258
+ policyKey: `channels.twitch.accounts.${resolvedAccountId}.allowedRoles`,
259
+ allowFromKey: `channels.twitch.accounts.${resolvedAccountId}.allowFrom`
260
+ };
261
+ },
262
+ getCurrent: (cfg, accountId) => {
263
+ const account = getAccountConfig(cfg, resolveSetupAccountId(cfg, accountId));
264
+ if (account?.allowedRoles?.includes("all")) return "open";
265
+ if (account?.allowFrom && account.allowFrom.length > 0) return "allowlist";
266
+ return "disabled";
267
+ },
268
+ setPolicy: (cfg, policy, accountId) => {
269
+ return setTwitchAccessControl(cfg, policy === "open" ? ["all"] : policy === "allowlist" ? [] : ["moderator"], true, accountId);
270
+ },
271
+ promptAllowFrom: async ({ cfg, prompter, accountId }) => {
272
+ const resolvedAccountId = resolveSetupAccountId(cfg, accountId);
273
+ const account = getAccountConfig(cfg, resolvedAccountId);
274
+ const existingAllowFrom = account?.allowFrom ?? [];
275
+ const allowFrom = (await prompter.text({
276
+ message: "Twitch allowFrom (user IDs, one per line, recommended for security)",
277
+ placeholder: "123456789",
278
+ initialValue: existingAllowFrom[0] || void 0
279
+ }) ?? "").split(/[\n,;]+/g).map((s) => s.trim()).filter(Boolean);
280
+ return setTwitchAccount(cfg, {
281
+ ...account ?? void 0,
282
+ allowFrom
283
+ }, resolvedAccountId);
284
+ }
285
+ };
286
+ const twitchGroupAccess = {
287
+ label: "Twitch chat",
288
+ placeholder: "",
289
+ skipAllowlistEntries: true,
290
+ currentPolicy: ({ cfg, accountId }) => resolveTwitchGroupPolicy(cfg, accountId),
291
+ currentEntries: ({ cfg, accountId }) => {
292
+ return getAccountConfig(cfg, resolveSetupAccountId(cfg, accountId))?.allowFrom ?? [];
293
+ },
294
+ updatePrompt: ({ cfg, accountId }) => {
295
+ const account = getAccountConfig(cfg, resolveSetupAccountId(cfg, accountId));
296
+ return Boolean(account?.allowedRoles?.length || account?.allowFrom?.length);
297
+ },
298
+ setPolicy: ({ cfg, accountId, policy }) => setTwitchGroupPolicy(cfg, policy, accountId),
299
+ resolveAllowlist: async () => [],
300
+ applyAllowlist: ({ cfg }) => cfg
301
+ };
302
+ const twitchSetupAdapter = {
303
+ resolveAccountId: ({ cfg }) => resolveSetupAccountId(cfg),
304
+ applyAccountConfig: ({ cfg, accountId }) => setTwitchAccount(cfg, { enabled: true }, accountId)
305
+ };
306
+ const twitchSetupWizard = {
307
+ channel,
308
+ resolveAccountIdForConfigure: ({ cfg, accountOverride }) => resolveSetupAccountId(cfg, accountOverride),
309
+ resolveShouldPromptAccountIds: () => false,
310
+ status: {
311
+ configuredLabel: "configured",
312
+ unconfiguredLabel: "needs username, token, and clientId",
313
+ configuredHint: "configured",
314
+ unconfiguredHint: "needs setup",
315
+ resolveConfigured: ({ cfg, accountId }) => {
316
+ return resolveTwitchAccountContext(cfg, resolveSetupAccountId(cfg, accountId)).configured;
317
+ },
318
+ resolveStatusLines: ({ cfg, accountId }) => {
319
+ const resolvedAccountId = resolveSetupAccountId(cfg, accountId);
320
+ const configured = resolveTwitchAccountContext(cfg, resolvedAccountId).configured;
321
+ return [`Twitch${resolvedAccountId !== "default" ? ` (${resolvedAccountId})` : ""}: ${configured ? "configured" : "needs username, token, and clientId"}`];
322
+ }
323
+ },
324
+ credentials: [],
325
+ finalize: async ({ cfg, accountId: requestedAccountId, prompter, forceAllowFrom }) => {
326
+ const accountId = resolveSetupAccountId(cfg, requestedAccountId);
327
+ const account = getAccountConfig(cfg, accountId);
328
+ if (!account || !isAccountConfigured(account)) await noteTwitchSetupHelp(prompter);
329
+ const envToken = process.env.OPENCLAW_TWITCH_ACCESS_TOKEN?.trim();
330
+ if (accountId === "default" && envToken && !account?.accessToken) {
331
+ const envResult = await configureWithEnvToken(cfg, prompter, account, envToken, forceAllowFrom, twitchDmPolicy, accountId);
332
+ if (envResult) return envResult;
333
+ }
334
+ const username = await promptUsername(prompter, account);
335
+ const token = await promptToken(prompter, account, envToken);
336
+ const clientId = await promptClientId(prompter, account);
337
+ const channelName = await promptChannelName(prompter, account);
338
+ const { clientSecret, refreshToken } = await promptRefreshTokenSetup(prompter, account);
339
+ const cfgWithAccount = setTwitchAccount(cfg, {
340
+ username,
341
+ accessToken: token,
342
+ clientId,
343
+ channel: channelName,
344
+ clientSecret,
345
+ refreshToken,
346
+ enabled: true
347
+ }, accountId);
348
+ return { cfg: forceAllowFrom && twitchDmPolicy.promptAllowFrom ? await twitchDmPolicy.promptAllowFrom({
349
+ cfg: cfgWithAccount,
350
+ prompter,
351
+ accountId
352
+ }) : cfgWithAccount };
353
+ },
354
+ dmPolicy: twitchDmPolicy,
355
+ groupAccess: twitchGroupAccess,
356
+ disable: (cfg) => {
357
+ const twitch = cfg.channels?.twitch;
358
+ return {
359
+ ...cfg,
360
+ channels: {
361
+ ...cfg.channels,
362
+ twitch: {
363
+ ...twitch,
364
+ enabled: false
365
+ }
366
+ }
367
+ };
368
+ }
369
+ };
370
+ const twitchSetupPlugin = {
371
+ id: channel,
372
+ meta: getChatChannelMeta(channel),
373
+ capabilities: { chatTypes: ["group"] },
374
+ config: {
375
+ listAccountIds: (cfg) => listAccountIds(cfg),
376
+ resolveAccount: (cfg, accountId) => {
377
+ const resolvedAccountId = normalizeAccountId$1(accountId ?? resolveDefaultTwitchAccountId(cfg));
378
+ const account = getAccountConfig(cfg, resolvedAccountId);
379
+ if (!account) return {
380
+ accountId: resolvedAccountId,
381
+ username: "",
382
+ accessToken: "",
383
+ clientId: "",
384
+ channel: "",
385
+ enabled: false
386
+ };
387
+ return {
388
+ accountId: resolvedAccountId,
389
+ ...account
390
+ };
391
+ },
392
+ defaultAccountId: (cfg) => resolveDefaultTwitchAccountId(cfg),
393
+ isConfigured: (account, cfg) => resolveTwitchAccountContext(cfg, account?.accountId).configured,
394
+ isEnabled: (account) => account.enabled !== false
395
+ },
396
+ setup: twitchSetupAdapter,
397
+ setupWizard: twitchSetupWizard
398
+ };
399
+ //#endregion
400
+ export { getAccountConfig as a, resolveTwitchAccountContext as c, DEFAULT_ACCOUNT_ID$1 as i, resolveTwitchSnapshotAccountId as l, twitchSetupPlugin as n, listAccountIds as o, twitchSetupWizard as r, resolveDefaultTwitchAccountId as s, twitchSetupAdapter as t };
@@ -0,0 +1,131 @@
1
+ import { DEFAULT_ACCOUNT_ID, normalizeAccountId, resolveNormalizedAccountEntry } from "openclaw/plugin-sdk/account-resolution";
2
+ import { randomUUID } from "node:crypto";
3
+ //#region extensions/twitch/src/token.ts
4
+ /**
5
+ * Twitch access token resolution with environment variable support.
6
+ *
7
+ * Supports reading Twitch OAuth access tokens from config or environment variable.
8
+ * The OPENCLAW_TWITCH_ACCESS_TOKEN env var is only used for the default account.
9
+ *
10
+ * Token resolution priority:
11
+ * 1. Account access token from merged config (accounts.{id} or base-level for default)
12
+ * 2. Environment variable: OPENCLAW_TWITCH_ACCESS_TOKEN (default account only)
13
+ */
14
+ /**
15
+ * Normalize a Twitch OAuth token - ensure it has the oauth: prefix
16
+ */
17
+ function normalizeTwitchToken(raw) {
18
+ if (!raw) return;
19
+ const trimmed = raw.trim();
20
+ if (!trimmed) return;
21
+ return trimmed.startsWith("oauth:") ? trimmed : `oauth:${trimmed}`;
22
+ }
23
+ /**
24
+ * Resolve Twitch access token from config or environment variable.
25
+ *
26
+ * Priority:
27
+ * 1. Account access token (from merged config - base-level for default, or accounts.{accountId})
28
+ * 2. Environment variable: OPENCLAW_TWITCH_ACCESS_TOKEN (default account only)
29
+ *
30
+ * The getAccountConfig function handles merging base-level config with accounts.default,
31
+ * so this logic works for both simplified and multi-account patterns.
32
+ *
33
+ * @param cfg - OpenClaw config
34
+ * @param opts - Options including accountId and optional envToken override
35
+ * @returns Token resolution with source
36
+ */
37
+ function resolveTwitchToken(cfg, opts = {}) {
38
+ const accountId = normalizeAccountId(opts.accountId);
39
+ const twitchCfg = cfg?.channels?.twitch;
40
+ const accounts = twitchCfg?.accounts;
41
+ const accountCfg = resolveNormalizedAccountEntry(accounts, accountId, normalizeAccountId);
42
+ let token;
43
+ if (accountId === DEFAULT_ACCOUNT_ID) token = normalizeTwitchToken((typeof twitchCfg?.accessToken === "string" ? twitchCfg.accessToken : void 0) || accountCfg?.accessToken);
44
+ else token = normalizeTwitchToken(accountCfg?.accessToken);
45
+ if (token) return {
46
+ token,
47
+ source: "config"
48
+ };
49
+ const envToken = accountId === DEFAULT_ACCOUNT_ID ? normalizeTwitchToken(opts.envToken ?? process.env.OPENCLAW_TWITCH_ACCESS_TOKEN) : void 0;
50
+ if (envToken) return {
51
+ token: envToken,
52
+ source: "env"
53
+ };
54
+ return {
55
+ token: "",
56
+ source: "none"
57
+ };
58
+ }
59
+ //#endregion
60
+ //#region extensions/twitch/src/utils/twitch.ts
61
+ /**
62
+ * Twitch-specific utility functions
63
+ */
64
+ function normalizeLowercaseStringOrEmpty(value) {
65
+ return typeof value === "string" ? value.trim().toLowerCase() : "";
66
+ }
67
+ /**
68
+ * Normalize Twitch channel names.
69
+ *
70
+ * Removes the '#' prefix if present, converts to lowercase, and trims whitespace.
71
+ * Twitch channel names are case-insensitive and don't use the '#' prefix in the API.
72
+ *
73
+ * @param channel - The channel name to normalize
74
+ * @returns Normalized channel name
75
+ *
76
+ * @example
77
+ * normalizeTwitchChannel("#TwitchChannel") // "twitchchannel"
78
+ * normalizeTwitchChannel("MyChannel") // "mychannel"
79
+ */
80
+ function normalizeTwitchChannel(channel) {
81
+ const trimmed = normalizeLowercaseStringOrEmpty(channel);
82
+ return trimmed.startsWith("#") ? trimmed.slice(1) : trimmed;
83
+ }
84
+ /**
85
+ * Create a standardized error message for missing target.
86
+ *
87
+ * @param provider - The provider name (e.g., "Twitch")
88
+ * @param hint - Optional hint for how to fix the issue
89
+ * @returns Error object with descriptive message
90
+ */
91
+ function missingTargetError(provider, hint) {
92
+ return /* @__PURE__ */ new Error(`Delivering to ${provider} requires target${hint ? ` ${hint}` : ""}`);
93
+ }
94
+ /**
95
+ * Generate a unique message ID for Twitch messages.
96
+ *
97
+ * Twurple's say() doesn't return the message ID, so we generate one
98
+ * for tracking purposes.
99
+ *
100
+ * @returns A unique message ID
101
+ */
102
+ function generateMessageId() {
103
+ return `${Date.now()}-${randomUUID()}`;
104
+ }
105
+ /**
106
+ * Normalize OAuth token by removing the "oauth:" prefix if present.
107
+ *
108
+ * Twurple doesn't require the "oauth:" prefix, so we strip it for consistency.
109
+ *
110
+ * @param token - The OAuth token to normalize
111
+ * @returns Normalized token without "oauth:" prefix
112
+ *
113
+ * @example
114
+ * normalizeToken("oauth:abc123") // "abc123"
115
+ * normalizeToken("abc123") // "abc123"
116
+ */
117
+ function normalizeToken(token) {
118
+ return token.startsWith("oauth:") ? token.slice(6) : token;
119
+ }
120
+ /**
121
+ * Check if an account is properly configured with required credentials.
122
+ *
123
+ * @param account - The Twitch account config to check
124
+ * @returns true if the account has required credentials
125
+ */
126
+ function isAccountConfigured(account, resolvedToken) {
127
+ const token = resolvedToken ?? account?.accessToken;
128
+ return Boolean(account?.username && token && account?.clientId);
129
+ }
130
+ //#endregion
131
+ export { normalizeTwitchChannel as a, normalizeToken as i, isAccountConfigured as n, resolveTwitchToken as o, missingTargetError as r, generateMessageId as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openclaw/twitch",
3
- "version": "2026.5.2-beta.2",
3
+ "version": "2026.5.3-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.2-beta.2"
29
+ "pluginApi": ">=2026.5.3-beta.1"
30
30
  },
31
31
  "build": {
32
- "openclawVersion": "2026.5.2-beta.2"
32
+ "openclawVersion": "2026.5.3-beta.1"
33
33
  },
34
34
  "channel": {
35
35
  "id": "twitch",
@@ -44,6 +44,23 @@
44
44
  "release": {
45
45
  "publishToClawHub": true,
46
46
  "publishToNpm": true
47
+ },
48
+ "runtimeExtensions": [
49
+ "./dist/index.js"
50
+ ],
51
+ "runtimeSetupEntry": "./dist/setup-entry.js"
52
+ },
53
+ "files": [
54
+ "dist/**",
55
+ "openclaw.plugin.json",
56
+ "README.md"
57
+ ],
58
+ "peerDependencies": {
59
+ "openclaw": ">=2026.5.3-beta.1"
60
+ },
61
+ "peerDependenciesMeta": {
62
+ "openclaw": {
63
+ "optional": true
47
64
  }
48
65
  }
49
66
  }
package/api.ts DELETED
@@ -1,21 +0,0 @@
1
- export {
2
- type ChannelAccountSnapshot,
3
- type ChannelCapabilities,
4
- type ChannelGatewayContext,
5
- type ChannelLogSink,
6
- type ChannelMessageActionAdapter,
7
- type ChannelMessageActionContext,
8
- type ChannelMeta,
9
- type ChannelOutboundAdapter,
10
- type ChannelOutboundContext,
11
- type ChannelPlugin,
12
- type ChannelResolveKind,
13
- type ChannelResolveResult,
14
- type ChannelStatusAdapter,
15
- type OpenClawConfig,
16
- type OutboundDeliveryResult,
17
- type RuntimeEnv,
18
- type WizardPrompter,
19
- } from "./runtime-api.js";
20
- export { twitchPlugin } from "./src/plugin.js";
21
- export { setTwitchRuntime } from "./src/runtime.js";
@@ -1 +0,0 @@
1
- export { twitchPlugin } from "./src/plugin.js";
package/index.test.ts DELETED
@@ -1,13 +0,0 @@
1
- import { assertBundledChannelEntries } from "openclaw/plugin-sdk/channel-test-helpers";
2
- import { describe } from "vitest";
3
- import entry from "./index.js";
4
- import setupEntry from "./setup-entry.js";
5
-
6
- describe("twitch bundled entries", () => {
7
- assertBundledChannelEntries({
8
- entry,
9
- expectedId: "twitch",
10
- expectedName: "Twitch",
11
- setupEntry,
12
- });
13
- });
package/index.ts DELETED
@@ -1,16 +0,0 @@
1
- import { defineBundledChannelEntry } from "openclaw/plugin-sdk/channel-entry-contract";
2
-
3
- export default defineBundledChannelEntry({
4
- id: "twitch",
5
- name: "Twitch",
6
- description: "Twitch IRC chat channel plugin",
7
- importMetaUrl: import.meta.url,
8
- plugin: {
9
- specifier: "./channel-plugin-api.js",
10
- exportName: "twitchPlugin",
11
- },
12
- runtime: {
13
- specifier: "./api.js",
14
- exportName: "setTwitchRuntime",
15
- },
16
- });
package/runtime-api.ts DELETED
@@ -1,22 +0,0 @@
1
- // Private runtime barrel for the bundled Twitch extension.
2
- // Keep this barrel thin and aligned with the local extension surface.
3
-
4
- export type {
5
- ChannelAccountSnapshot,
6
- ChannelCapabilities,
7
- ChannelGatewayContext,
8
- ChannelLogSink,
9
- ChannelMessageActionAdapter,
10
- ChannelMessageActionContext,
11
- ChannelMeta,
12
- ChannelOutboundAdapter,
13
- ChannelOutboundContext,
14
- ChannelResolveKind,
15
- ChannelResolveResult,
16
- ChannelStatusAdapter,
17
- } from "openclaw/plugin-sdk/channel-contract";
18
- export type { ChannelPlugin } from "openclaw/plugin-sdk/channel-core";
19
- export type { OutboundDeliveryResult } from "openclaw/plugin-sdk/channel-send-result";
20
- export type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
21
- export type { RuntimeEnv } from "openclaw/plugin-sdk/runtime";
22
- export type { WizardPrompter } from "openclaw/plugin-sdk/setup";