opencode-discord-bot 0.0.3 → 0.0.4

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.3",
3
+ "version": "0.0.4",
4
4
  "description": "Discord bot bridge for a self-hosted opencode instance",
5
5
  "type": "module",
6
6
  "main": "src/Main.ts",
@@ -68,6 +68,7 @@
68
68
  "@commitlint/cli": "^21.0.2",
69
69
  "@commitlint/config-conventional": "^21.0.2",
70
70
  "@effect/vitest": "4.0.0-beta.74",
71
+ "@opencode-ai/plugin": "1.16.2",
71
72
  "@types/bun": "1.3.13",
72
73
  "@typescript/native-preview": "^7.0.0-dev.20260606.1",
73
74
  "@vitest/coverage-v8": "^4.1.8",
package/src/Main.test.ts CHANGED
@@ -41,6 +41,12 @@ const mentionMessage = {
41
41
  channelType: "guild"
42
42
  } satisfies DiscordMessage
43
43
 
44
+ const makeTestEnv = (projectDir: string, bridgePort: number) => ({
45
+ DISCORD_TOKEN: "token",
46
+ OPENCODE_PROJECT_DIR: projectDir,
47
+ DISCORD_BRIDGE_PORT: bridgePort.toString()
48
+ })
49
+
44
50
  describe("makeProgram", () => {
45
51
  test("runs Bun preflight, loads config, and scaffolds generated Discord tools", async () => {
46
52
  const projectDir = await mkdtemp(join(tmpdir(), "ocdb-program-"))
@@ -54,11 +60,11 @@ describe("makeProgram", () => {
54
60
  }
55
61
 
56
62
  try {
57
- await Effect.runPromise(makeProgram(projectDir, { DISCORD_TOKEN: "token", OPENCODE_PROJECT_DIR: projectDir }, factories))
63
+ await Effect.runPromise(makeProgram(projectDir, makeTestEnv(projectDir, 18_787), factories))
58
64
  const tool = await readFile(join(projectDir, ".opencode", "tools", "discord-bridge.ts"), "utf8")
59
65
 
60
66
  expect(tool).toContain("Generated by opencode-discord-bot")
61
- expect(tool).toContain("http://127.0.0.1:8787/tool")
67
+ expect(tool).toContain("http://127.0.0.1:18787/tool")
62
68
  } finally {
63
69
  await rm(projectDir, { recursive: true, force: true })
64
70
  }
@@ -152,7 +158,7 @@ describe("makeProgram callbacks", () => {
152
158
  }
153
159
 
154
160
  try {
155
- await Effect.runPromise(makeProgram(projectDir, { DISCORD_TOKEN: "token", OPENCODE_PROJECT_DIR: projectDir }, factories))
161
+ await Effect.runPromise(makeProgram(projectDir, makeTestEnv(projectDir, 18_788), factories))
156
162
  if (gatewayOptions === undefined) throw new Error("gateway options were not captured")
157
163
 
158
164
  await Effect.runPromise(gatewayOptions.onMessage(mentionMessage, { userId: "self" }))
@@ -0,0 +1,44 @@
1
+ import { tool } from "@opencode-ai/plugin"
2
+
3
+ const loopbackToolUrl = "__OPENCODE_DISCORD_BOT_LOOPBACK_URL__/tool"
4
+
5
+ export default tool({
6
+ description: "Perform safe Discord bridge actions through the local opencode-discord-bot process.",
7
+ args: {
8
+ action: tool.schema
9
+ .enum([
10
+ "followUpMessage",
11
+ "addReaction",
12
+ "removeReaction",
13
+ "fetchHistory",
14
+ "attachFile",
15
+ "createThread",
16
+ "editOwnMessage",
17
+ "deleteOwnMessage",
18
+ "postOtherChannel",
19
+ "pin",
20
+ "unpin"
21
+ ])
22
+ .describe("Discord bridge action to perform."),
23
+ target: tool.schema
24
+ .object({
25
+ guildId: tool.schema.string().optional(),
26
+ channelId: tool.schema.string().optional(),
27
+ threadId: tool.schema.string().optional(),
28
+ messageId: tool.schema.string().optional()
29
+ })
30
+ .describe("Discord target for the action."),
31
+ args: tool.schema
32
+ .record(tool.schema.string(), tool.schema.unknown())
33
+ .describe("Action-specific arguments, such as content, emoji, limit, path, or name.")
34
+ },
35
+ async execute(request) {
36
+ const response = await fetch(loopbackToolUrl, {
37
+ method: "POST",
38
+ headers: { "content-type": "application/json" },
39
+ body: JSON.stringify(request)
40
+ })
41
+ const payload: unknown = await response.json()
42
+ return JSON.stringify(payload, null, 2) ?? String(payload)
43
+ }
44
+ })
@@ -5,15 +5,24 @@ import { join } from "node:path"
5
5
  import { ensureDiscordTools, renderDiscordToolFile } from "./Scaffolding.ts"
6
6
 
7
7
  describe("Discord tool scaffolding", () => {
8
- test("renders a generated tool file with the injected loopback URL", () => {
9
- const source = renderDiscordToolFile("http://127.0.0.1:8787")
8
+ test("renders a generated opencode tool file with the injected loopback URL", async () => {
9
+ const source = await renderDiscordToolFile("http://127.0.0.1:8787")
10
10
 
11
11
  expect(source).toContain("Generated by opencode-discord-bot")
12
12
  expect(source).toContain("http://127.0.0.1:8787/tool")
13
+ expect(source).toContain('import { tool } from "@opencode-ai/plugin"')
14
+ expect(source).toContain("export default tool({")
15
+ expect(source).toContain("args: {")
16
+ expect(source).toContain(".enum([")
17
+ expect(source).toContain('"fetchHistory"')
18
+ expect(source).toContain('"addReaction"')
19
+ expect(source).toContain(".record(tool.schema.string(), tool.schema.unknown())")
13
20
  expect(source).toContain("fetch")
21
+ expect(source).not.toContain("export const parameters")
22
+ expect(source).not.toContain("__OPENCODE_DISCORD_BOT_LOOPBACK_URL__")
14
23
  })
15
24
 
16
- test("creates and refreshes only generated files", async () => {
25
+ test("creates and regenerates the tool file on every run", async () => {
17
26
  const projectDir = await mkdtemp(join(tmpdir(), "ocdb-tools-"))
18
27
 
19
28
  try {
@@ -28,16 +37,18 @@ describe("Discord tool scaffolding", () => {
28
37
  expect(await readFile(toolPath, "utf8")).toContain("http://127.0.0.1:9999/tool")
29
38
 
30
39
  await writeFile(toolPath, "// operator file\n")
31
- const skipped = await ensureDiscordTools({ projectDir, bridgePort: 7777, enabled: true, autoInstall: true })
40
+ const regenerated = await ensureDiscordTools({ projectDir, bridgePort: 7777, enabled: true, autoInstall: true })
32
41
 
33
- expect(skipped).toEqual([])
34
- expect(await readFile(toolPath, "utf8")).toBe("// operator file\n")
42
+ expect(regenerated).toEqual([toolPath])
43
+ expect(await readFile(toolPath, "utf8")).toContain("http://127.0.0.1:7777/tool")
44
+ expect(await readFile(toolPath, "utf8")).not.toContain("// operator file")
35
45
 
36
- await writeFile(toolPath, `${renderDiscordToolFile("http://127.0.0.1:9999")}\n// operator edit\n`)
37
- const edited = await ensureDiscordTools({ projectDir, bridgePort: 6666, enabled: true, autoInstall: true })
46
+ await writeFile(toolPath, "// Generated by opencode-discord-bot. DO NOT EDIT.\nexport const parameters = {}\n")
47
+ const migrated = await ensureDiscordTools({ projectDir, bridgePort: 6666, enabled: true, autoInstall: true })
38
48
 
39
- expect(edited).toEqual([])
40
- expect(await readFile(toolPath, "utf8")).toContain("// operator edit")
49
+ expect(migrated).toEqual([toolPath])
50
+ expect(await readFile(toolPath, "utf8")).toContain("http://127.0.0.1:6666/tool")
51
+ expect(await readFile(toolPath, "utf8")).not.toContain("export const parameters")
41
52
  } finally {
42
53
  await rm(projectDir, { recursive: true, force: true })
43
54
  }
@@ -2,7 +2,8 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"
2
2
  import { join } from "node:path"
3
3
 
4
4
  const header = "// Generated by opencode-discord-bot. DO NOT EDIT."
5
- const loopbackUrlPattern = /const loopbackToolUrl = "(http:\/\/127\.0\.0\.1:\d+)\/tool"/
5
+ const loopbackUrlPlaceholder = "__OPENCODE_DISCORD_BOT_LOOPBACK_URL__"
6
+ const discordToolSourceUrl = new URL("./DiscordBridgeTool.ts", import.meta.url)
6
7
 
7
8
  export type ToolScaffoldOptions = {
8
9
  readonly projectDir: string
@@ -11,50 +12,18 @@ export type ToolScaffoldOptions = {
11
12
  readonly autoInstall: boolean
12
13
  }
13
14
 
14
- export const renderDiscordToolFile = (loopbackUrl: string): string => `${header}
15
- const loopbackToolUrl = "${loopbackUrl}/tool"
16
-
17
- export const description = "Perform safe Discord bridge actions through the local opencode-discord-bot process."
18
-
19
- export const parameters = {
20
- type: "object",
21
- properties: {
22
- action: { type: "string" },
23
- target: { type: "object" },
24
- args: { type: "object" }
25
- },
26
- required: ["action", "target", "args"]
27
- }
28
-
29
- export async function execute(input: { action: string; target: object; args: object }) {
30
- const response = await fetch(loopbackToolUrl, {
31
- method: "POST",
32
- headers: { "content-type": "application/json" },
33
- body: JSON.stringify(input)
34
- })
35
- return await response.json()
15
+ export const renderDiscordToolFile = async (loopbackUrl: string): Promise<string> => {
16
+ const source = await readFile(discordToolSourceUrl, "utf8")
17
+ return `${header}\n${source.replaceAll(loopbackUrlPlaceholder, loopbackUrl)}`
36
18
  }
37
- `
38
-
39
- const generatedLoopbackUrl = (source: string): string | undefined => loopbackUrlPattern.exec(source)?.[1]
40
19
 
41
20
  export const ensureDiscordTools = async (options: ToolScaffoldOptions): Promise<ReadonlyArray<string>> => {
42
21
  if (!options.enabled || !options.autoInstall) return []
43
22
  const toolsDir = join(options.projectDir, ".opencode", "tools")
44
23
  const toolPath = join(toolsDir, "discord-bridge.ts")
45
- const next = renderDiscordToolFile(`http://127.0.0.1:${options.bridgePort}`)
24
+ const next = await renderDiscordToolFile(`http://127.0.0.1:${options.bridgePort}`)
46
25
 
47
26
  await mkdir(toolsDir, { recursive: true })
48
- try {
49
- const current = await readFile(toolPath, "utf8")
50
- if (!current.startsWith(header)) return []
51
- if (current === next) return [toolPath]
52
- const currentLoopbackUrl = generatedLoopbackUrl(current)
53
- if (currentLoopbackUrl === undefined || current !== renderDiscordToolFile(currentLoopbackUrl)) return []
54
- } catch (error) {
55
- if (!(error instanceof Error) || !Reflect.has(error, "code")) throw error
56
- }
57
-
58
27
  await writeFile(toolPath, next)
59
28
  return [toolPath]
60
29
  }