Bikin Discord Bot untuk Claude Code

Saya punya kebiasaan kerja yang mungkin agak aneh: saya ingin bisa ngobrol dengan Claude Code langsung dari Discord — bukan buka terminal, bukan buka browser, tapi langsung dari chat yang sudah saya buka sepanjang hari. Jadi saya bikin bot-nya sendiri.

Tulisan ini bukan tutorial step-by-step, tapi lebih ke sharing arsitektur — gimana semua bagiannya nyambung satu sama lain.

Gambaran Besar

Bot-nya satu binary TypeScript yang jalan di host machine (bukan di Docker), karena dia butuh akses ke claude -p CLI yang sudah login di sini. Kalau di-container-ize, session Claude-nya tidak akan terbawa.

Flow dasarnya begini:

User kirim pesan di Discord thread
         ↓
Bot terima event dari Discord Gateway
         ↓
Diteruskan ke tmux window (satu window per thread)
         ↓
Claude Code CLI jalan di sana, output ke file .jsonl
         ↓
Session monitor polling file itu tiap 2 detik
         ↓
Output dikirim balik ke Discord thread

Satu tmux Session untuk Semua

Bagian yang paling menarik menurut saya adalah penggunaan tmux sebagai “runtime” untuk Claude.

Semua sesi Claude berjalan dalam satu tmux session bernama claude-sessions. Tiap Discord thread punya satu tmux window sendiri. Jadi kalau ada 3 thread aktif, ada 3 window di tmux itu.

Kenapa tmux? Karena claude -p adalah CLI interaktif — dia butuh terminal. Tmux memberi kita terminal yang bisa kita kontrol secara programatik: bisa kirim keystrokes, bisa baca output-nya, bisa kita tinggal pergi dan balik lagi.

// Buka window baru untuk sesi Discord thread ini
await tmux.newWindow('claude-sessions', windowName, workingDir);

// Kirim pesan user ke Claude
await tmux.sendKeys(windowName, `${userMessage}\n`);

Membaca Output Claude

Claude Code menulis output-nya ke file .jsonl di ~/.claude/projects/{encoded_cwd}/{sessionId}.jsonl. Formatnya satu JSON object per baris, tiap baris adalah satu “event” — bisa teks biasa, tool use, error, dsb.

Session monitor polling file ini tiap 2 detik, membaca baris-baris baru yang belum diproses, lalu mem-format-nya untuk dikirim ke Discord.

Yang agak tricky adalah Discord punya limit 2000 karakter per pesan. Jadi kalau output Claude panjang, bot akan attach output tersebut sebagai file .txt alih-alih kirim sebagai pesan teks.

Tool use pun diformat supaya ringkas:

🔧 _Bash_ `cat package.json`

Slash Commands

Ada beberapa slash command yang tersedia:

Command Fungsi
/session [folder] Buka thread baru dengan sesi Claude interaktif
/new Mulai percakapan baru di dalam thread yang sama
/endsession Matikan sesi, lock thread
/ask <prompt> Query satu kali ke AI (tidak interaktif)
/summary Lihat ringkasan history percakapan
/reset Hapus history untuk channel ini
/sessions Lihat semua sesi aktif di guild
/remote [folder] Buka sesi Claude remote-control (dapat URL yang bisa dibuka di browser)

/session dan /ask punya tujuan yang berbeda. /ask itu ringan — satu pertanyaan, satu jawaban, ada history rolling-nya. /session itu berat — buka thread baru, spawn tmux window, streaming output penuh dari Claude Code termasuk tool use.

Dua Provider AI

Ada abstraksi AiProvider yang pluggable:

Untuk sesi interaktif (/session), routing-nya bypass provider ini — langsung ke tmux. Jadi provider hanya relevan untuk command one-shot seperti /ask.

SQLite untuk History

Conversation history untuk /ask disimpan di SQLite. Bot Discord punya database-nya sendiri (DISCORD_DATABASE_PATH), terpisah dari bot Telegram yang juga jalan di host yang sama.

Ada dua tabel utama: messages (rolling N pesan terakhir) dan summary (ringkasan yang di-refresh secara otomatis setelah N pesan baru). Ini supaya context tidak membengkak tanpa batas.

Access Control

Sebelum command apapun dieksekusi, bot cek dua hal:

  1. Apakah guild ini ada di DISCORD_ALLOWED_GUILD_IDS?
  2. Apakah user ini ada di DISCORD_ALLOWED_USER_IDS?

Kalau salah satu tidak lolos, perintah diabaikan tanpa balasan. Bot ini memang private — tidak dimaksudkan untuk publik.

Menjalankannya

cd chat
npm install
npm run dev:discord    # development dengan tsx watch
# atau
npm run build && npm run start:discord   # production

Env var minimal yang dibutuhkan: DISCORD_BOT_TOKEN dan DISCORD_APPLICATION_ID. Slash commands akan register per-guild kalau DISCORD_ALLOWED_GUILD_IDS di-set (propagasi instan), atau globally kalau tidak di-set (~1 jam).


Hasilnya? Saya sekarang bisa minta Claude baca file, jalankan perintah, atau review kode langsung dari Discord — tanpa pindah window. Cukup ketik di thread, dan Claude merespons seperti biasa.