opencode-discord-bot 0.0.9 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-discord-bot",
3
- "version": "0.0.9",
3
+ "version": "0.0.10",
4
4
  "description": "Discord bot bridge for a self-hosted opencode instance",
5
5
  "type": "module",
6
6
  "main": "src/Main.ts",
@@ -31,7 +31,7 @@ describe("context assembly", () => {
31
31
  })
32
32
  )
33
33
 
34
- expect(rendered).toContain("[Nick 1 | <@u-1> | 2026-06-05 14:03 UTC | messageId=1]")
34
+ expect(rendered).toContain("[Nick 1 | 2026-06-05 14:03 UTC | messageId=1]")
35
35
  expect(rendered).toContain("Can you refactor this?")
36
36
  expect(rendered).toContain("(discord target: guildId=g1 channelId=c1)")
37
37
  expect(rendered).toContain("(attachments: screenshot.png [image/png; 12 bytes; https://cdn/a1])")
@@ -51,13 +51,17 @@ describe("context assembly", () => {
51
51
 
52
52
  expect(prompt.messages.map((item) => item.id)).toEqual(["1", "2", "3"])
53
53
  expect(prompt.text.match(/latest <@999>/g)).toHaveLength(1)
54
+ expect(prompt.text).toContain("(participants)\nNick 1 - <@u-1>\nNick 2 - <@u-2>\nNick 3 - <@u-3>")
55
+ expect(prompt.text.match(/<@u-3>/g)).toHaveLength(1)
54
56
  expect(prompt.text).toContain("Plain assistant text is streamed to Discord automatically")
55
57
  expect(prompt.text).toContain("do not use bridge tools to send messages")
56
58
  expect(prompt.text).toContain("<@id> pings that user in Discord")
59
+ expect(prompt.text).toContain("use the participants list to map server nicknames/display names")
60
+ expect(prompt.text).toContain("When a display name is shared")
57
61
  expect(prompt.text).toContain("combine the discord default scope or message target override with the header messageId")
58
62
  expect(prompt.text).toContain("Do not emit @everyone, @here, or role pings")
59
63
  expect(prompt.text).toContain("(discord default scope: guildId=g1 channelId=c1)")
60
- expect(prompt.text).toContain("[Nick 3 | <@u-3> | 2026-06-05 14:03 UTC | messageId=3]")
64
+ expect(prompt.text).toContain("[Nick 3 | 2026-06-05 14:03 UTC | messageId=3]")
61
65
  expect(prompt.text).not.toContain("(discord target: guildId=g1 channelId=c1 messageId=3)")
62
66
  })
63
67
 
@@ -73,11 +77,32 @@ describe("context assembly", () => {
73
77
  })
74
78
 
75
79
  expect(prompt.text).toContain("(discord default scope: guildId=g1 channelId=c1)")
76
- expect(prompt.text).toContain("[Nick 1 | <@u-1> | 2026-06-05 14:03 UTC | messageId=1]")
80
+ expect(prompt.text).toContain("[Nick 1 | 2026-06-05 14:03 UTC | messageId=1]")
77
81
  expect(prompt.text).toContain("(discord target: guildId=g1 channelId=c2 threadId=t2)")
78
82
  expect(prompt.text).not.toContain("(discord target: guildId=g1 channelId=c1)")
79
83
  })
80
84
 
85
+ test("keeps author ids in headers when participant labels collide", () => {
86
+ const first = makeMessage("1", "from first Sam", {
87
+ author: { id: "sam-1", displayName: "Sam", nickname: "Sam", isBot: false }
88
+ })
89
+ const second = makeMessage("2", "from second Sam", {
90
+ author: { id: "sam-2", displayName: "Sam", nickname: "Sam", isBot: false }
91
+ })
92
+ const prompt = assembleContextPrompt({
93
+ botUserId: "999",
94
+ contextMessages: [first],
95
+ triggerMessage: second,
96
+ maxMessages: 30,
97
+ maxChars: 10_000,
98
+ maxAttachmentBytes: 10_000
99
+ })
100
+
101
+ expect(prompt.text).toContain("(participants)\nSam - <@sam-1>\nSam - <@sam-2>")
102
+ expect(prompt.text).toContain("[Sam | <@sam-1> | 2026-06-05 14:03 UTC | messageId=1]")
103
+ expect(prompt.text).toContain("[Sam | <@sam-2> | 2026-06-05 14:03 UTC | messageId=2]")
104
+ })
105
+
81
106
  test("applies top-N and character budgets without dropping the trigger", () => {
82
107
  const trigger = makeMessage("5", "trigger")
83
108
  const prompt = assembleContextPrompt({
@@ -18,7 +18,7 @@ type ContextInput = {
18
18
  }
19
19
 
20
20
  const preamble = (botUserId: string) =>
21
- `Discord bridge context for <@${botUserId}>. Plain assistant text is streamed to Discord automatically; do not use bridge tools to send messages. <@id> pings that user in Discord. For non-message bridge tools, combine the discord default scope or message target override with the header messageId. Do not emit @everyone, @here, or role pings unless explicitly allowed.`
21
+ `Discord bridge context for <@${botUserId}>. Plain assistant text is streamed to Discord automatically; do not use bridge tools to send messages. <@id> pings that user in Discord; use the participants list to map server nicknames/display names to their <@id> values. When a display name is shared, the message header also includes that author's <@id>. For non-message bridge tools, combine the discord default scope or message target override with the header messageId. Do not emit @everyone, @here, or role pings unless explicitly allowed.`
22
22
 
23
23
  const timestamp = (value: string): string => {
24
24
  const date = new Date(value)
@@ -72,10 +72,53 @@ const attachmentParts = (messages: ReadonlyArray<DiscordMessage>, maxBytes: numb
72
72
  })
73
73
  )
74
74
 
75
- export const formatDiscordMessage = (message: DiscordMessage, defaultScope?: DiscordScope): string => {
76
- const label = message.author.nickname ?? message.author.displayName
75
+ const authorLabel = (message: DiscordMessage): string => message.author.nickname ?? message.author.displayName
76
+
77
+ type ParticipantsSummary = {
78
+ readonly text: string | undefined
79
+ readonly ambiguousAuthorIds: ReadonlySet<string>
80
+ }
81
+
82
+ const participantsSummary = (messages: ReadonlyArray<DiscordMessage>): ParticipantsSummary => {
83
+ const participants: Array<{ readonly id: string; readonly label: string }> = []
84
+ const seenAuthorIds = new Set<string>()
85
+ const authorIdsByLabel = new Map<string, Set<string>>()
86
+
87
+ for (const message of messages) {
88
+ const id = message.author.id
89
+ if (seenAuthorIds.has(id)) continue
90
+ seenAuthorIds.add(id)
91
+
92
+ const label = authorLabel(message)
93
+ participants.push({ id, label })
94
+
95
+ const authorIds = authorIdsByLabel.get(label)
96
+ if (authorIds === undefined) authorIdsByLabel.set(label, new Set([id]))
97
+ else authorIds.add(id)
98
+ }
99
+
100
+ const ambiguousAuthorIds = new Set<string>()
101
+ for (const authorIds of authorIdsByLabel.values()) {
102
+ if (authorIds.size <= 1) continue
103
+ for (const id of authorIds) ambiguousAuthorIds.add(id)
104
+ }
105
+
106
+ return {
107
+ ambiguousAuthorIds,
108
+ text:
109
+ participants.length === 0 ? undefined : `(participants)\n${participants.map((item) => `${item.label} - <@${item.id}>`).join("\n")}`
110
+ }
111
+ }
112
+
113
+ export const formatDiscordMessage = (
114
+ message: DiscordMessage,
115
+ defaultScope?: DiscordScope,
116
+ ambiguousAuthorIds?: ReadonlySet<string>
117
+ ): string => {
118
+ const label = authorLabel(message)
119
+ const author = ambiguousAuthorIds?.has(message.author.id) === true ? `${label} | <@${message.author.id}>` : label
77
120
  const scope = scopeOf(message)
78
- const lines = [`[${label} | <@${message.author.id}> | ${timestamp(message.timestamp)} | messageId=${message.id}]`, message.content]
121
+ const lines = [`[${author} | ${timestamp(message.timestamp)} | messageId=${message.id}]`, message.content]
79
122
  if (defaultScope === undefined || !sameScope(scope, defaultScope)) lines.push(targetSummary(scope))
80
123
  const attachments = attachmentSummary(message)
81
124
  const reactions = reactionSummary(message)
@@ -115,11 +158,14 @@ const renderPrompt = (
115
158
  const skippedIds = new Set(skippedMessages.map((message) => message.id))
116
159
  const latestMessage = messages.at(-1)
117
160
  const defaultScope = latestMessage === undefined ? undefined : scopeOf(latestMessage)
161
+ const participants = participantsSummary(messages)
118
162
  return [
119
163
  preamble(botUserId),
164
+ ...(participants.text === undefined ? [] : [participants.text]),
120
165
  ...(defaultScope === undefined ? [] : [defaultScopeSummary(defaultScope)]),
121
166
  ...messages.map(
122
- (message) => `${skippedIds.has(message.id) ? "(queued intermediate message)\n" : ""}${formatDiscordMessage(message, defaultScope)}`
167
+ (message) =>
168
+ `${skippedIds.has(message.id) ? "(queued intermediate message)\n" : ""}${formatDiscordMessage(message, defaultScope, participants.ambiguousAuthorIds)}`
123
169
  )
124
170
  ].join("\n\n")
125
171
  }