@elizaos/plugin-discord 2.0.0-alpha.11 → 2.0.0-alpha.12

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 (42) hide show
  1. package/dist/actions/setup-credentials.d.ts +25 -0
  2. package/dist/actions/setup-credentials.d.ts.map +1 -0
  3. package/dist/compat.d.ts +1 -0
  4. package/dist/compat.d.ts.map +1 -1
  5. package/dist/debouncer.d.ts +25 -0
  6. package/dist/debouncer.d.ts.map +1 -0
  7. package/dist/draft-chunking.d.ts +9 -0
  8. package/dist/draft-chunking.d.ts.map +1 -0
  9. package/dist/draft-stream.d.ts +21 -0
  10. package/dist/draft-stream.d.ts.map +1 -0
  11. package/dist/environment.d.ts +1 -0
  12. package/dist/environment.d.ts.map +1 -1
  13. package/dist/identity.d.ts +6 -2
  14. package/dist/identity.d.ts.map +1 -1
  15. package/dist/inbound-envelope.d.ts +8 -0
  16. package/dist/inbound-envelope.d.ts.map +1 -0
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +3087 -550
  19. package/dist/index.js.map +24 -14
  20. package/dist/messages.d.ts +7 -0
  21. package/dist/messages.d.ts.map +1 -1
  22. package/dist/profileSync.d.ts +8 -0
  23. package/dist/profileSync.d.ts.map +1 -0
  24. package/dist/providers/channelState.d.ts.map +1 -1
  25. package/dist/providers/voiceState.d.ts.map +1 -1
  26. package/dist/reasoning-tags.d.ts +2 -0
  27. package/dist/reasoning-tags.d.ts.map +1 -0
  28. package/dist/service.d.ts +8 -0
  29. package/dist/service.d.ts.map +1 -1
  30. package/dist/slash-commands.d.ts +31 -0
  31. package/dist/slash-commands.d.ts.map +1 -0
  32. package/dist/status-reactions.d.ts +11 -0
  33. package/dist/status-reactions.d.ts.map +1 -0
  34. package/dist/types.d.ts +7 -1
  35. package/dist/types.d.ts.map +1 -1
  36. package/dist/typing.d.ts +7 -0
  37. package/dist/typing.d.ts.map +1 -0
  38. package/dist/utils.d.ts +5 -1
  39. package/dist/utils.d.ts.map +1 -1
  40. package/dist/voice.d.ts +4 -0
  41. package/dist/voice.d.ts.map +1 -1
  42. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -457,26 +457,26 @@ var require_channel = __commonJS((exports) => {
457
457
  ForumLayoutType2[ForumLayoutType2["ListView"] = 1] = "ListView";
458
458
  ForumLayoutType2[ForumLayoutType2["GalleryView"] = 2] = "GalleryView";
459
459
  })(ForumLayoutType || (exports.ForumLayoutType = ForumLayoutType = {}));
460
- var ChannelType9;
461
- (function(ChannelType10) {
462
- ChannelType10[ChannelType10["GuildText"] = 0] = "GuildText";
463
- ChannelType10[ChannelType10["DM"] = 1] = "DM";
464
- ChannelType10[ChannelType10["GuildVoice"] = 2] = "GuildVoice";
465
- ChannelType10[ChannelType10["GroupDM"] = 3] = "GroupDM";
466
- ChannelType10[ChannelType10["GuildCategory"] = 4] = "GuildCategory";
467
- ChannelType10[ChannelType10["GuildAnnouncement"] = 5] = "GuildAnnouncement";
468
- ChannelType10[ChannelType10["AnnouncementThread"] = 10] = "AnnouncementThread";
469
- ChannelType10[ChannelType10["PublicThread"] = 11] = "PublicThread";
470
- ChannelType10[ChannelType10["PrivateThread"] = 12] = "PrivateThread";
471
- ChannelType10[ChannelType10["GuildStageVoice"] = 13] = "GuildStageVoice";
472
- ChannelType10[ChannelType10["GuildDirectory"] = 14] = "GuildDirectory";
473
- ChannelType10[ChannelType10["GuildForum"] = 15] = "GuildForum";
474
- ChannelType10[ChannelType10["GuildMedia"] = 16] = "GuildMedia";
475
- ChannelType10[ChannelType10["GuildNews"] = 5] = "GuildNews";
476
- ChannelType10[ChannelType10["GuildNewsThread"] = 10] = "GuildNewsThread";
477
- ChannelType10[ChannelType10["GuildPublicThread"] = 11] = "GuildPublicThread";
478
- ChannelType10[ChannelType10["GuildPrivateThread"] = 12] = "GuildPrivateThread";
479
- })(ChannelType9 || (exports.ChannelType = ChannelType9 = {}));
460
+ var ChannelType10;
461
+ (function(ChannelType11) {
462
+ ChannelType11[ChannelType11["GuildText"] = 0] = "GuildText";
463
+ ChannelType11[ChannelType11["DM"] = 1] = "DM";
464
+ ChannelType11[ChannelType11["GuildVoice"] = 2] = "GuildVoice";
465
+ ChannelType11[ChannelType11["GroupDM"] = 3] = "GroupDM";
466
+ ChannelType11[ChannelType11["GuildCategory"] = 4] = "GuildCategory";
467
+ ChannelType11[ChannelType11["GuildAnnouncement"] = 5] = "GuildAnnouncement";
468
+ ChannelType11[ChannelType11["AnnouncementThread"] = 10] = "AnnouncementThread";
469
+ ChannelType11[ChannelType11["PublicThread"] = 11] = "PublicThread";
470
+ ChannelType11[ChannelType11["PrivateThread"] = 12] = "PrivateThread";
471
+ ChannelType11[ChannelType11["GuildStageVoice"] = 13] = "GuildStageVoice";
472
+ ChannelType11[ChannelType11["GuildDirectory"] = 14] = "GuildDirectory";
473
+ ChannelType11[ChannelType11["GuildForum"] = 15] = "GuildForum";
474
+ ChannelType11[ChannelType11["GuildMedia"] = 16] = "GuildMedia";
475
+ ChannelType11[ChannelType11["GuildNews"] = 5] = "GuildNews";
476
+ ChannelType11[ChannelType11["GuildNewsThread"] = 10] = "GuildNewsThread";
477
+ ChannelType11[ChannelType11["GuildPublicThread"] = 11] = "GuildPublicThread";
478
+ ChannelType11[ChannelType11["GuildPrivateThread"] = 12] = "GuildPrivateThread";
479
+ })(ChannelType10 || (exports.ChannelType = ChannelType10 = {}));
480
480
  var VideoQualityMode;
481
481
  (function(VideoQualityMode2) {
482
482
  VideoQualityMode2[VideoQualityMode2["Auto"] = 1] = "Auto";
@@ -924,20 +924,20 @@ var require_role = __commonJS((exports) => {
924
924
  var require_shared = __commonJS((exports) => {
925
925
  Object.defineProperty(exports, "__esModule", { value: true });
926
926
  exports.ApplicationCommandOptionType = undefined;
927
- var ApplicationCommandOptionType;
928
- (function(ApplicationCommandOptionType2) {
929
- ApplicationCommandOptionType2[ApplicationCommandOptionType2["Subcommand"] = 1] = "Subcommand";
930
- ApplicationCommandOptionType2[ApplicationCommandOptionType2["SubcommandGroup"] = 2] = "SubcommandGroup";
931
- ApplicationCommandOptionType2[ApplicationCommandOptionType2["String"] = 3] = "String";
932
- ApplicationCommandOptionType2[ApplicationCommandOptionType2["Integer"] = 4] = "Integer";
933
- ApplicationCommandOptionType2[ApplicationCommandOptionType2["Boolean"] = 5] = "Boolean";
934
- ApplicationCommandOptionType2[ApplicationCommandOptionType2["User"] = 6] = "User";
935
- ApplicationCommandOptionType2[ApplicationCommandOptionType2["Channel"] = 7] = "Channel";
936
- ApplicationCommandOptionType2[ApplicationCommandOptionType2["Role"] = 8] = "Role";
937
- ApplicationCommandOptionType2[ApplicationCommandOptionType2["Mentionable"] = 9] = "Mentionable";
938
- ApplicationCommandOptionType2[ApplicationCommandOptionType2["Number"] = 10] = "Number";
939
- ApplicationCommandOptionType2[ApplicationCommandOptionType2["Attachment"] = 11] = "Attachment";
940
- })(ApplicationCommandOptionType || (exports.ApplicationCommandOptionType = ApplicationCommandOptionType = {}));
927
+ var ApplicationCommandOptionType2;
928
+ (function(ApplicationCommandOptionType3) {
929
+ ApplicationCommandOptionType3[ApplicationCommandOptionType3["Subcommand"] = 1] = "Subcommand";
930
+ ApplicationCommandOptionType3[ApplicationCommandOptionType3["SubcommandGroup"] = 2] = "SubcommandGroup";
931
+ ApplicationCommandOptionType3[ApplicationCommandOptionType3["String"] = 3] = "String";
932
+ ApplicationCommandOptionType3[ApplicationCommandOptionType3["Integer"] = 4] = "Integer";
933
+ ApplicationCommandOptionType3[ApplicationCommandOptionType3["Boolean"] = 5] = "Boolean";
934
+ ApplicationCommandOptionType3[ApplicationCommandOptionType3["User"] = 6] = "User";
935
+ ApplicationCommandOptionType3[ApplicationCommandOptionType3["Channel"] = 7] = "Channel";
936
+ ApplicationCommandOptionType3[ApplicationCommandOptionType3["Role"] = 8] = "Role";
937
+ ApplicationCommandOptionType3[ApplicationCommandOptionType3["Mentionable"] = 9] = "Mentionable";
938
+ ApplicationCommandOptionType3[ApplicationCommandOptionType3["Number"] = 10] = "Number";
939
+ ApplicationCommandOptionType3[ApplicationCommandOptionType3["Attachment"] = 11] = "Attachment";
940
+ })(ApplicationCommandOptionType2 || (exports.ApplicationCommandOptionType = ApplicationCommandOptionType2 = {}));
941
941
  });
942
942
 
943
943
  // ../../../node_modules/.bun/discord-api-types@0.37.120/node_modules/discord-api-types/payloads/v10/_interactions/_applicationCommands/_chatInput/string.js
@@ -2702,7 +2702,7 @@ var require_v106 = __commonJS((exports) => {
2702
2702
  });
2703
2703
 
2704
2704
  // index.ts
2705
- import { logger as logger5 } from "@elizaos/core";
2705
+ import { logger as logger6 } from "@elizaos/core";
2706
2706
 
2707
2707
  // actions/chatWithAttachments.ts
2708
2708
  import fs from "node:fs";
@@ -6250,13 +6250,653 @@ var serverInfo = {
6250
6250
  };
6251
6251
  var serverInfo_default = serverInfo;
6252
6252
 
6253
+ // actions/setup-credentials.ts
6254
+ import * as fs2 from "node:fs";
6255
+ import * as os from "node:os";
6256
+ import * as path from "node:path";
6257
+ import {
6258
+ logger
6259
+ } from "@elizaos/core";
6260
+ import { ChannelType as ChannelType2 } from "discord.js";
6261
+ var SAFE_PRESET_NAME_RE = /^[A-Za-z0-9_-]+$/;
6262
+ var SESSION_TIMEOUT_MS = 5 * 60 * 1000;
6263
+ var presets = new Map;
6264
+ var activeSessions = new Map;
6265
+ function escapeRegex(value) {
6266
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6267
+ }
6268
+ function getCredentialsDir() {
6269
+ const configured = process.env.CREDENTIALS_DIR?.trim();
6270
+ if (configured) {
6271
+ return configured;
6272
+ }
6273
+ const home = (typeof os.homedir === "function" ? os.homedir() : "") || process.env.HOME || process.env.USERPROFILE;
6274
+ return home ? path.join(home, ".credentials") : path.join(process.cwd(), ".credentials");
6275
+ }
6276
+ function registerPreset(preset) {
6277
+ const normalizedName = preset.name.trim().toLowerCase();
6278
+ if (!SAFE_PRESET_NAME_RE.test(normalizedName)) {
6279
+ throw new Error(`Invalid credential preset name "${preset.name}". Only letters, numbers, underscores, and hyphens are allowed.`);
6280
+ }
6281
+ presets.set(normalizedName, { ...preset, name: normalizedName });
6282
+ }
6283
+ function getPreset(name) {
6284
+ return presets.get(name.toLowerCase());
6285
+ }
6286
+ function listPresets() {
6287
+ return [...presets.keys()];
6288
+ }
6289
+ registerPreset({
6290
+ name: "github",
6291
+ displayName: "GitHub",
6292
+ fields: [{ key: "token", label: "Personal Access Token", secret: true }],
6293
+ helpUrl: "https://github.com/settings/tokens",
6294
+ helpText: "Create a fine-grained PAT at the link above. Give it the repository permissions you need.",
6295
+ async validate(credentials) {
6296
+ try {
6297
+ const response = await fetch("https://api.github.com/user", {
6298
+ headers: {
6299
+ Authorization: `Bearer ${credentials.token}`,
6300
+ Accept: "application/vnd.github+json"
6301
+ }
6302
+ });
6303
+ if (!response.ok) {
6304
+ return {
6305
+ valid: false,
6306
+ error: `GitHub returned ${response.status}`
6307
+ };
6308
+ }
6309
+ const data = await response.json();
6310
+ return {
6311
+ valid: true,
6312
+ identity: data.login ? `@${data.login}` : "verified"
6313
+ };
6314
+ } catch (error) {
6315
+ return {
6316
+ valid: false,
6317
+ error: error instanceof Error ? error.message : String(error)
6318
+ };
6319
+ }
6320
+ }
6321
+ });
6322
+ registerPreset({
6323
+ name: "vercel",
6324
+ displayName: "Vercel",
6325
+ fields: [{ key: "token", label: "API Token", secret: true }],
6326
+ helpUrl: "https://vercel.com/account/tokens",
6327
+ helpText: "Create a token at the link above. Full Account scope works best.",
6328
+ async validate(credentials) {
6329
+ try {
6330
+ const response = await fetch("https://api.vercel.com/v9/projects", {
6331
+ headers: { Authorization: `Bearer ${credentials.token}` }
6332
+ });
6333
+ if (!response.ok) {
6334
+ return {
6335
+ valid: false,
6336
+ error: `Vercel returned ${response.status}`
6337
+ };
6338
+ }
6339
+ const data = await response.json();
6340
+ return {
6341
+ valid: true,
6342
+ identity: `${data.projects?.length ?? 0} project(s) accessible`
6343
+ };
6344
+ } catch (error) {
6345
+ return {
6346
+ valid: false,
6347
+ error: error instanceof Error ? error.message : String(error)
6348
+ };
6349
+ }
6350
+ }
6351
+ });
6352
+ registerPreset({
6353
+ name: "cloudflare",
6354
+ displayName: "Cloudflare",
6355
+ fields: [
6356
+ { key: "apiKey", label: "Global API Key", secret: true },
6357
+ { key: "email", label: "Account Email", secret: false }
6358
+ ],
6359
+ helpUrl: "https://dash.cloudflare.com/profile/api-tokens",
6360
+ helpText: 'Go to Cloudflare > Profile > API Tokens > "Global API Key". You will also need your account email.',
6361
+ async validate(credentials) {
6362
+ try {
6363
+ const response = await fetch("https://api.cloudflare.com/client/v4/zones", {
6364
+ headers: {
6365
+ "X-Auth-Key": credentials.apiKey,
6366
+ "X-Auth-Email": credentials.email
6367
+ }
6368
+ });
6369
+ if (!response.ok) {
6370
+ return {
6371
+ valid: false,
6372
+ error: `Cloudflare returned ${response.status}`
6373
+ };
6374
+ }
6375
+ const data = await response.json();
6376
+ return {
6377
+ valid: true,
6378
+ identity: data.result && data.result.length > 0 ? `zones: ${data.result.map((zone) => zone.name).join(", ")}` : "verified"
6379
+ };
6380
+ } catch (error) {
6381
+ return {
6382
+ valid: false,
6383
+ error: error instanceof Error ? error.message : String(error)
6384
+ };
6385
+ }
6386
+ }
6387
+ });
6388
+ registerPreset({
6389
+ name: "anthropic",
6390
+ displayName: "Anthropic",
6391
+ fields: [{ key: "apiKey", label: "API Key", secret: true }],
6392
+ helpUrl: "https://console.anthropic.com/settings/keys",
6393
+ helpText: "Create an API key in the Anthropic console.",
6394
+ async validate(credentials) {
6395
+ try {
6396
+ const response = await fetch("https://api.anthropic.com/v1/messages", {
6397
+ method: "POST",
6398
+ headers: {
6399
+ "x-api-key": credentials.apiKey,
6400
+ "anthropic-version": "2023-06-01",
6401
+ "Content-Type": "application/json"
6402
+ },
6403
+ body: JSON.stringify({
6404
+ model: "claude-3-5-haiku-20241022",
6405
+ max_tokens: 1,
6406
+ messages: [{ role: "user", content: "hi" }]
6407
+ })
6408
+ });
6409
+ if (response.ok || response.status === 429) {
6410
+ return { valid: true, identity: "key verified" };
6411
+ }
6412
+ return {
6413
+ valid: false,
6414
+ error: `Anthropic returned ${response.status}`
6415
+ };
6416
+ } catch (error) {
6417
+ return {
6418
+ valid: false,
6419
+ error: error instanceof Error ? error.message : String(error)
6420
+ };
6421
+ }
6422
+ }
6423
+ });
6424
+ registerPreset({
6425
+ name: "openai",
6426
+ displayName: "OpenAI",
6427
+ fields: [{ key: "apiKey", label: "API Key", secret: true }],
6428
+ helpUrl: "https://platform.openai.com/api-keys",
6429
+ helpText: "Create an API key at the OpenAI platform link above.",
6430
+ async validate(credentials) {
6431
+ try {
6432
+ const response = await fetch("https://api.openai.com/v1/models", {
6433
+ headers: { Authorization: `Bearer ${credentials.apiKey}` }
6434
+ });
6435
+ if (response.ok || response.status === 429) {
6436
+ return { valid: true, identity: "key verified" };
6437
+ }
6438
+ return {
6439
+ valid: false,
6440
+ error: `OpenAI returned ${response.status}`
6441
+ };
6442
+ } catch (error) {
6443
+ return {
6444
+ valid: false,
6445
+ error: error instanceof Error ? error.message : String(error)
6446
+ };
6447
+ }
6448
+ }
6449
+ });
6450
+ registerPreset({
6451
+ name: "fal",
6452
+ displayName: "fal.ai",
6453
+ fields: [{ key: "apiKey", label: "API Key", secret: true }],
6454
+ helpUrl: "https://fal.ai/dashboard/keys",
6455
+ helpText: "Generate an API key from your fal.ai dashboard.",
6456
+ async validate(credentials) {
6457
+ try {
6458
+ const response = await fetch("https://rest.fal.run/fal-ai/fast-sdxl", {
6459
+ method: "POST",
6460
+ headers: {
6461
+ Authorization: `Key ${credentials.apiKey}`,
6462
+ "Content-Type": "application/json"
6463
+ },
6464
+ body: JSON.stringify({
6465
+ prompt: "test",
6466
+ image_size: { width: 64, height: 64 },
6467
+ num_images: 1
6468
+ })
6469
+ });
6470
+ if (response.ok || response.status === 422 || response.status === 429) {
6471
+ return { valid: true, identity: "key verified" };
6472
+ }
6473
+ return {
6474
+ valid: false,
6475
+ error: `fal.ai returned ${response.status}`
6476
+ };
6477
+ } catch (error) {
6478
+ return {
6479
+ valid: false,
6480
+ error: error instanceof Error ? error.message : String(error)
6481
+ };
6482
+ }
6483
+ }
6484
+ });
6485
+ registerPreset({
6486
+ name: "generic",
6487
+ displayName: "Custom Credential",
6488
+ fields: [
6489
+ {
6490
+ key: "envName",
6491
+ label: "environment variable name (for example MY_API_KEY)",
6492
+ secret: false
6493
+ },
6494
+ { key: "value", label: "value", secret: true }
6495
+ ],
6496
+ helpUrl: "",
6497
+ helpText: "I'll store this as a generic credential. Give me the env var name and value.",
6498
+ async validate() {
6499
+ return { valid: true, identity: "stored (unvalidated)" };
6500
+ }
6501
+ });
6502
+ async function ensureCredentialsDir() {
6503
+ await fs2.promises.mkdir(getCredentialsDir(), {
6504
+ recursive: true,
6505
+ mode: 448
6506
+ });
6507
+ }
6508
+ async function storeCredentials(service, credentials) {
6509
+ await ensureCredentialsDir();
6510
+ const filePath = path.join(getCredentialsDir(), `${service}.json`);
6511
+ await fs2.promises.writeFile(filePath, JSON.stringify(credentials, null, 2), {
6512
+ mode: 384
6513
+ });
6514
+ }
6515
+ function cleanExpiredSessions() {
6516
+ const now = Date.now();
6517
+ for (const [userId, session] of activeSessions) {
6518
+ if (now - session.startedAt > SESSION_TIMEOUT_MS) {
6519
+ activeSessions.delete(userId);
6520
+ }
6521
+ }
6522
+ }
6523
+ async function tryDeleteMessage(discordService, channelId, messageId, fieldName) {
6524
+ try {
6525
+ const client = discordService.client;
6526
+ if (!client) {
6527
+ return false;
6528
+ }
6529
+ const channel = await client.channels.fetch(channelId);
6530
+ if (!channel || !("messages" in channel)) {
6531
+ return false;
6532
+ }
6533
+ const message = await channel.messages.fetch(messageId);
6534
+ await message.delete();
6535
+ return true;
6536
+ } catch (error) {
6537
+ logger.warn({
6538
+ src: "setup-credentials",
6539
+ channelId,
6540
+ messageId,
6541
+ fieldName,
6542
+ error: error instanceof Error ? error.message : String(error)
6543
+ }, "Could not delete Discord message containing a credential");
6544
+ return false;
6545
+ }
6546
+ }
6547
+ var TRIGGER_PATTERNS = [
6548
+ /\bsetup\s+(github|vercel|cloudflare|anthropic|openai|fal|credentials?)\b/i,
6549
+ /\badd\s+(my\s+)?(api\s+)?key\b/i,
6550
+ /\badd\s+credentials?\b/i,
6551
+ /\bconfigure\s+(github|vercel|cloudflare|anthropic|openai|fal)\b/i,
6552
+ /\bconnect\s+(github|vercel|cloudflare|anthropic|openai|fal)\b/i,
6553
+ /^\/setup\b/i
6554
+ ];
6555
+ function detectSetupIntent(text) {
6556
+ const lower = text.toLowerCase().trim();
6557
+ for (const presetName of presets.keys()) {
6558
+ if (presetName === "generic") {
6559
+ continue;
6560
+ }
6561
+ const pattern = new RegExp(`\\b(setup|configure|connect|add)\\s+(my\\s+)?${escapeRegex(presetName)}\\b`, "i");
6562
+ if (pattern.test(lower)) {
6563
+ return presetName;
6564
+ }
6565
+ }
6566
+ const slashMatch = lower.match(/^\/setup\s+(\w+)/);
6567
+ if (slashMatch) {
6568
+ const service = slashMatch[1].toLowerCase();
6569
+ if (presets.has(service)) {
6570
+ return service;
6571
+ }
6572
+ if (service === "custom") {
6573
+ return "generic";
6574
+ }
6575
+ }
6576
+ for (const pattern of TRIGGER_PATTERNS) {
6577
+ if (pattern.test(lower)) {
6578
+ return null;
6579
+ }
6580
+ }
6581
+ return;
6582
+ }
6583
+ function isSetupTrigger(text) {
6584
+ return TRIGGER_PATTERNS.some((pattern) => pattern.test(text.toLowerCase().trim()));
6585
+ }
6586
+ function buildServiceListMessage() {
6587
+ const services = listPresets().filter((presetName) => presetName !== "generic").map((presetName) => {
6588
+ const preset = getPreset(presetName);
6589
+ return `- **${preset?.displayName ?? presetName}** (\`${presetName}\`)`;
6590
+ });
6591
+ return [
6592
+ "Which service do you want to set up? Here's what I support:",
6593
+ "",
6594
+ ...services,
6595
+ "- **Custom** (`custom`) - any env var",
6596
+ "",
6597
+ "Just tell me the name, for example `github` or `custom`."
6598
+ ].join(`
6599
+ `);
6600
+ }
6601
+ function resolveDeletionTarget(message, defaultChannelId) {
6602
+ const contentRecord = message.content && typeof message.content === "object" ? message.content : null;
6603
+ const metadataRecord = message.metadata && typeof message.metadata === "object" ? message.metadata : null;
6604
+ const channelId = typeof contentRecord?.channelId === "string" && contentRecord.channelId || typeof metadataRecord?.discordChannelId === "string" && metadataRecord.discordChannelId || defaultChannelId;
6605
+ const messageId = typeof contentRecord?.messageId === "string" && contentRecord.messageId || typeof metadataRecord?.discordMessageId === "string" && metadataRecord.discordMessageId;
6606
+ return { channelId, messageId };
6607
+ }
6608
+ var setupCredentials = {
6609
+ name: "SETUP_CREDENTIALS",
6610
+ similes: [
6611
+ "ADD_CREDENTIALS",
6612
+ "CONFIGURE_SERVICE",
6613
+ "CONNECT_SERVICE",
6614
+ "ADD_API_KEY",
6615
+ "SETUP_SERVICE"
6616
+ ],
6617
+ description: "Guide the user through setting up API credentials for supported third-party services, validate them when possible, and store them securely.",
6618
+ validate: async (_runtime, message) => {
6619
+ if (message.content.source !== "discord") {
6620
+ return false;
6621
+ }
6622
+ const text = message.content.text?.trim() ?? "";
6623
+ const userId = message.entityId;
6624
+ return activeSessions.has(userId) || isSetupTrigger(text);
6625
+ },
6626
+ handler: async (runtime, message, state, _options, callback) => {
6627
+ const discordService = runtime.getService(DISCORD_SERVICE_NAME);
6628
+ if (!discordService?.client) {
6629
+ if (callback) {
6630
+ await callback({
6631
+ text: "Discord service isn't available right now.",
6632
+ source: "discord"
6633
+ });
6634
+ }
6635
+ return { success: false, error: "Discord service unavailable" };
6636
+ }
6637
+ const text = message.content.text?.trim() ?? "";
6638
+ const userId = message.entityId;
6639
+ const room = state?.data?.room || await runtime.getRoom(message.roomId);
6640
+ const channelId = room?.channelId || message.roomId;
6641
+ cleanExpiredSessions();
6642
+ let isDm = false;
6643
+ try {
6644
+ const channel = await discordService.client.channels.fetch(channelId);
6645
+ isDm = channel?.type === ChannelType2.DM || channel?.type === ChannelType2.GroupDM;
6646
+ } catch {
6647
+ isDm = false;
6648
+ }
6649
+ if (!isDm && !activeSessions.has(userId)) {
6650
+ if (callback) {
6651
+ await callback({
6652
+ text: "Let's do this in DMs for security. I'll message you there.",
6653
+ source: "discord"
6654
+ });
6655
+ }
6656
+ try {
6657
+ const discordUser = await discordService.client.users.fetch(userId);
6658
+ const dmChannel = await discordUser.createDM();
6659
+ const detectedService2 = detectSetupIntent(text);
6660
+ if (detectedService2 && presets.has(detectedService2)) {
6661
+ const preset = presets.get(detectedService2);
6662
+ if (!preset) {
6663
+ await dmChannel.send(buildServiceListMessage());
6664
+ return {
6665
+ success: false,
6666
+ error: `Unsupported credential preset: ${detectedService2}`
6667
+ };
6668
+ }
6669
+ activeSessions.set(userId, {
6670
+ preset,
6671
+ currentFieldIndex: 0,
6672
+ collected: {},
6673
+ channelId: dmChannel.id,
6674
+ startedAt: Date.now()
6675
+ });
6676
+ const firstField = preset.fields[0];
6677
+ const helpLine = preset.helpUrl ? `Here's where to get one: ${preset.helpUrl}` : "";
6678
+ await dmChannel.send([
6679
+ `Setting up **${preset.displayName}** credentials.`,
6680
+ preset.helpText,
6681
+ helpLine,
6682
+ "",
6683
+ `Please paste your **${firstField.label}** here. ${firstField.secret ? "I'll delete your message right after reading it." : ""}`
6684
+ ].filter(Boolean).join(`
6685
+ `));
6686
+ } else {
6687
+ await dmChannel.send(buildServiceListMessage());
6688
+ }
6689
+ } catch (error) {
6690
+ logger.warn({
6691
+ src: "setup-credentials",
6692
+ error: error instanceof Error ? error.message : String(error)
6693
+ }, "Could not open DM with user");
6694
+ if (callback) {
6695
+ await callback({
6696
+ text: "I couldn't send you a DM. Make sure your DMs are open, then try again.",
6697
+ source: "discord"
6698
+ });
6699
+ }
6700
+ }
6701
+ return { success: true, text: "Redirected credential setup to DMs" };
6702
+ }
6703
+ if (activeSessions.has(userId)) {
6704
+ const session = activeSessions.get(userId);
6705
+ if (!session) {
6706
+ return { success: false, error: "Credential session not found" };
6707
+ }
6708
+ const currentField = session.preset.fields[session.currentFieldIndex];
6709
+ if (currentField.secret) {
6710
+ const deletionTarget = resolveDeletionTarget(message, channelId);
6711
+ if (deletionTarget.messageId) {
6712
+ const deleted = await tryDeleteMessage(discordService, deletionTarget.channelId, deletionTarget.messageId, currentField.label);
6713
+ if (!deleted) {
6714
+ logger.warn({
6715
+ src: "setup-credentials",
6716
+ fieldName: currentField.label,
6717
+ channelId: deletionTarget.channelId,
6718
+ messageId: deletionTarget.messageId
6719
+ }, "Credential message could not be deleted automatically");
6720
+ }
6721
+ }
6722
+ }
6723
+ session.collected[currentField.key] = text;
6724
+ session.currentFieldIndex += 1;
6725
+ if (session.currentFieldIndex < session.preset.fields.length) {
6726
+ const nextField = session.preset.fields[session.currentFieldIndex];
6727
+ if (callback) {
6728
+ await callback({
6729
+ text: `Got it. Now paste your **${nextField.label}**${nextField.secret ? " (I'll delete your message)" : ""}.`,
6730
+ source: "discord"
6731
+ });
6732
+ }
6733
+ return { success: true, text: "Collecting next credential field" };
6734
+ }
6735
+ if (callback) {
6736
+ await callback({
6737
+ text: "Validating your credentials...",
6738
+ source: "discord"
6739
+ });
6740
+ }
6741
+ const validation = await session.preset.validate(session.collected);
6742
+ if (validation.valid) {
6743
+ const storageKey = session.preset.name === "generic" ? (session.collected.envName ?? "custom").toLowerCase().replace(/[^a-z0-9_-]/g, "_") : session.preset.name;
6744
+ await storeCredentials(storageKey, session.collected);
6745
+ activeSessions.delete(userId);
6746
+ if (callback) {
6747
+ await callback({
6748
+ text: `Connected${validation.identity ? ` as ${validation.identity}` : ""}. **${session.preset.displayName}** is ready.`,
6749
+ source: "discord"
6750
+ });
6751
+ }
6752
+ return { success: true, text: "Credentials stored" };
6753
+ }
6754
+ activeSessions.delete(userId);
6755
+ if (callback) {
6756
+ await callback({
6757
+ text: `Validation failed: ${validation.error ?? "unknown error"}. Please check your credentials and try again with \`/setup ${session.preset.name}\`.`,
6758
+ source: "discord"
6759
+ });
6760
+ }
6761
+ return {
6762
+ success: false,
6763
+ error: validation.error ?? "Validation failed"
6764
+ };
6765
+ }
6766
+ const detectedService = detectSetupIntent(text);
6767
+ if (detectedService && presets.has(detectedService)) {
6768
+ const preset = presets.get(detectedService);
6769
+ if (!preset) {
6770
+ return {
6771
+ success: false,
6772
+ error: `Unsupported credential preset: ${detectedService}`
6773
+ };
6774
+ }
6775
+ activeSessions.set(userId, {
6776
+ preset,
6777
+ currentFieldIndex: 0,
6778
+ collected: {},
6779
+ channelId,
6780
+ startedAt: Date.now()
6781
+ });
6782
+ const firstField = preset.fields[0];
6783
+ const helpLine = preset.helpUrl ? `Here's where to get one: ${preset.helpUrl}` : "";
6784
+ if (callback) {
6785
+ await callback({
6786
+ text: [
6787
+ `Setting up **${preset.displayName}** credentials.`,
6788
+ preset.helpText,
6789
+ helpLine,
6790
+ "",
6791
+ `Please paste your **${firstField.label}** here. ${firstField.secret ? "I'll delete your message right after reading it." : ""}`
6792
+ ].filter(Boolean).join(`
6793
+ `),
6794
+ source: "discord"
6795
+ });
6796
+ }
6797
+ return { success: true, text: `Started ${preset.displayName} setup` };
6798
+ }
6799
+ const serviceName = text.toLowerCase().trim();
6800
+ if (presets.has(serviceName) || serviceName === "custom") {
6801
+ const presetKey = serviceName === "custom" ? "generic" : serviceName;
6802
+ const preset = presets.get(presetKey);
6803
+ if (!preset) {
6804
+ return {
6805
+ success: false,
6806
+ error: `Unsupported credential preset: ${presetKey}`
6807
+ };
6808
+ }
6809
+ activeSessions.set(userId, {
6810
+ preset,
6811
+ currentFieldIndex: 0,
6812
+ collected: {},
6813
+ channelId,
6814
+ startedAt: Date.now()
6815
+ });
6816
+ const firstField = preset.fields[0];
6817
+ const helpLine = preset.helpUrl ? `Here's where to get one: ${preset.helpUrl}` : "";
6818
+ if (callback) {
6819
+ await callback({
6820
+ text: [
6821
+ `Setting up **${preset.displayName}** credentials.`,
6822
+ preset.helpText,
6823
+ helpLine,
6824
+ "",
6825
+ `Please paste your **${firstField.label}** here. ${firstField.secret ? "I'll delete your message right after reading it." : ""}`
6826
+ ].filter(Boolean).join(`
6827
+ `),
6828
+ source: "discord"
6829
+ });
6830
+ }
6831
+ return { success: true, text: `Started ${preset.displayName} setup` };
6832
+ }
6833
+ if (callback) {
6834
+ await callback({
6835
+ text: buildServiceListMessage(),
6836
+ source: "discord"
6837
+ });
6838
+ }
6839
+ return { success: true, text: "Showed credential setup service list" };
6840
+ },
6841
+ examples: [
6842
+ [
6843
+ {
6844
+ name: "{{user1}}",
6845
+ content: { text: "setup github" }
6846
+ },
6847
+ {
6848
+ name: "{{agentName}}",
6849
+ content: {
6850
+ text: `Setting up **GitHub** credentials.
6851
+ Create a fine-grained PAT at the link above.
6852
+ Here's where to get one: https://github.com/settings/tokens
6853
+
6854
+ Please paste your **Personal Access Token** here. I'll delete your message right after reading it.`,
6855
+ action: "SETUP_CREDENTIALS"
6856
+ }
6857
+ }
6858
+ ],
6859
+ [
6860
+ {
6861
+ name: "{{user1}}",
6862
+ content: { text: "add my vercel key" }
6863
+ },
6864
+ {
6865
+ name: "{{agentName}}",
6866
+ content: {
6867
+ text: `Setting up **Vercel** credentials.
6868
+ Create a token at the link above.
6869
+ Here's where to get one: https://vercel.com/account/tokens
6870
+
6871
+ Please paste your **API Token** here. I'll delete your message right after reading it.`,
6872
+ action: "SETUP_CREDENTIALS"
6873
+ }
6874
+ }
6875
+ ],
6876
+ [
6877
+ {
6878
+ name: "{{user1}}",
6879
+ content: { text: "/setup" }
6880
+ },
6881
+ {
6882
+ name: "{{agentName}}",
6883
+ content: {
6884
+ text: "Which service do you want to set up? I support GitHub, Vercel, Cloudflare, Anthropic, OpenAI, fal.ai, or a custom credential.",
6885
+ action: "SETUP_CREDENTIALS"
6886
+ }
6887
+ }
6888
+ ]
6889
+ ]
6890
+ };
6891
+ var setup_credentials_default = setupCredentials;
6892
+
6253
6893
  // actions/summarizeConversation.ts
6254
- import fs2 from "node:fs";
6894
+ import fs3 from "node:fs";
6255
6895
  import {
6256
6896
  ContentType as ContentType3,
6257
6897
  composePromptFromState as composePromptFromState15,
6258
6898
  getEntityDetails,
6259
- logger,
6899
+ logger as logger2,
6260
6900
  MemoryType as MemoryType5,
6261
6901
  ModelType as ModelType15,
6262
6902
  parseJSONObjectFromText as parseJSONObjectFromText15,
@@ -6302,7 +6942,7 @@ function parseTimeToTimestamp(input) {
6302
6942
  const milliseconds = value * (multipliers[unit] || 0);
6303
6943
  return Date.now() - milliseconds;
6304
6944
  }
6305
- logger.warn(`[parseTimeToTimestamp] Could not parse time value, using current time: ${input}`);
6945
+ logger2.warn(`[parseTimeToTimestamp] Could not parse time value, using current time: ${input}`);
6306
6946
  return Date.now();
6307
6947
  }
6308
6948
  var getDateRange = async (runtime, _message, state) => {
@@ -6320,7 +6960,7 @@ var getDateRange = async (runtime, _message, state) => {
6320
6960
  const startRaw = parseTimeToTimestamp(parsedResponse.start);
6321
6961
  const endRaw = parseTimeToTimestamp(parsedResponse.end);
6322
6962
  if (!Number.isFinite(startRaw) || !Number.isFinite(endRaw)) {
6323
- logger.warn(`[getDateRange] Invalid timestamps parsed: start=${startRaw}, end=${endRaw}, retrying...`);
6963
+ logger2.warn(`[getDateRange] Invalid timestamps parsed: start=${startRaw}, end=${endRaw}, retrying...`);
6324
6964
  continue;
6325
6965
  }
6326
6966
  let start = startRaw <= endRaw ? startRaw : endRaw;
@@ -6533,8 +7173,8 @@ ${currentSummary.trim()}
6533
7173
  const summaryDir = "cache";
6534
7174
  const summaryFilename = `${summaryDir}/conversation_summary_${Date.now()}`;
6535
7175
  await runtime.setCache(summaryFilename, currentSummary);
6536
- await fs2.promises.mkdir(summaryDir, { recursive: true });
6537
- await fs2.promises.writeFile(summaryFilename, currentSummary, "utf8");
7176
+ await fs3.promises.mkdir(summaryDir, { recursive: true });
7177
+ await fs3.promises.writeFile(summaryFilename, currentSummary, "utf8");
6538
7178
  if (callback) {
6539
7179
  await callback?.({
6540
7180
  ...callbackData,
@@ -7156,7 +7796,7 @@ function printBanner(options) {
7156
7796
  }
7157
7797
 
7158
7798
  // providers/channelState.ts
7159
- import { ChannelType as ChannelType2 } from "@elizaos/core";
7799
+ import { ChannelType as ChannelType3 } from "@elizaos/core";
7160
7800
 
7161
7801
  // types.ts
7162
7802
  var DiscordEventTypes;
@@ -7186,6 +7826,39 @@ var ServiceType2 = {
7186
7826
 
7187
7827
  // providers/channelState.ts
7188
7828
  var spec18 = requireProviderSpec("channelState");
7829
+ function asRecord(value) {
7830
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
7831
+ }
7832
+ function readString(value) {
7833
+ if (typeof value !== "string") {
7834
+ return;
7835
+ }
7836
+ const trimmed = value.trim();
7837
+ return trimmed.length > 0 ? trimmed : undefined;
7838
+ }
7839
+ function normalizeName(value) {
7840
+ return value.trim().toLowerCase();
7841
+ }
7842
+ function formatDiscordIdentity(displayName, userName) {
7843
+ if (displayName && userName && normalizeName(displayName) !== normalizeName(userName)) {
7844
+ return `${displayName} (discord username: ${userName})`;
7845
+ }
7846
+ return displayName ?? userName ?? "someone";
7847
+ }
7848
+ function describeCurrentSpeaker(message, fallback) {
7849
+ const metadata = asRecord(message.metadata);
7850
+ const discordMetadata = asRecord(metadata?.discord);
7851
+ const displayName = readString(metadata?.entityName) ?? readString(metadata?.displayName) ?? readString(discordMetadata?.name) ?? readString(discordMetadata?.globalName) ?? readString(fallback);
7852
+ const userName = readString(metadata?.entityUserName) ?? readString(discordMetadata?.userName) ?? readString(discordMetadata?.username);
7853
+ return formatDiscordIdentity(displayName, userName);
7854
+ }
7855
+ function describeAgentDiscordAccount(agentName, userName) {
7856
+ const candidate = readString(userName);
7857
+ if (!candidate || normalizeName(candidate) === normalizeName(agentName)) {
7858
+ return agentName;
7859
+ }
7860
+ return `${agentName} (discord username: ${candidate})`;
7861
+ }
7189
7862
  var channelStateProvider = {
7190
7863
  name: spec18.name,
7191
7864
  dynamic: true,
@@ -7201,15 +7874,17 @@ var channelStateProvider = {
7201
7874
  text: ""
7202
7875
  };
7203
7876
  }
7204
- const agentName = state?.agentName || "The agent";
7205
- const senderName = state?.senderName || "someone";
7877
+ const stateRecord = state;
7878
+ const agentName = readString(stateRecord?.agentName) ?? "The agent";
7879
+ const senderName = readString(stateRecord?.senderName) ?? "someone";
7880
+ const senderIdentity = describeCurrentSpeaker(message, senderName);
7206
7881
  let responseText = "";
7207
7882
  let channelType = "";
7208
7883
  let serverName = "";
7209
7884
  const channelId = room.channelId ?? "";
7210
- if (room.type === ChannelType2.DM) {
7885
+ if (room.type === ChannelType3.DM) {
7211
7886
  channelType = "DM";
7212
- responseText = `${agentName} is currently in a direct message conversation with ${senderName}. ${agentName} should engage in conversation, should respond to messages that are addressed to them and only ignore messages that seem to not require a response.`;
7887
+ responseText = `${agentName} is currently in a direct message conversation with ${senderIdentity}. ${agentName} should engage in conversation, should respond to messages that are addressed to them and only ignore messages that seem to not require a response.`;
7213
7888
  } else {
7214
7889
  channelType = "GROUP";
7215
7890
  if (!channelId) {
@@ -7283,8 +7958,15 @@ var channelStateProvider = {
7283
7958
  };
7284
7959
  }
7285
7960
  serverName = guild.name;
7961
+ const agentIdentity = describeAgentDiscordAccount(agentName, discordService.client?.user?.username);
7286
7962
  responseText = `${agentName} is currently having a conversation in the channel \`#${channel?.name || channelId}\` in the server \`${serverName}\``;
7287
7963
  responseText += `
7964
+ The current speaker is ${senderIdentity}.`;
7965
+ if (agentIdentity !== agentName) {
7966
+ responseText += `
7967
+ On Discord, ${agentName} is logged in as ${agentIdentity}.`;
7968
+ }
7969
+ responseText += `
7288
7970
  ${agentName} is in a room with other users and should be self-conscious and only participate when directly addressed or when the conversation is relevant to them.`;
7289
7971
  }
7290
7972
  return {
@@ -7453,8 +8135,7 @@ function formatGuildInfoText(guild, info) {
7453
8135
  }
7454
8136
 
7455
8137
  // providers/voiceState.ts
7456
- import { getVoiceConnection } from "@discordjs/voice";
7457
- import { ChannelType as ChannelType3 } from "@elizaos/core";
8138
+ import { ChannelType as ChannelType4 } from "@elizaos/core";
7458
8139
  var spec20 = requireProviderSpec("voiceState");
7459
8140
  var voiceStateProvider = {
7460
8141
  name: spec20.name,
@@ -7464,7 +8145,7 @@ var voiceStateProvider = {
7464
8145
  if (!room) {
7465
8146
  throw new Error("No room found");
7466
8147
  }
7467
- if (room.type !== ChannelType3.GROUP) {
8148
+ if (room.type !== ChannelType4.GROUP) {
7468
8149
  return {
7469
8150
  data: {
7470
8151
  isInVoiceChannel: false,
@@ -7533,7 +8214,7 @@ var voiceStateProvider = {
7533
8214
  text: `${agentName} is not currently in a voice channel`
7534
8215
  };
7535
8216
  }
7536
- const connection = getVoiceConnection(guildId);
8217
+ const connection = discordService.voiceManager?.getVoiceConnection(guildId);
7537
8218
  if (!connection) {
7538
8219
  return {
7539
8220
  data: {
@@ -7576,17 +8257,21 @@ var voiceStateProvider = {
7576
8257
 
7577
8258
  // service.ts
7578
8259
  import {
7579
- ChannelType as ChannelType7,
7580
- createUniqueUuid as createUniqueUuid6,
8260
+ ChannelType as ChannelType8,
8261
+ createUniqueUuid as createUniqueUuid7,
7581
8262
  EventType as EventType3,
7582
8263
  MemoryType as MemoryType7,
7583
8264
  Service,
7584
- stringToUuid as stringToUuid3
8265
+ stringToUuid as stringToUuid4
7585
8266
  } from "@elizaos/core";
8267
+ import {
8268
+ getConnectorAdminWhitelist,
8269
+ setConnectorAdminWhitelist
8270
+ } from "@elizaos/core/roles";
7586
8271
  import {
7587
8272
  AttachmentBuilder as AttachmentBuilder2,
7588
8273
  AuditLogEvent,
7589
- ChannelType as DiscordChannelType5,
8274
+ ChannelType as DiscordChannelType6,
7590
8275
  Client as DiscordJsClient,
7591
8276
  Events,
7592
8277
  GatewayIntentBits,
@@ -7625,6 +8310,171 @@ function createCompatRuntime(runtime) {
7625
8310
  });
7626
8311
  }
7627
8312
 
8313
+ // debouncer.ts
8314
+ var DEFAULT_DEBOUNCE_MS = 400;
8315
+ var DEFAULT_CHANNEL_DEBOUNCE_MS = 3000;
8316
+ function escapeRegex2(value) {
8317
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
8318
+ }
8319
+ function runSafely(callback) {
8320
+ try {
8321
+ callback();
8322
+ } catch {}
8323
+ }
8324
+ function createChannelDebouncer(onFlush, options = {}) {
8325
+ const debounceMs = options.debounceMs ?? DEFAULT_CHANNEL_DEBOUNCE_MS;
8326
+ const responseCooldownMs = options.responseCooldownMs ?? 30000;
8327
+ const botName = options.botName?.trim();
8328
+ const botNameRegex = botName && botName.length >= 2 ? new RegExp(`(^|[^\\p{L}\\p{N}])${escapeRegex2(botName)}(?=$|[^\\p{L}\\p{N}])`, "iu") : null;
8329
+ const pending = new Map;
8330
+ const lastResponseTime = new Map;
8331
+ const isBotTargeted = (message) => {
8332
+ const botId = options.getBotUserId?.() ?? options.botUserId;
8333
+ if (botId && message.mentions?.users?.has(botId)) {
8334
+ return true;
8335
+ }
8336
+ if (botId && message.reference?.messageId && message.mentions?.repliedUser?.id === botId) {
8337
+ return true;
8338
+ }
8339
+ return Boolean(botNameRegex?.test(message.content ?? ""));
8340
+ };
8341
+ const isInCooldown = (channelId) => {
8342
+ const lastRespondedAt = lastResponseTime.get(channelId);
8343
+ if (!lastRespondedAt) {
8344
+ return false;
8345
+ }
8346
+ if (Date.now() - lastRespondedAt >= responseCooldownMs) {
8347
+ lastResponseTime.delete(channelId);
8348
+ return false;
8349
+ }
8350
+ return true;
8351
+ };
8352
+ const flush = (channelId) => {
8353
+ const entry = pending.get(channelId);
8354
+ if (!entry) {
8355
+ return;
8356
+ }
8357
+ clearTimeout(entry.timer);
8358
+ pending.delete(channelId);
8359
+ if (entry.messages.length > 0) {
8360
+ runSafely(() => onFlush(entry.messages));
8361
+ }
8362
+ };
8363
+ const enqueue = (message) => {
8364
+ const channelId = message.channel.id;
8365
+ if (isBotTargeted(message)) {
8366
+ const entry = pending.get(channelId);
8367
+ if (entry) {
8368
+ clearTimeout(entry.timer);
8369
+ pending.delete(channelId);
8370
+ entry.messages.push(message);
8371
+ runSafely(() => onFlush(entry.messages));
8372
+ } else {
8373
+ runSafely(() => onFlush([message]));
8374
+ }
8375
+ return;
8376
+ }
8377
+ if (isInCooldown(channelId)) {
8378
+ return;
8379
+ }
8380
+ if (debounceMs <= 0) {
8381
+ runSafely(() => onFlush([message]));
8382
+ return;
8383
+ }
8384
+ const existing = pending.get(channelId);
8385
+ if (existing) {
8386
+ clearTimeout(existing.timer);
8387
+ existing.messages.push(message);
8388
+ existing.timer = setTimeout(() => flush(channelId), debounceMs);
8389
+ return;
8390
+ }
8391
+ pending.set(channelId, {
8392
+ messages: [message],
8393
+ timer: setTimeout(() => flush(channelId), debounceMs)
8394
+ });
8395
+ };
8396
+ return {
8397
+ enqueue,
8398
+ markResponded: (channelId) => {
8399
+ lastResponseTime.set(channelId, Date.now());
8400
+ },
8401
+ flushAll: () => {
8402
+ for (const key of [...pending.keys()]) {
8403
+ flush(key);
8404
+ }
8405
+ },
8406
+ pendingCount: () => pending.size,
8407
+ destroy: () => {
8408
+ for (const [, entry] of pending) {
8409
+ clearTimeout(entry.timer);
8410
+ }
8411
+ pending.clear();
8412
+ lastResponseTime.clear();
8413
+ }
8414
+ };
8415
+ }
8416
+ function createMessageDebouncer(onFlush, debounceMs = DEFAULT_DEBOUNCE_MS) {
8417
+ const pending = new Map;
8418
+ const makeKey = (message) => `${message.channel.id}:${message.author.id}`;
8419
+ const flush = (key) => {
8420
+ const entry = pending.get(key);
8421
+ if (!entry) {
8422
+ return;
8423
+ }
8424
+ clearTimeout(entry.timer);
8425
+ pending.delete(key);
8426
+ if (entry.messages.length > 0) {
8427
+ runSafely(() => onFlush(entry.messages));
8428
+ }
8429
+ };
8430
+ const hasMedia = (message) => (message.attachments?.size ?? 0) > 0 || (message.stickers?.size ?? 0) > 0;
8431
+ const enqueue = (message) => {
8432
+ if (debounceMs <= 0) {
8433
+ runSafely(() => onFlush([message]));
8434
+ return;
8435
+ }
8436
+ const key = makeKey(message);
8437
+ if (hasMedia(message)) {
8438
+ const entry = pending.get(key);
8439
+ if (entry) {
8440
+ clearTimeout(entry.timer);
8441
+ pending.delete(key);
8442
+ if (entry.messages.length > 0) {
8443
+ runSafely(() => onFlush(entry.messages));
8444
+ }
8445
+ }
8446
+ runSafely(() => onFlush([message]));
8447
+ return;
8448
+ }
8449
+ const existing = pending.get(key);
8450
+ if (existing) {
8451
+ clearTimeout(existing.timer);
8452
+ existing.messages.push(message);
8453
+ existing.timer = setTimeout(() => flush(key), debounceMs);
8454
+ return;
8455
+ }
8456
+ pending.set(key, {
8457
+ messages: [message],
8458
+ timer: setTimeout(() => flush(key), debounceMs)
8459
+ });
8460
+ };
8461
+ return {
8462
+ enqueue,
8463
+ flushAll: () => {
8464
+ for (const key of [...pending.keys()]) {
8465
+ flush(key);
8466
+ }
8467
+ },
8468
+ pendingCount: () => pending.size,
8469
+ destroy: () => {
8470
+ for (const [, entry] of pending) {
8471
+ clearTimeout(entry.timer);
8472
+ }
8473
+ pending.clear();
8474
+ }
8475
+ };
8476
+ }
8477
+
7628
8478
  // environment.ts
7629
8479
  import { parseBooleanFromText } from "@elizaos/core";
7630
8480
  import { z } from "zod";
@@ -7647,8 +8497,9 @@ var DISCORD_DEFAULTS = {
7647
8497
  SHOULD_IGNORE_DIRECT_MESSAGES: getEnvBoolean("DISCORD_SHOULD_IGNORE_DIRECT_MESSAGES", false),
7648
8498
  SHOULD_RESPOND_ONLY_TO_MENTIONS: getEnvBoolean("DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS", false),
7649
8499
  ALLOWED_CHANNEL_IDS: getEnvArray("CHANNEL_IDS", []),
7650
- DM_POLICY: process.env?.DISCORD_DM_POLICY || "open",
7651
- ALLOW_FROM: getEnvArray("DISCORD_ALLOW_FROM", [])
8500
+ DM_POLICY: process.env?.DISCORD_DM_POLICY || "pairing",
8501
+ ALLOW_FROM: getEnvArray("DISCORD_ALLOW_FROM", []),
8502
+ SYNC_PROFILE: getEnvBoolean("DISCORD_SYNC_PROFILE", true)
7652
8503
  };
7653
8504
  var discordEnvSchema = z.object({
7654
8505
  DISCORD_API_TOKEN: z.string().min(1, "Discord API token is required"),
@@ -7681,50 +8532,128 @@ function getDiscordSettings(runtime) {
7681
8532
  }
7682
8533
  return DISCORD_DEFAULTS.DM_POLICY;
7683
8534
  }),
7684
- allowFrom: resolveSetting("DISCORD_ALLOW_FROM", characterSettings.allowFrom, DISCORD_DEFAULTS.ALLOW_FROM, (value) => value.split(",").map((s) => s.trim()).filter((s) => s.length > 0))
8535
+ allowFrom: resolveSetting("DISCORD_ALLOW_FROM", characterSettings.allowFrom, DISCORD_DEFAULTS.ALLOW_FROM, (value) => value.split(",").map((s) => s.trim()).filter((s) => s.length > 0)),
8536
+ syncProfile: resolveSetting("DISCORD_SYNC_PROFILE", characterSettings.syncProfile, DISCORD_DEFAULTS.SYNC_PROFILE, parseBooleanFromText),
8537
+ profileName: resolveSetting("DISCORD_PROFILE_NAME", characterSettings.profileName, undefined, (value) => value.trim()),
8538
+ profileAvatar: resolveSetting("DISCORD_PROFILE_AVATAR", characterSettings.profileAvatar, undefined, (value) => value.trim())
7685
8539
  };
7686
8540
  }
7687
8541
 
7688
8542
  // identity.ts
7689
8543
  import {
7690
8544
  createUniqueUuid as createUniqueUuid3,
7691
- Role
8545
+ Role,
8546
+ stringToUuid
7692
8547
  } from "@elizaos/core";
7693
- var CANONICAL_OWNER_SETTING_KEY = "MILADY_ADMIN_ENTITY_ID";
8548
+ var CANONICAL_OWNER_SETTING_KEYS = [
8549
+ "ELIZA_ADMIN_ENTITY_ID",
8550
+ "MILADY_ADMIN_ENTITY_ID"
8551
+ ];
8552
+ var DISCORD_SNOWFLAKE_PATTERN = /^\d{15,20}$/;
7694
8553
  function getCanonicalOwnerId(runtime) {
7695
- const value = runtime.getSetting?.(CANONICAL_OWNER_SETTING_KEY);
8554
+ for (const key of CANONICAL_OWNER_SETTING_KEYS) {
8555
+ const value = runtime.getSetting?.(key);
8556
+ if (typeof value !== "string") {
8557
+ continue;
8558
+ }
8559
+ const trimmed = value.trim();
8560
+ if (trimmed.length > 0) {
8561
+ return trimmed;
8562
+ }
8563
+ }
8564
+ return;
8565
+ }
8566
+ function asRecord2(value) {
8567
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
8568
+ }
8569
+ function readDiscordSnowflake(value) {
7696
8570
  if (typeof value !== "string") {
7697
- return;
8571
+ return null;
7698
8572
  }
7699
8573
  const trimmed = value.trim();
7700
- return trimmed.length > 0 ? trimmed : undefined;
8574
+ return DISCORD_SNOWFLAKE_PATTERN.test(trimmed) ? trimmed : null;
7701
8575
  }
7702
- function buildDiscordWorldMetadata(runtime, guildOwnerId) {
7703
- const ownerId = getCanonicalOwnerId(runtime);
7704
- if (ownerId) {
7705
- return {
7706
- ownership: { ownerId },
7707
- roles: {
7708
- [ownerId]: Role.OWNER
7709
- }
7710
- };
8576
+ function readUserIdFromOwnerLike(value) {
8577
+ const owner = asRecord2(value);
8578
+ if (!owner) {
8579
+ return null;
7711
8580
  }
7712
- if (!guildOwnerId) {
7713
- return;
8581
+ return readDiscordSnowflake(owner.id) ?? readDiscordSnowflake(asRecord2(owner.user)?.id) ?? readDiscordSnowflake(owner.ownerId) ?? readDiscordSnowflake(owner.ownerUserId);
8582
+ }
8583
+ function resolveMiladyOwnerEntityId(runtime) {
8584
+ const configuredOwnerId = getCanonicalOwnerId(runtime);
8585
+ if (configuredOwnerId) {
8586
+ return configuredOwnerId;
8587
+ }
8588
+ const agentName = runtime.character?.name?.trim() || runtime.agentId;
8589
+ return stringToUuid(`${agentName}-admin-entity`);
8590
+ }
8591
+ function resolveDiscordRuntimeEntityId(runtime, userId, ownerDiscordUserIds = []) {
8592
+ for (const ownerUserId of ownerDiscordUserIds) {
8593
+ if (ownerUserId === userId) {
8594
+ return resolveMiladyOwnerEntityId(runtime);
8595
+ }
8596
+ }
8597
+ return createUniqueUuid3(runtime, userId);
8598
+ }
8599
+ function extractDiscordOwnerUserIds(application) {
8600
+ const applicationRecord = asRecord2(application);
8601
+ if (!applicationRecord) {
8602
+ return [];
8603
+ }
8604
+ const ownerCandidates = new Set;
8605
+ const directOwnerId = readUserIdFromOwnerLike(applicationRecord.owner);
8606
+ if (directOwnerId) {
8607
+ ownerCandidates.add(directOwnerId);
8608
+ }
8609
+ const team = asRecord2(applicationRecord.team);
8610
+ const teamOwnerId = readDiscordSnowflake(team?.ownerId) ?? readDiscordSnowflake(team?.ownerUserId);
8611
+ if (teamOwnerId) {
8612
+ ownerCandidates.add(teamOwnerId);
7714
8613
  }
7715
- const discordOwnerId = createUniqueUuid3(runtime, guildOwnerId);
8614
+ const teamMembers = team?.members;
8615
+ if (Array.isArray(teamMembers)) {
8616
+ for (const member of teamMembers) {
8617
+ const memberId = readUserIdFromOwnerLike(member);
8618
+ if (memberId) {
8619
+ ownerCandidates.add(memberId);
8620
+ }
8621
+ }
8622
+ }
8623
+ return [...ownerCandidates];
8624
+ }
8625
+ function parseDiscordOwnerUserIds(value) {
8626
+ const rawValues = (() => {
8627
+ if (Array.isArray(value)) {
8628
+ return value;
8629
+ }
8630
+ if (typeof value !== "string" || value.trim().length === 0) {
8631
+ return [];
8632
+ }
8633
+ try {
8634
+ const parsed = JSON.parse(value);
8635
+ return Array.isArray(parsed) ? parsed : [];
8636
+ } catch {
8637
+ return [];
8638
+ }
8639
+ })();
8640
+ return rawValues.map((entry) => readDiscordSnowflake(entry)).filter((entry) => Boolean(entry));
8641
+ }
8642
+ function buildDiscordWorldMetadata(runtime, _guildOwnerId) {
8643
+ const ownerId = resolveMiladyOwnerEntityId(runtime);
7716
8644
  return {
7717
- ownership: { ownerId: discordOwnerId },
8645
+ ownership: { ownerId },
7718
8646
  roles: {
7719
- [discordOwnerId]: Role.OWNER
8647
+ [ownerId]: Role.OWNER
7720
8648
  }
7721
8649
  };
7722
8650
  }
7723
- function buildDiscordEntityMetadata(userId, userName, name, globalName) {
8651
+ function buildDiscordEntityMetadata(userId, userName, name, globalName, avatarUrl) {
7724
8652
  return {
7725
8653
  default: {
7726
8654
  username: userName,
7727
- name
8655
+ name,
8656
+ ...typeof avatarUrl === "string" && avatarUrl.length > 0 ? { avatarUrl } : {}
7728
8657
  },
7729
8658
  discord: {
7730
8659
  id: userId,
@@ -7732,33 +8661,35 @@ function buildDiscordEntityMetadata(userId, userName, name, globalName) {
7732
8661
  userName,
7733
8662
  username: userName,
7734
8663
  name,
7735
- ...typeof globalName === "string" && globalName.length > 0 ? { globalName } : {}
8664
+ ...typeof globalName === "string" && globalName.length > 0 ? { globalName } : {},
8665
+ ...typeof avatarUrl === "string" && avatarUrl.length > 0 ? { avatarUrl } : {}
7736
8666
  },
7737
8667
  originalId: userId,
7738
8668
  username: userName,
7739
- displayName: name
8669
+ displayName: name,
8670
+ ...typeof avatarUrl === "string" && avatarUrl.length > 0 ? { avatarUrl } : {}
7740
8671
  };
7741
8672
  }
7742
8673
 
7743
8674
  // messages.ts
7744
8675
  import {
7745
- ChannelType as ChannelType5,
8676
+ ChannelType as ChannelType6,
7746
8677
  checkPairingAllowed,
7747
8678
  createUniqueUuid as createUniqueUuid4,
7748
8679
  EventType,
7749
8680
  isInAllowlist,
7750
8681
  ServiceType as ServiceType4,
7751
- stringToUuid
8682
+ stringToUuid as stringToUuid2
7752
8683
  } from "@elizaos/core";
7753
8684
  import {
7754
8685
  AttachmentBuilder,
7755
- ChannelType as DiscordChannelType3
8686
+ ChannelType as DiscordChannelType4
7756
8687
  } from "discord.js";
7757
8688
 
7758
8689
  // attachments.ts
7759
- import fs3 from "node:fs";
7760
- import os from "node:os";
7761
- import path from "node:path";
8690
+ import fs4 from "node:fs";
8691
+ import os2 from "node:os";
8692
+ import path2 from "node:path";
7762
8693
  import {
7763
8694
  ModelType as ModelType19,
7764
8695
  ServiceType as ServiceType3
@@ -7768,7 +8699,7 @@ import ffmpeg from "fluent-ffmpeg";
7768
8699
 
7769
8700
  // utils.ts
7770
8701
  import {
7771
- logger as logger2,
8702
+ logger as logger3,
7772
8703
  ModelType as ModelType18,
7773
8704
  parseJSONObjectFromText as parseJSONObjectFromText18,
7774
8705
  trimTokens as trimTokens3
@@ -7776,13 +8707,13 @@ import {
7776
8707
  import {
7777
8708
  ActionRowBuilder,
7778
8709
  ButtonBuilder,
7779
- ChannelType as ChannelType4,
8710
+ ChannelType as ChannelType5,
7780
8711
  PermissionsBitField as PermissionsBitField4,
7781
8712
  StringSelectMenuBuilder,
7782
8713
  ThreadChannel
7783
8714
  } from "discord.js";
7784
8715
  function hasMessagingAPI(runtime) {
7785
- return "elizaOS" in runtime && typeof runtime.elizaOS === "object" && runtime.elizaOS !== null && typeof runtime.elizaOS.sendMessage === "function";
8716
+ return "elizaOS" in runtime && typeof runtime.elizaOS === "object" && runtime.elizaOS !== null && (typeof runtime.elizaOS.handleMessage === "function" || typeof runtime.elizaOS.sendMessage === "function");
7786
8717
  }
7787
8718
  function hasMessageService(runtime) {
7788
8719
  return runtime.messageService !== null && typeof runtime.messageService?.handleMessage === "function";
@@ -7800,6 +8731,59 @@ function getMessageService(runtime) {
7800
8731
  return null;
7801
8732
  }
7802
8733
  var MAX_MESSAGE_LENGTH = 1900;
8734
+ function collectStructuredText(value, seen) {
8735
+ if (typeof value === "string") {
8736
+ return value.trim() ? [value] : [];
8737
+ }
8738
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
8739
+ return [String(value)];
8740
+ }
8741
+ if (!value || typeof value !== "object") {
8742
+ return [];
8743
+ }
8744
+ if (seen.has(value)) {
8745
+ return [];
8746
+ }
8747
+ seen.add(value);
8748
+ if (Array.isArray(value)) {
8749
+ return value.flatMap((entry) => collectStructuredText(entry, seen));
8750
+ }
8751
+ const record = value;
8752
+ for (const key of ["text", "responseText", "message", "body"]) {
8753
+ const normalized = collectStructuredText(record[key], seen);
8754
+ if (normalized.length > 0) {
8755
+ return normalized;
8756
+ }
8757
+ }
8758
+ for (const key of [
8759
+ "content",
8760
+ "parts",
8761
+ "blocks",
8762
+ "items",
8763
+ "segments"
8764
+ ]) {
8765
+ const normalized = collectStructuredText(record[key], seen);
8766
+ if (normalized.length > 0) {
8767
+ return normalized;
8768
+ }
8769
+ }
8770
+ for (const key of ["title", "summary"]) {
8771
+ const normalized = collectStructuredText(record[key], seen);
8772
+ if (normalized.length > 0) {
8773
+ return normalized;
8774
+ }
8775
+ }
8776
+ return [];
8777
+ }
8778
+ function normalizeDiscordMessageText(value) {
8779
+ const fragments = collectStructuredText(value, new Set).map((fragment) => fragment.trim()).filter((fragment) => fragment.length > 0);
8780
+ if (fragments.length === 0) {
8781
+ return "";
8782
+ }
8783
+ return fragments.join(`
8784
+
8785
+ `);
8786
+ }
7803
8787
  function cleanUrl(url) {
7804
8788
  let clean = url;
7805
8789
  clean = clean.replace(/\\([._\-~])/g, "$1");
@@ -7923,6 +8907,13 @@ async function generateSummary(runtime, text) {
7923
8907
  function isDiscordAPIError(error) {
7924
8908
  return error instanceof Error && "code" in error;
7925
8909
  }
8910
+ function isReplyReferenceFailure(error) {
8911
+ if (!isDiscordAPIError(error)) {
8912
+ return false;
8913
+ }
8914
+ const errorMessage = error.message.toLowerCase();
8915
+ return error.code === 10008 || errorMessage.includes("unknown message") || errorMessage.includes("message reference") || errorMessage.includes("message_reference");
8916
+ }
7926
8917
  function isDiscordJsComponent(component) {
7927
8918
  return component !== null && typeof component === "object" && "toJSON" in component && typeof component.toJSON === "function";
7928
8919
  }
@@ -7934,16 +8925,20 @@ function safeStringify(obj) {
7934
8925
  }
7935
8926
  async function sendMessageInChunks(channel, content, inReplyTo, files, components, runtime) {
7936
8927
  const sentMessages = [];
8928
+ let lastSendError = null;
7937
8929
  let messages;
7938
8930
  if (runtime && content.length > MAX_MESSAGE_LENGTH && needsSmartSplit(content)) {
7939
8931
  messages = await smartSplitMessage(runtime, content);
7940
8932
  } else {
7941
8933
  messages = splitMessage(content);
7942
8934
  }
8935
+ if (messages.length === 0 && (files && files.length > 0 || components && components.length > 0)) {
8936
+ messages = [""];
8937
+ }
7943
8938
  try {
7944
8939
  for (let i = 0;i < messages.length; i++) {
7945
8940
  const message = messages[i];
7946
- if (message.trim().length > 0 || i === messages.length - 1 && files && files.length > 0 || components) {
8941
+ if (message.trim().length > 0 || i === messages.length - 1 && files && files.length > 0 || i === messages.length - 1 && components && components.length > 0) {
7947
8942
  const options = {
7948
8943
  content: message.trim()
7949
8944
  };
@@ -7957,26 +8952,26 @@ async function sendMessageInChunks(channel, content, inReplyTo, files, component
7957
8952
  }
7958
8953
  if (i === messages.length - 1 && components && components.length > 0) {
7959
8954
  try {
7960
- logger2.info(`Components received: ${safeStringify(components)}`);
8955
+ logger3.info(`Components received: ${safeStringify(components)}`);
7961
8956
  if (!Array.isArray(components)) {
7962
- logger2.warn("Components is not an array, skipping component processing");
8957
+ logger3.warn("Components is not an array, skipping component processing");
7963
8958
  } else if (isDiscordJsComponentArray(components)) {
7964
8959
  options.components = components;
7965
8960
  } else {
7966
8961
  const discordComponents = components.map((row) => {
7967
8962
  if (!row || typeof row !== "object" || row.type !== 1) {
7968
- logger2.warn("Invalid component row structure, skipping");
8963
+ logger3.warn("Invalid component row structure, skipping");
7969
8964
  return null;
7970
8965
  }
7971
8966
  if (row.type === 1) {
7972
8967
  const actionRow = new ActionRowBuilder;
7973
8968
  if (!Array.isArray(row.components)) {
7974
- logger2.warn("Row components is not an array, skipping");
8969
+ logger3.warn("Row components is not an array, skipping");
7975
8970
  return null;
7976
8971
  }
7977
8972
  const validComponents = row.components.map((comp) => {
7978
8973
  if (!comp || typeof comp !== "object") {
7979
- logger2.warn("Invalid component, skipping");
8974
+ logger3.warn("Invalid component, skipping");
7980
8975
  return null;
7981
8976
  }
7982
8977
  try {
@@ -8001,7 +8996,7 @@ async function sendMessageInChunks(channel, content, inReplyTo, files, component
8001
8996
  return selectMenu;
8002
8997
  }
8003
8998
  } catch (err) {
8004
- logger2.error(`Error creating component: ${err}`);
8999
+ logger3.error(`Error creating component: ${err}`);
8005
9000
  return null;
8006
9001
  }
8007
9002
  return null;
@@ -8018,15 +9013,15 @@ async function sendMessageInChunks(channel, content, inReplyTo, files, component
8018
9013
  }
8019
9014
  }
8020
9015
  } catch (error) {
8021
- logger2.error(`Error processing components: ${error}`);
9016
+ logger3.error(`Error processing components: ${error}`);
8022
9017
  }
8023
9018
  }
8024
9019
  try {
8025
9020
  const m = await channel.send(options);
8026
9021
  sentMessages.push(m);
8027
9022
  } catch (error) {
8028
- if (isDiscordAPIError(error) && error.code === 50035 && error.message && error.message.includes("Unknown message")) {
8029
- logger2.warn("Message reference no longer valid (message may have been deleted). Sending without reply threading.");
9023
+ if (isReplyReferenceFailure(error) && options.reply) {
9024
+ logger3.warn("Message reference no longer valid (message may have been deleted). Sending without reply threading.");
8030
9025
  const optionsWithoutReply = { ...options };
8031
9026
  delete optionsWithoutReply.reply;
8032
9027
  try {
@@ -8034,17 +9029,27 @@ async function sendMessageInChunks(channel, content, inReplyTo, files, component
8034
9029
  sentMessages.push(m);
8035
9030
  } catch (retryError) {
8036
9031
  const errorMessage = retryError instanceof Error ? retryError.message : String(retryError);
8037
- logger2.error(`Error sending message after removing reply reference: ${errorMessage}`);
9032
+ lastSendError = retryError;
9033
+ logger3.error(`Error sending message after removing reply reference: ${errorMessage}`);
8038
9034
  throw retryError;
8039
9035
  }
8040
9036
  } else {
9037
+ lastSendError = error;
8041
9038
  throw error;
8042
9039
  }
8043
9040
  }
8044
9041
  }
8045
9042
  }
8046
9043
  } catch (error) {
8047
- logger2.error(`Error sending message: ${error}`);
9044
+ lastSendError = error;
9045
+ logger3.error(`Error sending message: ${error}`);
9046
+ }
9047
+ const attemptedSend = content.trim().length > 0 || files && files.length > 0 || components && components.length > 0;
9048
+ if (attemptedSend && sentMessages.length === 0) {
9049
+ if (lastSendError instanceof Error) {
9050
+ throw lastSendError;
9051
+ }
9052
+ throw new Error("Discord message send completed without delivering any chunks");
8048
9053
  }
8049
9054
  return sentMessages;
8050
9055
  }
@@ -8165,7 +9170,7 @@ function canSendMessage(channel) {
8165
9170
  reason: "No channel given"
8166
9171
  };
8167
9172
  }
8168
- if (channel.type === ChannelType4.DM) {
9173
+ if (channel.type === ChannelType5.DM) {
8169
9174
  return {
8170
9175
  canSend: true,
8171
9176
  reason: null
@@ -8346,12 +9351,12 @@ class AttachmentManager {
8346
9351
  }
8347
9352
  }
8348
9353
  async extractAudioFromMP4(mp4Data) {
8349
- const tmpDir = os.tmpdir();
9354
+ const tmpDir = os2.tmpdir();
8350
9355
  const timestamp = Date.now();
8351
- const tempMP4File = path.join(tmpDir, `discord_video_${timestamp}.mp4`);
8352
- const tempAudioFile = path.join(tmpDir, `discord_audio_${timestamp}.mp3`);
9356
+ const tempMP4File = path2.join(tmpDir, `discord_video_${timestamp}.mp4`);
9357
+ const tempAudioFile = path2.join(tmpDir, `discord_audio_${timestamp}.mp3`);
8353
9358
  try {
8354
- fs3.writeFileSync(tempMP4File, Buffer.from(mp4Data));
9359
+ fs4.writeFileSync(tempMP4File, Buffer.from(mp4Data));
8355
9360
  await new Promise((resolve, reject) => {
8356
9361
  ffmpeg.ffprobe(tempMP4File, (err, metadata) => {
8357
9362
  if (err) {
@@ -8383,7 +9388,7 @@ class AttachmentManager {
8383
9388
  reject(err);
8384
9389
  }).output(tempAudioFile).run();
8385
9390
  });
8386
- const audioData = fs3.readFileSync(tempAudioFile);
9391
+ const audioData = fs4.readFileSync(tempAudioFile);
8387
9392
  this.runtime.logger.debug({
8388
9393
  src: "plugin:discord",
8389
9394
  agentId: this.runtime.agentId,
@@ -8392,11 +9397,11 @@ class AttachmentManager {
8392
9397
  return audioData;
8393
9398
  } finally {
8394
9399
  try {
8395
- if (fs3.existsSync(tempMP4File)) {
8396
- fs3.unlinkSync(tempMP4File);
9400
+ if (fs4.existsSync(tempMP4File)) {
9401
+ fs4.unlinkSync(tempMP4File);
8397
9402
  }
8398
- if (fs3.existsSync(tempAudioFile)) {
8399
- fs3.unlinkSync(tempAudioFile);
9403
+ if (fs4.existsSync(tempAudioFile)) {
9404
+ fs4.unlinkSync(tempAudioFile);
8400
9405
  }
8401
9406
  } catch (cleanupError) {
8402
9407
  this.runtime.logger.warn({
@@ -8562,99 +9567,614 @@ class AttachmentManager {
8562
9567
  }
8563
9568
  }
8564
9569
 
8565
- // messages.ts
8566
- class MessageManager {
8567
- client;
8568
- runtime;
8569
- attachmentManager;
8570
- getChannelType;
8571
- discordSettings;
8572
- discordService;
8573
- constructor(discordService, runtime) {
8574
- if (!discordService.client) {
8575
- const errorMsg = "Discord client not initialized - cannot create MessageManager";
8576
- runtime.logger.error({ src: "plugin:discord", agentId: runtime.agentId }, errorMsg);
8577
- throw new Error(errorMsg);
8578
- }
8579
- this.client = discordService.client;
8580
- this.runtime = runtime;
8581
- this.attachmentManager = new AttachmentManager(this.runtime);
8582
- this.getChannelType = discordService.getChannelType;
8583
- this.discordService = discordService;
8584
- this.discordSettings = getDiscordSettings(this.runtime);
9570
+ // draft-chunking.ts
9571
+ var DEFAULT_DRAFT_CHUNK_CONFIG = {
9572
+ minChars: 80,
9573
+ maxChars: 1900,
9574
+ breakPreference: "sentence"
9575
+ };
9576
+ function findBreakPoint(text, maxLen, breakPreference = "sentence") {
9577
+ if (text.length <= maxLen) {
9578
+ return text.length;
8585
9579
  }
8586
- async checkDmAccess(message) {
8587
- const policy = this.discordSettings.dmPolicy ?? "open";
8588
- const userId = message.author.id;
8589
- if (policy === "disabled") {
8590
- this.runtime.logger.debug({
8591
- src: "plugin:discord",
8592
- agentId: this.runtime.agentId,
8593
- userId
8594
- }, "DM blocked: policy is disabled");
8595
- return { allowed: false };
8596
- }
8597
- if (policy === "open") {
8598
- return { allowed: true };
9580
+ const region = text.slice(0, maxLen);
9581
+ if (breakPreference === "paragraph" || breakPreference === "newline") {
9582
+ const paragraphBreak = region.lastIndexOf(`
9583
+
9584
+ `);
9585
+ if (paragraphBreak > maxLen * 0.3) {
9586
+ return paragraphBreak + 2;
8599
9587
  }
8600
- if (policy === "allowlist") {
8601
- if (this.discordSettings.allowFrom?.includes(userId)) {
8602
- return { allowed: true };
8603
- }
8604
- const inDynamicAllowlist = await isInAllowlist(this.runtime, "discord", userId);
8605
- if (inDynamicAllowlist) {
8606
- return { allowed: true };
8607
- }
8608
- this.runtime.logger.debug({
8609
- src: "plugin:discord",
8610
- agentId: this.runtime.agentId,
8611
- userId
8612
- }, "DM blocked: user not in allowlist");
8613
- return { allowed: false };
9588
+ }
9589
+ if (breakPreference !== "sentence") {
9590
+ const newlineBreak = region.lastIndexOf(`
9591
+ `);
9592
+ if (newlineBreak > maxLen * 0.3) {
9593
+ return newlineBreak + 1;
8614
9594
  }
8615
- if (policy === "pairing") {
8616
- if (this.discordSettings.allowFrom?.includes(userId)) {
8617
- return { allowed: true };
8618
- }
8619
- const result = await checkPairingAllowed(this.runtime, {
8620
- channel: "discord",
8621
- senderId: userId,
8622
- metadata: {
8623
- username: message.author.username,
8624
- displayName: message.author.displayName ?? message.author.username,
8625
- discriminator: message.author.discriminator ?? ""
8626
- }
8627
- });
8628
- if (result.allowed) {
8629
- return { allowed: true };
8630
- }
8631
- this.runtime.logger.debug({
8632
- src: "plugin:discord",
8633
- agentId: this.runtime.agentId,
8634
- userId,
8635
- pairingCode: result.pairingCode,
8636
- newRequest: result.newRequest
8637
- }, "DM blocked: pairing required");
8638
- return {
8639
- allowed: false,
8640
- replyMessage: result.newRequest ? result.replyMessage : undefined
8641
- };
9595
+ }
9596
+ const sentenceMatch = region.match(/[.!?]\s+(?=[A-Z])/g);
9597
+ if (sentenceMatch) {
9598
+ const lastSentenceEnd = region.lastIndexOf(sentenceMatch[sentenceMatch.length - 1]);
9599
+ if (lastSentenceEnd > maxLen * 0.3) {
9600
+ return lastSentenceEnd + sentenceMatch[sentenceMatch.length - 1].length;
8642
9601
  }
8643
- return { allowed: true };
8644
9602
  }
8645
- async handleMessage(message) {
8646
- const clientUser = this.client.user;
8647
- if (message.interaction || clientUser && message.author.id === clientUser.id) {
8648
- return;
9603
+ const simpleSentenceBreak = region.lastIndexOf(". ");
9604
+ if (simpleSentenceBreak > maxLen * 0.3) {
9605
+ return simpleSentenceBreak + 2;
9606
+ }
9607
+ const wordBreak = region.lastIndexOf(" ");
9608
+ if (wordBreak > maxLen * 0.5) {
9609
+ return wordBreak + 1;
9610
+ }
9611
+ return maxLen;
9612
+ }
9613
+
9614
+ // draft-stream.ts
9615
+ var DEFAULT_THROTTLE_MS = 1200;
9616
+ var DEFAULT_MIN_INITIAL_CHARS = 40;
9617
+ var DISCORD_MAX_CHARS = 2000;
9618
+ function createDraftStreamController(options = {}) {
9619
+ const throttleMs = Math.max(250, options.throttleMs ?? DEFAULT_THROTTLE_MS);
9620
+ const minInitialChars = options.minInitialChars ?? DEFAULT_MIN_INITIAL_CHARS;
9621
+ const maxChars = Math.min(options.maxChars ?? 1900, DISCORD_MAX_CHARS);
9622
+ const log = options.log ?? (() => {});
9623
+ const warn = options.warn ?? (() => {});
9624
+ let channel = null;
9625
+ let draftMessage = null;
9626
+ let lastSentText = "";
9627
+ let pendingText = null;
9628
+ let throttleTimer = null;
9629
+ let started = false;
9630
+ let done = false;
9631
+ const clearThrottle = () => {
9632
+ if (throttleTimer) {
9633
+ clearTimeout(throttleTimer);
9634
+ throttleTimer = null;
8649
9635
  }
8650
- if (this.discordSettings.shouldIgnoreBotMessages && message.author && message.author.bot) {
8651
- return;
9636
+ };
9637
+ const sendOrEdit = async (text) => {
9638
+ if (done || !channel) {
9639
+ return false;
8652
9640
  }
8653
- if (this.discordSettings.shouldIgnoreDirectMessages && message.channel.type === DiscordChannelType3.DM) {
8654
- return;
9641
+ const trimmed = text.trimEnd();
9642
+ if (!trimmed) {
9643
+ return false;
8655
9644
  }
8656
- if (message.channel.type === DiscordChannelType3.DM) {
8657
- const accessCheck = await this.checkDmAccess(message);
9645
+ const displayText = trimmed.length > maxChars ? `${trimmed.slice(0, maxChars - 3)}...` : trimmed;
9646
+ if (displayText === lastSentText) {
9647
+ return true;
9648
+ }
9649
+ try {
9650
+ if (draftMessage) {
9651
+ await draftMessage.edit({ content: displayText });
9652
+ } else {
9653
+ warn("draft-stream: sendOrEdit called before start, sending new message");
9654
+ draftMessage = await channel.send({ content: displayText });
9655
+ }
9656
+ lastSentText = displayText;
9657
+ return true;
9658
+ } catch (error) {
9659
+ const errorMessage = error instanceof Error ? error.message : String(error);
9660
+ if (errorMessage.includes("Unknown Message") || errorMessage.includes("10008")) {
9661
+ warn("draft-stream: message was deleted externally, stopping");
9662
+ done = true;
9663
+ return false;
9664
+ }
9665
+ warn(`draft-stream: edit failed: ${errorMessage}`);
9666
+ return false;
9667
+ }
9668
+ };
9669
+ const flush = async () => {
9670
+ clearThrottle();
9671
+ if (pendingText !== null) {
9672
+ const text = pendingText;
9673
+ pendingText = null;
9674
+ await sendOrEdit(text);
9675
+ }
9676
+ };
9677
+ const scheduleUpdate = (text) => {
9678
+ pendingText = text;
9679
+ if (!throttleTimer) {
9680
+ throttleTimer = setTimeout(async () => {
9681
+ throttleTimer = null;
9682
+ await flush();
9683
+ }, throttleMs);
9684
+ }
9685
+ };
9686
+ const start = async (nextChannel, replyToMessageId) => {
9687
+ if (started) {
9688
+ warn("draft-stream: start() called twice, ignoring");
9689
+ return draftMessage;
9690
+ }
9691
+ started = true;
9692
+ channel = nextChannel;
9693
+ try {
9694
+ const sendOptions = { content: "..." };
9695
+ if (replyToMessageId) {
9696
+ sendOptions.reply = { messageReference: replyToMessageId };
9697
+ }
9698
+ draftMessage = await nextChannel.send(sendOptions);
9699
+ lastSentText = "...";
9700
+ log(`draft-stream: started (messageId=${draftMessage.id}, throttle=${throttleMs}ms)`);
9701
+ return draftMessage;
9702
+ } catch (error) {
9703
+ const errorMessage = error instanceof Error ? error.message : String(error);
9704
+ warn(`draft-stream: failed to send initial message: ${errorMessage}`);
9705
+ done = true;
9706
+ return null;
9707
+ }
9708
+ };
9709
+ const update = (text) => {
9710
+ if (done || !started) {
9711
+ return;
9712
+ }
9713
+ if (draftMessage && lastSentText === "..." && text.length < minInitialChars) {
9714
+ return;
9715
+ }
9716
+ scheduleUpdate(text);
9717
+ };
9718
+ const finalize = async (text) => {
9719
+ if (done) {
9720
+ return draftMessage ? [draftMessage] : [];
9721
+ }
9722
+ clearThrottle();
9723
+ pendingText = null;
9724
+ if (!started || !draftMessage) {
9725
+ warn("draft-stream: finalize called before start");
9726
+ done = true;
9727
+ return [];
9728
+ }
9729
+ const trimmed = text.trimEnd();
9730
+ if (!trimmed) {
9731
+ try {
9732
+ await draftMessage.delete();
9733
+ } catch {}
9734
+ done = true;
9735
+ return [];
9736
+ }
9737
+ if (trimmed.length <= maxChars) {
9738
+ await sendOrEdit(trimmed);
9739
+ done = true;
9740
+ log("draft-stream: finalized (single message)");
9741
+ return [draftMessage];
9742
+ }
9743
+ const chunkConfig = {
9744
+ ...DEFAULT_DRAFT_CHUNK_CONFIG,
9745
+ ...options.chunkConfig
9746
+ };
9747
+ const breakPoint = findBreakPoint(trimmed, maxChars, chunkConfig.breakPreference);
9748
+ const firstChunk = trimmed.slice(0, breakPoint).trimEnd();
9749
+ let remaining = trimmed.slice(breakPoint).trimStart();
9750
+ await sendOrEdit(firstChunk);
9751
+ const allMessages = [draftMessage];
9752
+ while (remaining.length > 0 && channel) {
9753
+ const nextBreak = findBreakPoint(remaining, maxChars, chunkConfig.breakPreference);
9754
+ const chunk = remaining.slice(0, nextBreak).trimEnd();
9755
+ remaining = remaining.slice(nextBreak).trimStart();
9756
+ if (!chunk) {
9757
+ continue;
9758
+ }
9759
+ try {
9760
+ const overflowMessage = await channel.send({ content: chunk });
9761
+ allMessages.push(overflowMessage);
9762
+ } catch (error) {
9763
+ warn(`draft-stream: overflow send failed: ${error instanceof Error ? error.message : String(error)}`);
9764
+ break;
9765
+ }
9766
+ }
9767
+ done = true;
9768
+ log("draft-stream: finalized (multi-message)");
9769
+ return allMessages;
9770
+ };
9771
+ const abort = async (reason) => {
9772
+ if (done) {
9773
+ return;
9774
+ }
9775
+ done = true;
9776
+ clearThrottle();
9777
+ pendingText = null;
9778
+ if (!draftMessage) {
9779
+ return;
9780
+ }
9781
+ const errorText = reason ? `⚠️ ${reason}` : "⚠️ Response generation was interrupted.";
9782
+ try {
9783
+ await draftMessage.edit({ content: errorText });
9784
+ } catch {
9785
+ try {
9786
+ await draftMessage.delete();
9787
+ } catch {}
9788
+ }
9789
+ log("draft-stream: aborted");
9790
+ };
9791
+ return {
9792
+ start,
9793
+ update,
9794
+ finalize,
9795
+ abort,
9796
+ messageId: () => draftMessage?.id,
9797
+ isStarted: () => started,
9798
+ isDone: () => done
9799
+ };
9800
+ }
9801
+
9802
+ // inbound-envelope.ts
9803
+ import {
9804
+ ChannelType as DiscordChannelType3
9805
+ } from "discord.js";
9806
+ var WEEKDAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
9807
+ function formatTimestamp(timestamp) {
9808
+ const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
9809
+ const weekday = WEEKDAYS[date.getDay()];
9810
+ const month = String(date.getMonth() + 1).padStart(2, "0");
9811
+ const day = String(date.getDate()).padStart(2, "0");
9812
+ const year = date.getFullYear();
9813
+ const hours = String(date.getHours()).padStart(2, "0");
9814
+ const minutes = String(date.getMinutes()).padStart(2, "0");
9815
+ let timezone = "UTC";
9816
+ try {
9817
+ timezone = date.toLocaleTimeString("en-US", { timeZoneName: "short" }).split(" ").pop() ?? "UTC";
9818
+ } catch {}
9819
+ return `${weekday} ${month}/${day}/${year} ${hours}:${minutes} ${timezone}`;
9820
+ }
9821
+ function detectChatType(message) {
9822
+ const channelType = message.channel.type;
9823
+ if (channelType === DiscordChannelType3.DM || channelType === DiscordChannelType3.GroupDM) {
9824
+ return "dm";
9825
+ }
9826
+ if (channelType === DiscordChannelType3.PublicThread || channelType === DiscordChannelType3.PrivateThread || channelType === DiscordChannelType3.AnnouncementThread) {
9827
+ const thread = message.channel;
9828
+ if (thread.parent?.type === DiscordChannelType3.GuildForum) {
9829
+ return "forum";
9830
+ }
9831
+ return "thread";
9832
+ }
9833
+ return "channel";
9834
+ }
9835
+ function getSenderName(message) {
9836
+ if (message.member?.nickname) {
9837
+ return message.member.nickname;
9838
+ }
9839
+ if (message.author.globalName) {
9840
+ return message.author.globalName;
9841
+ }
9842
+ return message.author.displayName ?? message.author.username;
9843
+ }
9844
+ function buildChannelLabel(message, chatType) {
9845
+ if (chatType === "dm") {
9846
+ return "DM";
9847
+ }
9848
+ const guildName = message.guild?.name;
9849
+ let channelPart;
9850
+ if (chatType === "thread" || chatType === "forum") {
9851
+ const thread = message.channel;
9852
+ channelPart = `#${thread.parent?.name ?? "unknown"} › ${thread.name ?? "thread"}`;
9853
+ } else {
9854
+ const channel = message.channel;
9855
+ channelPart = `#${channel.name ?? message.channel.id}`;
9856
+ }
9857
+ return guildName ? `${channelPart} | ${guildName}` : channelPart;
9858
+ }
9859
+ async function formatInboundEnvelope(message, rawContent) {
9860
+ const chatType = detectChatType(message);
9861
+ const channelLabel = buildChannelLabel(message, chatType);
9862
+ const senderName = getSenderName(message);
9863
+ const timestamp = formatTimestamp(message.createdTimestamp ?? Date.now());
9864
+ let replyContext = "";
9865
+ if (message.reference?.messageId) {
9866
+ try {
9867
+ const refMessage = await message.fetchReference();
9868
+ const refAuthor = refMessage.author?.displayName ?? refMessage.author?.username ?? "unknown";
9869
+ const refContent = refMessage.content ?? "";
9870
+ const truncated = refContent.length > 200 ? `${refContent.slice(0, 200)}...` : refContent;
9871
+ replyContext = truncated ? ` replying to @${refAuthor}:
9872
+ > ${truncated}
9873
+ ` : ` replying to @${refAuthor}:
9874
+ `;
9875
+ } catch {}
9876
+ }
9877
+ const header = `[Discord ${channelLabel}] @${senderName} (${timestamp})`;
9878
+ return {
9879
+ formattedContent: replyContext ? `${header}${replyContext}${rawContent}` : `${header}: ${rawContent}`,
9880
+ chatType
9881
+ };
9882
+ }
9883
+
9884
+ // reasoning-tags.ts
9885
+ var REASONING_TAGS = [
9886
+ "thinking",
9887
+ "reasoning",
9888
+ "reflection",
9889
+ "scratchpad",
9890
+ "thought",
9891
+ "antthinking"
9892
+ ];
9893
+ var SELF_CLOSING_ARTIFACTS_RE = /<(?:STOP|END|end_turn|eot_id)\s*\/?>|<\|(?:end|stop|im_end|eot_id)\|>/gi;
9894
+ var QUICK_TAG_RE = /<\/?(?:thinking|reasoning|reflection|scratchpad|thought|antthinking|final|STOP|END|end_turn)\b|<\|(?:end|stop|im_end)/i;
9895
+ var CODE_BLOCK_RE = /```[\s\S]*?```/g;
9896
+ var PLACEHOLDER_PREFIX = "\x00CB";
9897
+ function stripReasoningTags(text) {
9898
+ if (!text || !QUICK_TAG_RE.test(text)) {
9899
+ return text;
9900
+ }
9901
+ const codeBlocks = [];
9902
+ let processed = text.replace(CODE_BLOCK_RE, (match) => {
9903
+ const index = codeBlocks.length;
9904
+ codeBlocks.push(match);
9905
+ return `${PLACEHOLDER_PREFIX}${index}${PLACEHOLDER_PREFIX}`;
9906
+ });
9907
+ processed = processed.replace(SELF_CLOSING_ARTIFACTS_RE, "");
9908
+ for (const tag of REASONING_TAGS) {
9909
+ const paired = new RegExp(`<${tag}\\b[^>]*>([\\s\\S]*?)<\\/${tag}>`, "gi");
9910
+ processed = processed.replace(paired, "");
9911
+ const unclosed = new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*$`, "gi");
9912
+ processed = processed.replace(unclosed, "");
9913
+ }
9914
+ processed = processed.replace(/<final\b[^>]*>([\s\S]*?)<\/final>/gi, "$1");
9915
+ for (let index = 0;index < codeBlocks.length; index++) {
9916
+ processed = processed.replace(`${PLACEHOLDER_PREFIX}${index}${PLACEHOLDER_PREFIX}`, codeBlocks[index]);
9917
+ }
9918
+ return processed.replace(/\n{3,}/g, `
9919
+
9920
+ `).trim();
9921
+ }
9922
+
9923
+ // status-reactions.ts
9924
+ var EMOJI_QUEUED = "⏳";
9925
+ var EMOJI_THINKING = "\uD83E\uDD14";
9926
+ var EMOJI_DONE = "✅";
9927
+ var EMOJI_ERROR = "❌";
9928
+ function shouldShowStatusReaction(scope, message, botId) {
9929
+ if (scope === "none") {
9930
+ return false;
9931
+ }
9932
+ if (scope === "all") {
9933
+ return true;
9934
+ }
9935
+ if (!message.guild) {
9936
+ return true;
9937
+ }
9938
+ const isMentioned = Boolean(botId && message.mentions.users?.has(botId));
9939
+ const isReplyToBot = message.mentions.repliedUser?.id === botId;
9940
+ return isMentioned || isReplyToBot;
9941
+ }
9942
+ function createStatusReactionController(message) {
9943
+ let currentEmoji = null;
9944
+ let finished = false;
9945
+ let chain = Promise.resolve();
9946
+ const botId = message.client?.user?.id;
9947
+ const transition = (emoji, terminal = false) => {
9948
+ if (finished) {
9949
+ return;
9950
+ }
9951
+ chain = chain.then(async () => {
9952
+ if (finished && !terminal) {
9953
+ return;
9954
+ }
9955
+ try {
9956
+ if (currentEmoji && currentEmoji !== emoji && botId) {
9957
+ try {
9958
+ const reaction = message.reactions.resolve(currentEmoji);
9959
+ if (reaction) {
9960
+ await reaction.users.remove(botId);
9961
+ }
9962
+ } catch {}
9963
+ }
9964
+ await message.react(emoji);
9965
+ currentEmoji = emoji;
9966
+ } catch {} finally {
9967
+ if (terminal) {
9968
+ finished = true;
9969
+ }
9970
+ }
9971
+ });
9972
+ };
9973
+ return {
9974
+ setQueued: () => transition(EMOJI_QUEUED),
9975
+ setThinking: () => transition(EMOJI_THINKING),
9976
+ setDone: () => transition(EMOJI_DONE, true),
9977
+ setError: () => transition(EMOJI_ERROR, true)
9978
+ };
9979
+ }
9980
+
9981
+ // typing.ts
9982
+ var HEARTBEAT_MS = 9000;
9983
+ var DEFAULT_MAX_DURATION_MS = 20 * 60 * 1000;
9984
+ function createTypingController(channel, maxDurationMs = DEFAULT_MAX_DURATION_MS) {
9985
+ let interval = null;
9986
+ let ttlTimeout = null;
9987
+ let started = false;
9988
+ let stopped = false;
9989
+ const sendTyping = () => {
9990
+ if (stopped || typeof channel.sendTyping !== "function") {
9991
+ return;
9992
+ }
9993
+ try {
9994
+ const result = channel.sendTyping();
9995
+ if (result && typeof result.catch === "function") {
9996
+ result.catch(() => {});
9997
+ }
9998
+ } catch {}
9999
+ };
10000
+ const stop = () => {
10001
+ if (stopped) {
10002
+ return;
10003
+ }
10004
+ stopped = true;
10005
+ if (interval) {
10006
+ clearInterval(interval);
10007
+ interval = null;
10008
+ }
10009
+ if (ttlTimeout) {
10010
+ clearTimeout(ttlTimeout);
10011
+ ttlTimeout = null;
10012
+ }
10013
+ };
10014
+ const start = () => {
10015
+ if (started || stopped) {
10016
+ return;
10017
+ }
10018
+ started = true;
10019
+ sendTyping();
10020
+ interval = setInterval(sendTyping, HEARTBEAT_MS);
10021
+ ttlTimeout = setTimeout(stop, maxDurationMs);
10022
+ };
10023
+ return { start, stop };
10024
+ }
10025
+
10026
+ // messages.ts
10027
+ function escapeRegex3(value) {
10028
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10029
+ }
10030
+ function textMentionsAnyName(text, names) {
10031
+ if (!text) {
10032
+ return false;
10033
+ }
10034
+ return names.some((name) => {
10035
+ const candidate = name?.trim();
10036
+ if (!candidate) {
10037
+ return false;
10038
+ }
10039
+ const pattern = new RegExp(`(^|[^\\p{L}\\p{N}])${escapeRegex3(candidate)}(?=$|[^\\p{L}\\p{N}])`, "iu");
10040
+ return pattern.test(text);
10041
+ });
10042
+ }
10043
+
10044
+ class MessageManager {
10045
+ client;
10046
+ runtime;
10047
+ attachmentManager;
10048
+ getChannelType;
10049
+ discordSettings;
10050
+ discordService;
10051
+ statusReactionScope;
10052
+ envelopeEnabled;
10053
+ draftStreamingEnabled;
10054
+ recentlyProcessedMessageIds = new Map;
10055
+ static PROCESSED_MESSAGE_TTL_MS = 2 * 60 * 1000;
10056
+ constructor(discordService, runtime) {
10057
+ if (!discordService.client) {
10058
+ const errorMsg = "Discord client not initialized - cannot create MessageManager";
10059
+ runtime.logger.error({ src: "plugin:discord", agentId: runtime.agentId }, errorMsg);
10060
+ throw new Error(errorMsg);
10061
+ }
10062
+ this.client = discordService.client;
10063
+ this.runtime = runtime;
10064
+ this.attachmentManager = new AttachmentManager(this.runtime);
10065
+ this.getChannelType = discordService.getChannelType;
10066
+ this.discordService = discordService;
10067
+ this.discordSettings = getDiscordSettings(this.runtime);
10068
+ const reactionScopeSetting = this.runtime.getSetting("DISCORD_STATUS_REACTIONS");
10069
+ this.statusReactionScope = ["all", "group-mentions", "none"].includes(reactionScopeSetting ?? "") ? reactionScopeSetting : "group-mentions";
10070
+ const envelopeSetting = this.runtime.getSetting("DISCORD_ENVELOPE_ENABLED");
10071
+ this.envelopeEnabled = envelopeSetting !== "false" && envelopeSetting !== "0";
10072
+ const draftStreamSetting = this.runtime.getSetting("DISCORD_DRAFT_STREAMING");
10073
+ this.draftStreamingEnabled = draftStreamSetting === "true" || draftStreamSetting === "1";
10074
+ }
10075
+ async checkDmAccess(message) {
10076
+ const policy = this.discordSettings.dmPolicy ?? "pairing";
10077
+ const userId = message.author.id;
10078
+ if (policy === "disabled") {
10079
+ this.runtime.logger.debug({
10080
+ src: "plugin:discord",
10081
+ agentId: this.runtime.agentId,
10082
+ userId
10083
+ }, "DM blocked: policy is disabled");
10084
+ return { allowed: false };
10085
+ }
10086
+ if (policy === "open") {
10087
+ return { allowed: true };
10088
+ }
10089
+ if (policy === "allowlist") {
10090
+ if (this.discordSettings.allowFrom?.includes(userId)) {
10091
+ return { allowed: true };
10092
+ }
10093
+ const inDynamicAllowlist = await isInAllowlist(this.runtime, "discord", userId);
10094
+ if (inDynamicAllowlist) {
10095
+ return { allowed: true };
10096
+ }
10097
+ this.runtime.logger.debug({
10098
+ src: "plugin:discord",
10099
+ agentId: this.runtime.agentId,
10100
+ userId
10101
+ }, "DM blocked: user not in allowlist");
10102
+ return { allowed: false };
10103
+ }
10104
+ if (policy === "pairing") {
10105
+ if (this.discordSettings.allowFrom?.includes(userId)) {
10106
+ return { allowed: true };
10107
+ }
10108
+ const result = await checkPairingAllowed(this.runtime, {
10109
+ channel: "discord",
10110
+ senderId: userId,
10111
+ metadata: {
10112
+ username: message.author.username,
10113
+ displayName: message.author.displayName ?? message.author.username,
10114
+ discriminator: message.author.discriminator ?? ""
10115
+ }
10116
+ });
10117
+ if (result.allowed) {
10118
+ return { allowed: true };
10119
+ }
10120
+ this.runtime.logger.debug({
10121
+ src: "plugin:discord",
10122
+ agentId: this.runtime.agentId,
10123
+ userId,
10124
+ pairingCode: result.pairingCode,
10125
+ newRequest: result.newRequest
10126
+ }, "DM blocked: pairing required");
10127
+ return {
10128
+ allowed: false,
10129
+ replyMessage: result.newRequest ? result.replyMessage : undefined
10130
+ };
10131
+ }
10132
+ return { allowed: true };
10133
+ }
10134
+ async persistInboundMemory(memory) {
10135
+ if (!memory.id) {
10136
+ return;
10137
+ }
10138
+ const existing = await this.runtime.getMemoryById(memory.id);
10139
+ if (existing) {
10140
+ return;
10141
+ }
10142
+ await this.runtime.createMemory(memory, "messages");
10143
+ }
10144
+ markMessageAsProcessing(messageId) {
10145
+ const now = Date.now();
10146
+ for (const [candidateId, processedAt] of this.recentlyProcessedMessageIds) {
10147
+ if (now - processedAt > MessageManager.PROCESSED_MESSAGE_TTL_MS) {
10148
+ this.recentlyProcessedMessageIds.delete(candidateId);
10149
+ }
10150
+ }
10151
+ if (this.recentlyProcessedMessageIds.has(messageId)) {
10152
+ return false;
10153
+ }
10154
+ this.recentlyProcessedMessageIds.set(messageId, now);
10155
+ return true;
10156
+ }
10157
+ async handleMessage(message) {
10158
+ const clientUser = this.client.user;
10159
+ if (message.interaction || clientUser && message.author.id === clientUser.id) {
10160
+ return;
10161
+ }
10162
+ if (this.discordSettings.shouldIgnoreBotMessages && message.author && message.author.bot) {
10163
+ return;
10164
+ }
10165
+ if (message.id && !this.markMessageAsProcessing(message.id)) {
10166
+ this.runtime.logger.debug({
10167
+ src: "plugin:discord",
10168
+ agentId: this.runtime.agentId,
10169
+ messageId: message.id
10170
+ }, "Skipping duplicate Discord message");
10171
+ return;
10172
+ }
10173
+ if (this.discordSettings.shouldIgnoreDirectMessages && message.channel.type === DiscordChannelType4.DM) {
10174
+ return;
10175
+ }
10176
+ if (message.channel.type === DiscordChannelType4.DM) {
10177
+ const accessCheck = await this.checkDmAccess(message);
8658
10178
  if (!accessCheck.allowed) {
8659
10179
  if (accessCheck.replyMessage) {
8660
10180
  try {
@@ -8676,36 +10196,14 @@ class MessageManager {
8676
10196
  const mentionedOtherUsers = message.mentions.users ? Array.from(message.mentions.users.values()).some((user) => user.id !== clientUser?.id && user.id !== message.author.id) : false;
8677
10197
  const isReplyToOtherUser = !!message.reference?.messageId && !!message.mentions.repliedUser?.id && message.mentions.repliedUser.id !== clientUser?.id && message.mentions.repliedUser.id !== message.author.id;
8678
10198
  const isInThread = message.channel.isThread();
8679
- const isDM = message.channel.type === DiscordChannelType3.DM;
8680
- if (!isDM && (mentionedOtherUsers || isReplyToOtherUser)) {
8681
- this.runtime.logger.debug({
8682
- src: "plugin:discord",
8683
- agentId: this.runtime.agentId,
8684
- channelId: message.channel.id
8685
- }, "Ignoring message that targets another mentioned user");
8686
- return;
8687
- }
8688
- if (this.discordSettings.shouldRespondOnlyToMentions) {
8689
- const shouldProcess = isDM || isBotMentioned || isReplyToBot;
8690
- if (!shouldProcess) {
8691
- this.runtime.logger.debug({
8692
- src: "plugin:discord",
8693
- agentId: this.runtime.agentId,
8694
- channelId: message.channel.id
8695
- }, "Strict mode: ignoring message (no mention or reply)");
8696
- return;
8697
- }
8698
- this.runtime.logger.debug({
8699
- src: "plugin:discord",
8700
- agentId: this.runtime.agentId,
8701
- channelId: message.channel.id
8702
- }, "Strict mode: processing message");
8703
- }
8704
- const entityId = createUniqueUuid4(this.runtime, message.author.id);
10199
+ const isDM = message.channel.type === DiscordChannelType4.DM;
10200
+ const strictModeEnabled = this.discordSettings.shouldRespondOnlyToMentions === true;
10201
+ const strictModeShouldProcess = isDM || isBotMentioned || isReplyToBot;
8705
10202
  const userName = message.author.bot ? `${message.author.username}#${message.author.discriminator}` : message.author.username;
8706
- const name = message.author.displayName;
10203
+ const name = message.member?.displayName ?? message.author.globalName ?? message.author.displayName ?? message.author.username;
8707
10204
  const channelId = message.channel.id;
8708
10205
  const roomId = createUniqueUuid4(this.runtime, channelId);
10206
+ const roomName = message.guild && "name" in message.channel && typeof message.channel.name === "string" ? message.channel.name : name || userName;
8709
10207
  let type;
8710
10208
  let messageServerId;
8711
10209
  if (message.guild) {
@@ -8720,43 +10218,27 @@ class MessageManager {
8720
10218
  }
8721
10219
  messageServerId = guild.id;
8722
10220
  } else {
8723
- type = ChannelType5.DM;
10221
+ type = ChannelType6.DM;
8724
10222
  messageServerId = message.channel.id;
8725
10223
  }
8726
- await this.runtime.ensureConnection({
8727
- entityId,
8728
- roomId,
8729
- userName,
8730
- name,
8731
- source: "discord",
8732
- channelId: message.channel.id,
8733
- messageServerId: messageServerId ? stringToUuid(messageServerId) : undefined,
8734
- type,
8735
- worldId: createUniqueUuid4(this.runtime, messageServerId ?? roomId),
8736
- worldName: message.guild?.name,
8737
- userId: message.author.id,
8738
- metadata: buildDiscordWorldMetadata(this.runtime, message.guild?.ownerId ?? undefined)
8739
- });
8740
10224
  try {
8741
- const canSendResult = canSendMessage(message.channel);
8742
- if (!canSendResult.canSend) {
8743
- return this.runtime.logger.warn({
8744
- src: "plugin:discord",
8745
- agentId: this.runtime.agentId,
8746
- channelId: message.channel.id,
8747
- reason: canSendResult.reason
8748
- }, "Cannot send message to channel");
10225
+ let { processedContent, attachments } = await this.processMessage(message);
10226
+ if (this.envelopeEnabled && processedContent) {
10227
+ try {
10228
+ const envelope = await formatInboundEnvelope(message, processedContent);
10229
+ processedContent = envelope.formattedContent;
10230
+ } catch {}
8749
10231
  }
8750
- const { processedContent, attachments } = await this.processMessage(message);
8751
10232
  if (!processedContent && !attachments?.length) {
8752
10233
  return;
8753
10234
  }
8754
- const channel = message.channel;
8755
- const typingData = {
8756
- interval: null,
8757
- cleared: false,
8758
- started: false
8759
- };
10235
+ const explicitlyAddressesBotByName = textMentionsAnyName(processedContent, [
10236
+ this.runtime.character.name,
10237
+ this.runtime.character.username,
10238
+ clientUser?.globalName,
10239
+ clientUser?.username
10240
+ ]);
10241
+ const ignoresOtherTarget = !isDM && !isBotMentioned && !isReplyToBot && !explicitlyAddressesBotByName && (mentionedOtherUsers || isReplyToOtherUser);
8760
10242
  const newMessage = await this.discordService.buildMemoryFromMessage(message, {
8761
10243
  processedContent,
8762
10244
  processedAttachments: attachments,
@@ -8771,9 +10253,15 @@ class MessageManager {
8771
10253
  extraMetadata: {
8772
10254
  replyToAuthor: message.mentions.repliedUser ? {
8773
10255
  id: message.mentions.repliedUser.id,
10256
+ displayName: message.mentions.repliedUser.globalName ?? message.mentions.repliedUser.username,
8774
10257
  username: message.mentions.repliedUser.username,
8775
10258
  isBot: message.mentions.repliedUser.bot
8776
- } : undefined
10259
+ } : undefined,
10260
+ replyToMessageId: message.reference?.messageId ? createUniqueUuid4(this.runtime, message.reference.messageId) : undefined,
10261
+ replyToExternalMessageId: message.reference?.messageId,
10262
+ replyToSenderId: message.mentions.repliedUser?.id,
10263
+ replyToSenderName: message.mentions.repliedUser?.globalName ?? message.mentions.repliedUser?.username,
10264
+ replyToSenderUserName: message.mentions.repliedUser?.username
8777
10265
  }
8778
10266
  });
8779
10267
  if (!newMessage) {
@@ -8784,37 +10272,154 @@ class MessageManager {
8784
10272
  }, "Failed to build memory from message");
8785
10273
  return;
8786
10274
  }
10275
+ await this.runtime.ensureConnection({
10276
+ entityId: newMessage.entityId,
10277
+ roomId,
10278
+ roomName,
10279
+ userName,
10280
+ name,
10281
+ source: "discord",
10282
+ channelId: message.channel.id,
10283
+ messageServerId: messageServerId ? stringToUuid2(messageServerId) : undefined,
10284
+ type,
10285
+ worldId: createUniqueUuid4(this.runtime, messageServerId ?? roomId),
10286
+ worldName: message.guild?.name,
10287
+ userId: message.author.id,
10288
+ metadata: buildDiscordWorldMetadata(this.runtime, message.guild?.ownerId ?? undefined)
10289
+ });
10290
+ if (ignoresOtherTarget) {
10291
+ await this.persistInboundMemory(newMessage);
10292
+ this.runtime.logger.debug({
10293
+ src: "plugin:discord",
10294
+ agentId: this.runtime.agentId,
10295
+ channelId: message.channel.id
10296
+ }, "Ignoring message that targets another mentioned user");
10297
+ return;
10298
+ }
10299
+ if (strictModeEnabled && !strictModeShouldProcess) {
10300
+ await this.persistInboundMemory(newMessage);
10301
+ this.runtime.logger.debug({
10302
+ src: "plugin:discord",
10303
+ agentId: this.runtime.agentId,
10304
+ channelId: message.channel.id
10305
+ }, "Strict mode: ignoring message (no mention or reply)");
10306
+ return;
10307
+ }
10308
+ if (strictModeEnabled) {
10309
+ this.runtime.logger.debug({
10310
+ src: "plugin:discord",
10311
+ agentId: this.runtime.agentId,
10312
+ channelId: message.channel.id
10313
+ }, "Strict mode: processing message");
10314
+ }
10315
+ const canSendResult = canSendMessage(message.channel);
10316
+ if (!canSendResult.canSend) {
10317
+ await this.persistInboundMemory(newMessage);
10318
+ return this.runtime.logger.warn({
10319
+ src: "plugin:discord",
10320
+ agentId: this.runtime.agentId,
10321
+ channelId: message.channel.id,
10322
+ reason: canSendResult.reason
10323
+ }, "Cannot send message to channel");
10324
+ }
8787
10325
  const messageId = newMessage.id;
10326
+ const channel = message.channel;
10327
+ const typingController = createTypingController(channel);
10328
+ const clientUserId = this.client.user?.id;
10329
+ const useReactions = shouldShowStatusReaction(this.statusReactionScope, message, clientUserId);
10330
+ const statusReactions = useReactions ? createStatusReactionController(message) : null;
10331
+ const draftStream = this.draftStreamingEnabled ? createDraftStreamController({
10332
+ log: (entry) => this.runtime.logger.debug({ src: "plugin:discord", agentId: this.runtime.agentId }, entry),
10333
+ warn: (entry) => this.runtime.logger.warn({ src: "plugin:discord", agentId: this.runtime.agentId }, entry)
10334
+ }) : null;
10335
+ let typingStarted = false;
10336
+ let responseEmitted = false;
10337
+ const finalizePendingDraft = async () => {
10338
+ if (draftStream?.isStarted() && !draftStream.isDone()) {
10339
+ await draftStream.finalize("");
10340
+ }
10341
+ };
10342
+ const abortPendingDraft = async () => {
10343
+ if (draftStream?.isStarted() && !draftStream.isDone()) {
10344
+ await draftStream.abort("An error occurred while generating the response.");
10345
+ }
10346
+ };
10347
+ if (draftStream) {
10348
+ await draftStream.start(channel, message.id);
10349
+ }
10350
+ statusReactions?.setQueued();
10351
+ statusReactions?.setThinking();
8788
10352
  const callback = async (content) => {
8789
10353
  try {
8790
10354
  if (content.target && typeof content.target === "string" && content.target.toLowerCase() !== "discord") {
8791
10355
  return [];
8792
10356
  }
8793
- if (!typingData.started) {
8794
- typingData.started = true;
8795
- const startTyping = () => {
10357
+ if (message.id && !content.inReplyTo) {
10358
+ content.inReplyTo = createUniqueUuid4(this.runtime, message.id);
10359
+ }
10360
+ if (typeof content.text === "string" && content.text.length > 0) {
10361
+ content.text = stripReasoningTags(content.text);
10362
+ }
10363
+ const textContent = normalizeDiscordMessageText(content.text);
10364
+ const hasText = textContent.trim().length > 0;
10365
+ const attachmentCount = Array.isArray(content.attachments) ? content.attachments.filter((media) => Boolean(media?.url)).length : 0;
10366
+ if (!hasText && attachmentCount === 0) {
10367
+ return [];
10368
+ }
10369
+ if (!typingStarted) {
10370
+ typingStarted = true;
10371
+ typingController.start();
10372
+ }
10373
+ if (hasText && content.inReplyTo) {
10374
+ const dedupKey = `${content.inReplyTo}::${textContent.replace(/\s+/g, " ").trim()}`;
10375
+ const callbackDedup = message;
10376
+ callbackDedup._miladySentReplyKeys ??= new Set;
10377
+ if (callbackDedup._miladySentReplyKeys.has(dedupKey)) {
10378
+ const err = new Error("Duplicate callback reply for same inbound message — runtime emitted identical text twice");
10379
+ this.runtime.logger.error({
10380
+ src: "plugin:discord",
10381
+ agentId: this.runtime.agentId,
10382
+ messageId: message.id,
10383
+ inReplyTo: content.inReplyTo,
10384
+ textPreview: textContent.replace(/\s+/g, " ").trim().slice(0, 200)
10385
+ }, err.message);
10386
+ throw err;
10387
+ }
10388
+ callbackDedup._miladySentReplyKeys.add(dedupKey);
10389
+ }
10390
+ const files = [];
10391
+ if (content.attachments && content.attachments.length > 0) {
10392
+ for (const media of content.attachments) {
10393
+ if (media.url) {
10394
+ const fileName = getAttachmentFileName(media);
10395
+ files.push(new AttachmentBuilder(media.url, { name: fileName }));
10396
+ }
10397
+ }
10398
+ }
10399
+ let messages = [];
10400
+ if (draftStream?.isStarted() && !draftStream.isDone()) {
10401
+ if (hasText || files.length === 0) {
10402
+ messages = await draftStream.finalize(textContent);
10403
+ } else {
10404
+ await finalizePendingDraft();
10405
+ }
10406
+ if (files.length > 0) {
8796
10407
  try {
8797
- if (channel.sendTyping) {
8798
- channel.sendTyping();
8799
- }
8800
- } catch (err) {
10408
+ const attachmentMessage = await channel.send({
10409
+ files
10410
+ });
10411
+ messages.push(attachmentMessage);
10412
+ } catch (error) {
8801
10413
  this.runtime.logger.warn({
8802
10414
  src: "plugin:discord",
8803
10415
  agentId: this.runtime.agentId,
8804
- error: err instanceof Error ? err.message : String(err)
8805
- }, "Error sending typing indicator");
10416
+ error: error instanceof Error ? error.message : String(error)
10417
+ }, "Failed to send Discord attachments after draft finalize");
8806
10418
  }
8807
- };
8808
- startTyping();
8809
- typingData.interval = setInterval(startTyping, 8000);
8810
- }
8811
- if (message.id && !content.inReplyTo) {
8812
- content.inReplyTo = createUniqueUuid4(this.runtime, message.id);
8813
- }
8814
- let messages = [];
8815
- if (content && content.channelType === "DM") {
8816
- const u = await this.client.users.fetch(message.author.id);
8817
- if (!u) {
10419
+ }
10420
+ } else if (content && content.channelType === "DM") {
10421
+ const user = await this.client.users.fetch(message.author.id);
10422
+ if (!user) {
8818
10423
  this.runtime.logger.warn({
8819
10424
  src: "plugin:discord",
8820
10425
  agentId: this.runtime.agentId,
@@ -8822,41 +10427,21 @@ class MessageManager {
8822
10427
  }, "User not found for DM");
8823
10428
  return [];
8824
10429
  }
8825
- const files = [];
8826
- if (content.attachments && content.attachments.length > 0) {
8827
- for (const media of content.attachments) {
8828
- if (media.url) {
8829
- const fileName = getAttachmentFileName(media);
8830
- files.push(new AttachmentBuilder(media.url, { name: fileName }));
8831
- }
8832
- }
8833
- }
8834
- const textContent = content.text ?? "";
8835
- const hasText = textContent.trim().length > 0;
8836
- if (!hasText && files.length === 0) {
8837
- this.runtime.logger.warn({ src: "plugin:discord", agentId: this.runtime.agentId }, "Skipping DM response: no text or attachments");
8838
- return [];
8839
- }
8840
- const dmMessage = await u.send({
10430
+ const dmMessage = await user.send({
8841
10431
  content: textContent,
8842
10432
  files: files.length > 0 ? files : undefined
8843
10433
  });
8844
10434
  messages = [dmMessage];
8845
10435
  } else {
8846
- const files = [];
8847
- if (content.attachments && content.attachments.length > 0) {
8848
- for (const media of content.attachments) {
8849
- if (media.url) {
8850
- const fileName = getAttachmentFileName(media);
8851
- files.push(new AttachmentBuilder(media.url, { name: fileName }));
8852
- }
8853
- }
8854
- }
8855
10436
  if (!message.id) {
8856
10437
  this.runtime.logger.warn({ src: "plugin:discord", agentId: this.runtime.agentId }, "Cannot send message: message.id is missing");
8857
10438
  return [];
8858
10439
  }
8859
- messages = await sendMessageInChunks(channel, content.text ?? "", message.id, files, undefined, this.runtime);
10440
+ messages = await sendMessageInChunks(channel, textContent, message.id, files, undefined, this.runtime);
10441
+ }
10442
+ const attemptedSend = hasText || attachmentCount > 0;
10443
+ if (attemptedSend && messages.length === 0) {
10444
+ throw new Error("Discord response callback completed without sending any messages");
8860
10445
  }
8861
10446
  const memories = [];
8862
10447
  for (const m of messages) {
@@ -8868,7 +10453,8 @@ class MessageManager {
8868
10453
  agentId: this.runtime.agentId,
8869
10454
  content: {
8870
10455
  ...content,
8871
- text: m.content || content.text || " ",
10456
+ source: "discord",
10457
+ text: m.content || textContent || " ",
8872
10458
  actions,
8873
10459
  inReplyTo: messageId,
8874
10460
  url: m.url,
@@ -8883,9 +10469,28 @@ class MessageManager {
8883
10469
  for (const m of memories) {
8884
10470
  await this.runtime.createMemory(m, "messages");
8885
10471
  }
8886
- if (typingData.interval && !typingData.cleared) {
8887
- clearInterval(typingData.interval);
8888
- typingData.cleared = true;
10472
+ responseEmitted = memories.length > 0;
10473
+ typingController.stop();
10474
+ statusReactions?.setDone();
10475
+ if (hasText) {
10476
+ const replyPreview = textContent.replace(/\s+/g, " ").slice(0, 200);
10477
+ const callbackState = message;
10478
+ callbackState._miladyReplyCount = (callbackState._miladyReplyCount ?? 0) + 1;
10479
+ if (callbackState._miladyReplyCount === 1) {
10480
+ callbackState._miladyFirstReplyPreview = replyPreview;
10481
+ } else {
10482
+ this.runtime.logger.warn({
10483
+ src: "plugin:discord",
10484
+ agentId: this.runtime.agentId,
10485
+ messageId: message.id,
10486
+ channelId: message.channel.id,
10487
+ replyCount: callbackState._miladyReplyCount,
10488
+ firstPreview: callbackState._miladyFirstReplyPreview,
10489
+ currentPreview: replyPreview,
10490
+ action: typeof content?.action === "string" ? String(content.action) : undefined,
10491
+ source: typeof content.source === "string" ? content.source : undefined
10492
+ }, "Multiple Discord replies emitted for one inbound message");
10493
+ }
8889
10494
  }
8890
10495
  return memories;
8891
10496
  } catch (error) {
@@ -8894,23 +10499,27 @@ class MessageManager {
8894
10499
  agentId: this.runtime.agentId,
8895
10500
  error: error instanceof Error ? error.message : String(error)
8896
10501
  }, "Error handling message callback");
8897
- if (typingData.interval && !typingData.cleared) {
8898
- clearInterval(typingData.interval);
8899
- typingData.cleared = true;
8900
- }
8901
- return [];
10502
+ typingController.stop();
10503
+ statusReactions?.setError();
10504
+ await abortPendingDraft();
10505
+ throw error;
8902
10506
  }
8903
10507
  };
8904
10508
  const messagingAPI = getMessagingAPI(this.runtime);
8905
10509
  const messageService = getMessageService(this.runtime);
8906
- if (messagingAPI) {
8907
- this.runtime.logger.debug({ src: "plugin:discord", agentId: this.runtime.agentId }, "Using messaging API");
10510
+ if (messageService) {
10511
+ this.runtime.logger.debug({ src: "plugin:discord", agentId: this.runtime.agentId }, "Using messageService API");
10512
+ await messageService.handleMessage(this.runtime, newMessage, callback);
10513
+ } else if (messagingAPI?.handleMessage) {
10514
+ this.runtime.logger.debug({ src: "plugin:discord", agentId: this.runtime.agentId }, "Using messaging API handleMessage");
10515
+ await messagingAPI.handleMessage(this.runtime.agentId, newMessage, {
10516
+ onResponse: callback
10517
+ });
10518
+ } else if (messagingAPI?.sendMessage) {
10519
+ this.runtime.logger.debug({ src: "plugin:discord", agentId: this.runtime.agentId }, "Using messaging API sendMessage");
8908
10520
  await messagingAPI.sendMessage(this.runtime.agentId, newMessage, {
8909
10521
  onResponse: callback
8910
10522
  });
8911
- } else if (messageService) {
8912
- this.runtime.logger.debug({ src: "plugin:discord", agentId: this.runtime.agentId }, "Using messageService API");
8913
- await messageService.handleMessage(this.runtime, newMessage, callback);
8914
10523
  } else {
8915
10524
  this.runtime.logger.debug({ src: "plugin:discord", agentId: this.runtime.agentId }, "Using event-based message handling");
8916
10525
  await this.runtime.emitEvent([EventType.MESSAGE_RECEIVED], {
@@ -8920,13 +10529,11 @@ class MessageManager {
8920
10529
  source: "discord"
8921
10530
  });
8922
10531
  }
8923
- setTimeout(() => {
8924
- if (typingData.started && typingData.interval && !typingData.cleared) {
8925
- clearInterval(typingData.interval);
8926
- typingData.cleared = true;
8927
- this.runtime.logger.warn({ src: "plugin:discord", agentId: this.runtime.agentId }, "Typing indicator failsafe timeout triggered");
8928
- }
8929
- }, 30000);
10532
+ if (!responseEmitted) {
10533
+ typingController.stop();
10534
+ statusReactions?.setDone();
10535
+ await finalizePendingDraft();
10536
+ }
8930
10537
  } catch (error) {
8931
10538
  this.runtime.logger.error({
8932
10539
  src: "plugin:discord",
@@ -8947,30 +10554,6 @@ Embed #${parseInt(i, 10) + 1}:
8947
10554
  processedContent += ` Title:${embed.title ?? "(none)"}
8948
10555
  `;
8949
10556
  processedContent += ` Description:${embed.description ?? "(none)"}
8950
- `;
8951
- }
8952
- }
8953
- if (message.reference) {
8954
- let messageId;
8955
- if (message.reference.messageId) {
8956
- messageId = createUniqueUuid4(this.runtime, message.reference.messageId);
8957
- } else {
8958
- try {
8959
- const refMsg = await message.fetchReference();
8960
- messageId = createUniqueUuid4(this.runtime, refMsg.id);
8961
- } catch {}
8962
- }
8963
- if (messageId) {
8964
- processedContent += `
8965
- Referencing MessageID ${messageId} (discord: ${message.reference.messageId})`;
8966
- if (message.reference.channelId !== message.channel.id) {
8967
- const roomId = createUniqueUuid4(this.runtime, message.reference.channelId);
8968
- processedContent += ` in channel ${roomId}`;
8969
- }
8970
- if (message.reference.guildId && message.guild && message.reference.guildId !== message.guild.id) {
8971
- processedContent += ` in guild ${message.reference.guildId}`;
8972
- }
8973
- processedContent += `
8974
10557
  `;
8975
10558
  }
8976
10559
  }
@@ -9028,7 +10611,7 @@ Referencing MessageID ${messageId} (discord: ${message.reference.messageId})`;
9028
10611
  } else {
9029
10612
  const browserService = this.runtime.getService(ServiceType4.BROWSER);
9030
10613
  if (!browserService) {
9031
- this.runtime.logger.warn({ src: "plugin:discord", agentId: this.runtime.agentId }, "Browser service not found");
10614
+ this.runtime.logger.debug({ src: "plugin:discord", agentId: this.runtime.agentId }, "Skipping URL enrichment because browser service is unavailable");
9032
10615
  continue;
9033
10616
  }
9034
10617
  try {
@@ -9121,74 +10704,723 @@ function getState(perm, allow, deny) {
9121
10704
  if (deny.includes(perm)) {
9122
10705
  return "DENY";
9123
10706
  }
9124
- return "NEUTRAL";
9125
- }
9126
- function overwriteToChanges(ow, isDelete = false) {
9127
- const changes = [];
9128
- for (const p of ow.allow.toArray()) {
9129
- changes.push({
9130
- permission: p,
9131
- oldState: isDelete ? "ALLOW" : "NEUTRAL",
9132
- newState: isDelete ? "NEUTRAL" : "ALLOW"
10707
+ return "NEUTRAL";
10708
+ }
10709
+ function overwriteToChanges(ow, isDelete = false) {
10710
+ const changes = [];
10711
+ for (const p of ow.allow.toArray()) {
10712
+ changes.push({
10713
+ permission: p,
10714
+ oldState: isDelete ? "ALLOW" : "NEUTRAL",
10715
+ newState: isDelete ? "NEUTRAL" : "ALLOW"
10716
+ });
10717
+ }
10718
+ for (const p of ow.deny.toArray()) {
10719
+ changes.push({
10720
+ permission: p,
10721
+ oldState: isDelete ? "DENY" : "NEUTRAL",
10722
+ newState: isDelete ? "NEUTRAL" : "DENY"
10723
+ });
10724
+ }
10725
+ return changes;
10726
+ }
10727
+ function diffOverwrites(oldOw, newOw) {
10728
+ if (!oldOw && !newOw) {
10729
+ return { changes: [], action: "UPDATE" };
10730
+ }
10731
+ if (!oldOw && newOw) {
10732
+ return { changes: overwriteToChanges(newOw, false), action: "CREATE" };
10733
+ }
10734
+ if (oldOw && !newOw) {
10735
+ return { changes: overwriteToChanges(oldOw, true), action: "DELETE" };
10736
+ }
10737
+ const changes = [];
10738
+ const oldAllow = oldOw?.allow.toArray();
10739
+ const oldDeny = oldOw?.deny.toArray();
10740
+ const newAllow = newOw?.allow.toArray();
10741
+ const newDeny = newOw?.deny.toArray();
10742
+ const allPerms = new Set([...oldAllow, ...oldDeny, ...newAllow, ...newDeny]);
10743
+ for (const perm of allPerms) {
10744
+ const oldState = getState(perm, oldAllow, oldDeny);
10745
+ const newState = getState(perm, newAllow, newDeny);
10746
+ if (oldState !== newState) {
10747
+ changes.push({ permission: perm, oldState, newState });
10748
+ }
10749
+ }
10750
+ return { changes, action: "UPDATE" };
10751
+ }
10752
+ function diffRolePermissions(oldRole, newRole) {
10753
+ const oldPerms = oldRole.permissions.toArray();
10754
+ const newPerms = newRole.permissions.toArray();
10755
+ const changes = [];
10756
+ for (const p of newPerms) {
10757
+ if (!oldPerms.includes(p)) {
10758
+ changes.push({ permission: p, oldState: "NEUTRAL", newState: "ALLOW" });
10759
+ }
10760
+ }
10761
+ for (const p of oldPerms) {
10762
+ if (!newPerms.includes(p)) {
10763
+ changes.push({ permission: p, oldState: "ALLOW", newState: "NEUTRAL" });
10764
+ }
10765
+ }
10766
+ return changes;
10767
+ }
10768
+ function diffMemberRoles(oldMember, newMember) {
10769
+ const oldRoles = oldMember.roles.cache;
10770
+ const newRoles = newMember.roles.cache;
10771
+ return {
10772
+ added: [...newRoles.filter((r) => !oldRoles.has(r.id)).values()],
10773
+ removed: [...oldRoles.filter((r) => !newRoles.has(r.id)).values()]
10774
+ };
10775
+ }
10776
+
10777
+ // profileSync.ts
10778
+ import { createHash } from "node:crypto";
10779
+ import fs5 from "node:fs/promises";
10780
+ import os3 from "node:os";
10781
+ import path3 from "node:path";
10782
+ var MAX_PROFILE_AVATAR_BYTES = 8 * 1024 * 1024;
10783
+ var PROFILE_SYNC_STATE_FILE = "discord-profile-sync.v1.json";
10784
+ var DEFAULT_DISCORD_PROFILE_AVATAR = "/avatars/eliza.png";
10785
+ function resolveUserPath(input) {
10786
+ const trimmed = input.trim();
10787
+ if (!trimmed) {
10788
+ return trimmed;
10789
+ }
10790
+ if (trimmed.startsWith("~")) {
10791
+ return path3.resolve(trimmed.replace(/^~(?=$|[\\/])/, os3.homedir()));
10792
+ }
10793
+ return path3.resolve(trimmed);
10794
+ }
10795
+ function resolveStateDirFromEnv(env = process.env) {
10796
+ const override = env.MILADY_STATE_DIR?.trim() || env.ELIZA_STATE_DIR?.trim();
10797
+ if (override) {
10798
+ return resolveUserPath(override);
10799
+ }
10800
+ const namespace = env.MILADY_NAMESPACE?.trim() || env.ELIZA_NAMESPACE?.trim() || "milady";
10801
+ return path3.join(os3.homedir(), `.${namespace}`);
10802
+ }
10803
+ function resolveProfileSyncStatePath(env = process.env) {
10804
+ return path3.join(resolveStateDirFromEnv(env), "cache", PROFILE_SYNC_STATE_FILE);
10805
+ }
10806
+ async function readPersistedProfileSyncState(env = process.env) {
10807
+ try {
10808
+ const raw = await fs5.readFile(resolveProfileSyncStatePath(env), "utf8");
10809
+ const parsed = JSON.parse(raw);
10810
+ return {
10811
+ ...typeof parsed.avatarHash === "string" ? { avatarHash: parsed.avatarHash } : {},
10812
+ ...typeof parsed.username === "string" ? { username: parsed.username } : {}
10813
+ };
10814
+ } catch {
10815
+ return {};
10816
+ }
10817
+ }
10818
+ async function writePersistedProfileSyncState(state, env = process.env) {
10819
+ const statePath = resolveProfileSyncStatePath(env);
10820
+ await fs5.mkdir(path3.dirname(statePath), { recursive: true });
10821
+ await fs5.writeFile(statePath, JSON.stringify(state, null, 2), {
10822
+ encoding: "utf8",
10823
+ mode: 384
10824
+ });
10825
+ }
10826
+ function normalizeDesiredDiscordName(runtime, settings) {
10827
+ const configured = settings.profileName?.trim();
10828
+ if (configured) {
10829
+ return configured;
10830
+ }
10831
+ const characterName = runtime.character.name?.trim();
10832
+ if (characterName) {
10833
+ return characterName;
10834
+ }
10835
+ const characterUserName = runtime.character.username?.trim();
10836
+ return characterUserName || undefined;
10837
+ }
10838
+ function readNestedOptionalString(value, pathSegments) {
10839
+ let cursor = value;
10840
+ for (const segment of pathSegments) {
10841
+ if (!cursor || typeof cursor !== "object") {
10842
+ return;
10843
+ }
10844
+ cursor = cursor[segment];
10845
+ }
10846
+ return typeof cursor === "string" && cursor.trim().length > 0 ? cursor.trim() : undefined;
10847
+ }
10848
+ function normalizeDesiredDiscordAvatarSource(runtime, settings) {
10849
+ const configured = settings.profileAvatar?.trim();
10850
+ if (configured) {
10851
+ return configured;
10852
+ }
10853
+ const character = runtime.character;
10854
+ const fromIdentity = readNestedOptionalString(character, ["identity", "avatar"]) ?? readNestedOptionalString(character, ["settings", "identity", "avatar"]);
10855
+ if (fromIdentity) {
10856
+ return fromIdentity;
10857
+ }
10858
+ const fromCharacter = readNestedOptionalString(character, ["avatar"]) ?? readNestedOptionalString(character, ["settings", "avatar"]);
10859
+ if (fromCharacter) {
10860
+ return fromCharacter;
10861
+ }
10862
+ return DEFAULT_DISCORD_PROFILE_AVATAR;
10863
+ }
10864
+ function extractDataUriPayload(source) {
10865
+ const match = source.match(/^data:image\/[^;]+;base64,([a-z0-9+/=]+)$/i);
10866
+ if (!match) {
10867
+ return null;
10868
+ }
10869
+ return Buffer.from(match[1], "base64");
10870
+ }
10871
+ function buildLocalAvatarPathCandidates(source) {
10872
+ const candidates = new Set;
10873
+ const trimmed = source.trim();
10874
+ if (!trimmed) {
10875
+ return [];
10876
+ }
10877
+ candidates.add(resolveUserPath(trimmed));
10878
+ const normalized = trimmed.replace(/\\/g, "/");
10879
+ const withoutLeadingSlash = normalized.replace(/^\/+/, "");
10880
+ if (!withoutLeadingSlash) {
10881
+ return [...candidates];
10882
+ }
10883
+ const repoRoot = process.cwd();
10884
+ const publicRoots = [
10885
+ path3.join(repoRoot, "cloud", "public"),
10886
+ path3.join(repoRoot, "apps", "web", "public"),
10887
+ path3.join(repoRoot, "public")
10888
+ ];
10889
+ for (const publicRoot of publicRoots) {
10890
+ candidates.add(path3.join(publicRoot, withoutLeadingSlash));
10891
+ if (!withoutLeadingSlash.startsWith("avatars/")) {
10892
+ candidates.add(path3.join(publicRoot, "avatars", withoutLeadingSlash));
10893
+ }
10894
+ }
10895
+ return [...candidates];
10896
+ }
10897
+ async function readAvatarBytesFromLocalCandidates(source) {
10898
+ let lastError = null;
10899
+ for (const candidate of buildLocalAvatarPathCandidates(source)) {
10900
+ try {
10901
+ return await fs5.readFile(candidate);
10902
+ } catch (error) {
10903
+ lastError = error;
10904
+ }
10905
+ }
10906
+ if (lastError instanceof Error) {
10907
+ throw lastError;
10908
+ }
10909
+ throw new Error(`Unable to resolve Discord profile avatar source: ${source}`);
10910
+ }
10911
+ async function loadDiscordProfileAvatarBytes(source, runtime) {
10912
+ const trimmed = source.trim();
10913
+ if (!trimmed) {
10914
+ return null;
10915
+ }
10916
+ let bytes = extractDataUriPayload(trimmed);
10917
+ if (!bytes) {
10918
+ let remoteUrl = null;
10919
+ try {
10920
+ const parsedUrl = new URL(trimmed);
10921
+ if (parsedUrl.protocol === "http:" || parsedUrl.protocol === "https:") {
10922
+ remoteUrl = parsedUrl;
10923
+ }
10924
+ } catch {}
10925
+ if (remoteUrl) {
10926
+ const fetchImpl = runtime.fetch ?? globalThis.fetch;
10927
+ if (typeof fetchImpl !== "function") {
10928
+ return null;
10929
+ }
10930
+ const response = await fetchImpl(trimmed, {
10931
+ headers: { Accept: "image/*" }
10932
+ });
10933
+ if (!response.ok) {
10934
+ throw new Error(`HTTP ${response.status}`);
10935
+ }
10936
+ const contentType = response.headers.get("content-type");
10937
+ if (!contentType?.toLowerCase().startsWith("image/")) {
10938
+ throw new Error(`Expected image content-type, got ${contentType ?? "unknown"}`);
10939
+ }
10940
+ bytes = Buffer.from(await response.arrayBuffer());
10941
+ } else {
10942
+ bytes = await readAvatarBytesFromLocalCandidates(trimmed);
10943
+ }
10944
+ }
10945
+ if (!bytes || bytes.length === 0) {
10946
+ return null;
10947
+ }
10948
+ if (bytes.length > MAX_PROFILE_AVATAR_BYTES) {
10949
+ throw new Error(`Discord profile avatar exceeds ${MAX_PROFILE_AVATAR_BYTES} bytes`);
10950
+ }
10951
+ return {
10952
+ bytes,
10953
+ hash: createHash("sha256").update(bytes).digest("hex")
10954
+ };
10955
+ }
10956
+ async function syncDiscordClientProfile(runtime, clientUser, settings) {
10957
+ if (settings.syncProfile === false) {
10958
+ return;
10959
+ }
10960
+ const desiredName = normalizeDesiredDiscordName(runtime, settings);
10961
+ const desiredAvatarSource = normalizeDesiredDiscordAvatarSource(runtime, settings);
10962
+ if (!desiredName && !desiredAvatarSource) {
10963
+ return;
10964
+ }
10965
+ const persisted = await readPersistedProfileSyncState();
10966
+ const nextState = { ...persisted };
10967
+ let stateChanged = false;
10968
+ if (desiredName) {
10969
+ if (persisted.username !== desiredName) {
10970
+ if (clientUser.username !== desiredName) {
10971
+ if (typeof clientUser.setUsername === "function") {
10972
+ await clientUser.setUsername(desiredName);
10973
+ runtime.logger.info({
10974
+ src: "plugin:discord",
10975
+ agentId: runtime.agentId,
10976
+ discordProfileName: desiredName
10977
+ }, "Synchronized Discord bot username from connector settings");
10978
+ }
10979
+ }
10980
+ nextState.username = desiredName;
10981
+ stateChanged = true;
10982
+ }
10983
+ }
10984
+ if (desiredAvatarSource) {
10985
+ const avatar = await loadDiscordProfileAvatarBytes(desiredAvatarSource, runtime);
10986
+ if (avatar && persisted.avatarHash !== avatar.hash) {
10987
+ if (typeof clientUser.setAvatar === "function") {
10988
+ await clientUser.setAvatar(avatar.bytes);
10989
+ runtime.logger.info({
10990
+ src: "plugin:discord",
10991
+ agentId: runtime.agentId
10992
+ }, "Synchronized Discord bot avatar from connector settings");
10993
+ }
10994
+ nextState.avatarHash = avatar.hash;
10995
+ stateChanged = true;
10996
+ }
10997
+ }
10998
+ if (stateChanged) {
10999
+ await writePersistedProfileSyncState(nextState);
11000
+ }
11001
+ }
11002
+
11003
+ // slash-commands.ts
11004
+ import { createUniqueUuid as createUniqueUuid5 } from "@elizaos/core";
11005
+ import { ApplicationCommandOptionType } from "discord.js";
11006
+ var OPTION_TYPE_MAP = {
11007
+ string: ApplicationCommandOptionType.String,
11008
+ number: ApplicationCommandOptionType.Number,
11009
+ boolean: ApplicationCommandOptionType.Boolean,
11010
+ user: ApplicationCommandOptionType.User,
11011
+ channel: ApplicationCommandOptionType.Channel,
11012
+ role: ApplicationCommandOptionType.Role
11013
+ };
11014
+ var commands = new Map;
11015
+ var cooldowns = new Map;
11016
+ var FALLBACK_KNOWN_MODELS = [
11017
+ "gpt-4o",
11018
+ "gpt-4o-mini",
11019
+ "gpt-4",
11020
+ "gpt-3.5-turbo",
11021
+ "claude-sonnet-4-20250514",
11022
+ "claude-opus-4-20250514",
11023
+ "claude-3.5-haiku",
11024
+ "llama-3.1-70b",
11025
+ "llama-3.1-8b",
11026
+ "gemini-2.5-pro",
11027
+ "gemini-2.5-flash",
11028
+ "mistral-large",
11029
+ "mistral-medium"
11030
+ ];
11031
+ function parseStringList(value) {
11032
+ if (Array.isArray(value)) {
11033
+ return value.map((entry) => typeof entry === "string" ? entry.trim() : "").filter(Boolean);
11034
+ }
11035
+ if (typeof value !== "string") {
11036
+ return [];
11037
+ }
11038
+ const trimmed = value.trim();
11039
+ if (!trimmed) {
11040
+ return [];
11041
+ }
11042
+ try {
11043
+ const parsed = JSON.parse(trimmed);
11044
+ if (Array.isArray(parsed)) {
11045
+ return parseStringList(parsed);
11046
+ }
11047
+ } catch {}
11048
+ return trimmed.split(",").map((entry) => entry.trim()).filter(Boolean);
11049
+ }
11050
+ function getKnownModels(runtime) {
11051
+ const configured = parseStringList(runtime.getSetting("DISCORD_KNOWN_MODELS")) ?? parseStringList(runtime.getSetting("KNOWN_MODELS"));
11052
+ return configured.length > 0 ? configured : [...FALLBACK_KNOWN_MODELS];
11053
+ }
11054
+ var helpCommand = {
11055
+ name: "help",
11056
+ description: "Show available commands and usage information",
11057
+ ephemeral: true,
11058
+ async execute(interaction) {
11059
+ const lines = [`**Available Commands**
11060
+ `];
11061
+ for (const [name, command] of commands) {
11062
+ const options = command.options ? command.options.map((option) => option.required ? `<${option.name}>` : `[${option.name}]`).join(" ") : "";
11063
+ lines.push(`/${name}${options ? ` ${options}` : ""} - ${command.description}`);
11064
+ }
11065
+ await interaction.reply({ content: lines.join(`
11066
+ `), ephemeral: true });
11067
+ }
11068
+ };
11069
+ var statusCommand = {
11070
+ name: "status",
11071
+ description: "Show the bot's current status and uptime",
11072
+ ephemeral: true,
11073
+ async execute(interaction, runtime) {
11074
+ const uptimeMs = process.uptime() * 1000;
11075
+ const hours = Math.floor(uptimeMs / 3600000);
11076
+ const minutes = Math.floor(uptimeMs % 3600000 / 60000);
11077
+ const seconds = Math.floor(uptimeMs % 60000 / 1000);
11078
+ const memoryUsage = process.memoryUsage();
11079
+ const heapMb = (memoryUsage.heapUsed / 1024 / 1024).toFixed(1);
11080
+ const rssMb = (memoryUsage.rss / 1024 / 1024).toFixed(1);
11081
+ await interaction.reply({
11082
+ content: [
11083
+ "**Bot Status**",
11084
+ `- Agent: **${runtime.character?.name ?? "Unknown"}**`,
11085
+ `- Uptime: **${hours}h ${minutes}m ${seconds}s**`,
11086
+ `- Memory: **${heapMb} MB** heap / **${rssMb} MB** RSS`,
11087
+ `- Guilds: **${interaction.client.guilds.cache.size}**`,
11088
+ `- Node: **${process.version}**`,
11089
+ `- Platform: **${process.platform}**`
11090
+ ].join(`
11091
+ `),
11092
+ ephemeral: true
11093
+ });
11094
+ }
11095
+ };
11096
+ var searchCommand = {
11097
+ name: "search",
11098
+ description: "Search conversation history in this channel",
11099
+ options: [
11100
+ {
11101
+ name: "query",
11102
+ description: "The search term or phrase",
11103
+ type: "string",
11104
+ required: true
11105
+ },
11106
+ {
11107
+ name: "limit",
11108
+ description: "Maximum results to return (default: 5)",
11109
+ type: "number"
11110
+ }
11111
+ ],
11112
+ ephemeral: true,
11113
+ cooldown: 10,
11114
+ async execute(interaction, runtime) {
11115
+ const query = interaction.options.getString("query", true);
11116
+ const limit = interaction.options.getNumber("limit") || 5;
11117
+ await interaction.deferReply({ ephemeral: true });
11118
+ try {
11119
+ const roomId = createUniqueUuid5(runtime, interaction.channelId);
11120
+ const memories = await runtime.getMemories({
11121
+ tableName: "messages",
11122
+ roomId,
11123
+ count: 100
11124
+ });
11125
+ const normalizedQuery = query.trim().toLowerCase();
11126
+ const filteredMemories = memories.filter((memory) => (memory.content?.text ?? "").toLowerCase().includes(normalizedQuery));
11127
+ if (filteredMemories.length === 0) {
11128
+ await interaction.editReply({
11129
+ content: `No results found for **"${query}"**`
11130
+ });
11131
+ return;
11132
+ }
11133
+ const results = filteredMemories.slice(0, limit).map((memory, index) => {
11134
+ const text = memory.content?.text || "(no text)";
11135
+ const truncated = text.length > 120 ? `${text.slice(0, 120)}...` : text;
11136
+ const date = memory.createdAt ? new Date(memory.createdAt).toLocaleDateString() : "unknown date";
11137
+ return `**${index + 1}.** ${truncated}
11138
+ _${date}_`;
11139
+ });
11140
+ await interaction.editReply({
11141
+ content: `**Search results for "${query}"**
11142
+
11143
+ ${results.join(`
11144
+
11145
+ `)}`
11146
+ });
11147
+ } catch (error) {
11148
+ await interaction.editReply({
11149
+ content: `Search failed: ${error instanceof Error ? error.message : String(error)}`
11150
+ });
11151
+ }
11152
+ }
11153
+ };
11154
+ var clearCommand = {
11155
+ name: "clear",
11156
+ description: "Explain how context clearing works in this channel",
11157
+ ephemeral: true,
11158
+ async execute(interaction) {
11159
+ await interaction.reply({
11160
+ content: "Context clearing is not wired up for Discord yet. I can search recent messages with `/search`, but I won't pretend existing memory was deleted.",
11161
+ ephemeral: true
9133
11162
  });
9134
11163
  }
9135
- for (const p of ow.deny.toArray()) {
9136
- changes.push({
9137
- permission: p,
9138
- oldState: isDelete ? "DENY" : "NEUTRAL",
9139
- newState: isDelete ? "NEUTRAL" : "DENY"
11164
+ };
11165
+ var settingsCommand = {
11166
+ name: "settings",
11167
+ description: "View the current Discord bot settings",
11168
+ options: [
11169
+ {
11170
+ name: "action",
11171
+ description: "What to do",
11172
+ type: "string",
11173
+ required: true,
11174
+ choices: [
11175
+ { name: "View current settings", value: "view" },
11176
+ { name: "Toggle response-only-on-mention", value: "toggle-mention" },
11177
+ { name: "Toggle ignore-bots", value: "toggle-ignore-bots" }
11178
+ ]
11179
+ }
11180
+ ],
11181
+ ephemeral: true,
11182
+ async execute(interaction, runtime) {
11183
+ const action = interaction.options.getString("action", true);
11184
+ if (action === "view") {
11185
+ await interaction.reply({
11186
+ content: [
11187
+ "**Current Settings**",
11188
+ `- Respond only to mentions: **${runtime.getSetting("DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS") ?? "false"}**`,
11189
+ `- Ignore bot messages: **${runtime.getSetting("DISCORD_SHOULD_IGNORE_BOT_MESSAGES") ?? "true"}**`,
11190
+ `- Allowed channels: **${runtime.getSetting("CHANNEL_IDS") ?? "(all channels)"}**`,
11191
+ `- Agent name: **${runtime.character?.name ?? "Unknown"}**`
11192
+ ].join(`
11193
+ `),
11194
+ ephemeral: true
11195
+ });
11196
+ return;
11197
+ }
11198
+ const content = action === "toggle-mention" ? "Respond-only-on-mention is controlled by `DISCORD_SHOULD_RESPOND_ONLY_TO_MENTIONS`. Update the setting and restart to change it." : "Ignore-bots is controlled by `DISCORD_SHOULD_IGNORE_BOT_MESSAGES`. Update the setting and restart to change it.";
11199
+ await interaction.reply({ content, ephemeral: true });
11200
+ }
11201
+ };
11202
+ var setupCommand = {
11203
+ name: "setup",
11204
+ description: "Set up API credentials for third-party services",
11205
+ options: [
11206
+ {
11207
+ name: "service",
11208
+ description: "Service to configure (github, vercel, cloudflare, anthropic, openai, fal, custom)",
11209
+ type: "string",
11210
+ choices: [
11211
+ { name: "GitHub", value: "github" },
11212
+ { name: "Vercel", value: "vercel" },
11213
+ { name: "Cloudflare", value: "cloudflare" },
11214
+ { name: "Anthropic", value: "anthropic" },
11215
+ { name: "OpenAI", value: "openai" },
11216
+ { name: "fal.ai", value: "fal" },
11217
+ { name: "Custom", value: "custom" }
11218
+ ]
11219
+ }
11220
+ ],
11221
+ ephemeral: true,
11222
+ async execute(interaction) {
11223
+ const service = interaction.options.getString("service");
11224
+ if (!service) {
11225
+ const services = listPresets().filter((presetName) => presetName !== "generic").map((presetName) => {
11226
+ const preset = getPreset(presetName);
11227
+ return `- **${preset?.displayName ?? presetName}** - \`/setup service:${presetName}\``;
11228
+ });
11229
+ await interaction.reply({
11230
+ content: [
11231
+ "**Credential Setup**",
11232
+ "Choose a service to configure:",
11233
+ "",
11234
+ ...services,
11235
+ "- **Custom** - `/setup service:custom`",
11236
+ "",
11237
+ "I'll walk you through it in DMs to keep your keys safe."
11238
+ ].join(`
11239
+ `),
11240
+ ephemeral: true
11241
+ });
11242
+ return;
11243
+ }
11244
+ await interaction.reply({
11245
+ content: `Starting **${service}** setup. Check your DMs - I'll walk you through it there to keep your keys private.`,
11246
+ ephemeral: true
9140
11247
  });
11248
+ try {
11249
+ const dmChannel = await interaction.user.createDM();
11250
+ const presetKey = service === "custom" ? "generic" : service;
11251
+ const preset = getPreset(presetKey);
11252
+ if (!preset) {
11253
+ await dmChannel.send(`I don't have a preset for "${service}". Try \`/setup\` to see available services.`);
11254
+ return;
11255
+ }
11256
+ const field = preset.fields[0];
11257
+ const helpLine = preset.helpUrl ? `Here's where to get one: ${preset.helpUrl}` : "";
11258
+ await dmChannel.send([
11259
+ `Setting up **${preset.displayName}** credentials.`,
11260
+ preset.helpText,
11261
+ helpLine,
11262
+ "",
11263
+ `Please paste your **${field.label}** here. ${field.secret ? "I'll delete your message right after reading it." : ""}`,
11264
+ "",
11265
+ "(Type `cancel` to abort setup)"
11266
+ ].filter(Boolean).join(`
11267
+ `));
11268
+ } catch {
11269
+ try {
11270
+ await interaction.followUp({
11271
+ content: "I couldn't send you a DM. Make sure your DMs are open and try again.",
11272
+ ephemeral: true
11273
+ });
11274
+ } catch {}
11275
+ }
9141
11276
  }
9142
- return changes;
9143
- }
9144
- function diffOverwrites(oldOw, newOw) {
9145
- if (!oldOw && !newOw) {
9146
- return { changes: [], action: "UPDATE" };
11277
+ };
11278
+ var modelCommand = {
11279
+ name: "model",
11280
+ description: "View or change the active AI model",
11281
+ options: [
11282
+ {
11283
+ name: "name",
11284
+ description: "Model name to switch to (leave empty to view current)",
11285
+ type: "string",
11286
+ autocomplete: true
11287
+ }
11288
+ ],
11289
+ ephemeral: true,
11290
+ async execute(interaction, runtime) {
11291
+ const modelName = interaction.options.getString("name");
11292
+ if (!modelName) {
11293
+ await interaction.reply({
11294
+ content: `**Current model:** \`${runtime.getSetting("MODEL") ?? runtime.getSetting("DEFAULT_MODEL") ?? "(not configured)"}\``,
11295
+ ephemeral: true
11296
+ });
11297
+ return;
11298
+ }
11299
+ await interaction.reply({
11300
+ content: `Model switching to \`${modelName}\` is noted. The runtime model is still controlled by configuration, so update the setting and restart to switch permanently.`,
11301
+ ephemeral: true
11302
+ });
11303
+ },
11304
+ async autocomplete(interaction) {
11305
+ const runtime = interaction.client.runtime;
11306
+ const models = runtime ? getKnownModels(runtime) : [...FALLBACK_KNOWN_MODELS];
11307
+ const focused = interaction.options.getFocused().toLowerCase();
11308
+ const filtered = models.filter((model) => model.toLowerCase().includes(focused)).slice(0, 25);
11309
+ await interaction.respond(filtered.map((model) => ({ name: model, value: model })));
9147
11310
  }
9148
- if (!oldOw && newOw) {
9149
- return { changes: overwriteToChanges(newOw, false), action: "CREATE" };
11311
+ };
11312
+ function registerBuiltins() {
11313
+ for (const command of [
11314
+ helpCommand,
11315
+ statusCommand,
11316
+ searchCommand,
11317
+ clearCommand,
11318
+ settingsCommand,
11319
+ modelCommand,
11320
+ setupCommand
11321
+ ]) {
11322
+ commands.set(command.name, command);
9150
11323
  }
9151
- if (oldOw && !newOw) {
9152
- return { changes: overwriteToChanges(oldOw, true), action: "DELETE" };
11324
+ }
11325
+ registerBuiltins();
11326
+ function toDiscordSlashCommand(command) {
11327
+ const options = command.options?.map((option) => ({
11328
+ name: option.name,
11329
+ description: option.description,
11330
+ type: OPTION_TYPE_MAP[option.type] ?? ApplicationCommandOptionType.String,
11331
+ required: option.required ?? false,
11332
+ ...option.choices ? { choices: option.choices } : {},
11333
+ ...option.autocomplete ? { autocomplete: option.autocomplete } : {}
11334
+ }));
11335
+ return {
11336
+ name: command.name,
11337
+ description: command.description,
11338
+ options
11339
+ };
11340
+ }
11341
+ async function registerSlashCommands(runtime) {
11342
+ const registered = [...commands.values()].map(toDiscordSlashCommand);
11343
+ runtime.logger.info({
11344
+ src: "slash-commands",
11345
+ count: registered.length,
11346
+ names: [...commands.keys()]
11347
+ }, "Registering built-in slash commands");
11348
+ await runtime.emitEvent(["DISCORD_REGISTER_COMMANDS"], {
11349
+ runtime,
11350
+ source: "discord",
11351
+ commands: registered
11352
+ });
11353
+ }
11354
+ async function handleSlashCommand(interaction, runtime) {
11355
+ const command = commands.get(interaction.commandName);
11356
+ if (!command) {
11357
+ return;
9153
11358
  }
9154
- const changes = [];
9155
- const oldAllow = oldOw?.allow.toArray();
9156
- const oldDeny = oldOw?.deny.toArray();
9157
- const newAllow = newOw?.allow.toArray();
9158
- const newDeny = newOw?.deny.toArray();
9159
- const allPerms = new Set([...oldAllow, ...oldDeny, ...newAllow, ...newDeny]);
9160
- for (const perm of allPerms) {
9161
- const oldState = getState(perm, oldAllow, oldDeny);
9162
- const newState = getState(perm, newAllow, newDeny);
9163
- if (oldState !== newState) {
9164
- changes.push({ permission: perm, oldState, newState });
11359
+ if (command.cooldown && command.cooldown > 0) {
11360
+ const userId = interaction.user.id;
11361
+ let commandCooldowns = cooldowns.get(command.name);
11362
+ if (!commandCooldowns) {
11363
+ commandCooldowns = new Map;
11364
+ cooldowns.set(command.name, commandCooldowns);
9165
11365
  }
9166
- }
9167
- return { changes, action: "UPDATE" };
9168
- }
9169
- function diffRolePermissions(oldRole, newRole) {
9170
- const oldPerms = oldRole.permissions.toArray();
9171
- const newPerms = newRole.permissions.toArray();
9172
- const changes = [];
9173
- for (const p of newPerms) {
9174
- if (!oldPerms.includes(p)) {
9175
- changes.push({ permission: p, oldState: "NEUTRAL", newState: "ALLOW" });
11366
+ const lastUsed = commandCooldowns.get(userId);
11367
+ const now = Date.now();
11368
+ if (lastUsed && now - lastUsed < command.cooldown * 1000) {
11369
+ const remaining = Math.ceil((command.cooldown * 1000 - (now - lastUsed)) / 1000);
11370
+ await interaction.reply({
11371
+ content: `Please wait **${remaining}s** before using \`/${command.name}\` again.`,
11372
+ ephemeral: true
11373
+ });
11374
+ return;
9176
11375
  }
11376
+ commandCooldowns.set(userId, now);
11377
+ setTimeout(() => {
11378
+ if (commandCooldowns?.get(userId) === now) {
11379
+ commandCooldowns.delete(userId);
11380
+ }
11381
+ }, command.cooldown * 1000);
9177
11382
  }
9178
- for (const p of oldPerms) {
9179
- if (!newPerms.includes(p)) {
9180
- changes.push({ permission: p, oldState: "ALLOW", newState: "NEUTRAL" });
11383
+ if (command.ownerOnly) {
11384
+ const guild = interaction.guild;
11385
+ if (guild && interaction.user.id !== guild.ownerId) {
11386
+ await interaction.reply({
11387
+ content: "This command can only be used by the server owner.",
11388
+ ephemeral: true
11389
+ });
11390
+ return;
9181
11391
  }
9182
11392
  }
9183
- return changes;
11393
+ try {
11394
+ await command.execute(interaction, runtime);
11395
+ } catch (error) {
11396
+ const content = `An error occurred while running \`/${command.name}\`: ${error instanceof Error ? error.message : String(error)}`;
11397
+ runtime.logger.error({
11398
+ src: "slash-commands",
11399
+ commandName: command.name,
11400
+ error: error instanceof Error ? error.message : String(error)
11401
+ }, "Error executing slash command");
11402
+ try {
11403
+ if (interaction.deferred) {
11404
+ await interaction.editReply({ content });
11405
+ } else if (!interaction.replied) {
11406
+ await interaction.reply({ content, ephemeral: true });
11407
+ }
11408
+ } catch {}
11409
+ }
9184
11410
  }
9185
- function diffMemberRoles(oldMember, newMember) {
9186
- const oldRoles = oldMember.roles.cache;
9187
- const newRoles = newMember.roles.cache;
9188
- return {
9189
- added: [...newRoles.filter((r) => !oldRoles.has(r.id)).values()],
9190
- removed: [...oldRoles.filter((r) => !newRoles.has(r.id)).values()]
9191
- };
11411
+ async function handleAutocomplete(interaction) {
11412
+ const command = commands.get(interaction.commandName);
11413
+ if (!command?.autocomplete) {
11414
+ await interaction.respond([]);
11415
+ return;
11416
+ }
11417
+ try {
11418
+ await command.autocomplete(interaction);
11419
+ } catch {
11420
+ try {
11421
+ await interaction.respond([]);
11422
+ } catch {}
11423
+ }
9192
11424
  }
9193
11425
 
9194
11426
  // voice.ts
@@ -9205,15 +11437,15 @@ import {
9205
11437
  VoiceConnectionStatus
9206
11438
  } from "@discordjs/voice";
9207
11439
  import {
9208
- ChannelType as ChannelType6,
9209
- createUniqueUuid as createUniqueUuid5,
11440
+ ChannelType as ChannelType7,
11441
+ createUniqueUuid as createUniqueUuid6,
9210
11442
  EventType as EventType2,
9211
- logger as logger3,
11443
+ logger as logger4,
9212
11444
  ModelType as ModelType20,
9213
- stringToUuid as stringToUuid2
11445
+ stringToUuid as stringToUuid3
9214
11446
  } from "@elizaos/core";
9215
11447
  import {
9216
- ChannelType as DiscordChannelType4
11448
+ ChannelType as DiscordChannelType5
9217
11449
  } from "discord.js";
9218
11450
  import prism from "prism-media";
9219
11451
  var DECODE_FRAME_SIZE = 1024;
@@ -9222,16 +11454,16 @@ function createOpusDecoder(options) {
9222
11454
  try {
9223
11455
  return new prism.opus.Decoder(options);
9224
11456
  } catch (error) {
9225
- logger3.warn({
11457
+ logger4.warn({
9226
11458
  src: "plugin:discord:service:voice",
9227
11459
  error: error instanceof Error ? error.message : String(error)
9228
11460
  }, "Failed to create opus decoder");
9229
11461
  try {
9230
11462
  const { generateDependencyReport } = __require("@discordjs/voice");
9231
11463
  const report = generateDependencyReport();
9232
- logger3.debug({ src: "plugin:discord:service:voice", report }, "Voice dependency report");
11464
+ logger4.debug({ src: "plugin:discord:service:voice", report }, "Voice dependency report");
9233
11465
  } catch (reportError) {
9234
- logger3.warn({
11466
+ logger4.warn({
9235
11467
  src: "plugin:discord:service:voice",
9236
11468
  error: reportError instanceof Error ? reportError.message : String(reportError)
9237
11469
  }, "Could not generate dependency report");
@@ -9278,7 +11510,7 @@ class AudioMonitor {
9278
11510
  }
9279
11511
  });
9280
11512
  this.readable.on("end", () => {
9281
- logger3.debug({ src: "plugin:discord:service:voice" }, "AudioMonitor ended");
11513
+ logger4.debug({ src: "plugin:discord:service:voice" }, "AudioMonitor ended");
9282
11514
  this.ended = true;
9283
11515
  if (this.lastFlagged < 0) {
9284
11516
  return;
@@ -9290,7 +11522,7 @@ class AudioMonitor {
9290
11522
  if (this.ended) {
9291
11523
  return;
9292
11524
  }
9293
- logger3.debug({ src: "plugin:discord:service:voice" }, "Speaking stopped");
11525
+ logger4.debug({ src: "plugin:discord:service:voice" }, "Speaking stopped");
9294
11526
  if (this.lastFlagged < 0) {
9295
11527
  return;
9296
11528
  }
@@ -9301,7 +11533,7 @@ class AudioMonitor {
9301
11533
  return;
9302
11534
  }
9303
11535
  onStart();
9304
- logger3.debug({ src: "plugin:discord:service:voice" }, "Speaking started");
11536
+ logger4.debug({ src: "plugin:discord:service:voice" }, "Speaking started");
9305
11537
  this.reset();
9306
11538
  });
9307
11539
  }
@@ -9361,9 +11593,9 @@ class VoiceManager extends EventEmitter {
9361
11593
  }
9362
11594
  async getChannelType(channel) {
9363
11595
  switch (channel.type) {
9364
- case DiscordChannelType4.GuildVoice:
9365
- case DiscordChannelType4.GuildStageVoice:
9366
- return ChannelType6.VOICE_GROUP;
11596
+ case DiscordChannelType5.GuildVoice:
11597
+ case DiscordChannelType5.GuildStageVoice:
11598
+ return ChannelType7.VOICE_GROUP;
9367
11599
  default:
9368
11600
  this.runtime.logger.error({
9369
11601
  src: "plugin:discord:service:voice",
@@ -9383,6 +11615,33 @@ class VoiceManager extends EventEmitter {
9383
11615
  ready: this.ready
9384
11616
  }, "VoiceManager ready status changed");
9385
11617
  }
11618
+ stop() {
11619
+ if (this.transcriptionTimeout) {
11620
+ clearTimeout(this.transcriptionTimeout);
11621
+ this.transcriptionTimeout = null;
11622
+ }
11623
+ for (const memberId of [...this.activeMonitors.keys()]) {
11624
+ this.stopMonitoringMember(memberId);
11625
+ }
11626
+ for (const connection of new Set(this.connections.values())) {
11627
+ try {
11628
+ connection.destroy();
11629
+ } catch (error) {
11630
+ this.runtime.logger.warn({
11631
+ src: "plugin:discord:service:voice",
11632
+ agentId: this.runtime.agentId,
11633
+ error: error instanceof Error ? error.message : String(error)
11634
+ }, "Failed to destroy Discord voice connection during shutdown");
11635
+ }
11636
+ }
11637
+ this.connections.clear();
11638
+ this.streams.clear();
11639
+ this.userStates.clear();
11640
+ this.processingVoice = false;
11641
+ this.cleanupAudioPlayer(this.activeAudioPlayer);
11642
+ this.removeAllListeners();
11643
+ this.ready = false;
11644
+ }
9386
11645
  isReady() {
9387
11646
  return this.ready;
9388
11647
  }
@@ -9839,23 +12098,24 @@ class VoiceManager extends EventEmitter {
9839
12098
  if (!message || message.trim() === "" || message.length < 3) {
9840
12099
  return { text: "", actions: ["IGNORE"] };
9841
12100
  }
9842
- const roomId = createUniqueUuid5(this.runtime, channelId);
9843
- const uniqueEntityId = createUniqueUuid5(this.runtime, entityId);
12101
+ const roomId = createUniqueUuid6(this.runtime, channelId);
12102
+ const uniqueEntityId = createUniqueUuid6(this.runtime, entityId);
9844
12103
  const type = await this.getChannelType(channel);
9845
12104
  await this.runtime.ensureConnection({
9846
12105
  entityId: uniqueEntityId,
9847
12106
  roomId,
12107
+ roomName: channel.name,
9848
12108
  userName,
9849
12109
  name,
9850
12110
  source: "discord",
9851
12111
  channelId,
9852
- messageServerId: stringToUuid2(channel.guild.id),
12112
+ messageServerId: stringToUuid3(channel.guild.id),
9853
12113
  type,
9854
- worldId: createUniqueUuid5(this.runtime, channel.guild.id),
12114
+ worldId: createUniqueUuid6(this.runtime, channel.guild.id),
9855
12115
  worldName: channel.guild.name
9856
12116
  });
9857
12117
  const memory = {
9858
- id: createUniqueUuid5(this.runtime, `${channelId}-voice-message-${Date.now()}`),
12118
+ id: createUniqueUuid6(this.runtime, `${channelId}-voice-message-${Date.now()}`),
9859
12119
  agentId: this.runtime.agentId,
9860
12120
  entityId: uniqueEntityId,
9861
12121
  roomId,
@@ -9872,12 +12132,14 @@ class VoiceManager extends EventEmitter {
9872
12132
  };
9873
12133
  const callback = async (content, _actionName) => {
9874
12134
  try {
12135
+ const responseText = normalizeDiscordMessageText(content.text);
9875
12136
  const responseMemory = {
9876
- id: createUniqueUuid5(this.runtime, `${memory.id}-voice-response-${Date.now()}`),
12137
+ id: createUniqueUuid6(this.runtime, `${memory.id}-voice-response-${Date.now()}`),
9877
12138
  entityId: this.runtime.agentId,
9878
12139
  agentId: this.runtime.agentId,
9879
12140
  content: {
9880
12141
  ...content,
12142
+ text: responseText || undefined,
9881
12143
  name: this.runtime.character.name,
9882
12144
  inReplyTo: memory.id,
9883
12145
  isVoiceMessage: true,
@@ -9889,8 +12151,8 @@ class VoiceManager extends EventEmitter {
9889
12151
  const responseMemoryContentText = responseMemory.content.text;
9890
12152
  if (responseMemoryContentText?.trim()) {
9891
12153
  await this.runtime.createMemory(responseMemory, "messages");
9892
- if (content.text) {
9893
- const responseStream = await this.runtime.useModel(ModelType20.TEXT_TO_SPEECH, content.text);
12154
+ if (responseText) {
12155
+ const responseStream = await this.runtime.useModel(ModelType20.TEXT_TO_SPEECH, responseText);
9894
12156
  if (responseStream) {
9895
12157
  const buffer = Buffer.isBuffer(responseStream) ? responseStream : Buffer.from(responseStream);
9896
12158
  const readable = Readable.from(buffer);
@@ -9954,7 +12216,7 @@ class VoiceManager extends EventEmitter {
9954
12216
  }
9955
12217
  }
9956
12218
  if (!chosenChannel) {
9957
- const channels = (await guild.channels.fetch()).filter((channel) => channel && channel.type === DiscordChannelType4.GuildVoice);
12219
+ const channels = (await guild.channels.fetch()).filter((channel) => channel && channel.type === DiscordChannelType5.GuildVoice);
9958
12220
  for (const [, channel] of channels) {
9959
12221
  const voiceChannel = channel;
9960
12222
  if (voiceChannel.members.size > 0 && (chosenChannel === null || voiceChannel.members.size > chosenChannel.members.size)) {
@@ -10048,7 +12310,7 @@ class VoiceManager extends EventEmitter {
10048
12310
  await interaction.editReply("Could not find guild.");
10049
12311
  return;
10050
12312
  }
10051
- const voiceChannel = interaction.guild.channels.cache.find((channel) => channel.id === channelId && channel.type === DiscordChannelType4.GuildVoice);
12313
+ const voiceChannel = interaction.guild.channels.cache.find((channel) => channel.id === channelId && channel.type === DiscordChannelType5.GuildVoice);
10052
12314
  if (!voiceChannel) {
10053
12315
  await interaction.editReply("Voice channel not found!");
10054
12316
  return;
@@ -10095,6 +12357,23 @@ class VoiceManager extends EventEmitter {
10095
12357
  }
10096
12358
 
10097
12359
  // service.ts
12360
+ var DISCORD_SNOWFLAKE_PATTERN2 = /^\d{15,20}$/;
12361
+ function normalizeDiscordTargetUserId(value) {
12362
+ if (typeof value !== "string") {
12363
+ return null;
12364
+ }
12365
+ const trimmed = value.trim();
12366
+ return DISCORD_SNOWFLAKE_PATTERN2.test(trimmed) ? trimmed : null;
12367
+ }
12368
+ function extractDiscordUserIdFromMetadata(metadata) {
12369
+ if (!metadata || typeof metadata !== "object") {
12370
+ return null;
12371
+ }
12372
+ const record = metadata;
12373
+ const discord = record.discord && typeof record.discord === "object" ? record.discord : null;
12374
+ return normalizeDiscordTargetUserId(discord?.userId) ?? normalizeDiscordTargetUserId(discord?.id) ?? normalizeDiscordTargetUserId(record.originalId);
12375
+ }
12376
+
10098
12377
  class DiscordService extends Service {
10099
12378
  static serviceType = DISCORD_SERVICE_NAME;
10100
12379
  capabilityDescription = "The agent is able to send and receive messages on discord";
@@ -10102,6 +12381,9 @@ class DiscordService extends Service {
10102
12381
  character;
10103
12382
  messageManager;
10104
12383
  voiceManager;
12384
+ messageDebouncer;
12385
+ channelDebouncer;
12386
+ _loginFailed = false;
10105
12387
  discordSettings;
10106
12388
  userSelections = new Map;
10107
12389
  timeouts = [];
@@ -10111,6 +12393,78 @@ class DiscordService extends Service {
10111
12393
  allowAllSlashCommands = new Set;
10112
12394
  allowedChannelIds;
10113
12395
  dynamicChannelIds = new Set;
12396
+ ownerDiscordUserIds = new Set;
12397
+ resolveDiscordEntityId(userId) {
12398
+ return resolveDiscordRuntimeEntityId(this.runtime, userId, this.ownerDiscordUserIds);
12399
+ }
12400
+ async refreshOwnerDiscordUserIds(client) {
12401
+ const explicitSetting = this.runtime.getSetting?.("MILADY_DISCORD_OWNER_USER_IDS_JSON");
12402
+ const hasExplicitSetting = explicitSetting !== undefined && explicitSetting !== null && !(typeof explicitSetting === "string" && explicitSetting.trim() === "");
12403
+ let ownerIds;
12404
+ if (hasExplicitSetting) {
12405
+ ownerIds = parseDiscordOwnerUserIds(Array.isArray(explicitSetting) ? explicitSetting : typeof explicitSetting === "string" ? explicitSetting : [String(explicitSetting)]);
12406
+ } else {
12407
+ const application = client.application && typeof client.application.fetch === "function" ? await client.application.fetch() : client.application;
12408
+ ownerIds = [...new Set(extractDiscordOwnerUserIds(application))];
12409
+ }
12410
+ this.ownerDiscordUserIds = new Set(ownerIds);
12411
+ if (ownerIds.length === 0) {
12412
+ return;
12413
+ }
12414
+ const existingWhitelist = getConnectorAdminWhitelist(this.runtime);
12415
+ const nextDiscordAdmins = [
12416
+ ...new Set([...existingWhitelist.discord ?? [], ...ownerIds])
12417
+ ];
12418
+ setConnectorAdminWhitelist(this.runtime, {
12419
+ ...existingWhitelist,
12420
+ discord: nextDiscordAdmins
12421
+ });
12422
+ this.runtime.logger.info({
12423
+ src: "plugin:discord",
12424
+ agentId: this.runtime.agentId,
12425
+ ownerDiscordUserIds: ownerIds
12426
+ }, "Resolved Discord owner identities for canonical Milady owner mapping");
12427
+ }
12428
+ async resolveDiscordTargetUserId(targetEntityId) {
12429
+ const directId = normalizeDiscordTargetUserId(targetEntityId);
12430
+ if (directId) {
12431
+ return directId;
12432
+ }
12433
+ if (targetEntityId === resolveMiladyOwnerEntityId(this.runtime)) {
12434
+ const knownOwnerUserId = this.ownerDiscordUserIds.values().next().value;
12435
+ if (typeof knownOwnerUserId === "string" && knownOwnerUserId.length > 0) {
12436
+ return knownOwnerUserId;
12437
+ }
12438
+ }
12439
+ const directEntity = this.runtime.getEntityById ? await this.runtime.getEntityById(targetEntityId) : null;
12440
+ const directMetadataUserId = extractDiscordUserIdFromMetadata(directEntity?.metadata);
12441
+ if (directMetadataUserId) {
12442
+ return directMetadataUserId;
12443
+ }
12444
+ if (typeof this.runtime.getRelationships !== "function") {
12445
+ return null;
12446
+ }
12447
+ const identityLinks = await this.runtime.getRelationships({
12448
+ entityIds: [targetEntityId],
12449
+ tags: ["identity_link"]
12450
+ });
12451
+ for (const relationship of identityLinks) {
12452
+ const metadata = relationship.metadata && typeof relationship.metadata === "object" ? relationship.metadata : null;
12453
+ if (metadata?.status !== "confirmed") {
12454
+ continue;
12455
+ }
12456
+ const linkedEntityId = relationship.sourceEntityId === targetEntityId ? relationship.targetEntityId : relationship.targetEntityId === targetEntityId ? relationship.sourceEntityId : null;
12457
+ if (!linkedEntityId || linkedEntityId === targetEntityId) {
12458
+ continue;
12459
+ }
12460
+ const linkedEntity = this.runtime.getEntityById ? await this.runtime.getEntityById(linkedEntityId) : null;
12461
+ const linkedMetadataUserId = extractDiscordUserIdFromMetadata(linkedEntity?.metadata);
12462
+ if (linkedMetadataUserId) {
12463
+ return linkedMetadataUserId;
12464
+ }
12465
+ }
12466
+ return null;
12467
+ }
10114
12468
  constructor(runtime) {
10115
12469
  super(runtime);
10116
12470
  this.discordSettings = getDiscordSettings(runtime);
@@ -10179,13 +12533,26 @@ class DiscordService extends Service {
10179
12533
  reject(error);
10180
12534
  });
10181
12535
  });
10182
- this.clientReadyPromise.catch((_error) => {});
12536
+ this.clientReadyPromise.catch((error) => {
12537
+ this.runtime.logger.error({
12538
+ src: "plugin:discord",
12539
+ agentId: this.runtime.agentId,
12540
+ error: error instanceof Error ? error.message : String(error)
12541
+ }, "Discord client ready promise rejected");
12542
+ this._loginFailed = true;
12543
+ });
10183
12544
  this.setupEventListeners();
10184
12545
  } catch (error) {
10185
12546
  runtime.logger.error(`Error initializing Discord client: ${error instanceof Error ? error.message : String(error)}`);
10186
12547
  this.client = null;
10187
12548
  }
10188
12549
  }
12550
+ isHealthy() {
12551
+ if (this._loginFailed || !this.client) {
12552
+ return false;
12553
+ }
12554
+ return this.client.isReady();
12555
+ }
10189
12556
  static async start(runtime) {
10190
12557
  const service = new DiscordService(runtime);
10191
12558
  return service;
@@ -10196,22 +12563,31 @@ class DiscordService extends Service {
10196
12563
  throw new Error("Discord client is not ready.");
10197
12564
  }
10198
12565
  const client = this.client;
10199
- if (target.channelId && this.allowedChannelIds && !this.isChannelAllowed(target.channelId)) {
10200
- runtime.logger.warn(`Channel ${target.channelId} not in allowed list, skipping send`);
10201
- return;
10202
- }
10203
12566
  let targetChannel = null;
12567
+ let resolvedChannelId = null;
10204
12568
  try {
10205
12569
  if (target.channelId) {
12570
+ resolvedChannelId = target.channelId;
10206
12571
  targetChannel = await client.channels.fetch(target.channelId);
12572
+ } else if (target.roomId) {
12573
+ const room = typeof runtime.getRoom === "function" ? await runtime.getRoom(target.roomId) : null;
12574
+ const roomChannelId = room?.channelId && typeof room.channelId === "string" ? room.channelId : null;
12575
+ if (!roomChannelId) {
12576
+ throw new Error(`Could not resolve Discord channel ID for room ${target.roomId}`);
12577
+ }
12578
+ resolvedChannelId = roomChannelId;
12579
+ targetChannel = await client.channels.fetch(roomChannelId);
10207
12580
  } else if (target.entityId) {
10208
- const discordUserId = target.entityId;
12581
+ const discordUserId = await this.resolveDiscordTargetUserId(target.entityId);
12582
+ if (!discordUserId) {
12583
+ throw new Error(`Could not resolve Discord user ID for runtime entity ${target.entityId}`);
12584
+ }
10209
12585
  const user = await client.users.fetch(discordUserId);
10210
12586
  if (user) {
10211
12587
  targetChannel = user.dmChannel ?? await user.createDM();
10212
12588
  }
10213
12589
  } else {
10214
- throw new Error("Discord SendHandler requires channelId or entityId.");
12590
+ throw new Error("Discord SendHandler requires channelId, roomId, or entityId.");
10215
12591
  }
10216
12592
  if (!targetChannel) {
10217
12593
  const targetStr = JSON.stringify(target, (_key, value) => {
@@ -10222,6 +12598,12 @@ class DiscordService extends Service {
10222
12598
  });
10223
12599
  throw new Error(`Could not find target Discord channel/DM for target: ${targetStr}`);
10224
12600
  }
12601
+ const allowedByParentThread = typeof targetChannel.isThread === "function" && targetChannel.isThread() && "parentId" in targetChannel && typeof targetChannel.parentId === "string" && targetChannel.parentId.length > 0 && this.isChannelAllowed(targetChannel.parentId);
12602
+ if (this.allowedChannelIds && !this.isChannelAllowed(targetChannel.id) && !allowedByParentThread) {
12603
+ const resolvedFromText = resolvedChannelId && resolvedChannelId !== targetChannel.id ? ` (resolved from ${resolvedChannelId})` : "";
12604
+ runtime.logger.warn(`Channel ${targetChannel.id}${resolvedFromText} not in allowed list, skipping send`);
12605
+ return;
12606
+ }
10225
12607
  if (targetChannel.isTextBased() && !targetChannel.isVoiceBased()) {
10226
12608
  if ("send" in targetChannel && typeof targetChannel.send === "function") {
10227
12609
  const files = [];
@@ -10234,11 +12616,12 @@ class DiscordService extends Service {
10234
12616
  }
10235
12617
  }
10236
12618
  const sentMessages = [];
10237
- const roomId = createUniqueUuid6(runtime, targetChannel.id);
12619
+ const roomId = createUniqueUuid7(runtime, targetChannel.id);
10238
12620
  const channelType = await this.getChannelType(targetChannel);
10239
- if (content.text || files.length > 0) {
10240
- if (content.text) {
10241
- const chunks = splitMessage(content.text, MAX_MESSAGE_LENGTH);
12621
+ const textContent = normalizeDiscordMessageText(content.text);
12622
+ if (textContent || files.length > 0) {
12623
+ if (textContent) {
12624
+ const chunks = splitMessage(textContent, MAX_MESSAGE_LENGTH);
10242
12625
  if (chunks.length > 1) {
10243
12626
  for (let i = 0;i < chunks.length - 1; i++) {
10244
12627
  const sent2 = await targetChannel.send(chunks[i]);
@@ -10267,17 +12650,18 @@ class DiscordService extends Service {
10267
12650
  }
10268
12651
  const targetChannelGuild = "guild" in targetChannel ? targetChannel.guild : null;
10269
12652
  const serverId = targetChannelGuild?.id ? targetChannelGuild.id : targetChannel.id;
10270
- const worldId = createUniqueUuid6(runtime, serverId);
12653
+ const worldId = createUniqueUuid7(runtime, serverId);
10271
12654
  const worldName = targetChannelGuild?.name ? targetChannelGuild.name : undefined;
10272
12655
  const clientUser = client.user;
10273
12656
  await this.runtime.ensureConnection({
10274
12657
  entityId: runtime.agentId,
10275
12658
  roomId,
12659
+ roomName: "name" in targetChannel && typeof targetChannel.name === "string" ? targetChannel.name : clientUser?.displayName || clientUser?.username || undefined,
10276
12660
  userName: clientUser?.username ? clientUser.username : undefined,
10277
12661
  name: clientUser?.displayName || clientUser?.username || undefined,
10278
12662
  source: "discord",
10279
12663
  channelId: targetChannel.id,
10280
- messageServerId: stringToUuid3(serverId),
12664
+ messageServerId: stringToUuid4(serverId),
10281
12665
  type: channelType,
10282
12666
  worldId,
10283
12667
  worldName
@@ -10286,12 +12670,12 @@ class DiscordService extends Service {
10286
12670
  try {
10287
12671
  const hasAttachments = sentMsg.attachments.size > 0;
10288
12672
  const memory = {
10289
- id: createUniqueUuid6(runtime, sentMsg.id),
12673
+ id: createUniqueUuid7(runtime, sentMsg.id),
10290
12674
  entityId: runtime.agentId,
10291
12675
  agentId: runtime.agentId,
10292
12676
  roomId,
10293
12677
  content: {
10294
- text: sentMsg.content || content.text || " ",
12678
+ text: sentMsg.content || textContent || " ",
10295
12679
  url: sentMsg.url,
10296
12680
  channelType,
10297
12681
  ...hasAttachments && content.attachments ? { attachments: content.attachments } : {},
@@ -10329,6 +12713,64 @@ class DiscordService extends Service {
10329
12713
  }
10330
12714
  const listenCidsRaw = this.runtime.getSetting("DISCORD_LISTEN_CHANNEL_IDS");
10331
12715
  const listenCids = Array.isArray(listenCidsRaw) ? listenCidsRaw : listenCidsRaw && typeof listenCidsRaw === "string" && listenCidsRaw.trim() ? listenCidsRaw.trim().split(",").map((s) => s.trim()).filter((s) => s.length > 0) : [];
12716
+ const debounceMsSetting = this.runtime.getSetting("DISCORD_DEBOUNCE_MS");
12717
+ const debounceMs = typeof debounceMsSetting === "number" ? debounceMsSetting : typeof debounceMsSetting === "string" && debounceMsSetting.trim() ? Number.parseInt(debounceMsSetting, 10) || 400 : 400;
12718
+ this.messageDebouncer = createMessageDebouncer((messages) => {
12719
+ if (!this.messageManager || messages.length === 0) {
12720
+ return;
12721
+ }
12722
+ if (messages.length === 1) {
12723
+ this.messageManager.handleMessage(messages[0]);
12724
+ return;
12725
+ }
12726
+ const anchor = messages[0];
12727
+ const combinedText = messages.map((message) => message.content).join(`
12728
+ `);
12729
+ const combined = Object.create(anchor, {
12730
+ content: { value: combinedText, writable: true, enumerable: true }
12731
+ });
12732
+ this.messageManager.handleMessage(combined);
12733
+ }, debounceMs);
12734
+ const channelDebounceMsSetting = this.runtime.getSetting("DISCORD_CHANNEL_DEBOUNCE_MS");
12735
+ const channelDebounceMs = typeof channelDebounceMsSetting === "number" ? channelDebounceMsSetting : typeof channelDebounceMsSetting === "string" && channelDebounceMsSetting.trim() ? Number.parseInt(channelDebounceMsSetting, 10) || 3000 : 3000;
12736
+ const responseCooldownMsSetting = this.runtime.getSetting("DISCORD_RESPONSE_COOLDOWN_MS");
12737
+ const responseCooldownMs = typeof responseCooldownMsSetting === "number" ? responseCooldownMsSetting : typeof responseCooldownMsSetting === "string" && responseCooldownMsSetting.trim() ? Number.parseInt(responseCooldownMsSetting, 10) || 30000 : 30000;
12738
+ this.channelDebouncer = createChannelDebouncer((messages) => {
12739
+ if (!this.messageManager || messages.length === 0) {
12740
+ return;
12741
+ }
12742
+ const clientUser = this.client?.user;
12743
+ const botId = clientUser?.id;
12744
+ const agentName = this.character?.name?.toLowerCase();
12745
+ let anchor;
12746
+ if (botId) {
12747
+ anchor = messages.find((message) => message.mentions?.users?.has(botId) || Boolean(agentName && agentName.length >= 2 && message.content?.toLowerCase().includes(agentName)));
12748
+ if (!anchor) {
12749
+ anchor = messages.find((message) => Boolean(message.reference?.messageId) && message.mentions?.repliedUser?.id === botId);
12750
+ }
12751
+ }
12752
+ anchor ??= messages[messages.length - 1];
12753
+ if (messages.length === 1) {
12754
+ this.messageManager.handleMessage(anchor);
12755
+ } else {
12756
+ const contextLines = messages.filter((message) => message.id !== anchor?.id).map((message) => `${message.member?.displayName ?? message.author.globalName ?? message.author.displayName ?? message.author.username}: ${message.content}`);
12757
+ const combinedText = contextLines.length > 0 ? `[Recent channel context]
12758
+ ${contextLines.join(`
12759
+ `)}
12760
+
12761
+ ${anchor.content || ""}` : anchor.content || "";
12762
+ const combined = Object.create(anchor, {
12763
+ content: { value: combinedText, writable: true, enumerable: true }
12764
+ });
12765
+ this.messageManager.handleMessage(combined);
12766
+ }
12767
+ this.channelDebouncer?.markResponded(messages[0].channel.id);
12768
+ }, {
12769
+ debounceMs: channelDebounceMs,
12770
+ responseCooldownMs,
12771
+ getBotUserId: () => this.client?.user?.id,
12772
+ botName: this.character?.name
12773
+ });
10332
12774
  this.client.on("messageCreate", async (message) => {
10333
12775
  const clientUser = this.client?.user;
10334
12776
  if (clientUser && message.author.id === clientUser.id || message.author.bot && this.discordSettings.shouldIgnoreBotMessages) {
@@ -10394,8 +12836,23 @@ class DiscordService extends Service {
10394
12836
  }
10395
12837
  }
10396
12838
  try {
10397
- if (this.messageManager) {
10398
- this.messageManager.handleMessage(message);
12839
+ if (!this.messageManager) {
12840
+ return;
12841
+ }
12842
+ const channelType = message.channel.type;
12843
+ const isDm = channelType === DiscordChannelType6.DM || channelType === DiscordChannelType6.GroupDM;
12844
+ if (isDm) {
12845
+ if (this.messageDebouncer) {
12846
+ this.messageDebouncer.enqueue(message);
12847
+ } else {
12848
+ await this.messageManager.handleMessage(message);
12849
+ }
12850
+ } else if (this.channelDebouncer) {
12851
+ this.channelDebouncer.enqueue(message);
12852
+ } else if (this.messageDebouncer) {
12853
+ this.messageDebouncer.enqueue(message);
12854
+ } else {
12855
+ await this.messageManager.handleMessage(message);
10399
12856
  }
10400
12857
  } catch (error) {
10401
12858
  this.runtime.logger.error({
@@ -10464,6 +12921,18 @@ class DiscordService extends Service {
10464
12921
  }
10465
12922
  });
10466
12923
  this.client.on("interactionCreate", async (interaction) => {
12924
+ if (interaction.isAutocomplete()) {
12925
+ try {
12926
+ await handleAutocomplete(interaction);
12927
+ } catch (error) {
12928
+ this.runtime.logger.error({
12929
+ src: "plugin:discord",
12930
+ agentId: this.runtime.agentId,
12931
+ error: error instanceof Error ? error.message : String(error)
12932
+ }, "Error handling Discord autocomplete interaction");
12933
+ }
12934
+ return;
12935
+ }
10467
12936
  const isSlashCommand = interaction.isCommand();
10468
12937
  const isModalSubmit = interaction.isModalSubmit();
10469
12938
  const isComponent = interaction.isMessageComponent();
@@ -10571,6 +13040,9 @@ class DiscordService extends Service {
10571
13040
  }
10572
13041
  try {
10573
13042
  await this.handleInteractionCreate(interaction);
13043
+ if (interaction.isChatInputCommand()) {
13044
+ await handleSlashCommand(interaction, this.runtime);
13045
+ }
10574
13046
  } catch (error) {
10575
13047
  this.runtime.logger.error({
10576
13048
  src: "plugin:discord",
@@ -10588,7 +13060,7 @@ class DiscordService extends Service {
10588
13060
  }
10589
13061
  });
10590
13062
  const auditLogSetting = this.runtime.getSetting("DISCORD_AUDIT_LOG_ENABLED");
10591
- const isAuditLogEnabled = auditLogSetting !== "false" && auditLogSetting !== false;
13063
+ const isAuditLogEnabled = auditLogSetting === "true" || auditLogSetting === true || auditLogSetting === "1" || auditLogSetting === 1;
10592
13064
  if (isAuditLogEnabled) {
10593
13065
  this.client.on("channelUpdate", async (oldChannel, newChannel) => {
10594
13066
  try {
@@ -10785,8 +13257,8 @@ class DiscordService extends Service {
10785
13257
  this.runtime.logger.info(`New member joined: ${member.user.username} (${member.id})`);
10786
13258
  const guild = member.guild;
10787
13259
  const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
10788
- const worldId = createUniqueUuid6(this.runtime, guild.id);
10789
- const entityId = createUniqueUuid6(this.runtime, member.id);
13260
+ const worldId = createUniqueUuid7(this.runtime, guild.id);
13261
+ const entityId = this.resolveDiscordEntityId(member.id);
10790
13262
  this.runtime.emitEvent(["DISCORD_USER_JOINED" /* ENTITY_JOINED */], {
10791
13263
  runtime: this.runtime,
10792
13264
  entityId,
@@ -10803,7 +13275,7 @@ class DiscordService extends Service {
10803
13275
  member
10804
13276
  });
10805
13277
  }
10806
- async registerSlashCommands(commands) {
13278
+ async registerSlashCommands(commands2) {
10807
13279
  await this.clientReadyPromise;
10808
13280
  const sanitizeCommandForLogging = (cmd) => {
10809
13281
  const sanitized = {
@@ -10823,11 +13295,11 @@ class DiscordService extends Service {
10823
13295
  }
10824
13296
  return sanitized;
10825
13297
  };
10826
- const sanitizedCommands = commands.map(sanitizeCommandForLogging);
13298
+ const sanitizedCommands = commands2.map(sanitizeCommandForLogging);
10827
13299
  this.runtime.logger.debug({
10828
13300
  src: "plugin:discord",
10829
13301
  agentId: this.runtime.agentId,
10830
- commandCount: commands.length,
13302
+ commandCount: commands2.length,
10831
13303
  commands: sanitizedCommands
10832
13304
  }, "Registering Discord commands");
10833
13305
  const clientApplication = this.client?.application;
@@ -10835,11 +13307,11 @@ class DiscordService extends Service {
10835
13307
  this.runtime.logger.warn({ src: "plugin:discord", agentId: this.runtime.agentId }, "Cannot register commands - Discord client application not available");
10836
13308
  return;
10837
13309
  }
10838
- if (!Array.isArray(commands) || commands.length === 0) {
13310
+ if (!Array.isArray(commands2) || commands2.length === 0) {
10839
13311
  this.runtime.logger.warn({ src: "plugin:discord", agentId: this.runtime.agentId }, "Cannot register commands - no commands provided");
10840
13312
  return;
10841
13313
  }
10842
- for (const cmd of commands) {
13314
+ for (const cmd of commands2) {
10843
13315
  if (!cmd.name || !cmd.description) {
10844
13316
  this.runtime.logger.warn({
10845
13317
  src: "plugin:discord",
@@ -10858,7 +13330,7 @@ class DiscordService extends Service {
10858
13330
  commandMap.set(cmd.name, cmd);
10859
13331
  }
10860
13332
  }
10861
- for (const cmd of commands) {
13333
+ for (const cmd of commands2) {
10862
13334
  if (cmd.name) {
10863
13335
  commandMap.set(cmd.name, cmd);
10864
13336
  }
@@ -10997,7 +13469,7 @@ class DiscordService extends Service {
10997
13469
  this.runtime.logger.info({
10998
13470
  src: "plugin:discord",
10999
13471
  agentId: this.runtime.agentId,
11000
- newCommands: commands.length,
13472
+ newCommands: commands2.length,
11001
13473
  totalCommands: this.slashCommands.length,
11002
13474
  globalCommands: transformedGlobalCommands.length,
11003
13475
  globalCommandsRegisteredForDMs: globalCommandsRegistered,
@@ -11086,7 +13558,7 @@ class DiscordService extends Service {
11086
13558
  }, "Failed to register commands to newly joined guild");
11087
13559
  }
11088
13560
  }
11089
- const worldId = createUniqueUuid6(this.runtime, fullGuild.id);
13561
+ const worldId = createUniqueUuid7(this.runtime, fullGuild.id);
11090
13562
  const standardizedData = {
11091
13563
  runtime: this.runtime,
11092
13564
  rooms: await this.buildStandardizedRooms(fullGuild, worldId),
@@ -11113,11 +13585,11 @@ class DiscordService extends Service {
11113
13585
  this.runtime.emitEvent([EventType3.WORLD_JOINED], standardizedData);
11114
13586
  }
11115
13587
  async handleInteractionCreate(interaction) {
11116
- const entityId = createUniqueUuid6(this.runtime, interaction.user.id);
13588
+ const entityId = this.resolveDiscordEntityId(interaction.user.id);
11117
13589
  const userName = interaction.user.bot ? `${interaction.user.username}#${interaction.user.discriminator}` : interaction.user.username;
11118
13590
  const name = interaction.user.displayName;
11119
13591
  const interactionChannelId = interaction.channel?.id;
11120
- const roomId = createUniqueUuid6(this.runtime, interactionChannelId || userName);
13592
+ const roomId = createUniqueUuid7(this.runtime, interactionChannelId || userName);
11121
13593
  let type;
11122
13594
  let serverId;
11123
13595
  if (interaction.guild) {
@@ -11132,19 +13604,20 @@ class DiscordService extends Service {
11132
13604
  }
11133
13605
  serverId = guild.id;
11134
13606
  } else {
11135
- type = ChannelType7.DM;
13607
+ type = ChannelType8.DM;
11136
13608
  serverId = interactionChannelId;
11137
13609
  }
11138
13610
  await this.runtime.ensureConnection({
11139
13611
  entityId,
11140
13612
  roomId,
13613
+ roomName: interaction.guild && interaction.channel && "name" in interaction.channel && typeof interaction.channel.name === "string" ? interaction.channel.name : name,
11141
13614
  userName,
11142
13615
  name,
11143
13616
  source: "discord",
11144
13617
  channelId: interactionChannelId,
11145
- messageServerId: serverId ? stringToUuid3(serverId) : undefined,
13618
+ messageServerId: serverId ? stringToUuid4(serverId) : undefined,
11146
13619
  type,
11147
- worldId: createUniqueUuid6(this.runtime, serverId ?? roomId),
13620
+ worldId: createUniqueUuid7(this.runtime, serverId ?? roomId),
11148
13621
  worldName: interaction.guild?.name || undefined,
11149
13622
  userId: interaction.user.id,
11150
13623
  metadata: buildDiscordWorldMetadata(this.runtime, interaction.guild?.ownerId)
@@ -11333,23 +13806,23 @@ class DiscordService extends Service {
11333
13806
  async buildStandardizedRooms(guild, _worldId) {
11334
13807
  const rooms = [];
11335
13808
  for (const [channelId, channel] of guild.channels.cache) {
11336
- if (channel.type === DiscordChannelType5.GuildText || channel.type === DiscordChannelType5.GuildVoice) {
11337
- const roomId = createUniqueUuid6(this.runtime, channelId);
13809
+ if (channel.type === DiscordChannelType6.GuildText || channel.type === DiscordChannelType6.GuildVoice) {
13810
+ const roomId = createUniqueUuid7(this.runtime, channelId);
11338
13811
  let channelType;
11339
13812
  switch (channel.type) {
11340
- case DiscordChannelType5.GuildText:
11341
- channelType = ChannelType7.GROUP;
13813
+ case DiscordChannelType6.GuildText:
13814
+ channelType = ChannelType8.GROUP;
11342
13815
  break;
11343
- case DiscordChannelType5.GuildVoice:
11344
- channelType = ChannelType7.VOICE_GROUP;
13816
+ case DiscordChannelType6.GuildVoice:
13817
+ channelType = ChannelType8.VOICE_GROUP;
11345
13818
  break;
11346
13819
  default:
11347
- channelType = ChannelType7.GROUP;
13820
+ channelType = ChannelType8.GROUP;
11348
13821
  }
11349
13822
  let participants = [];
11350
- if (guild.memberCount < 1000 && channel.type === DiscordChannelType5.GuildText) {
13823
+ if (guild.memberCount < 1000 && channel.type === DiscordChannelType6.GuildText) {
11351
13824
  try {
11352
- participants = Array.from(guild.members.cache.values()).filter((member) => channel.permissionsFor(member)?.has(PermissionsBitField5.Flags.ViewChannel)).map((member) => createUniqueUuid6(this.runtime, member.id));
13825
+ participants = Array.from(guild.members.cache.values()).filter((member) => channel.permissionsFor(member)?.has(PermissionsBitField5.Flags.ViewChannel)).map((member) => this.resolveDiscordEntityId(member.id));
11353
13826
  } catch (error) {
11354
13827
  this.runtime.logger.warn({
11355
13828
  src: "plugin:discord",
@@ -11390,14 +13863,14 @@ class DiscordService extends Service {
11390
13863
  const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
11391
13864
  if (member.id !== botId) {
11392
13865
  entities.push({
11393
- id: createUniqueUuid6(this.runtime, member.id),
13866
+ id: this.resolveDiscordEntityId(member.id),
11394
13867
  names: Array.from(new Set([
11395
13868
  member.user.username,
11396
13869
  member.displayName,
11397
13870
  member.user.globalName
11398
13871
  ].filter(Boolean))),
11399
13872
  agentId: this.runtime.agentId,
11400
- metadata: buildDiscordEntityMetadata(member.id, tag, member.displayName || member.user.username, member.user.globalName ?? undefined)
13873
+ metadata: buildDiscordEntityMetadata(member.id, tag, member.displayName || member.user.username, member.user.globalName ?? undefined, member.user.displayAvatarURL())
11401
13874
  });
11402
13875
  }
11403
13876
  }
@@ -11410,7 +13883,7 @@ class DiscordService extends Service {
11410
13883
  const onlineMembers = await guild.members.fetch({ limit: 100 });
11411
13884
  for (const [, member] of onlineMembers) {
11412
13885
  if (member.id !== botId) {
11413
- const entityId = createUniqueUuid6(this.runtime, member.id);
13886
+ const entityId = this.resolveDiscordEntityId(member.id);
11414
13887
  if (!entities.some((u) => u.id === entityId)) {
11415
13888
  const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
11416
13889
  entities.push({
@@ -11421,7 +13894,7 @@ class DiscordService extends Service {
11421
13894
  member.user.globalName
11422
13895
  ].filter(Boolean))),
11423
13896
  agentId: this.runtime.agentId,
11424
- metadata: buildDiscordEntityMetadata(member.id, tag, member.displayName || member.user.username, member.user.globalName ?? undefined)
13897
+ metadata: buildDiscordEntityMetadata(member.id, tag, member.displayName || member.user.username, member.user.globalName ?? undefined, member.user.displayAvatarURL())
11425
13898
  });
11426
13899
  }
11427
13900
  }
@@ -11445,14 +13918,14 @@ class DiscordService extends Service {
11445
13918
  if (member.id !== botId) {
11446
13919
  const tag = member.user.bot ? `${member.user.username}#${member.user.discriminator}` : member.user.username;
11447
13920
  entities.push({
11448
- id: createUniqueUuid6(this.runtime, member.id),
13921
+ id: this.resolveDiscordEntityId(member.id),
11449
13922
  names: Array.from(new Set([
11450
13923
  member.user.username,
11451
13924
  member.displayName,
11452
13925
  member.user.globalName
11453
13926
  ].filter(Boolean))),
11454
13927
  agentId: this.runtime.agentId,
11455
- metadata: buildDiscordEntityMetadata(member.id, tag, member.displayName || member.user.username, member.user.globalName ?? undefined)
13928
+ metadata: buildDiscordEntityMetadata(member.id, tag, member.displayName || member.user.username, member.user.globalName ?? undefined, member.user.displayAvatarURL())
11456
13929
  });
11457
13930
  }
11458
13931
  }
@@ -11469,13 +13942,30 @@ class DiscordService extends Service {
11469
13942
  }
11470
13943
  async onReady(readyClient) {
11471
13944
  this.runtime.logger.success("Discord client ready");
13945
+ const discordApiToken = this.runtime.getSetting("DISCORD_API_TOKEN");
13946
+ if (typeof discordApiToken === "string" && discordApiToken.trim().length > 0 && typeof readyClient.rest?.setToken === "function") {
13947
+ readyClient.rest.setToken(discordApiToken.trim());
13948
+ }
13949
+ await this.refreshOwnerDiscordUserIds(readyClient);
11472
13950
  this.slashCommands = [];
11473
13951
  this.runtime.registerEvent("DISCORD_REGISTER_COMMANDS", async (params) => {
11474
13952
  await this.registerSlashCommands(params.commands);
11475
13953
  });
13954
+ await registerSlashCommands(this.runtime);
11476
13955
  const auditLogSettingForInvite = this.runtime.getSetting("DISCORD_AUDIT_LOG_ENABLED");
11477
- const isAuditLogEnabledForInvite = auditLogSettingForInvite !== "false" && auditLogSettingForInvite !== false;
13956
+ const isAuditLogEnabledForInvite = auditLogSettingForInvite === "true" || auditLogSettingForInvite === true || auditLogSettingForInvite === "1" || auditLogSettingForInvite === 1;
11478
13957
  const readyClientUser = readyClient.user;
13958
+ if (readyClientUser) {
13959
+ try {
13960
+ await syncDiscordClientProfile(this.runtime, readyClientUser, this.discordSettings);
13961
+ } catch (error) {
13962
+ this.runtime.logger.warn({
13963
+ src: "plugin:discord",
13964
+ agentId: this.runtime.agentId,
13965
+ error: error instanceof Error ? error.message : String(error)
13966
+ }, "Failed to synchronize Discord bot profile from connector settings");
13967
+ }
13968
+ }
11479
13969
  const inviteUrl = readyClientUser?.id ? generateInviteUrl(readyClientUser.id, "MODERATOR_VOICE") : undefined;
11480
13970
  if (isAuditLogEnabledForInvite) {
11481
13971
  this.runtime.logger.info({ src: "plugin:discord", agentId: this.runtime.agentId }, "Audit log tracking enabled - ensure bot has ViewAuditLog permission in server settings");
@@ -11498,7 +13988,7 @@ class DiscordService extends Service {
11498
13988
  try {
11499
13989
  const fullGuild = await guild.fetch();
11500
13990
  this.runtime.logger.info(`Discord server connected: ${fullGuild.name} (${fullGuild.id})`);
11501
- const worldId = createUniqueUuid6(this.runtime, fullGuild.id);
13991
+ const worldId = createUniqueUuid7(this.runtime, fullGuild.id);
11502
13992
  const standardizedData = {
11503
13993
  name: fullGuild.name,
11504
13994
  runtime: this.runtime,
@@ -11535,7 +14025,7 @@ class DiscordService extends Service {
11535
14025
  this.timeouts.push(timeoutId);
11536
14026
  }
11537
14027
  const auditLogEnabled = this.runtime.getSetting("DISCORD_AUDIT_LOG_ENABLED");
11538
- if (auditLogEnabled !== "false" && auditLogEnabled !== false) {
14028
+ if (auditLogEnabled === "true" || auditLogEnabled === true || auditLogEnabled === "1" || auditLogEnabled === 1) {
11539
14029
  try {
11540
14030
  const testGuild = guilds.first();
11541
14031
  if (testGuild) {
@@ -11544,11 +14034,15 @@ class DiscordService extends Service {
11544
14034
  this.runtime.logger.debug("Audit log access verified for permission tracking");
11545
14035
  }
11546
14036
  } catch (err) {
11547
- this.runtime.logger.warn({
14037
+ const errorMessage = err instanceof Error ? err.message : String(err);
14038
+ const errorCode = typeof err === "object" && err !== null && "code" in err && typeof err.code !== "undefined" ? String(err.code) : "";
14039
+ const missingAuditLogPermission = errorCode === "50013" || errorMessage.includes("Missing Permissions");
14040
+ const logMethod = missingAuditLogPermission ? this.runtime.logger.info : this.runtime.logger.warn;
14041
+ logMethod.call(this.runtime.logger, {
11548
14042
  src: "plugin:discord",
11549
14043
  agentId: this.runtime.agentId,
11550
- error: err instanceof Error ? err.message : String(err)
11551
- }, "Cannot access audit logs - permission change alerts will not include executor info");
14044
+ error: errorMessage
14045
+ }, missingAuditLogPermission ? "Audit log access unavailable - permission change alerts will not include executor info" : "Cannot access audit logs - permission change alerts will not include executor info");
11552
14046
  }
11553
14047
  }
11554
14048
  if (this.client) {
@@ -11574,7 +14068,7 @@ class DiscordService extends Service {
11574
14068
  this.runtime.logger.error({ src: "plugin:discord", agentId: this.runtime.agentId, channelId }, "Channel not found");
11575
14069
  return [];
11576
14070
  }
11577
- if (channel.type !== DiscordChannelType5.GuildText) {
14071
+ if (channel.type !== DiscordChannelType6.GuildText) {
11578
14072
  this.runtime.logger.error({ src: "plugin:discord", agentId: this.runtime.agentId, channelId }, "Channel is not a text channel");
11579
14073
  return [];
11580
14074
  }
@@ -11706,9 +14200,9 @@ class DiscordService extends Service {
11706
14200
  }
11707
14201
  }
11708
14202
  const timestamp = Date.now();
11709
- const roomId = createUniqueUuid6(this.runtime, reaction.message.channel.id);
11710
- const entityId = createUniqueUuid6(this.runtime, user.id);
11711
- const reactionUUID = createUniqueUuid6(this.runtime, `${reaction.message.id}-${user.id}-${emoji}-${timestamp}`);
14203
+ const roomId = createUniqueUuid7(this.runtime, reaction.message.channel.id);
14204
+ const entityId = this.resolveDiscordEntityId(user.id);
14205
+ const reactionUUID = createUniqueUuid7(this.runtime, `${reaction.message.id}-${user.id}-${emoji}-${timestamp}`);
11712
14206
  if (!entityId || !roomId) {
11713
14207
  this.runtime.logger.error({
11714
14208
  src: "plugin:discord",
@@ -11728,18 +14222,19 @@ class DiscordService extends Service {
11728
14222
  await this.runtime.ensureConnection({
11729
14223
  entityId,
11730
14224
  roomId,
14225
+ roomName: "name" in reaction.message.channel && typeof reaction.message.channel.name === "string" ? reaction.message.channel.name : name,
11731
14226
  userName,
11732
- worldId: createUniqueUuid6(this.runtime, reaction.message.guild?.id ?? roomId),
14227
+ worldId: createUniqueUuid7(this.runtime, reaction.message.guild?.id ?? roomId),
11733
14228
  worldName: reaction.message.guild?.name || undefined,
11734
14229
  name,
11735
14230
  source: "discord",
11736
14231
  channelId: reaction.message.channel.id,
11737
- messageServerId: reaction.message.guild?.id ? stringToUuid3(reaction.message.guild.id) : undefined,
14232
+ messageServerId: reaction.message.guild?.id ? stringToUuid4(reaction.message.guild.id) : undefined,
11738
14233
  type: channelType,
11739
14234
  userId: user.id,
11740
14235
  metadata: buildDiscordWorldMetadata(this.runtime, reaction.message.guild?.ownerId)
11741
14236
  });
11742
- const inReplyTo = createUniqueUuid6(this.runtime, reaction.message.id);
14237
+ const inReplyTo = createUniqueUuid7(this.runtime, reaction.message.id);
11743
14238
  const memory = {
11744
14239
  id: reactionUUID,
11745
14240
  entityId,
@@ -11750,6 +14245,16 @@ class DiscordService extends Service {
11750
14245
  inReplyTo,
11751
14246
  channelType
11752
14247
  },
14248
+ metadata: {
14249
+ entityName: name,
14250
+ entityUserName: userName,
14251
+ fromId: user.id,
14252
+ discordReaction: {
14253
+ action: type,
14254
+ emoji,
14255
+ targetMessageId: inReplyTo
14256
+ }
14257
+ },
11753
14258
  roomId,
11754
14259
  createdAt: timestamp
11755
14260
  };
@@ -11758,7 +14263,11 @@ class DiscordService extends Service {
11758
14263
  this.runtime.logger.error({ src: "plugin:discord", agentId: this.runtime.agentId }, "No channel found for reaction message");
11759
14264
  return [];
11760
14265
  }
11761
- await reaction.message.channel.send(content.text ?? "");
14266
+ const responseText = normalizeDiscordMessageText(content.text);
14267
+ if (!responseText.trim()) {
14268
+ return [];
14269
+ }
14270
+ await reaction.message.channel.send(responseText);
11762
14271
  return [];
11763
14272
  };
11764
14273
  const events = type === "add" ? ["DISCORD_REACTION_RECEIVED" /* REACTION_RECEIVED */, EventType3.REACTION_RECEIVED] : ["DISCORD_REACTION_REMOVED" /* REACTION_REMOVED */];
@@ -11817,7 +14326,7 @@ class DiscordService extends Service {
11817
14326
  }
11818
14327
  async getSpiderState(channelId) {
11819
14328
  try {
11820
- const stateId = createUniqueUuid6(this.runtime, `discord-spider-state-${channelId}`);
14329
+ const stateId = createUniqueUuid7(this.runtime, `discord-spider-state-${channelId}`);
11821
14330
  const stateMemory = await this.runtime.getMemoryById(stateId);
11822
14331
  const stateMemoryContent = stateMemory?.content;
11823
14332
  if (stateMemoryContent?.text) {
@@ -11842,8 +14351,8 @@ class DiscordService extends Service {
11842
14351
  }
11843
14352
  async saveSpiderState(state) {
11844
14353
  try {
11845
- const stateId = createUniqueUuid6(this.runtime, `discord-spider-state-${state.channelId}`);
11846
- const roomId = createUniqueUuid6(this.runtime, state.channelId);
14354
+ const stateId = createUniqueUuid7(this.runtime, `discord-spider-state-${state.channelId}`);
14355
+ const roomId = createUniqueUuid7(this.runtime, state.channelId);
11847
14356
  this.runtime.logger.debug(`[SpiderState] Saving channel=${state.channelId} stateId=${stateId}`);
11848
14357
  let existing = null;
11849
14358
  try {
@@ -11875,7 +14384,7 @@ class DiscordService extends Service {
11875
14384
  }
11876
14385
  }
11877
14386
  } catch {}
11878
- worldId = createUniqueUuid6(this.runtime, serverId ?? state.channelId);
14387
+ worldId = createUniqueUuid7(this.runtime, serverId ?? state.channelId);
11879
14388
  const entityId = this.runtime.agentId;
11880
14389
  try {
11881
14390
  const entity = await this.runtime.getEntityById(entityId);
@@ -11899,7 +14408,7 @@ class DiscordService extends Service {
11899
14408
  id: worldId,
11900
14409
  name: serverId ? `Discord Server ${serverId}` : `Spider World ${state.channelId}`,
11901
14410
  agentId: this.runtime.agentId,
11902
- messageServerId: stringToUuid3(serverId ?? state.channelId)
14411
+ messageServerId: stringToUuid4(serverId ?? state.channelId)
11903
14412
  });
11904
14413
  this.runtime.logger.debug(`[SpiderState] World ensured: ${worldId}`);
11905
14414
  } catch (worldError) {
@@ -11911,9 +14420,9 @@ class DiscordService extends Service {
11911
14420
  id: roomId,
11912
14421
  name: channelName,
11913
14422
  source: "discord",
11914
- type: ChannelType7.GROUP,
14423
+ type: ChannelType8.GROUP,
11915
14424
  channelId: state.channelId,
11916
- messageServerId: stringToUuid3(serverId ?? state.channelId),
14425
+ messageServerId: stringToUuid4(serverId ?? state.channelId),
11917
14426
  worldId
11918
14427
  });
11919
14428
  this.runtime.logger.debug(`[SpiderState] Room ensured: ${roomId}`);
@@ -11997,24 +14506,24 @@ class DiscordService extends Service {
11997
14506
  }
11998
14507
  const channel = fetchedChannel;
11999
14508
  const serverId = "guild" in channel && channel.guild ? channel.guild.id : ("guildId" in channel) && channel.guildId ? channel.guildId : channel.id;
12000
- const worldId = serverId ? createUniqueUuid6(this.runtime, serverId) : this.runtime.agentId;
14509
+ const worldId = serverId ? createUniqueUuid7(this.runtime, serverId) : this.runtime.agentId;
12001
14510
  await this.runtime.ensureWorldExists({
12002
14511
  id: worldId,
12003
14512
  agentId: this.runtime.agentId,
12004
- messageServerId: stringToUuid3(serverId),
14513
+ messageServerId: stringToUuid4(serverId),
12005
14514
  name: (() => {
12006
14515
  const channelGuild = "guild" in channel ? channel.guild : null;
12007
14516
  return channelGuild?.name || "Discord";
12008
14517
  })()
12009
14518
  });
12010
14519
  await this.runtime.ensureRoomExists({
12011
- id: createUniqueUuid6(this.runtime, channel.id),
14520
+ id: createUniqueUuid7(this.runtime, channel.id),
12012
14521
  agentId: this.runtime.agentId,
12013
14522
  name: "name" in channel && channel.name || channel.id,
12014
14523
  source: "discord",
12015
14524
  type: await this.getChannelType(channel),
12016
14525
  channelId: channel.id,
12017
- messageServerId: stringToUuid3(serverId),
14526
+ messageServerId: stringToUuid4(serverId),
12018
14527
  worldId
12019
14528
  });
12020
14529
  const spiderState = options.force ? null : await this.getSpiderState(channelId);
@@ -12371,13 +14880,13 @@ class DiscordService extends Service {
12371
14880
  if (!message.author || !message.channel) {
12372
14881
  return null;
12373
14882
  }
12374
- const entityId = createUniqueUuid6(this.runtime, message.author.id);
12375
- const roomId = createUniqueUuid6(this.runtime, message.channel.id);
14883
+ const entityId = this.resolveDiscordEntityId(message.author.id);
14884
+ const roomId = createUniqueUuid7(this.runtime, message.channel.id);
12376
14885
  const channel = message.channel;
12377
14886
  const channelType = await this.getChannelType(channel);
12378
14887
  const channelGuild = "guild" in channel ? channel.guild : null;
12379
14888
  const serverId = channelGuild?.id ? channelGuild.id : message.guild?.id ?? message.channel.id;
12380
- const worldId = serverId ? createUniqueUuid6(this.runtime, serverId) : this.runtime.agentId;
14889
+ const worldId = serverId ? createUniqueUuid7(this.runtime, serverId) : this.runtime.agentId;
12381
14890
  let textContent;
12382
14891
  let attachments;
12383
14892
  const optionsProcessedContent = options?.processedContent;
@@ -12394,6 +14903,8 @@ class DiscordService extends Service {
12394
14903
  const metadata = {
12395
14904
  type: "custom",
12396
14905
  entityName: (message.member && "displayName" in message.member && typeof message.member.displayName === "string" ? message.member.displayName : undefined) ?? ("globalName" in message.author && typeof message.author.globalName === "string" ? message.author.globalName : undefined) ?? message.author.username,
14906
+ entityUserName: message.author.username,
14907
+ entityAvatarUrl: message.author.displayAvatarURL(),
12397
14908
  fromBot: message.author.bot,
12398
14909
  fromId: message.author.id,
12399
14910
  sourceId: entityId,
@@ -12407,7 +14918,7 @@ class DiscordService extends Service {
12407
14918
  ...options?.extraMetadata ? options.extraMetadata : {}
12408
14919
  };
12409
14920
  const memory = {
12410
- id: createUniqueUuid6(this.runtime, message.id),
14921
+ id: createUniqueUuid7(this.runtime, message.id),
12411
14922
  entityId,
12412
14923
  agentId: this.runtime.agentId,
12413
14924
  roomId,
@@ -12417,7 +14928,7 @@ class DiscordService extends Service {
12417
14928
  source: "discord",
12418
14929
  channelType,
12419
14930
  url: message.url,
12420
- inReplyTo: message.reference?.messageId ? createUniqueUuid6(this.runtime, message.reference.messageId) : undefined,
14931
+ inReplyTo: message.reference?.messageId ? createUniqueUuid7(this.runtime, message.reference.messageId) : undefined,
12421
14932
  ...options?.extraContent ? options.extraContent : {}
12422
14933
  },
12423
14934
  metadata,
@@ -12444,20 +14955,20 @@ class DiscordService extends Service {
12444
14955
  const channelType = await this.getChannelType(firstMessage.channel);
12445
14956
  const firstMessageChannelGuild = "guild" in firstMessage.channel ? firstMessage.channel.guild : null;
12446
14957
  const serverId = firstMessageChannelGuild?.id ? firstMessageChannelGuild.id : firstMessage.guild?.id ?? firstMessage.channel.id;
12447
- const worldId = serverId ? createUniqueUuid6(this.runtime, serverId) : this.runtime.agentId;
14958
+ const worldId = serverId ? createUniqueUuid7(this.runtime, serverId) : this.runtime.agentId;
12448
14959
  const entities = Array.from(uniqueAuthors.entries()).map(([authorId, message]) => {
12449
14960
  const userName = message.author.username;
12450
14961
  const name = (message.member && "displayName" in message.member && typeof message.member.displayName === "string" ? message.member.displayName : undefined) ?? ("globalName" in message.author && typeof message.author.globalName === "string" ? message.author.globalName : undefined) ?? userName;
12451
14962
  return {
12452
- id: createUniqueUuid6(this.runtime, authorId),
14963
+ id: this.resolveDiscordEntityId(authorId),
12453
14964
  names: [userName, name].filter((n) => typeof n === "string" && n.length > 0),
12454
- metadata: buildDiscordEntityMetadata(authorId, userName, name),
14965
+ metadata: buildDiscordEntityMetadata(authorId, userName, name, undefined, message.author.displayAvatarURL()),
12455
14966
  agentId: this.runtime.agentId
12456
14967
  };
12457
14968
  });
12458
14969
  const rooms = [
12459
14970
  {
12460
- id: createUniqueUuid6(this.runtime, firstMessage.channel.id),
14971
+ id: createUniqueUuid7(this.runtime, firstMessage.channel.id),
12461
14972
  channelId: firstMessage.channel.id,
12462
14973
  type: channelType,
12463
14974
  source: "discord"
@@ -12465,7 +14976,7 @@ class DiscordService extends Service {
12465
14976
  ];
12466
14977
  const world = {
12467
14978
  id: worldId,
12468
- messageServerId: stringToUuid3(serverId),
14979
+ messageServerId: stringToUuid4(serverId),
12469
14980
  name: firstMessage.guild?.name ?? `DM-${firstMessage.channel.id}`,
12470
14981
  agentId: this.runtime.agentId,
12471
14982
  metadata: buildDiscordWorldMetadata(this.runtime, firstMessageChannelGuild?.ownerId ?? firstMessage.guild?.ownerId ?? undefined)
@@ -12487,37 +14998,56 @@ class DiscordService extends Service {
12487
14998
  this.runtime.logger.info("Stopping Discord service");
12488
14999
  this.timeouts.forEach(clearTimeout);
12489
15000
  this.timeouts = [];
15001
+ this.messageDebouncer?.destroy();
15002
+ this.channelDebouncer?.destroy();
15003
+ this.messageDebouncer = undefined;
15004
+ this.channelDebouncer = undefined;
15005
+ this.userSelections.clear();
15006
+ if (this.voiceManager) {
15007
+ try {
15008
+ this.voiceManager.stop();
15009
+ } catch (error) {
15010
+ this.runtime.logger.warn(`Discord voice cleanup failed: ${error instanceof Error ? error.message : String(error)}`);
15011
+ }
15012
+ }
12490
15013
  if (this.client) {
12491
- await this.client.destroy();
12492
- this.client = null;
12493
- this.runtime.logger.info("Discord client destroyed");
15014
+ try {
15015
+ await this.client.destroy();
15016
+ this.runtime.logger.info("Discord client destroyed");
15017
+ } catch (error) {
15018
+ this.runtime.logger.warn(`Discord client destroy failed: ${error instanceof Error ? error.message : String(error)}`);
15019
+ } finally {
15020
+ this.client = null;
15021
+ }
12494
15022
  }
12495
- if (this.voiceManager) {}
15023
+ this.clientReadyPromise = null;
15024
+ this.messageManager = undefined;
15025
+ this.voiceManager = undefined;
12496
15026
  this.runtime.logger.info("Discord service stopped");
12497
15027
  }
12498
15028
  async getChannelType(channel) {
12499
15029
  switch (channel.type) {
12500
- case DiscordChannelType5.DM:
12501
- return ChannelType7.DM;
12502
- case DiscordChannelType5.GroupDM:
12503
- return ChannelType7.DM;
12504
- case DiscordChannelType5.GuildText:
12505
- case DiscordChannelType5.GuildNews:
12506
- case DiscordChannelType5.PublicThread:
12507
- case DiscordChannelType5.PrivateThread:
12508
- case DiscordChannelType5.AnnouncementThread:
12509
- case DiscordChannelType5.GuildForum:
12510
- return ChannelType7.GROUP;
12511
- case DiscordChannelType5.GuildVoice:
12512
- case DiscordChannelType5.GuildStageVoice:
12513
- return ChannelType7.VOICE_GROUP;
15030
+ case DiscordChannelType6.DM:
15031
+ return ChannelType8.DM;
15032
+ case DiscordChannelType6.GroupDM:
15033
+ return ChannelType8.DM;
15034
+ case DiscordChannelType6.GuildText:
15035
+ case DiscordChannelType6.GuildNews:
15036
+ case DiscordChannelType6.PublicThread:
15037
+ case DiscordChannelType6.PrivateThread:
15038
+ case DiscordChannelType6.AnnouncementThread:
15039
+ case DiscordChannelType6.GuildForum:
15040
+ return ChannelType8.GROUP;
15041
+ case DiscordChannelType6.GuildVoice:
15042
+ case DiscordChannelType6.GuildStageVoice:
15043
+ return ChannelType8.VOICE_GROUP;
12514
15044
  default:
12515
15045
  this.runtime.logger.debug({
12516
15046
  src: "plugin:discord",
12517
15047
  agentId: this.runtime.agentId,
12518
15048
  channelType: channel.type
12519
15049
  }, "Unknown channel type, defaulting to GROUP");
12520
- return ChannelType7.GROUP;
15050
+ return ChannelType8.GROUP;
12521
15051
  }
12522
15052
  }
12523
15053
  }
@@ -12532,12 +15062,12 @@ import {
12532
15062
  VoiceConnectionStatus as VoiceConnectionStatus2
12533
15063
  } from "@discordjs/voice";
12534
15064
  import {
12535
- logger as logger4,
15065
+ logger as logger5,
12536
15066
  ModelType as ModelType21
12537
15067
  } from "@elizaos/core";
12538
15068
  import {
12539
15069
  AttachmentBuilder as AttachmentBuilder3,
12540
- ChannelType as ChannelType8,
15070
+ ChannelType as ChannelType9,
12541
15071
  Events as Events2
12542
15072
  } from "discord.js";
12543
15073
  var TEST_IMAGE_URL = "https://github.com/elizaOS/awesome-eliza/blob/main/assets/eliza-logo.jpg?raw=true";
@@ -12582,9 +15112,9 @@ class DiscordTestSuite {
12582
15112
  }
12583
15113
  const discordClient = this.discordClient.client;
12584
15114
  if (discordClient?.isReady()) {
12585
- logger4.success("DiscordService is already ready.");
15115
+ logger5.success("DiscordService is already ready.");
12586
15116
  } else {
12587
- logger4.info("Waiting for DiscordService to be ready...");
15117
+ logger5.info("Waiting for DiscordService to be ready...");
12588
15118
  if (!discordClient) {
12589
15119
  throw new Error("Discord client instance is missing within the service.");
12590
15120
  }
@@ -12618,14 +15148,14 @@ class DiscordTestSuite {
12618
15148
  guild: channel.guild,
12619
15149
  deferReply: async () => {},
12620
15150
  editReply: async (message) => {
12621
- logger4.info(`JoinChannel Slash Command Response: ${message}`);
15151
+ logger5.info(`JoinChannel Slash Command Response: ${message}`);
12622
15152
  }
12623
15153
  };
12624
15154
  if (!this.discordClient.voiceManager) {
12625
15155
  throw new Error("VoiceManager is not available on the Discord client.");
12626
15156
  }
12627
15157
  await this.discordClient.voiceManager.handleJoinChannelCommand(fakeJoinInteraction);
12628
- logger4.success("Join voice slash command test completed successfully.");
15158
+ logger5.success("Join voice slash command test completed successfully.");
12629
15159
  } catch (error) {
12630
15160
  throw new Error(`Error in join voice slash commands test: ${error}`);
12631
15161
  }
@@ -12643,14 +15173,14 @@ class DiscordTestSuite {
12643
15173
  const fakeLeaveInteraction = {
12644
15174
  guildId: channel.guildId,
12645
15175
  reply: async (message) => {
12646
- logger4.info(`LeaveChannel Slash Command Response: ${message}`);
15176
+ logger5.info(`LeaveChannel Slash Command Response: ${message}`);
12647
15177
  }
12648
15178
  };
12649
15179
  if (!this.discordClient.voiceManager) {
12650
15180
  throw new Error("VoiceManager is not available on the Discord client.");
12651
15181
  }
12652
15182
  await this.discordClient.voiceManager.handleLeaveChannelCommand(fakeLeaveInteraction);
12653
- logger4.success("Leave voice slash command test completed successfully.");
15183
+ logger5.success("Leave voice slash command test completed successfully.");
12654
15184
  } catch (error) {
12655
15185
  throw new Error(`Error in leave voice slash commands test: ${error}`);
12656
15186
  }
@@ -12662,7 +15192,7 @@ class DiscordTestSuite {
12662
15192
  try {
12663
15193
  await this.waitForVoiceManagerReady(this.discordClient);
12664
15194
  const channel = await this.getTestChannel(runtime);
12665
- if (!channel || channel.type !== ChannelType8.GuildVoice) {
15195
+ if (!channel || channel.type !== ChannelType9.GuildVoice) {
12666
15196
  throw new Error("Invalid voice channel.");
12667
15197
  }
12668
15198
  if (!this.discordClient.voiceManager) {
@@ -12680,7 +15210,7 @@ class DiscordTestSuite {
12680
15210
  }
12681
15211
  try {
12682
15212
  await entersState2(connection, VoiceConnectionStatus2.Ready, 1e4);
12683
- logger4.success(`Voice connection is ready in guild: ${guildId}`);
15213
+ logger5.success(`Voice connection is ready in guild: ${guildId}`);
12684
15214
  } catch (error) {
12685
15215
  throw new Error(`Voice connection failed to become ready: ${error}`);
12686
15216
  }
@@ -12774,10 +15304,10 @@ class DiscordTestSuite {
12774
15304
  const audioResource = createAudioResource2(responseStream);
12775
15305
  audioPlayer.play(audioResource);
12776
15306
  connection.subscribe(audioPlayer);
12777
- logger4.success("TTS playback started successfully.");
15307
+ logger5.success("TTS playback started successfully.");
12778
15308
  await new Promise((resolve, reject) => {
12779
15309
  audioPlayer.once(AudioPlayerStatus.Idle, () => {
12780
- logger4.info("TTS playback finished.");
15310
+ logger5.info("TTS playback finished.");
12781
15311
  resolve();
12782
15312
  });
12783
15313
  audioPlayer.once("error", (error) => {
@@ -13718,7 +16248,7 @@ var ActivityFlags = import_v10.default.ActivityFlags;
13718
16248
  var ActivityPlatform = import_v10.default.ActivityPlatform;
13719
16249
  var ActivityType = import_v10.default.ActivityType;
13720
16250
  var AllowedMentionsTypes = import_v10.default.AllowedMentionsTypes;
13721
- var ApplicationCommandOptionType = import_v10.default.ApplicationCommandOptionType;
16251
+ var ApplicationCommandOptionType2 = import_v10.default.ApplicationCommandOptionType;
13722
16252
  var ApplicationCommandPermissionType = import_v10.default.ApplicationCommandPermissionType;
13723
16253
  var ApplicationCommandType = import_v10.default.ApplicationCommandType;
13724
16254
  var ApplicationFlags = import_v10.default.ApplicationFlags;
@@ -13737,7 +16267,7 @@ var AutoModerationRuleTriggerType = import_v10.default.AutoModerationRuleTrigger
13737
16267
  var ButtonStyle = import_v10.default.ButtonStyle;
13738
16268
  var CDNRoutes = import_v10.default.CDNRoutes;
13739
16269
  var ChannelFlags = import_v10.default.ChannelFlags;
13740
- var ChannelType9 = import_v10.default.ChannelType;
16270
+ var ChannelType10 = import_v10.default.ChannelType;
13741
16271
  var ComponentType = import_v10.default.ComponentType;
13742
16272
  var ConnectionService = import_v10.default.ConnectionService;
13743
16273
  var ConnectionVisibility = import_v10.default.ConnectionVisibility;
@@ -13839,7 +16369,7 @@ function buildDiscordCommandOptions(args) {
13839
16369
  return {
13840
16370
  name: arg.name,
13841
16371
  description: arg.description,
13842
- type: ApplicationCommandOptionType.Number,
16372
+ type: ApplicationCommandOptionType2.Number,
13843
16373
  required
13844
16374
  };
13845
16375
  }
@@ -13847,7 +16377,7 @@ function buildDiscordCommandOptions(args) {
13847
16377
  return {
13848
16378
  name: arg.name,
13849
16379
  description: arg.description,
13850
- type: ApplicationCommandOptionType.Boolean,
16380
+ type: ApplicationCommandOptionType2.Boolean,
13851
16381
  required
13852
16382
  };
13853
16383
  }
@@ -13858,7 +16388,7 @@ function buildDiscordCommandOptions(args) {
13858
16388
  return {
13859
16389
  name: arg.name,
13860
16390
  description: arg.description,
13861
- type: ApplicationCommandOptionType.String,
16391
+ type: ApplicationCommandOptionType2.String,
13862
16392
  required,
13863
16393
  choices
13864
16394
  };
@@ -13876,7 +16406,7 @@ function buildDiscordSlashCommand(spec21) {
13876
16406
  {
13877
16407
  name: "input",
13878
16408
  description: "Command input",
13879
- type: ApplicationCommandOptionType.String,
16409
+ type: ApplicationCommandOptionType2.String,
13880
16410
  required: false
13881
16411
  }
13882
16412
  ] : undefined);
@@ -14028,12 +16558,14 @@ var discordPlugin = {
14028
16558
  unpinMessage_default,
14029
16559
  serverInfo_default,
14030
16560
  editMessage_default,
14031
- deleteMessage_default
16561
+ deleteMessage_default,
16562
+ setup_credentials_default
14032
16563
  ],
14033
16564
  providers: [channelStateProvider, voiceStateProvider, guildInfoProvider],
14034
16565
  tests: [new DiscordTestSuite],
14035
16566
  init: async (_config, runtime) => {
14036
16567
  const token = runtime.getSetting("DISCORD_API_TOKEN");
16568
+ const botTokens = runtime.getSetting("DISCORD_BOT_TOKENS");
14037
16569
  const applicationId = runtime.getSetting("DISCORD_APPLICATION_ID");
14038
16570
  const voiceChannelId = runtime.getSetting("DISCORD_VOICE_CHANNEL_ID");
14039
16571
  const channelIds = runtime.getSetting("CHANNEL_IDS");
@@ -14057,6 +16589,11 @@ var discordPlugin = {
14057
16589
  name: "DISCORD_APPLICATION_ID",
14058
16590
  value: applicationId
14059
16591
  },
16592
+ {
16593
+ name: "DISCORD_BOT_TOKENS",
16594
+ value: botTokens,
16595
+ sensitive: true
16596
+ },
14060
16597
  {
14061
16598
  name: "DISCORD_VOICE_CHANNEL_ID",
14062
16599
  value: voiceChannelId
@@ -14087,9 +16624,9 @@ var discordPlugin = {
14087
16624
  ],
14088
16625
  runtime
14089
16626
  });
14090
- if (!token || token.trim() === "") {
14091
- logger5.warn("Discord API Token not provided - Discord plugin is loaded but will not be functional");
14092
- logger5.warn("To enable Discord functionality, please provide DISCORD_API_TOKEN in your .eliza/.env file");
16627
+ if ((!token || token.trim() === "") && (!botTokens || botTokens.trim() === "")) {
16628
+ logger6.warn("Discord bot token not provided - Discord plugin is loaded but will not be functional");
16629
+ logger6.warn("To enable Discord functionality, provide DISCORD_API_TOKEN or DISCORD_BOT_TOKENS in your .env file");
14093
16630
  }
14094
16631
  }
14095
16632
  };
@@ -14171,5 +16708,5 @@ export {
14171
16708
  COMMAND_ARG_CUSTOM_ID_KEY
14172
16709
  };
14173
16710
 
14174
- //# debugId=B20F70AAC326598264756E2164756E21
16711
+ //# debugId=4CB6B945E40708D364756E2164756E21
14175
16712
  //# sourceMappingURL=index.js.map