Gemini CLI with Discord
Gemini CLIをDiscordから呼び出せるようにしてみた

概要
前回に引き続きGeminiネタ
現状Raspberry Piの上で常時Gemini CLIが稼働できる状態になっている.
このままでも悪くはないが,使いたいときはSSHしてdocker compose execして...というように手数がかかる.
Discordからプロンプトを入力して,回答もそこで得られるようにしてみる
構成

実装
リポジトリとして公開できるほど整備してないのでコードだけ一部紹介.
まずはdocker-compose.yamlから.
services:
gemini:
build: .
container_name: gemini
command: python3 /root/bot.py
restart: always
environment:
- DISCORD_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxx
- MODELS=gemini-2.5-flash
stdin_open: true
tty: true
volumes:
- ./home:/root
- ./app:/app
docker-compose.yaml
- ボリュームは適宜バインド
- 環境変数でGeminiのモデルとDiscord Bot用のトークンを設定
続いてDockerfile.
FROM python:3.11-slim
RUN apt-get update && apt-get install -y curl \
&& curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \
&& apt-get install -y nodejs \
&& npm install -g @google/gemini-cli
RUN set -eux; \
npm install -g @google/gemini-cli > /dev/null 2>&1; \
apt-get update -qq; \
apt-get install -y --no-install-recommends \
python3 \
make \
g++ \
man-db \
curl \
dnsutils \
less \
jq \
bc \
gh \
git \
unzip \
rsync \
ripgrep \
procps \
psmisc \
lsof \
socat \
ca-certificates \
lsb-release \
wget > /dev/null 2>&1; \
install -m 0755 -d /etc/apt/keyrings; \
wget -qO - https://packages.adoptium.net/artifactory/api/gpg/key/public | tee /etc/apt/keyrings/adoptium.asc > /dev/null; \
echo "deb [signed-by=/etc/apt/keyrings/adoptium.asc] https://packages.adoptium.net/artifactory/deb $(lsb_release -cs) main" \
| tee /etc/apt/sources.list.d/adoptium.list > /dev/null; \
apt-get update -qq; \
apt-get install -y --no-install-recommends temurin-17-jdk > /dev/null 2>&1; \
apt-get clean; \
rm -rf /var/lib/apt/lists/*
COPY home/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
WORKDIR /app
CMD ["python", "~/bot.py"]
Dockerfile
- ベースはPythonにしてDiscord Botをメインにして動くようにする.
- Gemini CLIと共存させる
- 実行環境を作るために必要なJavaと各種utilsを適宜インストール (任意)
最後にBot向けのコード.(ChatGPT頼み)
discord.py
python-dotenv
requirements.txt
import discord
from discord.ext import commands
from dotenv import load_dotenv
import os
import shlex
import asyncio
load_dotenv()
DISCORD_TOKEN = os.getenv("DISCORD_TOKEN")
if not DISCORD_TOKEN:
raise ValueError("DISCORD_TOKEN is not set in environment variables.")
MODELS = os.getenv("MODELS", "gemini-2.5-pro")
intents = discord.Intents.default()
intents.message_content = True
bot = commands.Bot(command_prefix="/", intents=intents)
@bot.command()
async def gemini(ctx, *, question: str):
session_id = f"thread-{ctx.channel.id}" # unused
escaped_question = shlex.quote(question)
cmd = f"gemini -m {MODELS} -p {escaped_question} --yolo"
try:
await ctx.message.add_reaction("🤖")
except discord.HTTPException:
pass
try:
print(f"[gemini] Running command: {cmd}")
proc = await asyncio.create_subprocess_shell(
cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
stdout, stderr = await proc.communicate()
if stderr:
error_msg = stderr.decode().strip()
raise RuntimeError(error_msg)
output = stdout.decode().strip()
print(output)
if not output:
raise RuntimeError("No output from gemini.")
# Discordの2000文字制限に対応して分割送信
for i in range(0, len(output), 2000):
await ctx.send(output[i:i+2000])
try:
await ctx.message.remove_reaction("🤖", bot.user)
await ctx.message.add_reaction("✅")
except discord.HTTPException:
pass
except Exception as e:
try:
await ctx.message.remove_reaction("🤖", bot.user)
await ctx.message.add_reaction("❌")
except discord.HTTPException:
pass
error_msg = str(e)
if len(error_msg) <= 2000:
await ctx.send(f"Error: {error_msg}")
else:
await ctx.send("Error occurred. Output too long to display.")
bot.run(DISCORD_TOKEN)
bot.py
- 環境変数からDiscordトークン設定
- コマンド
/gemini
を受け取ったら処理開始- geminiを呼び出すためのコマンド生成 (同じコンテナ内のサブプロセスで)
cmd = f"gemini -m {MODELS} -p {escaped_question} --yolo"
-m
でモデル指定.
- geminiを呼び出すためのコマンド生成 (同じコンテナ内のサブプロセスで)
-p
でDiscordから送ったプロンプトを渡す--yolo
オプションでコマンド実行許可(コンテナ内なので壊れてOK)- ログ出力
- Geminiからの回答をDiscordへ送信
- 2000文字制限がある(らしい)ので分割して送信
- ログには出力しておく
- リアクション
- コマンドを受け取ったあと🤖で反応
- Gemini CLI側で処理成功の場合✅,NGの場合❌
これをいい感じに配置する
.
├── Dockerfile
├── README.md
├── app
│ └── simple-api # 適当なJavaアプリケーション
├── docker-compose.yaml
└── home
├── bot.py
└── requirements.txt
72 directories, 50 files
tree
動作例
呼び出し

↓

コンテナログ出力
raspberrypi4% docker compose logs -f
...
gemini | [gemini] Running command: gemini -m gemini-2.5-flash -p hello --yolo
gemini | こんにちは!何かお手伝いできることはありますか?
コマンド実行

できてそう
アプリケーション概要説明
単純な機能を持つSpringBootのサンプルアプリケーションについて説明させてみる.

概要すぎるけどいい感じ.もっと指示すればたぶん回答してもらえる
コード修正
Issueを読んでPRを作成してくれる

ghコマンドを使えるようにトークン発行や認証は事前にしてあげている

ここまで何度か実行していたのでコンテナ内にはすでに変更された状態が残っていたみたい.このあたりは割と汲み取って指示してあげる必要あり.まだ気を使わないといけないところ.
まあでも期待していたPRが出てきた.まだ簡単な処理だからイケてるように見えているが,アプリケーションの複雑性が増すとだめかも...
ビルド確認
Javaを入れているのでビルドしてテストが通るかは確認してくれる.

PRコメント
変更差分を見て概要をPRコメントさせる


Tips
システムプロンプト GEMINI.md
root@33e02bcb854e:/app# cat ~/.gemini/GEMINI.md
- 日本語で回答してください.
- gitの操作はgitコマンドを使用できます.
- GitHubのPRに関する操作はghコマンドを使用してください.
- GitHubのPR作成については,指示がなければmainブランチへのPRとしてください.
- リファクタリングできる点があった場合は相談してください.
- 現在のブランチと関連性がない新機能を実装する場合はブランチを切って作業してください.
- コミット/プッシュする前にビルドが通ることを確認してください.
- 作業する前はmainブランチを最新化してください.
- 回答はDiscordへ送信されるため,Marddown形式で読みやすくなるように心がけてください.
場所によってグローバル,プロジェクトごとに設定を変えられる
ここのチューニングをがんばって育てるしかない.あとはモデルの進化待ち
まとめ
気を使うところはあるものの,ひとり仲間が増えたみたいでうれしい.
flash
なのでpro
を使えばもっと賢くなるはず.
実用性もだが,ChatOps的にやってみたかった欲が満たせたので自己満足
Pros
- MacではなくRaspberry Piで動かせる
- Macの開発に支障が出ない.環境が汚れない
- 常にGeminiが動ける状態
- sshせずにDiscord(=スマホ) から指示可
- コンテナ内なので環境が壊れることを気にしなくてよい
- Issue や PR など外部の情報を参照して,それなりに考えて動いてくれる
Cons
- 1発では決まらない
- 汲み取って途中で指示してあげる必要あり.
- RaspberryPiのスペックに依存する
- Gemini CLI自体はそこまででない(メモリ200MB程度)
- ビルドはものによっては重い
- サブプロセスで毎回単発でgeminiコマンドを叩くのでコンテキストが保てない
gemini-2.5-pro
だとなぜかうまくいかなかった.エラーになる- 動作確認はすべて
gemini-2.5-flash
使用
- 動作確認はすべて