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 +2 -1
- package/src/Main.test.ts +9 -3
- package/src/Tools/DiscordBridgeTool.ts +44 -0
- package/src/Tools/Scaffolding.test.ts +21 -10
- package/src/Tools/Scaffolding.ts +6 -37
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-discord-bot",
|
|
3
|
-
"version": "0.0.
|
|
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,
|
|
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:
|
|
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,
|
|
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
|
|
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
|
|
40
|
+
const regenerated = await ensureDiscordTools({ projectDir, bridgePort: 7777, enabled: true, autoInstall: true })
|
|
32
41
|
|
|
33
|
-
expect(
|
|
34
|
-
expect(await readFile(toolPath, "utf8")).
|
|
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,
|
|
37
|
-
const
|
|
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(
|
|
40
|
-
expect(await readFile(toolPath, "utf8")).toContain("
|
|
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
|
}
|
package/src/Tools/Scaffolding.ts
CHANGED
|
@@ -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
|
|
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 =>
|
|
15
|
-
const
|
|
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
|
}
|