opencode-discord-bot 0.0.7 → 0.0.8

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.7",
3
+ "version": "0.0.8",
4
4
  "description": "Discord bot bridge for a self-hosted opencode instance",
5
5
  "type": "module",
6
6
  "main": "src/Main.ts",
@@ -31,9 +31,9 @@ describe("context assembly", () => {
31
31
  })
32
32
  )
33
33
 
34
- expect(rendered).toContain("[Nick 1 | <@u-1> | 2026-06-05 14:03 UTC]")
34
+ expect(rendered).toContain("[Nick 1 | <@u-1> | 2026-06-05 14:03 UTC | messageId=1]")
35
35
  expect(rendered).toContain("Can you refactor this?")
36
- expect(rendered).toContain("(discord target: guildId=g1 channelId=c1 messageId=1)")
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])")
38
38
  expect(rendered).toContain("(reactions: thumbs_up x2, tada x1)")
39
39
  })
@@ -54,8 +54,28 @@ describe("context assembly", () => {
54
54
  expect(prompt.text).toContain("Plain assistant text is streamed to Discord automatically")
55
55
  expect(prompt.text).toContain("do not use bridge tools to send messages")
56
56
  expect(prompt.text).toContain("<@id> pings that user in Discord")
57
- expect(prompt.text).toContain("Use discord target metadata when calling non-message bridge tools")
57
+ expect(prompt.text).toContain("combine the discord default scope or message target override with the header messageId")
58
58
  expect(prompt.text).toContain("Do not emit @everyone, @here, or role pings")
59
+ 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]")
61
+ expect(prompt.text).not.toContain("(discord target: guildId=g1 channelId=c1 messageId=3)")
62
+ })
63
+
64
+ test("renders a target override when a message scope differs from the default", () => {
65
+ const trigger = makeMessage("2", "trigger")
66
+ const prompt = assembleContextPrompt({
67
+ botUserId: "999",
68
+ contextMessages: [makeMessage("1", "different channel", { channelId: "c2", threadId: "t2" })],
69
+ triggerMessage: trigger,
70
+ maxMessages: 30,
71
+ maxChars: 10_000,
72
+ maxAttachmentBytes: 10_000
73
+ })
74
+
75
+ 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]")
77
+ expect(prompt.text).toContain("(discord target: guildId=g1 channelId=c2 threadId=t2)")
78
+ expect(prompt.text).not.toContain("(discord target: guildId=g1 channelId=c1)")
59
79
  })
60
80
 
61
81
  test("applies top-N and character budgets without dropping the trigger", () => {
@@ -1,5 +1,5 @@
1
1
  import type { OpencodePromptFilePart } from "../Opencode/OpencodePort.ts"
2
- import type { DiscordMessage } from "../Schema.ts"
2
+ import type { DiscordMessage, DiscordScope } from "../Schema.ts"
3
3
 
4
4
  export type ContextPrompt = {
5
5
  readonly text: string
@@ -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. Use discord target metadata when calling non-message bridge tools. 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. 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)
@@ -42,11 +42,24 @@ const reactionSummary = (message: DiscordMessage): string | undefined => {
42
42
  return `(reactions: ${message.reactions.map((item) => `${item.emoji} x${item.count}`).join(", ")})`
43
43
  }
44
44
 
45
- const targetSummary = (message: DiscordMessage): string => {
46
- const thread = message.threadId === undefined ? "" : ` threadId=${message.threadId}`
47
- return `(discord target: guildId=${message.guildId} channelId=${message.channelId}${thread} messageId=${message.id})`
45
+ const scopeOf = (message: DiscordMessage): DiscordScope => ({
46
+ guildId: message.guildId,
47
+ channelId: message.channelId,
48
+ ...(message.threadId === undefined ? {} : { threadId: message.threadId })
49
+ })
50
+
51
+ const sameScope = (left: DiscordScope, right: DiscordScope): boolean =>
52
+ left.guildId === right.guildId && left.channelId === right.channelId && left.threadId === right.threadId
53
+
54
+ const scopeSummary = (scope: DiscordScope): string => {
55
+ const thread = scope.threadId === undefined ? "" : ` threadId=${scope.threadId}`
56
+ return `guildId=${scope.guildId} channelId=${scope.channelId}${thread}`
48
57
  }
49
58
 
59
+ const defaultScopeSummary = (scope: DiscordScope): string => `(discord default scope: ${scopeSummary(scope)})`
60
+
61
+ const targetSummary = (scope: DiscordScope): string => `(discord target: ${scopeSummary(scope)})`
62
+
50
63
  const isForwardableMime = (mime: string): boolean =>
51
64
  mime.startsWith("image/") || mime === "application/pdf" || mime.startsWith("audio/") || mime.startsWith("text/")
52
65
 
@@ -59,10 +72,11 @@ const attachmentParts = (messages: ReadonlyArray<DiscordMessage>, maxBytes: numb
59
72
  })
60
73
  )
61
74
 
62
- export const formatDiscordMessage = (message: DiscordMessage): string => {
75
+ export const formatDiscordMessage = (message: DiscordMessage, defaultScope?: DiscordScope): string => {
63
76
  const label = message.author.nickname ?? message.author.displayName
64
- const lines = [`[${label} | <@${message.author.id}> | ${timestamp(message.timestamp)}]`, message.content]
65
- lines.push(targetSummary(message))
77
+ const scope = scopeOf(message)
78
+ const lines = [`[${label} | <@${message.author.id}> | ${timestamp(message.timestamp)} | messageId=${message.id}]`, message.content]
79
+ if (defaultScope === undefined || !sameScope(scope, defaultScope)) lines.push(targetSummary(scope))
66
80
  const attachments = attachmentSummary(message)
67
81
  const reactions = reactionSummary(message)
68
82
  if (attachments !== undefined) lines.push(attachments)
@@ -99,9 +113,14 @@ const renderPrompt = (
99
113
  skippedMessages: ReadonlyArray<DiscordMessage>
100
114
  ): string => {
101
115
  const skippedIds = new Set(skippedMessages.map((message) => message.id))
116
+ const latestMessage = messages.at(-1)
117
+ const defaultScope = latestMessage === undefined ? undefined : scopeOf(latestMessage)
102
118
  return [
103
119
  preamble(botUserId),
104
- ...messages.map((message) => `${skippedIds.has(message.id) ? "(queued intermediate message)\n" : ""}${formatDiscordMessage(message)}`)
120
+ ...(defaultScope === undefined ? [] : [defaultScopeSummary(defaultScope)]),
121
+ ...messages.map(
122
+ (message) => `${skippedIds.has(message.id) ? "(queued intermediate message)\n" : ""}${formatDiscordMessage(message, defaultScope)}`
123
+ )
105
124
  ].join("\n\n")
106
125
  }
107
126