@elizaos/plugin-twitch 2.0.0-alpha.7 → 2.0.0-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/index.js CHANGED
@@ -1,6 +1,122 @@
1
1
  // src/index.ts
2
- import { logger as logger2 } from "@elizaos/core";
2
+ import { getConnectorAccountManager, logger as logger2 } from "@elizaos/core";
3
3
 
4
+ // src/accounts.ts
5
+ var DEFAULT_TWITCH_ACCOUNT_ID = "default";
6
+ function stringSetting(runtime, key) {
7
+ const value = runtime.getSetting(key);
8
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
9
+ }
10
+ function characterConfig(runtime) {
11
+ const settings = runtime.character?.settings;
12
+ const raw = settings?.twitch;
13
+ return raw && typeof raw === "object" ? raw : {};
14
+ }
15
+ function parseAccountsJson(runtime) {
16
+ const raw = stringSetting(runtime, "TWITCH_ACCOUNTS");
17
+ if (!raw)
18
+ return {};
19
+ try {
20
+ const parsed = JSON.parse(raw);
21
+ if (Array.isArray(parsed)) {
22
+ return Object.fromEntries(parsed.filter((item) => Boolean(item) && typeof item === "object").map((item) => [
23
+ normalizeTwitchAccountId(item.accountId ?? item.id),
24
+ item
25
+ ]));
26
+ }
27
+ return parsed && typeof parsed === "object" ? parsed : {};
28
+ } catch {
29
+ return {};
30
+ }
31
+ }
32
+ function allAccountConfigs(runtime) {
33
+ return {
34
+ ...characterConfig(runtime).accounts ?? {},
35
+ ...parseAccountsJson(runtime)
36
+ };
37
+ }
38
+ function accountConfig(runtime, accountId) {
39
+ const accounts = allAccountConfigs(runtime);
40
+ return accounts[accountId] ?? accounts[normalizeTwitchAccountId(accountId)] ?? {};
41
+ }
42
+ function boolValue(value, fallback = false) {
43
+ if (typeof value === "boolean")
44
+ return value;
45
+ if (typeof value === "string")
46
+ return value.trim().toLowerCase() === "true";
47
+ return fallback;
48
+ }
49
+ function stringList(value) {
50
+ if (Array.isArray(value))
51
+ return value.map((item) => String(item).trim()).filter(Boolean);
52
+ if (typeof value === "string") {
53
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
54
+ }
55
+ return [];
56
+ }
57
+ function roleList(value) {
58
+ const values = stringList(value).map((role) => role.toLowerCase());
59
+ return values.length ? values : ["all"];
60
+ }
61
+ function normalizeTwitchAccountId(accountId) {
62
+ if (typeof accountId !== "string")
63
+ return DEFAULT_TWITCH_ACCOUNT_ID;
64
+ const trimmed = accountId.trim();
65
+ return trimmed || DEFAULT_TWITCH_ACCOUNT_ID;
66
+ }
67
+ function listTwitchAccountIds(runtime) {
68
+ const ids = new Set;
69
+ const config = characterConfig(runtime);
70
+ if (stringSetting(runtime, "TWITCH_ACCESS_TOKEN") || config.accessToken) {
71
+ ids.add(DEFAULT_TWITCH_ACCOUNT_ID);
72
+ }
73
+ for (const id of Object.keys(allAccountConfigs(runtime))) {
74
+ ids.add(normalizeTwitchAccountId(id));
75
+ }
76
+ return Array.from(ids.size ? ids : new Set([DEFAULT_TWITCH_ACCOUNT_ID])).sort((a, b) => a.localeCompare(b));
77
+ }
78
+ function resolveDefaultTwitchAccountId(runtime) {
79
+ const requested = stringSetting(runtime, "TWITCH_DEFAULT_ACCOUNT_ID") ?? stringSetting(runtime, "TWITCH_ACCOUNT_ID");
80
+ if (requested)
81
+ return normalizeTwitchAccountId(requested);
82
+ const ids = listTwitchAccountIds(runtime);
83
+ return ids.includes(DEFAULT_TWITCH_ACCOUNT_ID) ? DEFAULT_TWITCH_ACCOUNT_ID : ids[0];
84
+ }
85
+ function readTwitchAccountId(...sources) {
86
+ for (const source of sources) {
87
+ if (!source || typeof source !== "object")
88
+ continue;
89
+ const record = source;
90
+ const parameters = record.parameters && typeof record.parameters === "object" ? record.parameters : {};
91
+ const data = record.data && typeof record.data === "object" ? record.data : {};
92
+ const metadata = record.metadata && typeof record.metadata === "object" ? record.metadata : {};
93
+ const twitch = data.twitch && typeof data.twitch === "object" ? data.twitch : {};
94
+ const value = record.accountId ?? parameters.accountId ?? data.accountId ?? twitch.accountId ?? metadata.accountId;
95
+ if (typeof value === "string" && value.trim())
96
+ return normalizeTwitchAccountId(value);
97
+ }
98
+ return;
99
+ }
100
+ function resolveTwitchAccountSettings(runtime, requestedAccountId) {
101
+ const accountId = normalizeTwitchAccountId(requestedAccountId ?? resolveDefaultTwitchAccountId(runtime));
102
+ const base = characterConfig(runtime);
103
+ const account = accountConfig(runtime, accountId);
104
+ const allowEnv = accountId === DEFAULT_TWITCH_ACCOUNT_ID;
105
+ return {
106
+ accountId,
107
+ username: account.username ?? base.username ?? (allowEnv ? stringSetting(runtime, "TWITCH_USERNAME") : undefined) ?? "",
108
+ clientId: account.clientId ?? base.clientId ?? (allowEnv ? stringSetting(runtime, "TWITCH_CLIENT_ID") : undefined) ?? "",
109
+ accessToken: account.accessToken ?? base.accessToken ?? (allowEnv ? stringSetting(runtime, "TWITCH_ACCESS_TOKEN") : undefined) ?? "",
110
+ clientSecret: account.clientSecret ?? base.clientSecret ?? (allowEnv ? stringSetting(runtime, "TWITCH_CLIENT_SECRET") : undefined),
111
+ refreshToken: account.refreshToken ?? base.refreshToken ?? (allowEnv ? stringSetting(runtime, "TWITCH_REFRESH_TOKEN") : undefined),
112
+ channel: account.channel ?? base.channel ?? (allowEnv ? stringSetting(runtime, "TWITCH_CHANNEL") : undefined) ?? "",
113
+ additionalChannels: stringList(account.additionalChannels ?? account.channels ?? base.additionalChannels ?? base.channels ?? (allowEnv ? stringSetting(runtime, "TWITCH_CHANNELS") : undefined)),
114
+ requireMention: boolValue(account.requireMention ?? base.requireMention ?? (allowEnv ? stringSetting(runtime, "TWITCH_REQUIRE_MENTION") : undefined)),
115
+ allowedRoles: roleList(account.allowedRoles ?? base.allowedRoles ?? (allowEnv ? stringSetting(runtime, "TWITCH_ALLOWED_ROLES") : undefined)),
116
+ allowedUserIds: stringList(account.allowedUserIds ?? base.allowedUserIds),
117
+ enabled: boolValue(account.enabled ?? base.enabled, true)
118
+ };
119
+ }
4
120
  // src/service.ts
5
121
  import {
6
122
  logger,
@@ -100,6 +216,59 @@ class TwitchApiError extends TwitchPluginError {
100
216
  }
101
217
 
102
218
  // src/service.ts
219
+ var TWITCH_CONNECTOR_CONTEXTS = ["social", "connectors"];
220
+ var TWITCH_CONNECTOR_CAPABILITIES = [
221
+ "send_message",
222
+ "resolve_targets",
223
+ "list_rooms",
224
+ "join",
225
+ "leave",
226
+ "chat_context"
227
+ ];
228
+ function normalizeTwitchConnectorQuery(value) {
229
+ return normalizeChannel(value.trim().replace(/^@/, "")).toLowerCase();
230
+ }
231
+ function scoreTwitchChannelMatch(query, channel) {
232
+ const normalized = normalizeTwitchConnectorQuery(channel);
233
+ if (!query) {
234
+ return 0.45;
235
+ }
236
+ if (normalized === query) {
237
+ return 1;
238
+ }
239
+ if (normalized.startsWith(query)) {
240
+ return 0.85;
241
+ }
242
+ if (normalized.includes(query)) {
243
+ return 0.7;
244
+ }
245
+ return 0;
246
+ }
247
+ async function logTwurpleCall(op, context, fn) {
248
+ const startedAt = Date.now();
249
+ logger.debug({ sdk: "twurple", op, ...context }, `[TwitchService] ${op} started`);
250
+ try {
251
+ const result = await fn();
252
+ logger.info({
253
+ sdk: "twurple",
254
+ op,
255
+ ...context,
256
+ durationMs: Date.now() - startedAt
257
+ }, `[TwitchService] ${op} ok`);
258
+ return result;
259
+ } catch (error) {
260
+ const message = error instanceof Error ? error.message : String(error);
261
+ logger.warn({
262
+ sdk: "twurple",
263
+ op,
264
+ ...context,
265
+ durationMs: Date.now() - startedAt,
266
+ error: message
267
+ }, `[TwitchService] ${op} failed`);
268
+ throw error;
269
+ }
270
+ }
271
+
103
272
  class TwitchService extends Service {
104
273
  static serviceType = TWITCH_SERVICE_NAME;
105
274
  capabilityDescription = "Provides Twitch chat integration for sending and receiving messages";
@@ -107,11 +276,46 @@ class TwitchService extends Service {
107
276
  client;
108
277
  connected = false;
109
278
  joinedChannels = new Set;
279
+ accountServices = new Map;
110
280
  static async start(runtime) {
111
281
  const service = new TwitchService;
112
282
  await service.initialize(runtime);
113
283
  return service;
114
284
  }
285
+ static registerSendHandlers(runtime, serviceInstance) {
286
+ if (!serviceInstance) {
287
+ return;
288
+ }
289
+ for (const accountService of serviceInstance.getAccountServiceList()) {
290
+ const accountId = accountService.getAccountId(runtime);
291
+ const sendHandler = accountService.handleSendMessage.bind(accountService);
292
+ if (typeof runtime.registerMessageConnector === "function") {
293
+ runtime.registerMessageConnector({
294
+ source: "twitch",
295
+ accountId,
296
+ label: "Twitch",
297
+ description: "Twitch public chat connector for sending messages to joined channels.",
298
+ capabilities: [...TWITCH_CONNECTOR_CAPABILITIES],
299
+ supportedTargetKinds: ["channel"],
300
+ contexts: [...TWITCH_CONNECTOR_CONTEXTS],
301
+ metadata: {
302
+ accountId,
303
+ service: TWITCH_SERVICE_NAME,
304
+ maxMessageLength: MAX_TWITCH_MESSAGE_LENGTH
305
+ },
306
+ resolveTargets: accountService.resolveConnectorTargets.bind(accountService),
307
+ listRecentTargets: accountService.listRecentConnectorTargets.bind(accountService),
308
+ listRooms: accountService.listConnectorRooms.bind(accountService),
309
+ joinHandler: accountService.handleJoinChannel.bind(accountService),
310
+ leaveHandler: accountService.handleLeaveChannel.bind(accountService),
311
+ getChatContext: accountService.getConnectorChatContext.bind(accountService),
312
+ sendHandler
313
+ });
314
+ runtime.logger.info({ src: "plugin:twitch", agentId: runtime.agentId }, "Registered Twitch chat connector");
315
+ continue;
316
+ }
317
+ }
318
+ }
115
319
  static async stopRuntime(runtime) {
116
320
  const service = runtime.getService(TWITCH_SERVICE_NAME);
117
321
  if (service) {
@@ -120,7 +324,26 @@ class TwitchService extends Service {
120
324
  }
121
325
  async initialize(runtime) {
122
326
  this.runtime = runtime;
123
- this.settings = this.loadSettings();
327
+ const startedAccounts = [];
328
+ for (const accountId of listTwitchAccountIds(runtime)) {
329
+ const settings = resolveTwitchAccountSettings(runtime, accountId);
330
+ if (settings.enabled === false) {
331
+ continue;
332
+ }
333
+ const accountService = new TwitchService;
334
+ await accountService.initializeAccount(runtime, accountId);
335
+ this.accountServices.set(accountService.getAccountId(), accountService);
336
+ startedAccounts.push(accountService.getAccountId());
337
+ }
338
+ if (startedAccounts.length === 0) {
339
+ logger.warn("No enabled Twitch accounts configured");
340
+ return;
341
+ }
342
+ logger.info(`Twitch service started ${startedAccounts.length} account(s): ${startedAccounts.join(", ")}`);
343
+ }
344
+ async initializeAccount(runtime, accountId) {
345
+ this.runtime = runtime;
346
+ this.settings = this.loadSettings(accountId);
124
347
  this.validateSettings();
125
348
  const authProvider = await this.createAuthProvider();
126
349
  const allChannels = [
@@ -136,31 +359,8 @@ class TwitchService extends Service {
136
359
  await this.connect();
137
360
  logger.info(`Twitch service initialized for ${this.settings.username}, joined channels: ${allChannels.join(", ")}`);
138
361
  }
139
- loadSettings() {
140
- const username = this.runtime.getSetting("TWITCH_USERNAME");
141
- const clientId = this.runtime.getSetting("TWITCH_CLIENT_ID");
142
- const accessToken = this.runtime.getSetting("TWITCH_ACCESS_TOKEN");
143
- const clientSecret = this.runtime.getSetting("TWITCH_CLIENT_SECRET");
144
- const refreshToken = this.runtime.getSetting("TWITCH_REFRESH_TOKEN");
145
- const channel = this.runtime.getSetting("TWITCH_CHANNEL");
146
- const additionalChannelsStr = this.runtime.getSetting("TWITCH_CHANNELS");
147
- const requireMentionStr = this.runtime.getSetting("TWITCH_REQUIRE_MENTION");
148
- const allowedRolesStr = this.runtime.getSetting("TWITCH_ALLOWED_ROLES");
149
- const additionalChannels = typeof additionalChannelsStr === "string" && additionalChannelsStr ? additionalChannelsStr.split(",").map((c) => c.trim()).filter(Boolean) : [];
150
- const allowedRoles = typeof allowedRolesStr === "string" && allowedRolesStr ? allowedRolesStr.split(",").map((r) => r.trim().toLowerCase()) : ["all"];
151
- return {
152
- username: typeof username === "string" ? username : "",
153
- clientId: typeof clientId === "string" ? clientId : "",
154
- accessToken: typeof accessToken === "string" ? accessToken : "",
155
- clientSecret: typeof clientSecret === "string" ? clientSecret : undefined,
156
- refreshToken: typeof refreshToken === "string" ? refreshToken : undefined,
157
- channel: typeof channel === "string" ? channel : "",
158
- additionalChannels,
159
- requireMention: requireMentionStr === "true",
160
- allowedRoles,
161
- allowedUserIds: [],
162
- enabled: true
163
- };
362
+ loadSettings(accountId) {
363
+ return resolveTwitchAccountSettings(this.runtime, accountId);
164
364
  }
165
365
  validateSettings() {
166
366
  if (!this.settings.username) {
@@ -209,7 +409,8 @@ class TwitchService extends Service {
209
409
  this.connected = true;
210
410
  logger.info("Twitch chat connected");
211
411
  this.runtime.emitEvent("TWITCH_CONNECTION_READY" /* CONNECTION_READY */, {
212
- runtime: this.runtime
412
+ runtime: this.runtime,
413
+ accountId: this.getAccountId()
213
414
  });
214
415
  });
215
416
  this.client.onDisconnect((_manually, reason) => {
@@ -217,6 +418,7 @@ class TwitchService extends Service {
217
418
  logger.warn(`Twitch chat disconnected: ${reason || "unknown reason"}`);
218
419
  this.runtime.emitEvent("TWITCH_CONNECTION_LOST" /* CONNECTION_LOST */, {
219
420
  runtime: this.runtime,
421
+ accountId: this.getAccountId(),
220
422
  reason
221
423
  });
222
424
  });
@@ -227,6 +429,7 @@ class TwitchService extends Service {
227
429
  logger.info(`Joined Twitch channel: ${normalized}`);
228
430
  this.runtime.emitEvent("TWITCH_JOIN_CHANNEL" /* JOIN_CHANNEL */, {
229
431
  runtime: this.runtime,
432
+ accountId: this.getAccountId(),
230
433
  channel: normalized
231
434
  });
232
435
  }
@@ -238,6 +441,7 @@ class TwitchService extends Service {
238
441
  logger.info(`Left Twitch channel: ${normalized}`);
239
442
  this.runtime.emitEvent("TWITCH_LEAVE_CHANNEL" /* LEAVE_CHANNEL */, {
240
443
  runtime: this.runtime,
444
+ accountId: this.getAccountId(),
241
445
  channel: normalized
242
446
  });
243
447
  }
@@ -289,6 +493,7 @@ class TwitchService extends Service {
289
493
  logger.debug(`Twitch message from ${userInfo.displayName} in #${normalizedChannel}: ${text.slice(0, 50)}...`);
290
494
  this.runtime.emitEvent("TWITCH_MESSAGE_RECEIVED" /* MESSAGE_RECEIVED */, {
291
495
  runtime: this.runtime,
496
+ accountId: this.getAccountId(),
292
497
  message
293
498
  });
294
499
  }
@@ -297,6 +502,12 @@ class TwitchService extends Service {
297
502
  this.connected = true;
298
503
  }
299
504
  async stop() {
505
+ if (this.accountServices?.size > 0) {
506
+ await Promise.all(Array.from(this.accountServices.values()).map((service) => service.stop()));
507
+ this.accountServices.clear();
508
+ logger.info("Twitch service stopped");
509
+ return;
510
+ }
300
511
  if (this.client) {
301
512
  this.client.quit();
302
513
  }
@@ -304,18 +515,200 @@ class TwitchService extends Service {
304
515
  this.joinedChannels.clear();
305
516
  logger.info("Twitch service stopped");
306
517
  }
518
+ getAccountServiceList() {
519
+ return this.accountServices?.size > 0 ? Array.from(this.accountServices.values()) : [this];
520
+ }
521
+ getDefaultAccountService() {
522
+ if (!this.accountServices || this.accountServices.size === 0) {
523
+ return this;
524
+ }
525
+ const defaultAccountId = normalizeTwitchAccountId(resolveDefaultTwitchAccountId(this.runtime));
526
+ return this.accountServices.get(defaultAccountId) ?? Array.from(this.accountServices.values())[0];
527
+ }
528
+ getAccountService(accountId) {
529
+ if (!this.accountServices || this.accountServices.size === 0) {
530
+ const ownAccountId = this.getAccountId();
531
+ if (normalizeTwitchAccountId(accountId) !== ownAccountId) {
532
+ throw new Error(`Twitch account '${accountId}' is not available in this service instance`);
533
+ }
534
+ return this;
535
+ }
536
+ const normalized = normalizeTwitchAccountId(accountId);
537
+ const service = this.accountServices.get(normalized);
538
+ if (!service) {
539
+ throw new Error(`Twitch account '${normalized}' is not available`);
540
+ }
541
+ return service;
542
+ }
307
543
  isConnected() {
544
+ if (this.accountServices?.size > 0) {
545
+ return Array.from(this.accountServices.values()).some((service) => service.isConnected());
546
+ }
308
547
  return this.connected;
309
548
  }
310
549
  getBotUsername() {
550
+ if (this.accountServices?.size > 0) {
551
+ return this.getDefaultAccountService().getBotUsername();
552
+ }
311
553
  return this.settings.username;
312
554
  }
555
+ getAccountId(runtime) {
556
+ if (this.accountServices?.size > 0) {
557
+ return this.getDefaultAccountService().getAccountId(runtime);
558
+ }
559
+ return normalizeTwitchAccountId(this.settings?.accountId ?? (runtime ? resolveDefaultTwitchAccountId(runtime) : undefined));
560
+ }
313
561
  getPrimaryChannel() {
562
+ if (this.accountServices?.size > 0) {
563
+ return this.getDefaultAccountService().getPrimaryChannel();
564
+ }
314
565
  return this.settings.channel;
315
566
  }
316
567
  getJoinedChannels() {
568
+ if (this.accountServices?.size > 0) {
569
+ return this.getDefaultAccountService().getJoinedChannels();
570
+ }
317
571
  return Array.from(this.joinedChannels);
318
572
  }
573
+ async handleSendMessage(runtime, target, content) {
574
+ const requestedAccountId = normalizeTwitchAccountId(target.accountId ?? readTwitchAccountId(content, target) ?? this.getAccountId());
575
+ if (this.accountServices?.size > 0) {
576
+ await this.getAccountService(requestedAccountId).handleSendMessage(runtime, target, content);
577
+ return;
578
+ }
579
+ if (requestedAccountId !== this.getAccountId()) {
580
+ throw new Error(`Twitch account '${requestedAccountId}' is not available in this service instance`);
581
+ }
582
+ const text = typeof content.text === "string" ? content.text.trim() : "";
583
+ if (!text) {
584
+ throw new Error("Twitch connector requires non-empty text content.");
585
+ }
586
+ let channel = target.channelId;
587
+ let replyTo = target.threadId;
588
+ if (target.roomId && !channel) {
589
+ const room = await runtime.getRoom(target.roomId);
590
+ channel = room?.channelId;
591
+ const metadata = room?.metadata;
592
+ replyTo = replyTo ?? (typeof metadata?.twitchReplyTo === "string" ? metadata.twitchReplyTo : undefined);
593
+ }
594
+ await this.sendMessage(text, {
595
+ channel: channel ? normalizeChannel(channel) : this.getPrimaryChannel(),
596
+ replyTo
597
+ });
598
+ }
599
+ async resolveConnectorTargets(query, _context) {
600
+ const normalizedQuery = normalizeTwitchConnectorQuery(query);
601
+ return this.getConnectorChannels().map((channel) => {
602
+ const score = scoreTwitchChannelMatch(normalizedQuery, channel);
603
+ return score > 0 ? this.buildChannelTarget(channel, score) : null;
604
+ }).filter((target) => Boolean(target)).slice(0, 25);
605
+ }
606
+ async listConnectorRooms(_context) {
607
+ return this.getConnectorChannels().map((channel) => this.buildChannelTarget(channel, 0.5)).slice(0, 50);
608
+ }
609
+ async listRecentConnectorTargets(context) {
610
+ const targets = [];
611
+ const room = context.roomId && typeof context.runtime.getRoom === "function" ? await context.runtime.getRoom(context.roomId) : null;
612
+ const channel = context.target?.channelId ?? room?.channelId;
613
+ if (channel) {
614
+ targets.push(this.buildChannelTarget(channel, 0.95));
615
+ }
616
+ targets.push(...await this.listConnectorRooms(context));
617
+ const seen = new Set;
618
+ return targets.filter((target) => {
619
+ const channelId = target.target.channelId;
620
+ if (!channelId || seen.has(channelId)) {
621
+ return false;
622
+ }
623
+ seen.add(channelId);
624
+ return true;
625
+ }).slice(0, 25);
626
+ }
627
+ async handleJoinChannel(_runtime, params) {
628
+ const channel = this.resolveChannelOpTarget(params);
629
+ if (!channel) {
630
+ throw new Error("Twitch MESSAGE operation=join requires channelId, alias, or target channel.");
631
+ }
632
+ if (this.joinedChannels.has(channel)) {
633
+ return;
634
+ }
635
+ await this.joinChannel(channel);
636
+ }
637
+ async handleLeaveChannel(_runtime, params) {
638
+ const channel = this.resolveChannelOpTarget(params);
639
+ if (!channel) {
640
+ throw new Error("Twitch MESSAGE operation=leave requires channelId, alias, or target channel.");
641
+ }
642
+ if (channel === normalizeChannel(this.getPrimaryChannel())) {
643
+ throw new Error(`Cannot leave the primary Twitch channel #${channel}.`);
644
+ }
645
+ if (!this.joinedChannels.has(channel)) {
646
+ throw new Error(`Not currently in Twitch channel #${channel}.`);
647
+ }
648
+ await this.leaveChannel(channel);
649
+ }
650
+ async getConnectorChatContext(target, context) {
651
+ let channel = target.channelId;
652
+ if (!channel && target.roomId) {
653
+ const room = await context.runtime.getRoom(target.roomId);
654
+ channel = room?.channelId;
655
+ }
656
+ channel = channel ? normalizeChannel(channel) : this.getPrimaryChannel();
657
+ return {
658
+ target: {
659
+ source: "twitch",
660
+ accountId: this.getAccountId(),
661
+ channelId: channel
662
+ },
663
+ label: formatChannelForDisplay(channel),
664
+ summary: "Twitch chat messages are public and visible to viewers in the channel.",
665
+ metadata: {
666
+ accountId: this.getAccountId(),
667
+ twitchChannel: channel,
668
+ botUsername: this.getBotUsername(),
669
+ joined: this.joinedChannels.has(channel)
670
+ }
671
+ };
672
+ }
673
+ getConnectorChannels() {
674
+ const channels = new Set;
675
+ if (this.settings?.channel) {
676
+ channels.add(normalizeChannel(this.settings.channel));
677
+ }
678
+ for (const channel of this.settings?.additionalChannels ?? []) {
679
+ channels.add(normalizeChannel(channel));
680
+ }
681
+ for (const channel of this.joinedChannels) {
682
+ channels.add(normalizeChannel(channel));
683
+ }
684
+ return Array.from(channels);
685
+ }
686
+ resolveChannelOpTarget(params) {
687
+ const targetRecord = params.target;
688
+ const raw = params.target?.channelId ?? params.channelId ?? params.alias ?? targetRecord?.alias ?? targetRecord?.name;
689
+ return typeof raw === "string" && raw.trim() ? normalizeChannel(raw.trim()) : null;
690
+ }
691
+ buildChannelTarget(channel, score) {
692
+ const normalized = normalizeChannel(channel);
693
+ return {
694
+ target: {
695
+ source: "twitch",
696
+ accountId: this.getAccountId(),
697
+ channelId: normalized
698
+ },
699
+ label: formatChannelForDisplay(normalized),
700
+ kind: "channel",
701
+ description: "Twitch public chat channel",
702
+ score,
703
+ contexts: [...TWITCH_CONNECTOR_CONTEXTS],
704
+ metadata: {
705
+ accountId: this.getAccountId(),
706
+ twitchChannel: normalized,
707
+ joined: this.joinedChannels.has(normalized),
708
+ primary: normalized === this.getPrimaryChannel()
709
+ }
710
+ };
711
+ }
319
712
  isUserAllowed(user) {
320
713
  if (this.settings.allowedUserIds.length > 0 && !this.settings.allowedUserIds.includes(user.userId)) {
321
714
  return false;
@@ -338,6 +731,10 @@ class TwitchService extends Service {
338
731
  return false;
339
732
  }
340
733
  async sendMessage(text, options) {
734
+ if (this.accountServices?.size > 0) {
735
+ const accountId = normalizeTwitchAccountId(options?.accountId ?? this.getAccountId());
736
+ return this.getAccountService(accountId).sendMessage(text, options);
737
+ }
341
738
  if (!this.connected) {
342
739
  throw new TwitchNotConnectedError;
343
740
  }
@@ -349,11 +746,15 @@ class TwitchService extends Service {
349
746
  const chunks = splitMessageForTwitch(cleanedText);
350
747
  let lastMessageId;
351
748
  for (const chunk of chunks) {
352
- if (options?.replyTo) {
353
- await this.client.say(channel, chunk, { replyTo: options.replyTo });
354
- } else {
355
- await this.client.say(channel, chunk);
356
- }
749
+ await logTwurpleCall("say", { channel, chunkLen: chunk.length, replyTo: options?.replyTo }, async () => {
750
+ if (options?.replyTo) {
751
+ await this.client.say(channel, chunk, {
752
+ replyTo: options.replyTo
753
+ });
754
+ } else {
755
+ await this.client.say(channel, chunk);
756
+ }
757
+ });
357
758
  lastMessageId = crypto.randomUUID();
358
759
  if (chunks.length > 1) {
359
760
  await new Promise((resolve) => setTimeout(resolve, 300));
@@ -361,6 +762,7 @@ class TwitchService extends Service {
361
762
  }
362
763
  this.runtime.emitEvent("TWITCH_MESSAGE_SENT" /* MESSAGE_SENT */, {
363
764
  runtime: this.runtime,
765
+ accountId: this.getAccountId(),
364
766
  channel,
365
767
  text: cleanedText,
366
768
  messageId: lastMessageId
@@ -368,672 +770,135 @@ class TwitchService extends Service {
368
770
  return { success: true, messageId: lastMessageId };
369
771
  }
370
772
  async joinChannel(channel) {
773
+ if (this.accountServices?.size > 0) {
774
+ await this.getDefaultAccountService().joinChannel(channel);
775
+ return;
776
+ }
371
777
  const normalized = normalizeChannel(channel);
372
- await this.client.join(normalized);
778
+ await logTwurpleCall("join", { channel: normalized }, async () => {
779
+ await this.client.join(normalized);
780
+ });
373
781
  this.joinedChannels.add(normalized);
374
782
  }
375
783
  async leaveChannel(channel) {
784
+ if (this.accountServices?.size > 0) {
785
+ await this.getDefaultAccountService().leaveChannel(channel);
786
+ return;
787
+ }
376
788
  const normalized = normalizeChannel(channel);
377
- await this.client.part(normalized);
789
+ await logTwurpleCall("part", { channel: normalized }, async () => {
790
+ await this.client.part(normalized);
791
+ });
378
792
  this.joinedChannels.delete(normalized);
379
793
  }
380
794
  }
381
- // src/actions/joinChannel.ts
382
- import {
383
- composePromptFromState,
384
- ModelType,
385
- parseJSONObjectFromText
386
- } from "@elizaos/core";
387
- var JOIN_CHANNEL_TEMPLATE = `You are helping to extract a Twitch channel name.
388
-
389
- The user wants to join a Twitch channel.
390
-
391
- Recent conversation:
392
- {{recentMessages}}
393
-
394
- Extract the channel name to join (without the # prefix).
395
-
396
- Respond with a JSON object like:
397
- {
398
- "channel": "channelname"
795
+ // src/connector-account-provider.ts
796
+ var TWITCH_PROVIDER_ID = "twitch";
797
+ function toConnectorAccount(settings) {
798
+ const now = Date.now();
799
+ const configured = Boolean(settings.username && settings.clientId && settings.accessToken);
800
+ return {
801
+ id: normalizeTwitchAccountId(settings.accountId),
802
+ provider: TWITCH_PROVIDER_ID,
803
+ label: settings.username || settings.channel || settings.accountId,
804
+ role: "OWNER",
805
+ purpose: ["messaging"],
806
+ accessGate: "open",
807
+ status: settings.enabled !== false && configured ? "connected" : "disabled",
808
+ externalId: settings.username || undefined,
809
+ displayHandle: settings.username || undefined,
810
+ createdAt: now,
811
+ updatedAt: now,
812
+ metadata: {
813
+ channel: settings.channel ?? "",
814
+ additionalChannels: settings.additionalChannels ?? [],
815
+ requireMention: settings.requireMention ?? false,
816
+ hasRefreshToken: Boolean(settings.refreshToken)
817
+ }
818
+ };
399
819
  }
400
-
401
- Only respond with the JSON object, no other text.`;
402
- var joinChannel = {
403
- name: "TWITCH_JOIN_CHANNEL",
404
- similes: ["JOIN_TWITCH_CHANNEL", "ENTER_CHANNEL", "CONNECT_CHANNEL"],
405
- description: "Join a Twitch channel to listen and send messages",
406
- validate: async (runtime, message, state, options) => {
407
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
408
- const __avText = __avTextRaw.toLowerCase();
409
- const __avKeywords = ["twitch", "join", "channel"];
410
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw)) || String(message?.content?.source ?? message?.source ?? "") === "twitch";
411
- const __avRegex = new RegExp("\\b(?:twitch|join|channel)\\b", "i");
412
- const __avRegexOk = __avRegex.test(__avText) || String(message?.content?.source ?? message?.source ?? "") === "twitch";
413
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
414
- const __avExpectedSource = "twitch";
415
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
416
- const __avOptions = options && typeof options === "object" ? options : {};
417
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object") || String(message?.content?.source ?? message?.source ?? "") === "twitch";
418
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
419
- return false;
420
- }
421
- const __avLegacyValidate = async (_runtime, message2, _state) => {
422
- return message2.content.source === "twitch";
423
- };
424
- try {
425
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
426
- } catch {
427
- return false;
428
- }
429
- },
430
- handler: async (runtime, message, state, _options, callback) => {
431
- const twitchService = runtime.getService(TWITCH_SERVICE_NAME);
432
- if (!twitchService || !twitchService.isConnected()) {
433
- if (callback) {
434
- callback({
435
- text: "Twitch service is not available.",
436
- source: "twitch"
437
- });
438
- }
439
- return { success: false, error: "Twitch service not available" };
440
- }
441
- const currentState = state ?? await runtime.composeState(message);
442
- const prompt = await composePromptFromState({
443
- template: JOIN_CHANNEL_TEMPLATE,
444
- state: currentState
445
- });
446
- let channelName = null;
447
- for (let attempt = 0;attempt < 3; attempt++) {
448
- const response = await runtime.useModel(ModelType.TEXT_SMALL, {
449
- prompt
450
- });
451
- const parsed = parseJSONObjectFromText(String(response));
452
- if (parsed?.channel) {
453
- channelName = normalizeChannel(String(parsed.channel));
454
- break;
455
- }
456
- }
457
- if (!channelName) {
458
- if (callback) {
459
- callback({
460
- text: "I couldn't understand which channel you want me to join. Please specify the channel name.",
461
- source: "twitch"
462
- });
463
- }
464
- return { success: false, error: "Could not extract channel name" };
465
- }
466
- if (twitchService.getJoinedChannels().includes(channelName)) {
467
- if (callback) {
468
- callback({
469
- text: `Already in channel #${channelName}.`,
470
- source: "twitch"
471
- });
820
+ function createTwitchConnectorAccountProvider(runtime) {
821
+ return {
822
+ provider: TWITCH_PROVIDER_ID,
823
+ label: "Twitch",
824
+ listAccounts: async (_manager) => {
825
+ const ids = listTwitchAccountIds(runtime);
826
+ if (ids.length === 0) {
827
+ return [
828
+ toConnectorAccount(resolveTwitchAccountSettings(runtime, DEFAULT_TWITCH_ACCOUNT_ID))
829
+ ];
472
830
  }
831
+ return ids.map((id) => toConnectorAccount(resolveTwitchAccountSettings(runtime, id)));
832
+ },
833
+ createAccount: async (input, _manager) => {
473
834
  return {
474
- success: true,
475
- data: { channel: channelName, alreadyJoined: true }
835
+ ...input,
836
+ provider: TWITCH_PROVIDER_ID,
837
+ role: input.role ?? "OWNER",
838
+ purpose: input.purpose ?? ["messaging"],
839
+ accessGate: input.accessGate ?? "open",
840
+ status: input.status ?? "pending"
476
841
  };
477
- }
478
- await twitchService.joinChannel(channelName);
479
- if (callback) {
480
- callback({
481
- text: `Joined channel #${channelName}.`,
482
- source: message.content.source
483
- });
484
- }
485
- return {
486
- success: true,
487
- data: {
488
- channel: channelName
489
- }
490
- };
491
- },
492
- examples: [
493
- [
494
- {
495
- name: "{{user1}}",
496
- content: { text: "Join the channel shroud" }
497
- },
498
- {
499
- name: "{{agent}}",
500
- content: {
501
- text: "I'll join that channel.",
502
- actions: ["TWITCH_JOIN_CHANNEL"]
503
- }
504
- }
505
- ]
506
- ]
507
- };
508
-
509
- // src/actions/leaveChannel.ts
510
- import {
511
- composePromptFromState as composePromptFromState2,
512
- ModelType as ModelType2,
513
- parseJSONObjectFromText as parseJSONObjectFromText2
514
- } from "@elizaos/core";
515
- var LEAVE_CHANNEL_TEMPLATE = `You are helping to extract a Twitch channel name.
516
-
517
- The user wants to leave a Twitch channel.
518
-
519
- Recent conversation:
520
- {{recentMessages}}
521
-
522
- Currently joined channels: {{joinedChannels}}
523
-
524
- Extract the channel name to leave (without the # prefix).
525
-
526
- Respond with a JSON object like:
527
- {
528
- "channel": "channelname"
529
- }
530
-
531
- Only respond with the JSON object, no other text.`;
532
- var leaveChannel = {
533
- name: "TWITCH_LEAVE_CHANNEL",
534
- similes: [
535
- "LEAVE_TWITCH_CHANNEL",
536
- "EXIT_CHANNEL",
537
- "PART_CHANNEL",
538
- "DISCONNECT_CHANNEL"
539
- ],
540
- description: "Leave a Twitch channel",
541
- validate: async (runtime, message, state, options) => {
542
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
543
- const __avText = __avTextRaw.toLowerCase();
544
- const __avKeywords = ["twitch", "leave", "channel"];
545
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw)) || String(message?.content?.source ?? message?.source ?? "") === "twitch";
546
- const __avRegex = new RegExp("\\b(?:twitch|leave|channel)\\b", "i");
547
- const __avRegexOk = __avRegex.test(__avText) || String(message?.content?.source ?? message?.source ?? "") === "twitch";
548
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
549
- const __avExpectedSource = "twitch";
550
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
551
- const __avOptions = options && typeof options === "object" ? options : {};
552
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object") || String(message?.content?.source ?? message?.source ?? "") === "twitch";
553
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
554
- return false;
555
- }
556
- const __avLegacyValidate = async (_runtime, message2, _state) => {
557
- return message2.content.source === "twitch";
558
- };
559
- try {
560
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
561
- } catch {
562
- return false;
563
- }
564
- },
565
- handler: async (runtime, message, state, _options, callback) => {
566
- const twitchService = runtime.getService(TWITCH_SERVICE_NAME);
567
- if (!twitchService || !twitchService.isConnected()) {
568
- if (callback) {
569
- callback({
570
- text: "Twitch service is not available.",
571
- source: "twitch"
572
- });
573
- }
574
- return { success: false, error: "Twitch service not available" };
575
- }
576
- const joinedChannels = twitchService.getJoinedChannels();
577
- const currentState = state ?? await runtime.composeState(message);
578
- const enrichedState = {
579
- ...currentState,
580
- joinedChannels: joinedChannels.join(", ")
581
- };
582
- const prompt = await composePromptFromState2({
583
- template: LEAVE_CHANNEL_TEMPLATE,
584
- state: enrichedState
585
- });
586
- let channelName = null;
587
- for (let attempt = 0;attempt < 3; attempt++) {
588
- const response = await runtime.useModel(ModelType2.TEXT_SMALL, {
589
- prompt
590
- });
591
- const parsed = parseJSONObjectFromText2(String(response));
592
- if (parsed?.channel) {
593
- channelName = normalizeChannel(String(parsed.channel));
594
- break;
595
- }
596
- }
597
- if (!channelName) {
598
- if (callback) {
599
- callback({
600
- text: "I couldn't understand which channel you want me to leave. Please specify the channel name.",
601
- source: "twitch"
602
- });
603
- }
604
- return { success: false, error: "Could not extract channel name" };
605
- }
606
- if (!joinedChannels.includes(channelName)) {
607
- if (callback) {
608
- callback({
609
- text: `Not currently in channel #${channelName}.`,
610
- source: "twitch"
611
- });
612
- }
613
- return { success: false, error: "Not in that channel" };
614
- }
615
- if (channelName === twitchService.getPrimaryChannel()) {
616
- if (callback) {
617
- callback({
618
- text: `Cannot leave the primary channel #${channelName}.`,
619
- source: "twitch"
620
- });
621
- }
622
- return { success: false, error: "Cannot leave primary channel" };
623
- }
624
- await twitchService.leaveChannel(channelName);
625
- if (callback) {
626
- callback({
627
- text: `Left channel #${channelName}.`,
628
- source: message.content.source
629
- });
630
- }
631
- return {
632
- success: true,
633
- data: {
634
- channel: channelName
635
- }
636
- };
637
- },
638
- examples: [
639
- [
640
- {
641
- name: "{{user1}}",
642
- content: { text: "Leave the channel shroud" }
643
- },
644
- {
645
- name: "{{agent}}",
646
- content: {
647
- text: "I'll leave that channel.",
648
- actions: ["TWITCH_LEAVE_CHANNEL"]
649
- }
650
- }
651
- ]
652
- ]
653
- };
654
-
655
- // src/actions/listChannels.ts
656
- var listChannels = {
657
- name: "TWITCH_LIST_CHANNELS",
658
- similes: [
659
- "LIST_TWITCH_CHANNELS",
660
- "SHOW_CHANNELS",
661
- "GET_CHANNELS",
662
- "CURRENT_CHANNELS"
663
- ],
664
- description: "List all Twitch channels the bot is currently in",
665
- validate: async (runtime, message, state, options) => {
666
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
667
- const __avText = __avTextRaw.toLowerCase();
668
- const __avKeywords = ["twitch", "list", "channels"];
669
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw)) || String(message?.content?.source ?? message?.source ?? "") === "twitch";
670
- const __avRegex = new RegExp("\\b(?:twitch|list|channels)\\b", "i");
671
- const __avRegexOk = __avRegex.test(__avText) || String(message?.content?.source ?? message?.source ?? "") === "twitch";
672
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
673
- const __avExpectedSource = "twitch";
674
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
675
- const __avOptions = options && typeof options === "object" ? options : {};
676
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object") || String(message?.content?.source ?? message?.source ?? "") === "twitch";
677
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
678
- return false;
679
- }
680
- const __avLegacyValidate = async (_runtime, message2, _state) => {
681
- return message2.content.source === "twitch";
682
- };
683
- try {
684
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
685
- } catch {
686
- return false;
687
- }
688
- },
689
- handler: async (runtime, message, _state, _options, callback) => {
690
- const twitchService = runtime.getService(TWITCH_SERVICE_NAME);
691
- if (!twitchService || !twitchService.isConnected()) {
692
- if (callback) {
693
- callback({
694
- text: "Twitch service is not available.",
695
- source: "twitch"
696
- });
697
- }
698
- return { success: false, error: "Twitch service not available" };
699
- }
700
- const joinedChannels = twitchService.getJoinedChannels();
701
- const primaryChannel = twitchService.getPrimaryChannel();
702
- const channelList = joinedChannels.map((channel) => {
703
- const displayName = formatChannelForDisplay(channel);
704
- const isPrimary = channel === primaryChannel;
705
- return isPrimary ? `${displayName} (primary)` : displayName;
706
- });
707
- const responseText = joinedChannels.length > 0 ? `Currently in ${joinedChannels.length} channel(s):
708
- ${channelList.map((c) => `• ${c}`).join(`
709
- `)}` : "Not currently in any channels.";
710
- if (callback) {
711
- callback({
712
- text: responseText,
713
- source: message.content.source
714
- });
715
- }
716
- return {
717
- success: true,
718
- data: {
719
- channelCount: joinedChannels.length,
720
- channels: joinedChannels,
721
- primaryChannel
722
- }
723
- };
724
- },
725
- examples: [
726
- [
727
- {
728
- name: "{{user1}}",
729
- content: { text: "What channels are you in?" }
730
- },
731
- {
732
- name: "{{agent}}",
733
- content: {
734
- text: "I'll list the channels I'm currently in.",
735
- actions: ["TWITCH_LIST_CHANNELS"]
736
- }
737
- }
738
- ]
739
- ]
740
- };
741
-
742
- // src/actions/sendMessage.ts
743
- import {
744
- composePromptFromState as composePromptFromState3,
745
- ModelType as ModelType3,
746
- parseJSONObjectFromText as parseJSONObjectFromText3
747
- } from "@elizaos/core";
748
- var SEND_MESSAGE_TEMPLATE = `You are helping to extract send message parameters for Twitch chat.
749
-
750
- The user wants to send a message to a Twitch channel.
751
-
752
- Recent conversation:
753
- {{recentMessages}}
754
-
755
- Extract the following:
756
- 1. text: The message text to send
757
- 2. channel: The channel name to send to (without # prefix), or "current" for the current channel
758
-
759
- Respond with a JSON object like:
760
- {
761
- "text": "The message to send",
762
- "channel": "current"
842
+ },
843
+ patchAccount: async (_accountId, patch, _manager) => {
844
+ return { ...patch, provider: TWITCH_PROVIDER_ID };
845
+ },
846
+ deleteAccount: async (_accountId, _manager) => {}
847
+ };
763
848
  }
764
849
 
765
- Only respond with the JSON object, no other text.`;
766
- var sendMessage = {
767
- name: "TWITCH_SEND_MESSAGE",
768
- similes: [
769
- "SEND_TWITCH_MESSAGE",
770
- "TWITCH_CHAT",
771
- "CHAT_TWITCH",
772
- "SAY_IN_TWITCH"
773
- ],
774
- description: "Send a message to a Twitch channel",
775
- validate: async (runtime, message, state, options) => {
776
- const __avTextRaw = typeof message?.content?.text === "string" ? message.content.text : "";
777
- const __avText = __avTextRaw.toLowerCase();
778
- const __avKeywords = ["twitch", "send", "message"];
779
- const __avKeywordOk = __avKeywords.length > 0 && __avKeywords.some((kw) => kw.length > 0 && __avText.includes(kw)) || String(message?.content?.source ?? message?.source ?? "") === "twitch";
780
- const __avRegex = new RegExp("\\b(?:twitch|send|message)\\b", "i");
781
- const __avRegexOk = __avRegex.test(__avText) || String(message?.content?.source ?? message?.source ?? "") === "twitch";
782
- const __avSource = String(message?.content?.source ?? message?.source ?? "");
783
- const __avExpectedSource = "twitch";
784
- const __avSourceOk = __avExpectedSource ? __avSource === __avExpectedSource : Boolean(__avSource || state || runtime?.agentId || runtime?.getService);
785
- const __avOptions = options && typeof options === "object" ? options : {};
786
- const __avInputOk = __avText.trim().length > 0 || Object.keys(__avOptions).length > 0 || Boolean(message?.content && typeof message.content === "object") || String(message?.content?.source ?? message?.source ?? "") === "twitch";
787
- if (!(__avKeywordOk && __avRegexOk && __avSourceOk && __avInputOk)) {
788
- return false;
789
- }
790
- const __avLegacyValidate = async (_runtime, message2, _state) => {
791
- return message2.content.source === "twitch";
792
- };
793
- try {
794
- return Boolean(await __avLegacyValidate(runtime, message, state, options));
795
- } catch {
796
- return false;
797
- }
798
- },
799
- handler: async (runtime, message, state, _options, callback) => {
800
- const twitchService = runtime.getService(TWITCH_SERVICE_NAME);
801
- if (!twitchService || !twitchService.isConnected()) {
802
- if (callback) {
803
- callback({
804
- text: "Twitch service is not available.",
805
- source: "twitch"
806
- });
807
- }
808
- return { success: false, error: "Twitch service not available" };
809
- }
810
- const currentState = state ?? await runtime.composeState(message);
811
- const prompt = await composePromptFromState3({
812
- template: SEND_MESSAGE_TEMPLATE,
813
- state: currentState
814
- });
815
- let messageInfo = null;
816
- for (let attempt = 0;attempt < 3; attempt++) {
817
- const response = await runtime.useModel(ModelType3.TEXT_SMALL, {
818
- prompt
819
- });
820
- const parsed = parseJSONObjectFromText3(String(response));
821
- if (parsed?.text) {
822
- messageInfo = {
823
- text: String(parsed.text),
824
- channel: String(parsed.channel || "current")
825
- };
826
- break;
827
- }
828
- }
829
- if (!messageInfo || !messageInfo.text) {
830
- if (callback) {
831
- callback({
832
- text: "I couldn't understand what message you want me to send. Please try again.",
833
- source: "twitch"
834
- });
835
- }
836
- return { success: false, error: "Could not extract message parameters" };
837
- }
838
- let targetChannel = twitchService.getPrimaryChannel();
839
- if (messageInfo.channel && messageInfo.channel !== "current") {
840
- targetChannel = normalizeChannel(messageInfo.channel);
841
- }
842
- if (currentState?.data?.room?.channelId) {
843
- targetChannel = normalizeChannel(currentState.data.room.channelId);
844
- }
845
- const result = await twitchService.sendMessage(messageInfo.text, {
846
- channel: targetChannel
847
- });
848
- if (!result.success) {
849
- if (callback) {
850
- callback({
851
- text: `Failed to send message: ${result.error}`,
852
- source: "twitch"
853
- });
854
- }
855
- return { success: false, error: result.error };
856
- }
857
- if (callback) {
858
- callback({
859
- text: "Message sent successfully.",
860
- source: message.content.source
861
- });
862
- }
863
- return {
864
- success: true,
865
- data: {
866
- channel: targetChannel,
867
- messageId: result.messageId
868
- }
869
- };
870
- },
871
- examples: [
872
- [
873
- {
874
- name: "{{user1}}",
875
- content: { text: "Send a message to chat saying 'Hello everyone!'" }
876
- },
877
- {
878
- name: "{{agent}}",
879
- content: {
880
- text: "I'll send that message to the chat.",
881
- actions: ["TWITCH_SEND_MESSAGE"]
882
- }
883
- }
884
- ]
885
- ]
886
- };
850
+ // src/workflow-credential-provider.ts
851
+ import { Service as Service2 } from "@elizaos/core";
852
+ var WORKFLOW_CREDENTIAL_PROVIDER_TYPE = "workflow_credential_provider";
853
+ var SUPPORTED = ["httpHeaderAuth"];
887
854
 
888
- // src/providers/channelState.ts
889
- var channelStateProvider = {
890
- name: "twitchChannelState",
891
- description: "Provides information about the current Twitch channel context",
892
- dynamic: true,
893
- get: async (runtime, message, state) => {
894
- if (message.content.source !== "twitch") {
895
- return {
896
- data: {},
897
- values: {},
898
- text: ""
899
- };
900
- }
901
- const twitchService = runtime.getService(TWITCH_SERVICE_NAME);
902
- if (!twitchService || !twitchService.isConnected()) {
903
- return {
904
- data: {
905
- connected: false
906
- },
907
- values: {
908
- connected: false
909
- },
910
- text: ""
911
- };
912
- }
913
- const agentName = state?.agentName || "The agent";
914
- const room = state?.data?.room;
915
- const channelId = room?.channelId;
916
- const channel = channelId ? normalizeChannel(channelId) : twitchService.getPrimaryChannel();
917
- const joinedChannels = twitchService.getJoinedChannels();
918
- const isPrimaryChannel = channel === twitchService.getPrimaryChannel();
919
- const botUsername = twitchService.getBotUsername();
920
- let responseText = `${agentName} is currently in Twitch channel ${formatChannelForDisplay(channel)}.`;
921
- if (isPrimaryChannel) {
922
- responseText += " This is the primary channel.";
923
- }
924
- responseText += `
925
-
926
- Twitch is a live streaming platform. Chat messages are public and visible to all viewers.`;
927
- responseText += ` ${agentName} is logged in as @${botUsername}.`;
928
- responseText += ` Currently connected to ${joinedChannels.length} channel(s).`;
855
+ class TwitchWorkflowCredentialProvider extends Service2 {
856
+ static serviceType = WORKFLOW_CREDENTIAL_PROVIDER_TYPE;
857
+ capabilityDescription = "Supplies Twitch credentials to the workflow plugin.";
858
+ static async start(runtime) {
859
+ return new TwitchWorkflowCredentialProvider(runtime);
860
+ }
861
+ async stop() {}
862
+ async resolve(_userId, credType) {
863
+ if (credType !== "httpHeaderAuth")
864
+ return null;
865
+ const accessToken = this.runtime.getSetting("TWITCH_ACCESS_TOKEN");
866
+ if (!accessToken?.trim())
867
+ return null;
929
868
  return {
930
- data: {
931
- channel,
932
- displayChannel: formatChannelForDisplay(channel),
933
- isPrimaryChannel,
934
- botUsername,
935
- joinedChannels,
936
- channelCount: joinedChannels.length,
937
- connected: true
938
- },
939
- values: {
940
- channel,
941
- displayChannel: formatChannelForDisplay(channel),
942
- isPrimaryChannel,
943
- botUsername,
944
- channelCount: joinedChannels.length
945
- },
946
- text: responseText
869
+ status: "credential_data",
870
+ data: { name: "Authorization", value: `Bearer ${accessToken.trim()}` }
947
871
  };
948
872
  }
949
- };
950
-
951
- // src/providers/userContext.ts
952
- var userContextProvider = {
953
- name: "twitchUserContext",
954
- description: "Provides information about the Twitch user in the current conversation",
955
- dynamic: true,
956
- get: async (runtime, message, state) => {
957
- if (message.content.source !== "twitch") {
958
- return {
959
- data: {},
960
- values: {},
961
- text: ""
962
- };
963
- }
964
- const twitchService = runtime.getService(TWITCH_SERVICE_NAME);
965
- if (!twitchService || !twitchService.isConnected()) {
966
- return {
967
- data: {},
968
- values: {},
969
- text: ""
970
- };
971
- }
972
- const agentName = state?.agentName || "The agent";
973
- const metadata = message.content.metadata;
974
- const userInfo = metadata?.user;
975
- if (!userInfo) {
976
- return {
977
- data: {},
978
- values: {},
979
- text: ""
980
- };
981
- }
982
- const displayName = getTwitchUserDisplayName(userInfo);
983
- const roles = [];
984
- if (userInfo.isBroadcaster) {
985
- roles.push("broadcaster");
986
- }
987
- if (userInfo.isModerator) {
988
- roles.push("moderator");
989
- }
990
- if (userInfo.isVip) {
991
- roles.push("VIP");
992
- }
993
- if (userInfo.isSubscriber) {
994
- roles.push("subscriber");
995
- }
996
- const roleText = roles.length > 0 ? roles.join(", ") : "viewer";
997
- let responseText = `${agentName} is talking to ${displayName} (${roleText}) in Twitch chat.`;
998
- if (userInfo.isBroadcaster) {
999
- responseText += ` ${displayName} is the channel owner/broadcaster.`;
1000
- } else if (userInfo.isModerator) {
1001
- responseText += ` ${displayName} is a channel moderator.`;
1002
- }
873
+ checkCredentialTypes(credTypes) {
1003
874
  return {
1004
- data: {
1005
- userId: userInfo.userId,
1006
- username: userInfo.username,
1007
- displayName,
1008
- isBroadcaster: userInfo.isBroadcaster,
1009
- isModerator: userInfo.isModerator,
1010
- isVip: userInfo.isVip,
1011
- isSubscriber: userInfo.isSubscriber,
1012
- roles,
1013
- color: userInfo.color
1014
- },
1015
- values: {
1016
- userId: userInfo.userId,
1017
- username: userInfo.username,
1018
- displayName,
1019
- roleText,
1020
- isBroadcaster: userInfo.isBroadcaster,
1021
- isModerator: userInfo.isModerator
1022
- },
1023
- text: responseText
875
+ supported: credTypes.filter((t) => SUPPORTED.includes(t)),
876
+ unsupported: credTypes.filter((t) => !SUPPORTED.includes(t))
1024
877
  };
1025
878
  }
1026
- };
879
+ }
1027
880
 
1028
881
  // src/index.ts
1029
882
  var twitchPlugin = {
1030
883
  name: "twitch",
1031
884
  description: "Twitch chat integration plugin for ElizaOS with real-time messaging",
1032
- services: [TwitchService],
1033
- actions: [sendMessage, joinChannel, leaveChannel, listChannels],
1034
- providers: [channelStateProvider, userContextProvider],
885
+ services: [TwitchService, TwitchWorkflowCredentialProvider],
886
+ actions: [],
887
+ providers: [],
1035
888
  tests: [],
889
+ autoEnable: {
890
+ connectorKeys: ["twitch"]
891
+ },
1036
892
  init: async (_config, runtime) => {
893
+ try {
894
+ const manager = getConnectorAccountManager(runtime);
895
+ manager.registerProvider(createTwitchConnectorAccountProvider(runtime));
896
+ } catch (err) {
897
+ logger2.warn({
898
+ src: "plugin:twitch",
899
+ err: err instanceof Error ? err.message : String(err)
900
+ }, "Failed to register Twitch provider with ConnectorAccountManager");
901
+ }
1037
902
  const username = runtime.getSetting("TWITCH_USERNAME");
1038
903
  const clientId = runtime.getSetting("TWITCH_CLIENT_ID");
1039
904
  const accessToken = runtime.getSetting("TWITCH_ACCESS_TOKEN");
@@ -1081,18 +946,17 @@ var twitchPlugin = {
1081
946
  };
1082
947
  var src_default = twitchPlugin;
1083
948
  export {
1084
- userContextProvider,
1085
949
  stripMarkdownForTwitch,
1086
950
  splitMessageForTwitch,
1087
- sendMessage,
951
+ resolveTwitchAccountSettings,
952
+ resolveDefaultTwitchAccountId,
953
+ readTwitchAccountId,
954
+ normalizeTwitchAccountId,
1088
955
  normalizeChannel,
1089
- listChannels,
1090
- leaveChannel,
1091
- joinChannel,
956
+ listTwitchAccountIds,
1092
957
  getTwitchUserDisplayName,
1093
958
  formatChannelForDisplay,
1094
959
  src_default as default,
1095
- channelStateProvider,
1096
960
  TwitchServiceNotInitializedError,
1097
961
  TwitchService,
1098
962
  TwitchPluginError,
@@ -1101,7 +965,8 @@ export {
1101
965
  TwitchConfigurationError,
1102
966
  TwitchApiError,
1103
967
  TWITCH_SERVICE_NAME,
1104
- MAX_TWITCH_MESSAGE_LENGTH
968
+ MAX_TWITCH_MESSAGE_LENGTH,
969
+ DEFAULT_TWITCH_ACCOUNT_ID
1105
970
  };
1106
971
 
1107
- //# debugId=9A799CC811D81DB364756E2164756E21
972
+ //# debugId=E272CF35229333E264756E2164756E21