Browse Source

feat: add Slack integration package with Bolt framework

Dax Raad 5 months ago
parent
commit
1923ddab6e

File diff suppressed because it is too large
+ 352 - 24
bun.lock


+ 2 - 1
package.json

@@ -13,7 +13,8 @@
     "packages": [
       "packages/*",
       "packages/console/*",
-      "packages/sdk/js"
+      "packages/sdk/js",
+      "packages/slack"
     ],
     "catalog": {
       "@types/bun": "1.3.0",

+ 12 - 0
packages/opencode/src/tool/write.ts

@@ -10,6 +10,8 @@ import { FileTime } from "../file/time"
 import { Filesystem } from "../util/filesystem"
 import { Instance } from "../project/instance"
 import { Agent } from "../agent/agent"
+import { createTwoFilesPatch } from "diff"
+import { trimDiff } from "./edit"
 
 export const WriteTool = Tool.define("write", {
   description: DESCRIPTION,
@@ -27,6 +29,13 @@ export const WriteTool = Tool.define("write", {
     const exists = await file.exists()
     if (exists) await FileTime.assert(ctx.sessionID, filepath)
 
+    let oldContent = ""
+    let diff = ""
+
+    if (exists) {
+      oldContent = await file.text()
+    }
+
     const agent = await Agent.get(ctx.agent)
     if (agent.permission.edit === "ask")
       await Permission.ask({
@@ -48,6 +57,9 @@ export const WriteTool = Tool.define("write", {
     })
     FileTime.read(ctx.sessionID, filepath)
 
+    // Generate diff for the write operation
+    diff = trimDiff(createTwoFilesPatch(filepath, filepath, oldContent, params.content))
+
     let output = ""
     await LSP.touchFile(filepath, true)
     const diagnostics = await LSP.diagnostics()

+ 3 - 0
packages/slack/.env.example

@@ -0,0 +1,3 @@
+SLACK_BOT_TOKEN=xoxb-your-bot-token
+SLACK_SIGNING_SECRET=your-signing-secret
+SLACK_APP_TOKEN=xapp-your-app-token

+ 4 - 0
packages/slack/.gitignore

@@ -0,0 +1,4 @@
+node_modules
+dist
+.env
+.DS_Store

+ 27 - 0
packages/slack/README.md

@@ -0,0 +1,27 @@
+# @opencode-ai/slack
+
+Slack bot integration for opencode that creates threaded conversations.
+
+## Setup
+
+1. Create a Slack app at https://api.slack.com/apps
+2. Enable Socket Mode
+3. Add the following OAuth scopes:
+   - `chat:write`
+   - `app_mentions:read`
+   - `channels:history`
+   - `groups:history`
+4. Install the app to your workspace
+5. Set environment variables in `.env`:
+   - `SLACK_BOT_TOKEN` - Bot User OAuth Token
+   - `SLACK_SIGNING_SECRET` - Signing Secret from Basic Information
+   - `SLACK_APP_TOKEN` - App-Level Token from Basic Information
+
+## Usage
+
+```bash
+# Edit .env with your Slack app credentials
+bun dev
+```
+
+The bot will respond to messages in channels where it's added, creating separate opencode sessions for each thread.

+ 17 - 0
packages/slack/package.json

@@ -0,0 +1,17 @@
+{
+  "name": "@opencode-ai/slack",
+  "version": "0.1.0",
+  "type": "module",
+  "scripts": {
+    "dev": "bun run src/index.ts",
+    "typecheck": "tsc --noEmit"
+  },
+  "dependencies": {
+    "@opencode-ai/sdk": "workspace:*",
+    "@slack/bolt": "^3.17.1"
+  },
+  "devDependencies": {
+    "@types/node": "catalog:",
+    "typescript": "catalog:"
+  }
+}

+ 105 - 0
packages/slack/src/index.ts

@@ -0,0 +1,105 @@
+import { App } from "@slack/bolt"
+import { createOpencode } from "@opencode-ai/sdk"
+
+const app = new App({
+  token: process.env.SLACK_BOT_TOKEN,
+  signingSecret: process.env.SLACK_SIGNING_SECRET,
+  socketMode: true,
+  appToken: process.env.SLACK_APP_TOKEN,
+})
+
+console.log("🔧 Bot configuration:")
+console.log("- Bot token present:", !!process.env.SLACK_BOT_TOKEN)
+console.log("- Signing secret present:", !!process.env.SLACK_SIGNING_SECRET)
+console.log("- App token present:", !!process.env.SLACK_APP_TOKEN)
+
+console.log("🚀 Starting opencode server...")
+const opencode = await createOpencode({
+  port: 0,
+})
+console.log("✅ Opencode server ready")
+
+const sessions = new Map<string, { client: any; server: any; sessionId: string; channel: string; thread: string }>()
+
+app.use(async ({ next, context }) => {
+  console.log("📡 Raw Slack event:", JSON.stringify(context, null, 2))
+  await next()
+})
+
+app.message(async ({ message, say }) => {
+  console.log("📨 Received message event:", JSON.stringify(message, null, 2))
+
+  if (message.subtype || !("text" in message) || !message.text) {
+    console.log("⏭️ Skipping message - no text or has subtype")
+    return
+  }
+
+  console.log("✅ Processing message:", message.text)
+
+  const channel = message.channel
+  const thread = (message as any).thread_ts || message.ts
+  const sessionKey = `${channel}-${thread}`
+
+  let session = sessions.get(sessionKey)
+
+  if (!session) {
+    console.log("🆕 Creating new opencode session...")
+    const { client, server } = opencode
+
+    const createResult = await client.session.create({
+      body: { title: `Slack thread ${thread}` },
+    })
+
+    if (createResult.error) {
+      console.error("❌ Failed to create session:", createResult.error)
+      await say({ text: "Sorry, I had trouble creating a session. Please try again.", thread_ts: thread })
+      return
+    }
+
+    console.log("✅ Created opencode session:", createResult.data.id)
+    session = { client, server, sessionId: createResult.data.id, channel, thread }
+    sessions.set(sessionKey, session)
+
+    const shareResult = await client.session.share({ path: { id: createResult.data.id } })
+    if (!shareResult.error && shareResult.data) {
+      const sessionUrl = shareResult.data.share?.url!
+      console.log("🔗 Session shared:", sessionUrl)
+      await app.client.chat.postMessage({ channel, thread_ts: thread, text: sessionUrl })
+    }
+  }
+
+  console.log("📝 Sending to opencode:", message.text)
+  const result = await session.client.session.prompt({
+    path: { id: session.sessionId },
+    body: { parts: [{ type: "text", text: message.text }] },
+  })
+
+  console.log("📤 Opencode response:", JSON.stringify(result, null, 2))
+
+  if (result.error) {
+    console.error("❌ Failed to send message:", result.error)
+    await say({ text: "Sorry, I had trouble processing your message. Please try again.", thread_ts: thread })
+    return
+  }
+
+  const response = result.data
+  const responseText =
+    response.info?.content ||
+    response.parts
+      ?.filter((p: any) => p.type === "text")
+      .map((p: any) => p.text)
+      .join("\n") ||
+    "I received your message but didn't have a response."
+
+  console.log("💬 Sending response:", responseText)
+  await say({ text: responseText, thread_ts: thread })
+})
+
+app.command("/test", async ({ command, ack, say }) => {
+  await ack()
+  console.log("🧪 Test command received:", JSON.stringify(command, null, 2))
+  await say("🤖 Bot is working! I can hear you loud and clear.")
+})
+
+await app.start()
+console.log("⚡️ Slack bot is running!")

+ 3 - 0
packages/slack/sst-env.d.ts

@@ -0,0 +1,3 @@
+/// <reference types="../../../sst-env.d.ts" />
+
+export {}

+ 5 - 0
packages/slack/tsconfig.json

@@ -0,0 +1,5 @@
+{
+  "$schema": "https://json.schemastore.org/tsconfig",
+  "extends": "@tsconfig/bun/tsconfig.json",
+  "compilerOptions": {}
+}

Some files were not shown because too many files changed in this diff