@gakr-gakr/discord 0.1.0

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 (353) hide show
  1. package/account-inspect-api.ts +6 -0
  2. package/action-runtime-api.ts +1 -0
  3. package/api.ts +130 -0
  4. package/autobot.plugin.json +15 -0
  5. package/channel-config-api.ts +1 -0
  6. package/channel-plugin-api.ts +3 -0
  7. package/config-api.ts +4 -0
  8. package/configured-state.ts +6 -0
  9. package/contract-api.ts +21 -0
  10. package/directory-contract-api.ts +4 -0
  11. package/doctor-contract-api.ts +1 -0
  12. package/index.ts +24 -0
  13. package/package.json +79 -0
  14. package/runtime-api.actions.ts +15 -0
  15. package/runtime-api.lookup.ts +22 -0
  16. package/runtime-api.monitor.ts +50 -0
  17. package/runtime-api.send.ts +79 -0
  18. package/runtime-api.threads.ts +31 -0
  19. package/runtime-api.ts +181 -0
  20. package/runtime-setter-api.ts +3 -0
  21. package/secret-contract-api.ts +4 -0
  22. package/security-audit-contract-api.ts +1 -0
  23. package/security-contract-api.ts +4 -0
  24. package/session-key-api.ts +1 -0
  25. package/setup-entry.ts +9 -0
  26. package/setup-plugin-api.ts +3 -0
  27. package/src/account-inspect.ts +131 -0
  28. package/src/accounts.ts +205 -0
  29. package/src/actions/handle-action.guild-admin.ts +421 -0
  30. package/src/actions/handle-action.ts +402 -0
  31. package/src/actions/runtime.guild.ts +446 -0
  32. package/src/actions/runtime.messaging.messages.ts +226 -0
  33. package/src/actions/runtime.messaging.reactions.ts +67 -0
  34. package/src/actions/runtime.messaging.runtime.ts +73 -0
  35. package/src/actions/runtime.messaging.send.ts +336 -0
  36. package/src/actions/runtime.messaging.shared.ts +97 -0
  37. package/src/actions/runtime.messaging.ts +37 -0
  38. package/src/actions/runtime.moderation-shared.ts +48 -0
  39. package/src/actions/runtime.moderation.ts +116 -0
  40. package/src/actions/runtime.presence.ts +117 -0
  41. package/src/actions/runtime.shared.ts +86 -0
  42. package/src/actions/runtime.ts +87 -0
  43. package/src/api.ts +219 -0
  44. package/src/approval-handler.runtime.ts +636 -0
  45. package/src/approval-native.ts +219 -0
  46. package/src/approval-runtime.ts +14 -0
  47. package/src/approval-shared.ts +56 -0
  48. package/src/audit-core.ts +178 -0
  49. package/src/audit.ts +32 -0
  50. package/src/channel-actions.runtime.ts +1 -0
  51. package/src/channel-actions.ts +254 -0
  52. package/src/channel-api.ts +29 -0
  53. package/src/channel.conversation.ts +159 -0
  54. package/src/channel.loaders.ts +50 -0
  55. package/src/channel.runtime.ts +1 -0
  56. package/src/channel.setup.ts +12 -0
  57. package/src/channel.ts +728 -0
  58. package/src/chunk.ts +321 -0
  59. package/src/client.ts +143 -0
  60. package/src/component-custom-id.ts +72 -0
  61. package/src/components-registry.ts +356 -0
  62. package/src/components.builders.ts +410 -0
  63. package/src/components.modal.ts +124 -0
  64. package/src/components.parse.ts +407 -0
  65. package/src/components.ts +54 -0
  66. package/src/components.types.ts +187 -0
  67. package/src/config-schema.ts +6 -0
  68. package/src/config-ui-hints.ts +354 -0
  69. package/src/conversation-identity.ts +58 -0
  70. package/src/delivery-retry.ts +56 -0
  71. package/src/directory-cache.ts +116 -0
  72. package/src/directory-config.ts +58 -0
  73. package/src/directory-live.ts +135 -0
  74. package/src/doctor-contract.ts +477 -0
  75. package/src/doctor-shared.ts +5 -0
  76. package/src/doctor.ts +340 -0
  77. package/src/draft-chunking.ts +43 -0
  78. package/src/draft-stream.ts +162 -0
  79. package/src/error-body.ts +38 -0
  80. package/src/exec-approvals.ts +110 -0
  81. package/src/gateway-logging.ts +67 -0
  82. package/src/group-policy.ts +113 -0
  83. package/src/guilds.ts +29 -0
  84. package/src/inbound-event-delivery.ts +135 -0
  85. package/src/interactive-dispatch.ts +104 -0
  86. package/src/internal/api.commands.ts +51 -0
  87. package/src/internal/api.guild.ts +164 -0
  88. package/src/internal/api.interactions.ts +53 -0
  89. package/src/internal/api.messages.ts +113 -0
  90. package/src/internal/api.reactions.ts +38 -0
  91. package/src/internal/api.ts +61 -0
  92. package/src/internal/api.users.ts +19 -0
  93. package/src/internal/api.webhooks.ts +13 -0
  94. package/src/internal/client.ts +310 -0
  95. package/src/internal/command-deploy.ts +352 -0
  96. package/src/internal/commands.ts +188 -0
  97. package/src/internal/components.base.ts +65 -0
  98. package/src/internal/components.message.ts +279 -0
  99. package/src/internal/components.modal.ts +95 -0
  100. package/src/internal/components.ts +31 -0
  101. package/src/internal/discord.ts +11 -0
  102. package/src/internal/embeds.ts +35 -0
  103. package/src/internal/entity-cache.ts +98 -0
  104. package/src/internal/event-queue.ts +185 -0
  105. package/src/internal/gateway-close-codes.ts +25 -0
  106. package/src/internal/gateway-dispatch.ts +96 -0
  107. package/src/internal/gateway-identify-limiter.ts +26 -0
  108. package/src/internal/gateway-lifecycle.ts +75 -0
  109. package/src/internal/gateway-rate-limit.ts +104 -0
  110. package/src/internal/gateway.ts +479 -0
  111. package/src/internal/interaction-dispatch.ts +162 -0
  112. package/src/internal/interaction-options.ts +98 -0
  113. package/src/internal/interaction-response.ts +53 -0
  114. package/src/internal/interactions.ts +378 -0
  115. package/src/internal/listeners.ts +91 -0
  116. package/src/internal/modal-fields.ts +95 -0
  117. package/src/internal/payload.ts +69 -0
  118. package/src/internal/rest-body.ts +115 -0
  119. package/src/internal/rest-errors.ts +88 -0
  120. package/src/internal/rest-routes.ts +50 -0
  121. package/src/internal/rest-scheduler.ts +557 -0
  122. package/src/internal/rest.ts +322 -0
  123. package/src/internal/schemas.ts +36 -0
  124. package/src/internal/structures.ts +280 -0
  125. package/src/internal/test-builders.test-support.ts +167 -0
  126. package/src/internal/voice.ts +49 -0
  127. package/src/media-detection.ts +28 -0
  128. package/src/mentions.ts +147 -0
  129. package/src/monitor/ack-reactions.ts +70 -0
  130. package/src/monitor/agent-components-auth.ts +7 -0
  131. package/src/monitor/agent-components-context.ts +154 -0
  132. package/src/monitor/agent-components-data.ts +224 -0
  133. package/src/monitor/agent-components-dm-auth.ts +177 -0
  134. package/src/monitor/agent-components-guild-auth.ts +322 -0
  135. package/src/monitor/agent-components-helpers.runtime.ts +3 -0
  136. package/src/monitor/agent-components-helpers.ts +34 -0
  137. package/src/monitor/agent-components-reply.ts +10 -0
  138. package/src/monitor/agent-components.deps.runtime.ts +2 -0
  139. package/src/monitor/agent-components.dispatch.ts +359 -0
  140. package/src/monitor/agent-components.handlers.ts +303 -0
  141. package/src/monitor/agent-components.modal.ts +160 -0
  142. package/src/monitor/agent-components.plugin-interactive.ts +187 -0
  143. package/src/monitor/agent-components.runtime.ts +14 -0
  144. package/src/monitor/agent-components.system-controls.ts +215 -0
  145. package/src/monitor/agent-components.ts +70 -0
  146. package/src/monitor/agent-components.types.ts +58 -0
  147. package/src/monitor/agent-components.wildcard-controls.ts +171 -0
  148. package/src/monitor/allow-list.ts +631 -0
  149. package/src/monitor/auto-presence.ts +356 -0
  150. package/src/monitor/channel-access.ts +102 -0
  151. package/src/monitor/commands.ts +9 -0
  152. package/src/monitor/dm-command-auth.ts +259 -0
  153. package/src/monitor/dm-command-decision.ts +49 -0
  154. package/src/monitor/exec-approvals.ts +161 -0
  155. package/src/monitor/format.ts +45 -0
  156. package/src/monitor/gateway-handle.ts +34 -0
  157. package/src/monitor/gateway-metadata.ts +298 -0
  158. package/src/monitor/gateway-plugin.ts +302 -0
  159. package/src/monitor/gateway-registry.ts +37 -0
  160. package/src/monitor/gateway-supervisor.ts +206 -0
  161. package/src/monitor/inbound-context.ts +95 -0
  162. package/src/monitor/inbound-dedupe.ts +79 -0
  163. package/src/monitor/inbound-job.ts +118 -0
  164. package/src/monitor/listeners.queue.ts +91 -0
  165. package/src/monitor/listeners.reactions.ts +594 -0
  166. package/src/monitor/listeners.ts +150 -0
  167. package/src/monitor/message-channel-info.ts +96 -0
  168. package/src/monitor/message-forwarded.ts +114 -0
  169. package/src/monitor/message-handler.batch-gate.ts +19 -0
  170. package/src/monitor/message-handler.context.ts +492 -0
  171. package/src/monitor/message-handler.dm-preflight.ts +119 -0
  172. package/src/monitor/message-handler.draft-preview.ts +436 -0
  173. package/src/monitor/message-handler.hydration.ts +198 -0
  174. package/src/monitor/message-handler.module-test-helpers.ts +31 -0
  175. package/src/monitor/message-handler.preflight-channel-access.ts +86 -0
  176. package/src/monitor/message-handler.preflight-channel-context.ts +58 -0
  177. package/src/monitor/message-handler.preflight-context.ts +54 -0
  178. package/src/monitor/message-handler.preflight-helpers.ts +164 -0
  179. package/src/monitor/message-handler.preflight-history.ts +23 -0
  180. package/src/monitor/message-handler.preflight-logging.ts +36 -0
  181. package/src/monitor/message-handler.preflight-pluralkit.ts +28 -0
  182. package/src/monitor/message-handler.preflight-runtime.ts +28 -0
  183. package/src/monitor/message-handler.preflight-thread.ts +49 -0
  184. package/src/monitor/message-handler.preflight.ts +822 -0
  185. package/src/monitor/message-handler.preflight.types.ts +115 -0
  186. package/src/monitor/message-handler.process.ts +1033 -0
  187. package/src/monitor/message-handler.routing-preflight.ts +112 -0
  188. package/src/monitor/message-handler.ts +309 -0
  189. package/src/monitor/message-media.ts +536 -0
  190. package/src/monitor/message-run-queue.ts +101 -0
  191. package/src/monitor/message-text.ts +171 -0
  192. package/src/monitor/message-utils.ts +34 -0
  193. package/src/monitor/model-picker-preferences.ts +184 -0
  194. package/src/monitor/model-picker.state.ts +364 -0
  195. package/src/monitor/model-picker.test-utils.ts +26 -0
  196. package/src/monitor/model-picker.ts +38 -0
  197. package/src/monitor/model-picker.view.ts +722 -0
  198. package/src/monitor/native-command-agent-reply.ts +125 -0
  199. package/src/monitor/native-command-arg-ui.ts +233 -0
  200. package/src/monitor/native-command-auth.ts +309 -0
  201. package/src/monitor/native-command-bypass.ts +13 -0
  202. package/src/monitor/native-command-context.ts +109 -0
  203. package/src/monitor/native-command-dispatch.ts +35 -0
  204. package/src/monitor/native-command-model-picker-apply.ts +209 -0
  205. package/src/monitor/native-command-model-picker-interaction.ts +516 -0
  206. package/src/monitor/native-command-model-picker-ui.ts +357 -0
  207. package/src/monitor/native-command-reply.ts +185 -0
  208. package/src/monitor/native-command-route.ts +91 -0
  209. package/src/monitor/native-command-status.ts +76 -0
  210. package/src/monitor/native-command-ui.ts +26 -0
  211. package/src/monitor/native-command-ui.types.ts +20 -0
  212. package/src/monitor/native-command.args.ts +45 -0
  213. package/src/monitor/native-command.options.ts +153 -0
  214. package/src/monitor/native-command.runtime.ts +51 -0
  215. package/src/monitor/native-command.ts +747 -0
  216. package/src/monitor/native-command.types.ts +9 -0
  217. package/src/monitor/native-interaction-channel-context.ts +50 -0
  218. package/src/monitor/preflight-audio.runtime.ts +9 -0
  219. package/src/monitor/preflight-audio.ts +130 -0
  220. package/src/monitor/presence-cache.ts +61 -0
  221. package/src/monitor/presence.ts +50 -0
  222. package/src/monitor/provider-session.runtime.ts +12 -0
  223. package/src/monitor/provider.acp.ts +89 -0
  224. package/src/monitor/provider.allowlist.ts +398 -0
  225. package/src/monitor/provider.cleanup.ts +41 -0
  226. package/src/monitor/provider.commands.ts +129 -0
  227. package/src/monitor/provider.config-log.ts +45 -0
  228. package/src/monitor/provider.deploy-errors.ts +362 -0
  229. package/src/monitor/provider.deploy.ts +221 -0
  230. package/src/monitor/provider.interactions.ts +160 -0
  231. package/src/monitor/provider.lifecycle.ts +562 -0
  232. package/src/monitor/provider.runtime.ts +1 -0
  233. package/src/monitor/provider.startup-log.ts +32 -0
  234. package/src/monitor/provider.startup.ts +323 -0
  235. package/src/monitor/provider.ts +688 -0
  236. package/src/monitor/reply-context.ts +64 -0
  237. package/src/monitor/reply-delivery.ts +216 -0
  238. package/src/monitor/reply-safety.ts +96 -0
  239. package/src/monitor/rest-fetch.ts +97 -0
  240. package/src/monitor/route-resolution.ts +140 -0
  241. package/src/monitor/sender-identity.ts +81 -0
  242. package/src/monitor/startup-status.ts +10 -0
  243. package/src/monitor/status.ts +22 -0
  244. package/src/monitor/system-events.ts +55 -0
  245. package/src/monitor/thread-bindings.config.ts +35 -0
  246. package/src/monitor/thread-bindings.discord-api.ts +310 -0
  247. package/src/monitor/thread-bindings.lifecycle.ts +354 -0
  248. package/src/monitor/thread-bindings.manager.ts +554 -0
  249. package/src/monitor/thread-bindings.messages.ts +6 -0
  250. package/src/monitor/thread-bindings.persona.ts +25 -0
  251. package/src/monitor/thread-bindings.session-adapter.ts +229 -0
  252. package/src/monitor/thread-bindings.session-shared.ts +59 -0
  253. package/src/monitor/thread-bindings.session-updates.ts +35 -0
  254. package/src/monitor/thread-bindings.state.ts +540 -0
  255. package/src/monitor/thread-bindings.ts +48 -0
  256. package/src/monitor/thread-bindings.types.ts +83 -0
  257. package/src/monitor/thread-channel-context.ts +112 -0
  258. package/src/monitor/thread-session-close.ts +63 -0
  259. package/src/monitor/thread-title.ts +181 -0
  260. package/src/monitor/threading.auto-thread.ts +287 -0
  261. package/src/monitor/threading.cache.ts +45 -0
  262. package/src/monitor/threading.starter.ts +288 -0
  263. package/src/monitor/threading.ts +20 -0
  264. package/src/monitor/threading.types.ts +102 -0
  265. package/src/monitor/timeouts.ts +84 -0
  266. package/src/monitor/typing.ts +17 -0
  267. package/src/monitor.gateway.ts +75 -0
  268. package/src/monitor.ts +28 -0
  269. package/src/network-config.ts +79 -0
  270. package/src/normalize.ts +86 -0
  271. package/src/outbound-adapter.ts +327 -0
  272. package/src/outbound-approval.ts +29 -0
  273. package/src/outbound-components.ts +86 -0
  274. package/src/outbound-payload.ts +208 -0
  275. package/src/outbound-send-context.ts +92 -0
  276. package/src/outbound-session-route.ts +72 -0
  277. package/src/pluralkit.ts +58 -0
  278. package/src/preview-streaming.ts +18 -0
  279. package/src/probe.runtime.ts +1 -0
  280. package/src/probe.ts +237 -0
  281. package/src/proxy-fetch.ts +92 -0
  282. package/src/proxy-request-client.ts +21 -0
  283. package/src/recipient-resolution.ts +39 -0
  284. package/src/resolve-allowlist-common.ts +39 -0
  285. package/src/resolve-channels.ts +369 -0
  286. package/src/resolve-users.ts +184 -0
  287. package/src/retry.ts +98 -0
  288. package/src/runtime-api.ts +64 -0
  289. package/src/runtime-config.ts +16 -0
  290. package/src/runtime.ts +23 -0
  291. package/src/secret-config-contract.ts +140 -0
  292. package/src/security-audit.runtime.ts +1 -0
  293. package/src/security-audit.ts +208 -0
  294. package/src/security-contract.ts +47 -0
  295. package/src/security-doctor.ts +20 -0
  296. package/src/security.ts +60 -0
  297. package/src/send-target-parsing.ts +14 -0
  298. package/src/send.channels.ts +139 -0
  299. package/src/send.components.ts +391 -0
  300. package/src/send.emojis-stickers.ts +57 -0
  301. package/src/send.guild.ts +170 -0
  302. package/src/send.message-request.ts +112 -0
  303. package/src/send.messages.ts +229 -0
  304. package/src/send.outbound.ts +459 -0
  305. package/src/send.permissions.ts +283 -0
  306. package/src/send.reactions.ts +155 -0
  307. package/src/send.receipt.ts +69 -0
  308. package/src/send.shared.ts +469 -0
  309. package/src/send.ts +82 -0
  310. package/src/send.types.ts +191 -0
  311. package/src/send.typing.ts +9 -0
  312. package/src/send.voice.ts +140 -0
  313. package/src/send.webhook.ts +137 -0
  314. package/src/session-contract.ts +3 -0
  315. package/src/session-key-normalization.ts +47 -0
  316. package/src/setup-account-state.ts +144 -0
  317. package/src/setup-adapter.ts +14 -0
  318. package/src/setup-core.ts +215 -0
  319. package/src/setup-runtime-helpers.ts +10 -0
  320. package/src/setup-surface.ts +132 -0
  321. package/src/shared-interactive.ts +167 -0
  322. package/src/shared.ts +197 -0
  323. package/src/status-issues.ts +201 -0
  324. package/src/subagent-hooks.ts +232 -0
  325. package/src/target-parsing.ts +70 -0
  326. package/src/target-resolver.ts +129 -0
  327. package/src/targets.ts +12 -0
  328. package/src/token.ts +107 -0
  329. package/src/ui-colors.ts +27 -0
  330. package/src/ui.ts +20 -0
  331. package/src/voice/access.ts +126 -0
  332. package/src/voice/audio.ts +249 -0
  333. package/src/voice/capture-state.ts +120 -0
  334. package/src/voice/command.ts +284 -0
  335. package/src/voice/config.ts +8 -0
  336. package/src/voice/ingress.ts +164 -0
  337. package/src/voice/manager.runtime.ts +14 -0
  338. package/src/voice/manager.ts +1155 -0
  339. package/src/voice/prompt.ts +22 -0
  340. package/src/voice/realtime.ts +1370 -0
  341. package/src/voice/receive-recovery.ts +159 -0
  342. package/src/voice/sanitize.ts +29 -0
  343. package/src/voice/sdk-runtime.ts +14 -0
  344. package/src/voice/segment.ts +160 -0
  345. package/src/voice/session.ts +81 -0
  346. package/src/voice/speaker-context.ts +127 -0
  347. package/src/voice/tts.ts +151 -0
  348. package/src/voice-message.ts +474 -0
  349. package/subagent-hooks-api.ts +27 -0
  350. package/test-api.ts +4 -0
  351. package/thread-binding-api.ts +1 -0
  352. package/timeouts.ts +6 -0
  353. package/tsconfig.json +16 -0
@@ -0,0 +1,64 @@
1
+ import type { Guild, Message, User } from "../internal/discord.js";
2
+ import { resolveTimestampMs } from "./format.js";
3
+ import { resolveDiscordSenderIdentity } from "./sender-identity.js";
4
+
5
+ type DiscordReplyContext = {
6
+ id: string;
7
+ channelId: string;
8
+ sender: string;
9
+ senderId?: string;
10
+ senderName?: string;
11
+ senderTag?: string;
12
+ memberRoleIds?: string[];
13
+ body: string;
14
+ timestamp?: number;
15
+ };
16
+
17
+ export function resolveReplyContext(
18
+ message: Message,
19
+ resolveDiscordMessageText: (message: Message, options?: { includeForwarded?: boolean }) => string,
20
+ ): DiscordReplyContext | null {
21
+ const referenced = message.referencedMessage;
22
+ if (!referenced?.author) {
23
+ return null;
24
+ }
25
+ const referencedText = resolveDiscordMessageText(referenced, {
26
+ includeForwarded: true,
27
+ });
28
+ if (!referencedText) {
29
+ return null;
30
+ }
31
+ const sender = resolveDiscordSenderIdentity({
32
+ author: referenced.author,
33
+ pluralkitInfo: null,
34
+ });
35
+ return {
36
+ id: referenced.id,
37
+ channelId: referenced.channelId,
38
+ sender: sender.tag ?? sender.label ?? "unknown",
39
+ senderId: referenced.author.id,
40
+ senderName: referenced.author.username ?? undefined,
41
+ senderTag: sender.tag ?? undefined,
42
+ memberRoleIds: (() => {
43
+ const roles = (referenced as { member?: { roles?: string[] } }).member?.roles;
44
+ return Array.isArray(roles) ? roles.map((roleId) => roleId) : undefined;
45
+ })(),
46
+ body: referencedText,
47
+ timestamp: resolveTimestampMs(referenced.timestamp),
48
+ };
49
+ }
50
+
51
+ export function buildDirectLabel(author: User, tagOverride?: string) {
52
+ const username =
53
+ tagOverride?.trim() || resolveDiscordSenderIdentity({ author, pluralkitInfo: null }).tag;
54
+ return `${username ?? "unknown"} user id:${author.id}`;
55
+ }
56
+
57
+ export function buildGuildLabel(params: {
58
+ guild?: Guild<true> | Guild;
59
+ channelName: string;
60
+ channelId: string;
61
+ }) {
62
+ const { guild, channelName, channelId } = params;
63
+ return `${guild?.name ?? "Guild"} #${channelName} channel id:${channelId}`;
64
+ }
@@ -0,0 +1,216 @@
1
+ import { resolveAgentAvatar } from "autobot/plugin-sdk/agent-runtime";
2
+ import { sendDurableMessageBatch } from "autobot/plugin-sdk/channel-message";
3
+ import type {
4
+ MarkdownTableMode,
5
+ AutoBotConfig,
6
+ ReplyToMode,
7
+ } from "autobot/plugin-sdk/config-contracts";
8
+ import type { OutboundMediaAccess } from "autobot/plugin-sdk/media-runtime";
9
+ import {
10
+ buildOutboundSessionContext,
11
+ type OutboundDeliveryFormattingOptions,
12
+ type OutboundIdentity,
13
+ type OutboundSendDeps,
14
+ } from "autobot/plugin-sdk/outbound-runtime";
15
+ import type { ChunkMode } from "autobot/plugin-sdk/reply-chunking";
16
+ import type { ReplyPayload } from "autobot/plugin-sdk/reply-dispatch-runtime";
17
+ import type { RuntimeEnv } from "autobot/plugin-sdk/runtime-env";
18
+ import { normalizeOptionalString } from "autobot/plugin-sdk/string-coerce-runtime";
19
+ import type { RequestClient } from "../internal/discord.js";
20
+ import { sendMessageDiscord, sendVoiceMessageDiscord } from "../send.js";
21
+ import { sanitizeDiscordFrontChannelReplyPayloads } from "./reply-safety.js";
22
+
23
+ export type DiscordThreadBindingLookupRecord = {
24
+ accountId: string;
25
+ channelId: string;
26
+ threadId: string;
27
+ agentId: string;
28
+ label?: string;
29
+ webhookId?: string;
30
+ webhookToken?: string;
31
+ };
32
+
33
+ export type DiscordThreadBindingLookup = {
34
+ listBySessionKey: (targetSessionKey: string) => DiscordThreadBindingLookupRecord[];
35
+ touchThread?: (params: { threadId: string; at?: number; persist?: boolean }) => unknown;
36
+ };
37
+
38
+ function resolveTargetChannelId(target: string): string | undefined {
39
+ if (!target.startsWith("channel:")) {
40
+ return undefined;
41
+ }
42
+ const channelId = target.slice("channel:".length).trim();
43
+ return channelId || undefined;
44
+ }
45
+
46
+ function resolveBoundThreadBinding(params: {
47
+ threadBindings?: DiscordThreadBindingLookup;
48
+ sessionKey?: string;
49
+ target: string;
50
+ }): DiscordThreadBindingLookupRecord | undefined {
51
+ const sessionKey = params.sessionKey?.trim();
52
+ if (!params.threadBindings || !sessionKey) {
53
+ return undefined;
54
+ }
55
+ const targetChannelId = resolveTargetChannelId(params.target);
56
+ if (!targetChannelId) {
57
+ return undefined;
58
+ }
59
+ return params.threadBindings
60
+ .listBySessionKey(sessionKey)
61
+ .find((entry) => entry.threadId === targetChannelId);
62
+ }
63
+
64
+ function resolveBindingIdentity(
65
+ cfg: AutoBotConfig,
66
+ binding: DiscordThreadBindingLookupRecord | undefined,
67
+ ): OutboundIdentity | undefined {
68
+ if (!binding) {
69
+ return undefined;
70
+ }
71
+ const baseLabel = binding.label?.trim() || binding.agentId;
72
+ const identity: OutboundIdentity = {
73
+ name: (`🤖 ${baseLabel}`.trim() || "🤖 agent").slice(0, 80),
74
+ };
75
+ try {
76
+ const avatar = resolveAgentAvatar(cfg, binding.agentId);
77
+ if (avatar.kind === "remote") {
78
+ identity.avatarUrl = avatar.url;
79
+ }
80
+ } catch {
81
+ // Avatar is cosmetic; delivery should not depend on local identity config.
82
+ }
83
+ return identity;
84
+ }
85
+
86
+ function createDiscordDeliveryDeps(params: {
87
+ cfg: AutoBotConfig;
88
+ token: string;
89
+ rest?: RequestClient;
90
+ }): OutboundSendDeps {
91
+ return {
92
+ discord: (to: string, text: string, opts?: Parameters<typeof sendMessageDiscord>[2]) =>
93
+ sendMessageDiscord(to, text, {
94
+ ...opts,
95
+ cfg: opts?.cfg ?? params.cfg,
96
+ token: params.token,
97
+ rest: params.rest,
98
+ }),
99
+ discordVoice: (
100
+ to: string,
101
+ audioPath: string,
102
+ opts?: Parameters<typeof sendVoiceMessageDiscord>[2],
103
+ ) =>
104
+ sendVoiceMessageDiscord(to, audioPath, {
105
+ ...opts,
106
+ cfg: opts?.cfg ?? params.cfg,
107
+ token: params.token,
108
+ rest: params.rest,
109
+ }),
110
+ };
111
+ }
112
+
113
+ type DiscordDeliveryOptions = {
114
+ to: string;
115
+ threadId?: string;
116
+ agentId?: string;
117
+ identity?: OutboundIdentity;
118
+ mediaAccess?: OutboundMediaAccess;
119
+ replyToMode: ReplyToMode;
120
+ formatting: OutboundDeliveryFormattingOptions;
121
+ };
122
+
123
+ function resolveDiscordDeliveryOptions(params: {
124
+ cfg: AutoBotConfig;
125
+ target: string;
126
+ sessionKey?: string;
127
+ threadBindings?: DiscordThreadBindingLookup;
128
+ textLimit: number;
129
+ maxLinesPerMessage?: number;
130
+ tableMode?: MarkdownTableMode;
131
+ chunkMode?: ChunkMode;
132
+ replyToMode?: ReplyToMode;
133
+ mediaLocalRoots?: readonly string[];
134
+ }): DiscordDeliveryOptions {
135
+ const binding = resolveBoundThreadBinding({
136
+ threadBindings: params.threadBindings,
137
+ sessionKey: params.sessionKey,
138
+ target: params.target,
139
+ });
140
+ return {
141
+ to: binding ? `channel:${binding.channelId}` : params.target,
142
+ threadId: binding?.threadId,
143
+ agentId: binding?.agentId,
144
+ identity: resolveBindingIdentity(params.cfg, binding),
145
+ mediaAccess: params.mediaLocalRoots?.length
146
+ ? { localRoots: params.mediaLocalRoots }
147
+ : undefined,
148
+ replyToMode: params.replyToMode ?? "all",
149
+ formatting: {
150
+ textLimit: params.textLimit,
151
+ maxLinesPerMessage: params.maxLinesPerMessage,
152
+ tableMode: params.tableMode,
153
+ chunkMode: params.chunkMode,
154
+ },
155
+ };
156
+ }
157
+
158
+ export async function deliverDiscordReply(params: {
159
+ cfg: AutoBotConfig;
160
+ replies: ReplyPayload[];
161
+ target: string;
162
+ token: string;
163
+ accountId?: string;
164
+ rest?: RequestClient;
165
+ runtime: RuntimeEnv;
166
+ textLimit: number;
167
+ maxLinesPerMessage?: number;
168
+ replyToId?: string;
169
+ replyToMode?: ReplyToMode;
170
+ tableMode?: MarkdownTableMode;
171
+ chunkMode?: ChunkMode;
172
+ sessionKey?: string;
173
+ threadBindings?: DiscordThreadBindingLookup;
174
+ mediaLocalRoots?: readonly string[];
175
+ kind: "tool" | "block" | "final";
176
+ }) {
177
+ void params.runtime;
178
+
179
+ const delivery = resolveDiscordDeliveryOptions(params);
180
+ const payloads = sanitizeDiscordFrontChannelReplyPayloads(params.replies, { kind: params.kind });
181
+ if (payloads.length === 0) {
182
+ return;
183
+ }
184
+
185
+ const send = await sendDurableMessageBatch({
186
+ cfg: params.cfg,
187
+ channel: "discord",
188
+ to: delivery.to,
189
+ accountId: params.accountId,
190
+ payloads,
191
+ replyToId: normalizeOptionalString(params.replyToId),
192
+ replyToMode: delivery.replyToMode,
193
+ formatting: delivery.formatting,
194
+ threadId: delivery.threadId,
195
+ identity: delivery.identity,
196
+ deps: createDiscordDeliveryDeps({
197
+ cfg: params.cfg,
198
+ token: params.token,
199
+ rest: params.rest,
200
+ }),
201
+ mediaAccess: delivery.mediaAccess,
202
+ session: buildOutboundSessionContext({
203
+ cfg: params.cfg,
204
+ sessionKey: params.sessionKey,
205
+ agentId: delivery.agentId,
206
+ requesterAccountId: params.accountId,
207
+ }),
208
+ });
209
+ if (send.status === "failed" || send.status === "partial_failed") {
210
+ throw send.error;
211
+ }
212
+ const results = send.status === "sent" ? send.results : [];
213
+ if (results.length === 0) {
214
+ throw new Error(`discord final reply produced no delivered message for ${delivery.to}`);
215
+ }
216
+ }
@@ -0,0 +1,96 @@
1
+ import type { ReplyPayload } from "autobot/plugin-sdk/reply-dispatch-runtime";
2
+ import { resolveSendableOutboundReplyParts } from "autobot/plugin-sdk/reply-payload";
3
+ import { sanitizeAssistantVisibleText } from "autobot/plugin-sdk/text-chunking";
4
+
5
+ const DISCORD_INTERNAL_TRACE_LINE_RE =
6
+ /^(?:>\s*)?(?:📊|🛠️|📖|📝|🔍|🔎|⚙️)\s*(?:Session Status|Exec|Read|Edit|Write|Patch|Search|Open|Click|Find|Screenshot|Update Plan|Tool Call|Tool Result|Function Call|Shell|Command)\s*:/i;
7
+ const DISCORD_INTERNAL_COMPACT_COMMAND_TRACE_LINE_RE =
8
+ /^(?:>\s*)?🛠️\s*(?:(?:(?:elevated|pty)\b\s*(?:·|,)\s*)+)?(?:`{1,2}\s*\S|(?:run|check|fetch|pull|push|view|show|list|switch|create|merge|rebase|stage|restore|reset|stash|search|find|print|copy|move|remove|install|start|cd|git|pnpm|npm|yarn|bun|node|python|python3|bash|sh)\b)/i;
9
+ const DISCORD_INTERNAL_CHANNEL_LINE_RE =
10
+ /^(?:>\s*)?(?:analysis|commentary|tool[-_ ]?call|tool[-_ ]?result|function[-_ ]?call|thinking|reasoning)\s*[:=]/i;
11
+
12
+ function hasNonEmptyRecord(value: unknown): value is Record<string, unknown> {
13
+ return Boolean(
14
+ value && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0,
15
+ );
16
+ }
17
+
18
+ function hasInteractiveOrPresentationBlocks(value: unknown): boolean {
19
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
20
+ return false;
21
+ }
22
+ const record = value as Record<string, unknown>;
23
+ if (typeof record.title === "string" && record.title.trim().length > 0) {
24
+ return true;
25
+ }
26
+ return Array.isArray(record.blocks) && record.blocks.length > 0;
27
+ }
28
+
29
+ function hasNonTextReplyPayloadContent(payload: ReplyPayload): boolean {
30
+ return (
31
+ payload.audioAsVoice === true ||
32
+ hasNonEmptyRecord(payload.channelData) ||
33
+ hasInteractiveOrPresentationBlocks(payload.interactive) ||
34
+ hasInteractiveOrPresentationBlocks(payload.presentation)
35
+ );
36
+ }
37
+
38
+ function stripDiscordInternalTraceLines(text: string): string {
39
+ let inFence = false;
40
+ const kept: string[] = [];
41
+ for (const line of text.split(/\r?\n/)) {
42
+ if (/^\s*```/.test(line)) {
43
+ inFence = !inFence;
44
+ kept.push(line);
45
+ continue;
46
+ }
47
+ if (!inFence) {
48
+ const trimmed = line.trim();
49
+ if (
50
+ DISCORD_INTERNAL_TRACE_LINE_RE.test(trimmed) ||
51
+ DISCORD_INTERNAL_COMPACT_COMMAND_TRACE_LINE_RE.test(trimmed) ||
52
+ DISCORD_INTERNAL_CHANNEL_LINE_RE.test(trimmed)
53
+ ) {
54
+ continue;
55
+ }
56
+ }
57
+ kept.push(line);
58
+ }
59
+ return kept.join("\n");
60
+ }
61
+
62
+ function collapseExcessBlankLines(text: string): string {
63
+ return text.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n");
64
+ }
65
+
66
+ export function sanitizeDiscordFrontChannelText(text: string): string {
67
+ const withoutAssistantScaffolding = sanitizeAssistantVisibleText(text);
68
+ const withoutTraceLines = stripDiscordInternalTraceLines(withoutAssistantScaffolding);
69
+ return collapseExcessBlankLines(withoutTraceLines).trim();
70
+ }
71
+
72
+ export function sanitizeDiscordFrontChannelReplyPayloads(
73
+ payloads: readonly ReplyPayload[],
74
+ options: { kind?: "tool" | "block" | "final" } = {},
75
+ ): ReplyPayload[] {
76
+ const preserveVerboseToolProgress = options.kind === "tool";
77
+ const safePayloads: ReplyPayload[] = [];
78
+ for (const payload of payloads) {
79
+ const safeText =
80
+ typeof payload.text === "string"
81
+ ? preserveVerboseToolProgress
82
+ ? collapseExcessBlankLines(sanitizeAssistantVisibleText(payload.text)).trim()
83
+ : sanitizeDiscordFrontChannelText(payload.text)
84
+ : payload.text;
85
+ const nextPayload =
86
+ safeText === payload.text
87
+ ? payload
88
+ : ({ ...payload, text: safeText || undefined } as ReplyPayload);
89
+ const nextParts = resolveSendableOutboundReplyParts(nextPayload);
90
+ if (!nextParts.hasContent && !hasNonTextReplyPayloadContent(nextPayload)) {
91
+ continue;
92
+ }
93
+ safePayloads.push(nextPayload);
94
+ }
95
+ return safePayloads;
96
+ }
@@ -0,0 +1,97 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { formatErrorMessage } from "autobot/plugin-sdk/error-runtime";
3
+ import {
4
+ createHttp1EnvHttpProxyAgent,
5
+ createHttp1ProxyAgent,
6
+ resolveEnvHttpProxyAgentOptions,
7
+ wrapFetchWithAbortSignal,
8
+ } from "autobot/plugin-sdk/fetch-runtime";
9
+ import {
10
+ captureHttpExchange,
11
+ resolveEffectiveDebugProxyUrl,
12
+ } from "autobot/plugin-sdk/proxy-capture";
13
+ import { resolveRequestUrl } from "autobot/plugin-sdk/request-url";
14
+ import { danger } from "autobot/plugin-sdk/runtime-env";
15
+ import type { RuntimeEnv } from "autobot/plugin-sdk/runtime-env";
16
+ import { Agent, fetch as undiciFetch } from "undici";
17
+ import { createDiscordDnsLookup } from "../network-config.js";
18
+ import { withValidatedDiscordProxy } from "../proxy-fetch.js";
19
+
20
+ const discordDnsLookup = createDiscordDnsLookup();
21
+
22
+ type DiscordRestDispatcher =
23
+ | InstanceType<typeof Agent>
24
+ | ReturnType<typeof createHttp1EnvHttpProxyAgent>
25
+ | ReturnType<typeof createHttp1ProxyAgent>;
26
+
27
+ function createDirectDiscordRestDispatcher(): InstanceType<typeof Agent> {
28
+ return new Agent({
29
+ allowH2: false,
30
+ connect: { lookup: discordDnsLookup },
31
+ });
32
+ }
33
+
34
+ function createEnvProxyDiscordRestDispatcher(
35
+ runtime: RuntimeEnv,
36
+ ): ReturnType<typeof createHttp1EnvHttpProxyAgent> | undefined {
37
+ const envProxyOptions = resolveEnvHttpProxyAgentOptions();
38
+ if (!envProxyOptions) {
39
+ return undefined;
40
+ }
41
+ try {
42
+ return createHttp1EnvHttpProxyAgent({
43
+ ...envProxyOptions,
44
+ connect: { lookup: discordDnsLookup },
45
+ });
46
+ } catch (err) {
47
+ runtime.error?.(
48
+ danger(
49
+ `discord: env proxy unavailable for REST fetch; using direct dispatcher: ${formatErrorMessage(err)}`,
50
+ ),
51
+ );
52
+ return undefined;
53
+ }
54
+ }
55
+
56
+ function createDiscordRestFetchWithDispatcher(dispatcher: DiscordRestDispatcher): typeof fetch {
57
+ return wrapFetchWithAbortSignal(((input: RequestInfo | URL, init?: RequestInit) =>
58
+ (
59
+ undiciFetch(input as string | URL, {
60
+ ...(init as Record<string, unknown>),
61
+ dispatcher,
62
+ }) as unknown as Promise<Response>
63
+ ).then((response) => {
64
+ captureHttpExchange({
65
+ url: resolveRequestUrl(input),
66
+ method: init?.method ?? "GET",
67
+ requestHeaders: init?.headers as Headers | Record<string, string> | undefined,
68
+ requestBody: (init as RequestInit & { body?: BodyInit | null })?.body ?? null,
69
+ response,
70
+ flowId: randomUUID(),
71
+ meta: { subsystem: "discord-rest" },
72
+ });
73
+ return response;
74
+ })) as typeof fetch);
75
+ }
76
+
77
+ export function resolveDiscordRestFetch(
78
+ proxyUrl: string | undefined,
79
+ runtime: RuntimeEnv,
80
+ ): typeof fetch {
81
+ const effectiveProxyUrl = resolveEffectiveDebugProxyUrl(proxyUrl);
82
+ if (effectiveProxyUrl) {
83
+ const fetcher = withValidatedDiscordProxy(effectiveProxyUrl, runtime, (proxy) =>
84
+ createDiscordRestFetchWithDispatcher(createHttp1ProxyAgent({ uri: proxy })),
85
+ );
86
+ if (!fetcher) {
87
+ return fetch;
88
+ }
89
+ runtime.log?.("discord: rest proxy enabled");
90
+ return fetcher;
91
+ }
92
+
93
+ const fetcher = createDiscordRestFetchWithDispatcher(
94
+ createEnvProxyDiscordRestDispatcher(runtime) ?? createDirectDiscordRestDispatcher(),
95
+ );
96
+ return fetcher;
97
+ }
@@ -0,0 +1,140 @@
1
+ import type { AutoBotConfig } from "autobot/plugin-sdk/config-contracts";
2
+ import type { SessionBindingRecord } from "autobot/plugin-sdk/conversation-runtime";
3
+ import {
4
+ deriveLastRoutePolicy,
5
+ isAcpSessionKey,
6
+ isSubagentSessionKey,
7
+ parseAgentSessionKey,
8
+ resolveAgentRoute,
9
+ type ResolvedAgentRoute,
10
+ type RoutePeer,
11
+ } from "autobot/plugin-sdk/routing";
12
+ import { resolveAgentIdFromSessionKey } from "autobot/plugin-sdk/routing";
13
+
14
+ export function buildDiscordRoutePeer(params: {
15
+ isDirectMessage: boolean;
16
+ isGroupDm: boolean;
17
+ directUserId?: string | null;
18
+ conversationId: string;
19
+ }): RoutePeer {
20
+ return {
21
+ kind: params.isDirectMessage ? "direct" : params.isGroupDm ? "group" : "channel",
22
+ id: params.isDirectMessage
23
+ ? params.directUserId?.trim() || params.conversationId
24
+ : params.conversationId,
25
+ };
26
+ }
27
+
28
+ export function resolveDiscordConversationRoute(params: {
29
+ cfg: AutoBotConfig;
30
+ accountId?: string | null;
31
+ guildId?: string | null;
32
+ memberRoleIds?: string[];
33
+ peer: RoutePeer;
34
+ parentConversationId?: string | null;
35
+ }): ResolvedAgentRoute {
36
+ return resolveAgentRoute({
37
+ cfg: params.cfg,
38
+ channel: "discord",
39
+ accountId: params.accountId,
40
+ guildId: params.guildId ?? undefined,
41
+ memberRoleIds: params.memberRoleIds,
42
+ peer: params.peer,
43
+ parentPeer: params.parentConversationId
44
+ ? { kind: "channel", id: params.parentConversationId }
45
+ : undefined,
46
+ });
47
+ }
48
+
49
+ export function resolveDiscordBoundConversationRoute(params: {
50
+ cfg: AutoBotConfig;
51
+ accountId?: string | null;
52
+ guildId?: string | null;
53
+ memberRoleIds?: string[];
54
+ isDirectMessage: boolean;
55
+ isGroupDm: boolean;
56
+ directUserId?: string | null;
57
+ conversationId: string;
58
+ parentConversationId?: string | null;
59
+ boundSessionKey?: string | null;
60
+ configuredRoute?: { route: ResolvedAgentRoute } | null;
61
+ matchedBy?: ResolvedAgentRoute["matchedBy"];
62
+ }): ResolvedAgentRoute {
63
+ const route = resolveDiscordConversationRoute({
64
+ cfg: params.cfg,
65
+ accountId: params.accountId,
66
+ guildId: params.guildId,
67
+ memberRoleIds: params.memberRoleIds,
68
+ peer: buildDiscordRoutePeer({
69
+ isDirectMessage: params.isDirectMessage,
70
+ isGroupDm: params.isGroupDm,
71
+ directUserId: params.directUserId,
72
+ conversationId: params.conversationId,
73
+ }),
74
+ parentConversationId: params.parentConversationId,
75
+ });
76
+ return resolveDiscordEffectiveRoute({
77
+ route,
78
+ boundSessionKey: params.boundSessionKey,
79
+ configuredRoute: params.configuredRoute,
80
+ matchedBy: params.matchedBy,
81
+ });
82
+ }
83
+
84
+ export function resolveDiscordEffectiveRoute(params: {
85
+ route: ResolvedAgentRoute;
86
+ boundSessionKey?: string | null;
87
+ configuredRoute?: { route: ResolvedAgentRoute } | null;
88
+ matchedBy?: ResolvedAgentRoute["matchedBy"];
89
+ }): ResolvedAgentRoute {
90
+ const boundSessionKey = params.boundSessionKey?.trim();
91
+ if (!boundSessionKey) {
92
+ return params.configuredRoute?.route ?? params.route;
93
+ }
94
+ return {
95
+ ...params.route,
96
+ sessionKey: boundSessionKey,
97
+ agentId: resolveAgentIdFromSessionKey(boundSessionKey),
98
+ lastRoutePolicy: deriveLastRoutePolicy({
99
+ sessionKey: boundSessionKey,
100
+ mainSessionKey: params.route.mainSessionKey,
101
+ }),
102
+ ...(params.matchedBy ? { matchedBy: params.matchedBy } : {}),
103
+ };
104
+ }
105
+
106
+ function hasExplicitRuntimeBindingIntent(record: SessionBindingRecord): boolean {
107
+ if (record.targetKind === "subagent") {
108
+ return true;
109
+ }
110
+ if (isAcpSessionKey(record.targetSessionKey) || isSubagentSessionKey(record.targetSessionKey)) {
111
+ return true;
112
+ }
113
+ const metadata = record.metadata;
114
+ if (!metadata || typeof metadata !== "object") {
115
+ return false;
116
+ }
117
+ return (
118
+ typeof metadata.boundBy === "string" ||
119
+ typeof metadata.label === "string" ||
120
+ typeof metadata.threadName === "string" ||
121
+ metadata.pluginBindingOwner === "plugin"
122
+ );
123
+ }
124
+
125
+ export function shouldIgnoreStaleDiscordRouteBinding(params: {
126
+ bindingRecord?: SessionBindingRecord | null;
127
+ route: ResolvedAgentRoute;
128
+ }): boolean {
129
+ const bindingRecord = params.bindingRecord;
130
+ const boundSessionKey = bindingRecord?.targetSessionKey?.trim();
131
+ if (!bindingRecord || !boundSessionKey || hasExplicitRuntimeBindingIntent(bindingRecord)) {
132
+ return false;
133
+ }
134
+ const bound = parseAgentSessionKey(boundSessionKey);
135
+ const routed = parseAgentSessionKey(params.route.sessionKey);
136
+ if (!bound || !routed || bound.rest !== routed.rest) {
137
+ return false;
138
+ }
139
+ return bound.agentId !== params.route.agentId;
140
+ }
@@ -0,0 +1,81 @@
1
+ import { normalizeOptionalString } from "autobot/plugin-sdk/string-coerce-runtime";
2
+ import type { User } from "../internal/discord.js";
3
+ import type { PluralKitMessageInfo } from "../pluralkit.js";
4
+ import { formatDiscordUserTag } from "./format.js";
5
+
6
+ export type DiscordSenderIdentity = {
7
+ id: string;
8
+ name?: string;
9
+ tag?: string;
10
+ label: string;
11
+ isPluralKit: boolean;
12
+ pluralkit?: {
13
+ memberId: string;
14
+ memberName?: string;
15
+ systemId?: string;
16
+ systemName?: string;
17
+ };
18
+ };
19
+
20
+ type DiscordWebhookMessageLike = {
21
+ webhookId?: string | null;
22
+ webhook_id?: string | null;
23
+ };
24
+
25
+ type DiscordMemberLike = {
26
+ nickname?: string | null;
27
+ nick?: string | null;
28
+ };
29
+
30
+ export function resolveDiscordWebhookId(message: DiscordWebhookMessageLike): string | null {
31
+ const candidate = message.webhookId ?? message.webhook_id;
32
+ return typeof candidate === "string" && candidate.trim() ? candidate.trim() : null;
33
+ }
34
+
35
+ export function resolveDiscordSenderIdentity(params: {
36
+ author: User;
37
+ member?: DiscordMemberLike | null;
38
+ pluralkitInfo?: PluralKitMessageInfo | null;
39
+ }): DiscordSenderIdentity {
40
+ const pkInfo = params.pluralkitInfo ?? null;
41
+ const pkMember = pkInfo?.member ?? undefined;
42
+ const pkSystem = pkInfo?.system ?? undefined;
43
+ const memberId = pkMember?.id?.trim();
44
+ const memberNameRaw = pkMember?.display_name ?? pkMember?.name ?? "";
45
+ const memberName = memberNameRaw?.trim();
46
+ if (memberId && memberName) {
47
+ const systemName = pkSystem?.name?.trim();
48
+ const label = systemName ? `${memberName} (PK:${systemName})` : `${memberName} (PK)`;
49
+ return {
50
+ id: memberId,
51
+ name: memberName,
52
+ tag: normalizeOptionalString(pkMember?.name),
53
+ label,
54
+ isPluralKit: true,
55
+ pluralkit: {
56
+ memberId,
57
+ memberName,
58
+ systemId: normalizeOptionalString(pkSystem?.id),
59
+ systemName,
60
+ },
61
+ };
62
+ }
63
+
64
+ const senderTag = formatDiscordUserTag(params.author);
65
+ const senderDisplay =
66
+ params.member?.nickname ??
67
+ params.member?.nick ??
68
+ params.author.globalName ??
69
+ params.author.username;
70
+ const senderLabel =
71
+ senderDisplay && senderTag && senderDisplay !== senderTag
72
+ ? `${senderDisplay} (${senderTag})`
73
+ : (senderDisplay ?? senderTag ?? params.author.id);
74
+ return {
75
+ id: params.author.id,
76
+ name: params.author.username ?? undefined,
77
+ tag: senderTag,
78
+ label: senderLabel,
79
+ isPluralKit: false,
80
+ };
81
+ }