diff --git a/.claude/DISCORD_NOTIFY.md b/.claude/DISCORD_NOTIFY.md new file mode 100644 index 000000000..6f414aa7c --- /dev/null +++ b/.claude/DISCORD_NOTIFY.md @@ -0,0 +1,93 @@ +# Clawdbot Discord 通知設定 + +## 概要 + +Clawdbot の Discord コネクションを使用して、Claude Code の操作完了時に Discord 通知を送信します。 + +## 設定手順 + +### 1. Discord チャンネル ID を取得 + +通知を送信したい Discord チャンネルの ID を取得します: + +1. Discord の設定 → 詳細設定 → 開発者モード をオン +2. チャンネルを右クリック → ID をコピー +3. チャンネル ID: `123456789012345678` のような形式 + +### 2. .env に設定 + +```bash +cd ~/dev/clawdbot +echo "DISCORD_NOTIFY_CHANNEL=123456789012345678" >> .env +``` + +### 3. Clawdbot Discord 接続を設定 + +```bash +# Discord アカウント連携 +clawdbot login --channel discord + +# 接続確認 +clawdbot channels status --probe +``` + +### 4. 動作確認 + +```bash +# テスト通知 +clawdbot message send \ + --channel discord \ + --target "123456789012345678" \ + --message "🤖 テスト通知" +``` + +## 通知タイミング + +| タイミング | 内容 | +|-----------|------| +| PostToolUse | 操作完了時に Discord 通知を送信 | + +## 環境変数 + +| 変数 | 説明 | 例 | +|------|------|-----| +| `DISCORD_NOTIFY_CHANNEL` | 通知先 Discord チャンネル ID | `123456789012345678` | +| `CLAWDBOT_DIR` | clawdbot のパス(任意) | `$HOME/dev/clawdbot` | + +## ファイル構成 + +``` +.claude/ +├── hooks/ +│ └── discord-notify-clawdbot.sh # Discord 通知スクリプト +├── settings.json # Claude Code フック設定 +└── DISCORD_NOTIFY.md # このドキュメント +``` + +## トラブルシューティング + +### 通知が届かない場合 + +1. Discord 接続状態確認: + ```bash + clawdbot channels status + ``` + +2. チャンネル ID 確認: + ```bash + grep DISCORD_NOTIFY_CHANNEL .env + ``` + +3. 手動テスト: + ```bash + .claude/hooks/discord-notify-clawdbot.sh << EOF +{"content": "テスト通知"} +EOF + ``` + +### CLI がビルドされていない場合 + +```bash +cd ~/dev/clawdbot +pnpm build +``` diff --git a/.claude/SECOND_BRAIN.md b/.claude/SECOND_BRAIN.md new file mode 100644 index 000000000..60c6a0791 --- /dev/null +++ b/.claude/SECOND_BRAIN.md @@ -0,0 +1,170 @@ +# 🧠 Discord セカンドブレイン設定 + +## 概要 + +Claude Code のセッション情報を Discord に保存し、**検索可能なナレッジベース**として活用します。 + +--- + +## 🎯 保存される情報 + +### セッションメタデータ + +| 項目 | 説明 | +|------|------| +| 日時 | セッションの日時 | +| ユーザー | 誰が作業したか | +| ホスト | どのマシンで作業したか | +| プロジェクト | 作業対象プロジェクト | +| パス | ワークディレクトリ | + +### セクション構成 + +```markdown +# 🧠 Claude Code Memory + +## 💡 重要な発見・学習 +このセッションで得た知見 + +## 🔧 技術的メモ +技術的な詳細・設定変更など + +## ⚠️ 問題・エラーと解決策 +遭遇した問題と解決方法 + +## 🔗 関連リソース +参考ドキュメント・Issueなど + +## 📂 Git 変更 +変更ファイル一覧 +``` + +--- + +## 📁 保存場所 + +| 場所 | 説明 | +|------|------| +| **Discord** | `#general` (デフォルト) - 全体で共有 | +| **ローカル** | `~/.clawdbot/memory/YYYY-MM-DD-HHMMSS.md` | + +--- + +## 🔧 設定 + +### 環境変数 + +| 変数 | 説明 | デフォルト | +|------|------|----------| +| `DISCORD_BRAIN_CHANNEL` | 保存先チャンネルID | `1465087447225598207` (#general) | +| `DISCORD_NOTIFY_CHANNEL` | ステータス通知先 | `1465087451113722019` (#status) | + +### チャンネル変更例 + +```bash +cd ~/dev/clawdbot + +# #announcements に保存 +echo 'DISCORD_BRAIN_CHANNEL=1465087454963830825' >> .env + +# #task-queue に保存 +echo 'DISCORD_BRAIN_CHANNEL=1465087503622209757' >> .env +``` + +--- + +## 📊 フック実行順序 + +``` +SessionStart → タスク開始通知 + ↓ + [作業実行] + ↓ +SessionEnd → 1. セカンドブレイン保存 (discord-brain-save.sh) + → 2. タスク完了通知 (discord-task-end.sh) +``` + +--- + +## 🔍 検索・活用方法 + +### Discord 検索 + +``` +# プロジェクト別 +in:#general #clawcode + +# 日付別 +in:#general from:bot after:2026-01-26 + +# キーワード検索 +in:#general エラー 解決 +in:#general 設定 変更 +``` + +### ローカル検索 + +```bash +# メモリディレクトリから検索 +cd ~/.clawdbot/memory +grep -r "キーワード" *.md +``` + +--- + +## ✨ 活用シナリオ + +### 1. 問題解決の記録 + +- エラー内容と解決手順 +- トラブルシューティングのナレッジ蓄積 + +### 2. 設定変更の履歴 + +- どの設定をいつ変更したか +- 変更前後の比較 + +### 3. 学習の記録 + +- 新しく学んだ技術・概念 +- ベストプラクティスの発見 + +### 4. チーム共有 + +- 全員が同じ Discord を見れる +- 知見の共有・再利用 + +--- + +## 📝 セカンドブレイン運用ガイド + +### 効率的なタグ付け + +Discord のメッセージには自動的にタグが付与されます: + +- `#clawcode` - Claude Code セッション +- `#memory` - メモリ記録 +- `#プロジェクト名` - プロジェクト特定 + +### 手動追加推奨タグ + +``` +#解決済 - 問題解決記録 +#設定 - 設定変更 +#バグ - バグ報告 +#学習 - 新規学習 +``` + +--- + +## 🔄 保管・アーカイブ + +古いメモリの整理: + +1. 定期的に Discord メッセージをスレッド化 +2. 重要な情報は Wiki に昇格 +3. ローカルファイルは適宜削除・アーカイブ + +--- + +準備完了です!次回のセッション終了時に自動保存されます。 diff --git a/.claude/SESSION_NOTIFY.md b/.claude/SESSION_NOTIFY.md new file mode 100644 index 000000000..c4893aaa7 --- /dev/null +++ b/.claude/SESSION_NOTIFY.md @@ -0,0 +1,43 @@ +# Clawdbot セッション終了通知 + +## 設定完了 + +✅ **セッション終了時のみ Discord 通知** が設定されました + +### 通知内容 + +``` +🤖 Claude Code セッション終了 + +📅 日時: 2026-01-26 10:30:00 +👤 ユーザー: shunsukehayashi +🖥️ ホスト: Mac-mini + +✅ 作業完了 +``` + +### 設定値 + +| 項目 | 値 | +|------|-----| +| 通知チャンネル | **#status** | +| チャンネル ID | `1465087451113722019` | +| 通知タイミング | セッション終了時(1回のみ) | + +### チャンネル変更 + +通知先を変更する場合: + +```bash +cd ~/dev/clawdbot + +# #general に変更 +echo 'DISCORD_NOTIFY_CHANNEL=1465087447225598207' >> .env + +# #task-queue に変更 +echo 'DISCORD_NOTIFY_CHANNEL=1465087503622209757' >> .env +``` + +### テスト + +セッションを終了して通知を確認してください。 diff --git a/.claude/TASK_TRACKING.md b/.claude/TASK_TRACKING.md new file mode 100644 index 000000000..d62f7d5a7 --- /dev/null +++ b/.claude/TASK_TRACKING.md @@ -0,0 +1,86 @@ +# Clawdbot タスク追跡通知 + +## ✅ 設定完了 + +**セッション開始・終了時の Discord 通知**が設定されました。 + +--- + +## 📊 通知内容 + +### セッション開始時 (SessionStart) + +``` +🚀 Claude Code セッション開始 + +📅 開始時刻: 2026-01-26 10:30:00 +👤 ユーザー: shunsukehayashi +🖥️ ホスト: Mac-mini +📁 ワークディレクトリ: ~/dev/clawdbot + +⏳ 作業開始... +``` + +### セッション終了時 (SessionEnd) + +``` +✅ Claude Code セッション完了 + +📅 開始時刻: 2026-01-26 10:30:00 +📅 完了時刻: 2026-01-26 11:45:00 +⏱️ 経過時間: 1時間15分 +👤 ユーザー: shunsukehayashi +🖥️ ホスト: Mac-mini +📂 Git: feature/xxx (5ファイル変更) + +✨ 作業完了 +``` + +--- + +## 🔧 設定値 + +| 項目 | 値 | +|------|-----| +| 通知チャンネル | **#status** | +| チャンネル ID | `1465087451113722019` | +| 通知タイミング | セッション開始時 + 終了時 | + +--- + +## 📁 ファイル構成 + +``` +.claude/ +├── hooks/ +│ ├── discord-task-start.sh # セッション開始通知 +│ └── discord-task-end.sh # セッション終了通知 +├── settings.json # Claude Code フック設定 +└── TASK_TRACKING.md # このドキュメント +``` + +--- + +## 🧪 テスト方法 + +1. 新しい Claude Code セッションを開始 +2. Discord #status チャンネルで開始通知を確認 +3. セッションを終了 +4. 完了通知(経過時間付き)を確認 + +--- + +## 🔍 追跡される情報 + +| 項目 | 説明 | +|------|------| +| 開始/完了時刻 | 正確な作業時間 | +| 経過時間 | 自動計算(時間/分/秒) | +| ユーザー名 | 誰が作業したか | +| ホスト名 | どのマシンで作業したか | +| Git状態 | ブランチ名 + 変更ファイル数 | +| ワークディレクトリ | 作業場所 | + +--- + +準備完了です!次回の Claude Code セッションから通知が届きます。 diff --git a/.claude/hooks/discord-brain-save.sh b/.claude/hooks/discord-brain-save.sh new file mode 100755 index 000000000..fc2f26e5a --- /dev/null +++ b/.claude/hooks/discord-brain-save.sh @@ -0,0 +1,178 @@ +#!/bin/bash +# Clawdbot Discord Second Brain Hook +# SessionEnd: Save important insights to Discord (Knowledge Base) + +# clawdbot のパス +CLAWDBOT_DIR="${CLAWDBOT_DIR:-$HOME/dev/clawdbot}" +CLAWDBOT_CLI="$CLAWDBOT_DIR/dist/entry.js" + +# CLIが存在するか確認 +[ ! -f "$CLAWDBOT_CLI" ] && exit 0 + +# Discord 設定 +# セカンドブレイン用チャンネル(別途設定可能) +BRAIN_CHANNEL_ID="${DISCORD_BRAIN_CHANNEL:-1465087447225598207}" # デフォルト: #general +DISCORD_ACCOUNT_ID="${DISCORD_NOTIFY_ACCOUNT:-ppal}" + +# メモリ保存先 +MEMORY_FILE="/tmp/clawdbot-brain-${USER}-$$.md" + +# クリーンアップ用trap +trap "rm -f $MEMORY_FILE" EXIT INT TERM + +# セッション情報収集 +SESSION_DATE=$(date '+%Y-%m-%d') +SESSION_TIME=$(date '+%H:%M:%S') +SESSION_HOST=$(hostname -s 2>/dev/null || hostname | cut -d'.' -f1) +SESSION_PWD=$PWD + +# Git 変更情報収集 +GIT_INFO="" +if git rev-parse --git-dir > /dev/null 2>&1; then + GIT_BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown") + GIT_CHANGED=$(git status --short 2>/dev/null) + GIT_COMMIT=$(git log -1 --pretty=format:"%h - %s" 2>/dev/null || echo "no commit") + + if [ -n "$GIT_CHANGED" ]; then + GIT_INFO=" +## 📂 Git 変更 +\`\`\` +$GIT_CHANGED +\`\`\` +**ブランチ**: $GIT_BRANCH +**最新コミット**: $GIT_COMMIT" + fi +fi + +# 作業内容推測(ディレクトリ名から) +PROJECT_NAME=$(basename "$SESSION_PWD") +WORK_CONTEXT="" + +case "$PROJECT_NAME" in + *clawdbot*) + WORK_CONTEXT="🦞 Clawdbot 開発作業" + ;; + *miyabi*) + WORK_CONTEXT="🎭 Miyabi エージェント作業" + ;; + *ppal*) + WORK_CONTEXT="📚 PPAL コース作業" + ;; + gen-studio*) + WORK_CONTEXT="🎨 Gen-Studio 開発" + ;; + *) + WORK_CONTEXT="💻 $PROJECT_NAME 作業" + ;; +esac + +# メモリファイル作成 +cat > "$MEMORY_FILE" << EOF +# 🧠 Claude Code Memory - $SESSION_DATE + +## 📋 セッション情報 +- **日時**: $SESSION_DATE $SESSION_TIME +- **ユーザー**: ${USER:-unknown} +- **ホスト**: $SESSION_HOST +- **プロジェクト**: $WORK_CONTEXT +- **パス**: \`$SESSION_PWD\` + +--- + +## 💡 重要な発見・学習 + +- 追加あり次更新 + +--- + +## 🔧 技術的メモ + +- 追加あり次更新 + +--- + +## ⚠️ 問題・エラーと解決策 + +- なし + +--- + +## 🔗 関連リソース + +- 追加あり次更新 + +--- +$GIT_INFO + +**タグ**: #clawcode #memory #${PROJECT_NAME//-/} +EOF + +# Discord に送信(コードブロックとして整形) +BRAIN_MESSAGE="🧠 **セカンドブレインに保存** + +\`\`\`markdown +$(cat "$MEMORY_FILE") +\`\`\` + +--- + +💾 **記録完了**: ${SESSION_DATE} ${SESSION_TIME}" + +# jq が利用可能かチェック +USE_JQ=0 +if command -v jq >/dev/null 2>&1; then + USE_JQ=1 +fi + +# Discord 送信 +if [ -n "$DISCORD_WEBHOOK_URL" ]; then + if [ $USE_JQ -eq 1 ]; then + JSON_CONTENT=$(echo "$BRAIN_MESSAGE" | jq -Rs .) + else + # フォールバック: 簡易エスケープ + JSON_CONTENT=$(echo "$BRAIN_MESSAGE" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | tr '\n' ' ') + fi + ( + curl -s -X POST "$DISCORD_WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d "{\"content\": $JSON_CONTENT}" \ + >/dev/null 2>&1 + ) & +elif [ -n "$DISCORD_ACCOUNT_ID" ]; then + ( + # バイト数で判定(Discordの制限は2000バイト) + MESSAGE_BYTES=$(printf "%s" "$BRAIN_MESSAGE" | wc -c | tr -d ' ') + + if [ "$MESSAGE_BYTES" -gt 1900 ]; then + # 短縮版メッセージ + node "$CLAWDBOT_CLI" message send \ + --channel discord \ + --account "$DISCORD_ACCOUNT_ID" \ + --target "$BRAIN_CHANNEL_ID" \ + --message "🧠 **セカンドブレインに保存** + +セッションメモリが生成されました: +- 日時: ${SESSION_DATE} ${SESSION_TIME} +- プロジェクト: $WORK_CONTEXT +- サイズ: ${MESSAGE_BYTES}バイト + +詳細はローカルファイルを確認してください。" \ + >/dev/null 2>&1 + else + node "$CLAWDBOT_CLI" message send \ + --channel discord \ + --account "$DISCORD_ACCOUNT_ID" \ + --target "$BRAIN_CHANNEL_ID" \ + --message "$BRAIN_MESSAGE" \ + >/dev/null 2>&1 + fi + ) & +fi + +# ローカルにも保存(検索用) +MEMORY_DIR="$HOME/.clawdbot/memory" +mkdir -p "$MEMORY_DIR" +MEMORY_LOCAL="$MEMORY_DIR/${SESSION_DATE}-${USER}-$(date '+%H%M%S').md" +cp "$MEMORY_FILE" "$MEMORY_LOCAL" + +exit 0 diff --git a/.claude/hooks/discord-task-end.sh b/.claude/hooks/discord-task-end.sh new file mode 100755 index 000000000..e81bf2a80 --- /dev/null +++ b/.claude/hooks/discord-task-end.sh @@ -0,0 +1,96 @@ +#!/bin/bash +# Clawdbot Discord Task End Notification Hook +# SessionEnd: Report task completion + +# clawdbot のパス +CLAWDBOT_DIR="${CLAWDBOT_DIR:-$HOME/dev/clawdbot}" +CLAWDBOT_CLI="$CLAWDBOT_DIR/dist/entry.js" + +# CLIが存在するか確認 +[ ! -f "$CLAWDBOT_CLI" ] && exit 0 + +# Discord 設定 +DISCORD_CHANNEL_ID="${DISCORD_NOTIFY_CHANNEL:-1465087451113722019}" # #status +DISCORD_ACCOUNT_ID="${DISCORD_NOTIFY_ACCOUNT:-ppal}" + +# セッションID(PPIDを使用してサブシェルでも一貫性を確保) +SESSION_ID="${CLAUDE_SESSION_ID:-${PPID:-$$}}" + +# タスク情報ファイルを読み込み +TASK_INFO_FILE="/tmp/clawdbot-task-info-${USER}-${SESSION_ID}.txt" +TASK_START_TIME="不明" + +if [ -f "$TASK_INFO_FILE" ]; then + source "$TASK_INFO_FILE" + rm -f "$TASK_INFO_FILE" +fi + +# 経過時間計算(GNU/BSD date両対応) +if [ -n "$TASK_START_TIME" ] && [ "$TASK_START_TIME" != "不明" ]; then + # dateコマンドの種類を検出 + if date --version >/dev/null 2>&1; then + # GNU date (Linux) + START_SEC=$(date -d "$TASK_START_TIME" "+%s" 2>/dev/null || echo "0") + else + # BSD date (macOS) + START_SEC=$(date -j -f "%Y-%m-%d %H:%M:%S" "$TASK_START_TIME" "+%s" 2>/dev/null || echo "0") + fi + + END_SEC=$(date "+%s") + ELAPSED=$((END_SEC - START_SEC)) + + if [ $ELAPSED -gt 0 ]; then + if [ $ELAPSED -ge 3600 ]; then + DURATION="$((ELAPSED / 3600))時間$((ELAPSED % 3600 / 60))分" + elif [ $ELAPSED -ge 60 ]; then + DURATION="$((ELAPSED / 60))分$((ELAPSED % 60))秒" + else + DURATION="${ELAPSED}秒" + fi + else + DURATION="計測不可" + fi +else + DURATION="不明" +fi + +# Git 変更サマリー(gitリポジトリの場合) +GIT_SUMMARY="" +if git rev-parse --git-dir > /dev/null 2>&1; then + BRANCH=$(git branch --show-current 2>/dev/null || echo "unknown") + CHANGED_FILES=$(git status --short 2>/dev/null | wc -l | tr -d ' ') + GIT_SUMMARY=" +📂 Git: $BRANCH (${CHANGED_FILES}ファイル変更)" +fi + +# セッション終了メッセージ +END_MESSAGE="✅ **Claude Code セッション完了** + +📅 開始時刻: $TASK_START_TIME +📅 完了時刻: $(date '+%Y-%m-%d %H:%M:%S') +⏱️ 経過時間: ${DURATION} +👤 ユーザー: ${USER:-unknown} +🖥️ ホスト: $(hostname -s 2>/dev/null || hostname | cut -d'.' -f1)${GIT_SUMMARY} + +✨ 作業完了" + +# Discord 送信 +if [ -n "$DISCORD_WEBHOOK_URL" ]; then + ( + curl -s -X POST "$DISCORD_WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d "{\"content\": \"$END_MESSAGE\"}" \ + >/dev/null 2>&1 + ) & +elif [ -n "$DISCORD_ACCOUNT_ID" ]; then + ( + node "$CLAWDBOT_CLI" message send \ + --channel discord \ + --account "$DISCORD_ACCOUNT_ID" \ + --target "$DISCORD_CHANNEL_ID" \ + --message "$END_MESSAGE" \ + >/dev/null 2>&1 + ) & +fi + +exit 0 diff --git a/.claude/hooks/discord-task-start.sh b/.claude/hooks/discord-task-start.sh new file mode 100755 index 000000000..e025c0b15 --- /dev/null +++ b/.claude/hooks/discord-task-start.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Clawdbot Discord Task Start Notification Hook +# SessionStart: Declare task start + +# clawdbot のパス +CLAWDBOT_DIR="${CLAWDBOT_DIR:-$HOME/dev/clawdbot}" +CLAWDBOT_CLI="$CLAWDBOT_DIR/dist/entry.js" + +# CLIが存在するか確認 +[ ! -f "$CLAWDBOT_CLI" ] && exit 0 + +# Discord 設定 +DISCORD_CHANNEL_ID="${DISCORD_NOTIFY_CHANNEL:-1465087451113722019}" # #status +DISCORD_ACCOUNT_ID="${DISCORD_NOTIFY_ACCOUNT:-ppal}" + +# セッションID(PPIDを使用してサブシェルでも一貫性を確保) +SESSION_ID="${CLAUDE_SESSION_ID:-${PPID:-$$}}" + +# タスク情報保存先 +TASK_INFO_FILE="/tmp/clawdbot-task-info-${USER}-${SESSION_ID}.txt" + +# クリーンアップ用trap +trap "rm -f $TASK_INFO_FILE" EXIT INT TERM + +# タスク開始情報を保存 +cat > "$TASK_INFO_FILE" << EOF +TASK_START_TIME=$(date '+%Y-%m-%d %H:%M:%S') +TASK_USER=${USER:-unknown} +TASK_HOST=$(hostname) +TASK_PWD=$PWD +TASK_SESSION_ID=${SESSION_ID} +EOF + +# セッション開始メッセージ +START_MESSAGE="🚀 **Claude Code セッション開始** + +📅 開始時刻: $(date '+%Y-%m-%d %H:%M:%S') +👤 ユーザー: ${USER:-unknown} +🖥️ ホスト: $(hostname -s 2>/dev/null || hostname | cut -d'.' -f1) +📁 ワークディレクトリ: $PWD +🆔 セッションID: ${SESSION_ID} + +⏳ 作業開始..." + +# Discord 送信 +if [ -n "$DISCORD_WEBHOOK_URL" ]; then + ( + curl -s -X POST "$DISCORD_WEBHOOK_URL" \ + -H "Content-Type: application/json" \ + -d "{\"content\": \"$START_MESSAGE\"}" \ + >/dev/null 2>&1 + ) & +elif [ -n "$DISCORD_ACCOUNT_ID" ]; then + ( + node "$CLAWDBOT_CLI" message send \ + --channel discord \ + --account "$DISCORD_ACCOUNT_ID" \ + --target "$DISCORD_CHANNEL_ID" \ + --message "$START_MESSAGE" \ + >/dev/null 2>&1 + ) & +fi + +exit 0 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 000000000..b6ed9a344 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,32 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-settings.json", + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": ".claude/hooks/discord-task-start.sh", + "timeout": 10 + } + ] + } + ], + "SessionEnd": [ + { + "hooks": [ + { + "type": "command", + "command": ".claude/hooks/discord-brain-save.sh", + "timeout": 15 + }, + { + "type": "command", + "command": ".claude/hooks/discord-task-end.sh", + "timeout": 10 + } + ] + } + ] + } +} diff --git a/.env.bak b/.env.bak new file mode 100644 index 000000000..31c8efda9 --- /dev/null +++ b/.env.bak @@ -0,0 +1 @@ +DISCORD_NOTIFY_CHANNEL=1260121338811514880 diff --git a/.env.example b/.env.example index 29652fe46..1dfd2ed16 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,16 @@ TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token_here # Must be a WhatsApp-enabled Twilio number, prefixed with whatsapp: TWILIO_WHATSAPP_FROM=whatsapp:+17343367101 + +# Discord Webhook (for simple notifications) +DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/xxx/yyy + +# Discord Notification Channel (for clawdbot connection) +# Default: #status channel (PPAL Server) +# Use Discord channel ID (e.g., "1465087451113722019") +DISCORD_NOTIFY_CHANNEL=1465087451113722019 + +# Discord Second Brain Channel (for memory/knowledge storage) +# Default: #general channel (PPAL Server) +# Use Discord channel ID (e.g., "1465087447225598207") +DISCORD_BRAIN_CHANNEL=1465087447225598207 diff --git a/.gitignore b/.gitignore index e1dcd0188..2941c49c7 100644 --- a/.gitignore +++ b/.gitignore @@ -76,3 +76,11 @@ src/**/*.js.map src/**/*.d.ts src/**/*.d.ts.map + +# Terraform +workers/clawdbot-aws/terraform/.terraform/ +workers/clawdbot-aws/terraform/*.tfstate +workers/clawdbot-aws/terraform/*.tfstate.backup +workers/clawdbot-aws/terraform/*.tfplan +workers/clawdbot-aws/terraform/plan.tfplan +workers/clawdbot-aws/terraform/tfplan diff --git a/.oxlintrc.json b/.oxlintrc.json index 3876f8e04..6ae3380b5 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -8,5 +8,5 @@ "categories": { "correctness": "error" }, - "ignorePatterns": ["src/canvas-host/a2ui/a2ui.bundle.js"] + "ignorePatterns": ["src/canvas-host/a2ui/a2ui.bundle.js", "**/*.d.ts"] } diff --git a/.tmp b/.tmp new file mode 100644 index 000000000..e69de29bb diff --git a/Dockerfile b/Dockerfile index a33f0077d..fed40b114 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,8 @@ RUN pnpm install --frozen-lockfile COPY . . RUN pnpm build +# Create .buildstamp to prevent runtime rebuild (portable timestamp) +RUN mkdir -p dist && echo "$(date +%s)000000000" > dist/.buildstamp # Force pnpm for UI build (Bun may fail on ARM/Synology architectures) ENV CLAWDBOT_PREFER_PNPM=1 RUN pnpm ui:install diff --git a/scripts/list-discord-channels.ts b/scripts/list-discord-channels.ts new file mode 100644 index 000000000..64f9ce52c --- /dev/null +++ b/scripts/list-discord-channels.ts @@ -0,0 +1,76 @@ +#!/usr/bin/env node +/** + * List Discord Channels + * サーバー内の全チャンネルを一覧表示 + */ + +import { listGuildChannelsDiscord } from "../src/discord/send.guild.js"; +import { loadConfig } from "../src/config/config.js"; +import { resolveDiscordAccount } from "../src/discord/accounts.js"; +import { createDiscordClient } from "../src/discord/send.shared.js"; + +async function main() { + const GUILD_ID = "1260121338811514880"; // PPAL Server + + const cfg = loadConfig(); + const accountInfo = resolveDiscordAccount({ cfg, accountId: "ppal" }); + const { token } = createDiscordClient({}, cfg); + + console.log(`\n📋 Discord Server: ${GUILD_ID}`); + console.log(`🤖 Bot: ${accountInfo.accountId} (${accountInfo.config.name || "PPAL Bot"})\n`); + + try { + const channels = await listGuildChannelsDiscord(GUILD_ID, { token }); + + console.log("📺 チャンネル一覧:\n"); + console.log("".padEnd(25), "タイプ".padEnd(15), "ID"); + console.log("=".repeat(70)); + + for (const channel of channels) { + const type = + channel.type === 0 ? "テキスト" : + channel.type === 2 ? "音声" : + channel.type === 4 ? "カテゴリ" : + channel.type === 5 ? " announcements" : + channel.type === 15 ? "forum" : `type_${channel.type}`; + + const indent = channel.type === 4 ? "" : " "; + + console.log( + `${indent}${(channel.name || "").padEnd(25)} ` + + `${type.padEnd(15)} ` + + `${channel.id}` + ); + } + + console.log("\n"); + + // テキストチャンネルのみ抽出 + const textChannels = channels.filter(ch => ch.type === 0); + console.log(`\n📝 テキストチャンネル (${textChannels.length}件):\n`); + textChannels.forEach(ch => { + console.log(` #${ch.name} → ID: ${ch.id}`); + }); + + console.log("\n✅ おすすめの通知チャンネル:\n"); + const generalChannel = textChannels.find(ch => + ch.name.toLowerCase().includes("general") || + ch.name.toLowerCase().includes("通知") || + ch.name.toLowerCase().includes("notify") + ); + + if (generalChannel) { + console.log(` 👉 #${generalChannel.name} (ID: ${generalChannel.id})`); + } else if (textChannels.length > 0) { + console.log(` 👉 #${textChannels[0].name} (ID: ${textChannels[0].id})`); + } + + console.log(""); + + } catch (error: any) { + console.error("❌ エラー:", error.message); + process.exit(1); + } +} + +main(); diff --git a/src/artifacts/manager.test.ts b/src/artifacts/manager.test.ts index 660d3cdd2..17259c75f 100644 --- a/src/artifacts/manager.test.ts +++ b/src/artifacts/manager.test.ts @@ -3,18 +3,6 @@ */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - save, - saveFile, - getDownloadUrl, - get, - listBySession, - listByUser, - deleteArtifact, - deleteBySession, - initializeTable, -} from "./manager.js"; -import { ArtifactType } from "./types.js"; // AWS SDKモック vi.mock("@aws-sdk/client-s3"); @@ -23,9 +11,6 @@ vi.mock("@aws-sdk/lib-dynamodb"); vi.mock("@aws-sdk/s3-request-presigner"); describe("artifact manager", () => { - const mockSessionId = "test-session-123"; - const mockUserId = "user-456"; - beforeEach(() => { vi.clearAllMocks(); }); diff --git a/src/artifacts/manager.ts b/src/artifacts/manager.ts index e0267eca9..5cbf1bb14 100644 --- a/src/artifacts/manager.ts +++ b/src/artifacts/manager.ts @@ -25,7 +25,6 @@ import type { ArtifactType, SaveArtifactOptions, DownloadUrlOptions, - ArtifactFilter, } from "./types.js"; /** デフォルトTTL (24時間) */ diff --git a/src/response/discord-reply.test.ts b/src/response/discord-reply.test.ts index 4e9542717..35560e169 100644 --- a/src/response/discord-reply.test.ts +++ b/src/response/discord-reply.test.ts @@ -3,22 +3,11 @@ */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { sendReply, createDiscordReply } from "./discord-reply.js"; -import { ResponseFormat } from "./types.js"; // Carbonモック vi.mock("@buape/carbon"); describe("discord-reply", () => { - const mockApi = { - rest: { - post: vi.fn().mockResolvedValue({ id: "new-message-id" }), - }, - }; - - const mockChannelId = "123456789"; - const mockMessageId = "987654321"; - beforeEach(() => { vi.clearAllMocks(); }); diff --git a/src/response/discord-reply.ts b/src/response/discord-reply.ts index 62150ac3a..6f6ad3ad5 100644 --- a/src/response/discord-reply.ts +++ b/src/response/discord-reply.ts @@ -7,7 +7,6 @@ import type { Message } from "@buape/carbon"; import type { ReplyOptions, ReplyData, ReplyEmbed, QuoteMetadata, ReplyAuthor } from "./types.js"; -import { ResponseFormat } from "./types.js"; /** * Discordリプライオプション拡張 diff --git a/src/review/codex-reviewer.test.ts b/src/review/codex-reviewer.test.ts index 23aaf317d..80dd9be96 100644 --- a/src/review/codex-reviewer.test.ts +++ b/src/review/codex-reviewer.test.ts @@ -4,7 +4,6 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { - runCodexReview, parseCodexOutput, createReviewRequest, evaluateReview, diff --git a/src/review/codex-reviewer.ts b/src/review/codex-reviewer.ts index c057af390..22680eec7 100644 --- a/src/review/codex-reviewer.ts +++ b/src/review/codex-reviewer.ts @@ -6,7 +6,7 @@ import { exec } from "child_process"; import { promisify } from "util"; -import { mkdtempSync, writeFileSync, unlinkSync, rmSync, rmdirSync } from "node:fs"; +import { mkdtempSync, writeFileSync, unlinkSync, rmdirSync } from "node:fs"; import { join } from "node:path"; import type { CodexReview, @@ -323,7 +323,7 @@ function buildCodexCommand( } }, }; - } catch (error) { + } catch { // ファイル書き込みエラー時はフォールバック console.warn("[CodexReviewer] Failed to write temp file, using inline content"); } diff --git a/src/session/manager.test.ts b/src/session/manager.test.ts index 89af1c551..84a106f6e 100644 --- a/src/session/manager.test.ts +++ b/src/session/manager.test.ts @@ -3,47 +3,12 @@ */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - saveState, - restoreState, - getPendingSessions, - deleteSession, - updateStatus, - cleanupExpiredSessions, - initializeTable, -} from "./manager.js"; -import { SessionStatus } from "./types.js"; -import type { PersistedSessionState } from "./types.js"; // DynamoDBモック vi.mock("@aws-sdk/client-dynamodb"); vi.mock("@aws-sdk/lib-dynamodb"); describe("session manager", () => { - const mockSessionId = "test-session-123"; - const mockState: PersistedSessionState = { - metadata: { - sessionId: mockSessionId, - userId: "user-123", - channelId: "channel-456", - guildId: "guild-789", - startTime: Date.now(), - lastUpdateTime: Date.now(), - status: SessionStatus.RUNNING, - expiresAt: Math.floor(Date.now() / 1000) + 3600, - }, - thetaState: { - runId: "run-123", - startTime: Date.now(), - currentPhase: "θ₁ OBSERVE" as any, - events: [], - context: new Map(), - }, - context: { - testKey: "testValue", - }, - }; - beforeEach(() => { vi.clearAllMocks(); }); diff --git a/src/session/recovery.ts b/src/session/recovery.ts index d6d3a2a4e..7e009d9cc 100644 --- a/src/session/recovery.ts +++ b/src/session/recovery.ts @@ -69,7 +69,7 @@ export async function recoverPendingSessions( // 期限切れチェック const validSessions = sessions.filter((s) => { if (Date.now() > s.metadata.expiresAt * 1000) { - deleteSession(s.metadata.sessionId); + void deleteSession(s.metadata.sessionId); result.expired++; return false; } @@ -129,7 +129,7 @@ export async function recoverPendingSessions( * @param ttl - 新しいTTL (秒), デフォルト: 1時間 */ export async function heartbeatSession(sessionId: string, ttl: number = 3600): Promise { - const expiresAt = Math.floor(Date.now() / 1000) + ttl; + const _expiresAt = Math.floor(Date.now() / 1000) + ttl; // TODO: UpdateItemでexpiresAtを更新 // 現状のupdateStatus()はlastUpdateTimeのみ更新 @@ -169,7 +169,7 @@ export async function completeSession( * @param sessionId - セッションID * @param error - エラー情報 */ -export async function failSession(sessionId: string, error: Error): Promise { +export async function failSession(sessionId: string, _error: Error): Promise { await updateStatus(sessionId, SessionStatus.ERROR); // エラー情報を記録 diff --git a/src/session/types.ts b/src/session/types.ts index 60c88616e..cdb65bae1 100644 --- a/src/session/types.ts +++ b/src/session/types.ts @@ -4,7 +4,7 @@ * DynamoDBでθサイクルのセッション状態を永続化するための型 */ -import type { ThetaCycleState, ThetaPhase } from "../theta/types.js"; +import type { ThetaCycleState } from "../theta/types.js"; /** * セッションの状態 diff --git a/src/theta/execute.ts b/src/theta/execute.ts index ea0338837..2ad4252de 100644 --- a/src/theta/execute.ts +++ b/src/theta/execute.ts @@ -204,7 +204,7 @@ function withTimeout( }, timeoutMs); // Promiseが解決したらクリアンアップ(キャンセルされても実行) - promise.finally?.(() => clearTimeout(timeoutId)); + void promise.finally?.(() => clearTimeout(timeoutId)); }), ]); } diff --git a/src/theta/index.ts b/src/theta/index.ts index acff658c1..653554072 100644 --- a/src/theta/index.ts +++ b/src/theta/index.ts @@ -63,7 +63,10 @@ import { ThetaPhase } from "./types.js"; /** * 新しいθサイクル状態を作成する */ -export function createThetaCycleState(runId: string, options?: ThetaCycleOptions): ThetaCycleState { +export function createThetaCycleState( + runId: string, + _options?: ThetaCycleOptions, +): ThetaCycleState { return { runId, startTime: Date.now(), @@ -127,7 +130,7 @@ export async function runThetaCycle( const improveState = improveResult.state; // サマリー生成 - const summary = generateCycleSummary(improveState); + const _summary = generateCycleSummary(improveState); // オプションのコールバック if (options?.onEvent) { diff --git a/test/integration/agent-flow.test.ts b/test/integration/agent-flow.test.ts index c0754331d..52a1f30ad 100644 --- a/test/integration/agent-flow.test.ts +++ b/test/integration/agent-flow.test.ts @@ -15,9 +15,7 @@ * - θサイクル (Observe → Analyze → Decide → Allocate → Execute → Improve) */ -import { describe, it, expect, beforeAll, beforeEach } from "vitest"; -import { readFileSync } from "fs"; -import { join } from "path"; +import { describe, it, expect } from "vitest"; // Agent definitions based on Miyabi Agent Society interface Agent { @@ -121,13 +119,6 @@ function executeAgentTask(agentId: string, task: string): ThetaCycleResult { }; } -function validateDependencies(agentId: string): boolean { - const agent = MIYABI_AGENTS[agentId]; - if (!agent) return false; - - return agent.dependencies.every((dep) => MIYABI_AGENTS[dep]); -} - function validateHandoff(fromAgent: string, toAgent: string): boolean { const from = MIYABI_AGENTS[fromAgent]; const to = MIYABI_AGENTS[toAgent]; @@ -305,13 +296,6 @@ describe("Agent Flow Integration Tests", () => { describe("Escalation (エスカレーション)", () => { it("should escalate to しきるん on failure", () => { - const failureResult: ThetaCycleResult = { - phase: "execute", - agent: "kaede", - status: "blocked", - error: "Technical issue", - }; - // Escalate to conductor const escalation = { from: "kaede", @@ -483,7 +467,7 @@ ${handoff.context} // Simulate θサイクル ["observe", "analyze", "decide", "allocate", "execute", "improve"].forEach(() => { // Simulate phase - 1 + 1; + void 1; }); const cycleEnd = Date.now(); diff --git a/test/integration/githubops.test.ts b/test/integration/githubops.test.ts index e3b607465..f5ccf41bd 100644 --- a/test/integration/githubops.test.ts +++ b/test/integration/githubops.test.ts @@ -8,10 +8,9 @@ * 4. PR creation/review */ -import { describe, it, expect, beforeAll, afterAll, afterEach, vi } from "vitest"; +import { describe, it, expect, beforeAll, afterAll, afterEach } from "vitest"; import { execSync } from "child_process"; -import { readFileSync, unlinkSync, existsSync } from "fs"; -import { join } from "path"; +import { unlinkSync, existsSync } from "fs"; // Test repository configuration const TEST_REPO = process.env.GITHUBOPS_TEST_REPO || "ShunsukeHayashi/dev-workspace"; @@ -27,7 +26,7 @@ function runGhCommand(args: string): string { }); } catch (error: unknown) { const err = error as { stdout?: string; stderr?: string }; - throw new Error(`gh command failed: ${err.stderr || err.stdout || error}`); + throw new Error(`gh command failed: ${err.stderr || err.stdout || String(error)}`); } } @@ -35,7 +34,7 @@ function createTestBranch(branchName: string): void { try { execSync(`git checkout -b ${branchName}`, { encoding: "utf-8" }); } catch (error: unknown) { - throw new Error(`Failed to create branch: ${error}`); + throw new Error(`Failed to create branch: ${String(error)}`); } } diff --git a/test/unit/agent.test.ts b/test/unit/agent.test.ts index a10ffd832..db6b0f2ce 100644 --- a/test/unit/agent.test.ts +++ b/test/unit/agent.test.ts @@ -9,8 +9,8 @@ * - Session-based agent selection */ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { mkdirSync, rmSync, existsSync, writeFileSync } from "fs"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { mkdirSync, rmSync, existsSync } from "fs"; import { join } from "path"; import { tmpdir } from "os"; import { randomBytes } from "crypto"; diff --git a/test/unit/engine.test.ts b/test/unit/engine.test.ts index 4cb976bea..b470249d0 100644 --- a/test/unit/engine.test.ts +++ b/test/unit/engine.test.ts @@ -11,7 +11,7 @@ * - Auth profile resolution for model access */ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; // Mock dependencies vi.mock("../../src/agents/defaults.js", () => ({ @@ -42,7 +42,6 @@ import { getModelRefStatus, resolveAllowedModelRef, resolveThinkingDefault, - type ModelRef, type ThinkLevel, } from "../../src/agents/model-selection.js"; @@ -864,7 +863,7 @@ describe("Engine Unit Tests", () => { }, }); - const primary = resolveAgentModelPrimary(cfg, "main"); + const _primary = resolveAgentModelPrimary(cfg, "main"); const fallbacks = resolveAgentModelFallbacksOverride(cfg, "main"); // main agent has no model override, so it inherits from defaults diff --git a/test/unit/mcp.test.ts b/test/unit/mcp.test.ts index 2e70644d3..8161ca1cd 100644 --- a/test/unit/mcp.test.ts +++ b/test/unit/mcp.test.ts @@ -9,7 +9,7 @@ * - Error handling and retries */ -import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; // Mock MCP tool types interface MCPTool { diff --git a/workers/clawdbot-aws/.env.example b/workers/clawdbot-aws/.env.example new file mode 100644 index 000000000..1bf4426cb --- /dev/null +++ b/workers/clawdbot-aws/.env.example @@ -0,0 +1,33 @@ +# AWS Terraform Variables for Clawdbot ECS Fargate Deployment +# Copy this file to terraform.tfvars and fill in the values + +# AWS Configuration +aws_region = "ap-northeast-1" +aws_account_id = "123456789012" # Your AWS account ID + +# Environment +environment = "production" +project_name = "clawdbot" + +# ECS Configuration +ecs_task_cpu = 256 # 0.25 vCPU +ecs_task_memory = 2048 # 2 GB +ecs_service_desired_count = 1 + +# Discord Token (DEPRECATED - Use Secrets Manager instead) +# discord_token = "your_bot_token_here" # DO NOT use in production + +# Secrets Manager Secret Name for Discord Bot Token +discord_token_secret_name = "clawdbot/discord-token" +# Create the secret in AWS Secrets Manager before running Terraform: +# aws secretsmanager create-secret \ +# --name clawdbot/discord-token \ +# --secret-string "your_bot_token_here" + +# DynamoDB Tables +sessions_table_name = "clawdbot-sessions" +artifacts_table_name = "clawdbot-artifacts" + +# S3 Bucket +artifacts_bucket_name = "clawdbot-artifacts" +artifacts_bucket_ttl_days = 7 diff --git a/workers/clawdbot-aws/terraform/.terraform.lock.hcl b/workers/clawdbot-aws/terraform/.terraform.lock.hcl new file mode 100644 index 000000000..cdc1668d4 --- /dev/null +++ b/workers/clawdbot-aws/terraform/.terraform.lock.hcl @@ -0,0 +1,25 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/aws" { + version = "5.100.0" + constraints = "~> 5.0" + hashes = [ + "h1:Ijt7pOlB7Tr7maGQIqtsLFbl7pSMIj06TVdkoSBcYOw=", + "zh:054b8dd49f0549c9a7cc27d159e45327b7b65cf404da5e5a20da154b90b8a644", + "zh:0b97bf8d5e03d15d83cc40b0530a1f84b459354939ba6f135a0086c20ebbe6b2", + "zh:1589a2266af699cbd5d80737a0fe02e54ec9cf2ca54e7e00ac51c7359056f274", + "zh:6330766f1d85f01ae6ea90d1b214b8b74cc8c1badc4696b165b36ddd4cc15f7b", + "zh:7c8c2e30d8e55291b86fcb64bdf6c25489d538688545eb48fd74ad622e5d3862", + "zh:99b1003bd9bd32ee323544da897148f46a527f622dc3971af63ea3e251596342", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9f8b909d3ec50ade83c8062290378b1ec553edef6a447c56dadc01a99f4eaa93", + "zh:aaef921ff9aabaf8b1869a86d692ebd24fbd4e12c21205034bb679b9caf883a2", + "zh:ac882313207aba00dd5a76dbd572a0ddc818bb9cbf5c9d61b28fe30efaec951e", + "zh:bb64e8aff37becab373a1a0cc1080990785304141af42ed6aa3dd4913b000421", + "zh:dfe495f6621df5540d9c92ad40b8067376350b005c637ea6efac5dc15028add4", + "zh:f0ddf0eaf052766cfe09dea8200a946519f653c384ab4336e2a4a64fdd6310e9", + "zh:f1b7e684f4c7ae1eed272b6de7d2049bb87a0275cb04dbb7cda6636f600699c9", + "zh:ff461571e3f233699bf690db319dfe46aec75e58726636a0d97dd9ac6e32fb70", + ] +} diff --git a/workers/clawdbot-aws/terraform/README.md b/workers/clawdbot-aws/terraform/README.md new file mode 100644 index 000000000..f0c98d077 --- /dev/null +++ b/workers/clawdbot-aws/terraform/README.md @@ -0,0 +1,141 @@ +# Clawdbot AWS Deployment + +ECS Fargateを使用したClawdbotのAWSデプロイメント設定 + +## 前提条件 + +- AWS CLI設定済み +- Terraformインストール済み +- Dockerインストール済み + +## セットアップ手順 + +### 1. Discord Bot TokenをSecrets Managerに保存 + +```bash +# Secrets Managerにシークレットを作成 +aws secretsmanager create-secret \ + --name clawdbot/discord-token \ + --description "Discord Bot Token for Clawdbot" \ + --secret-string "your_discord_bot_token_here" + +# シークレットを確認 +aws secretsmanager get-secret-value --secret-id clawdbot/discord-token +``` + +### 2. Terraform変数の設定 + +`terraform.tfvars.example`をコピーして`terraform.tfvars`を作成: + +```bash +cp terraform.tfvars.example terraform.tfvars +``` + +`terraform.tfvars`を編集して以下の値を設定: +- `aws_account_id`: AWSアカウントID(13桁の数字) +- `discord_token_secret_name`: Secrets Managerのシークレット名(デフォルト: `clawdbot/discord-token`) + +**重要**: `aws_account_id`は必須です。AWSコンソールまたは以下のコマンドで確認できます: + +```bash +aws sts get-caller-identity --query Account --output text +``` + +### 3. Terraformの実行 + +```bash +# Terraform初期化 +terraform init + +# 設定確認 +terraform plan + +# デプロイ実行 +terraform apply +``` + +### 4. Dockerイメージのプッシュ + +```bash +# ECRログイン +aws ecr get-login-password --region ap-northeast-1 | \ + docker login --username AWS --password-stdin .dkr.ecr.ap-northeast-1.amazonaws.com + +# イメージビルド +docker build -t clawdbot . + +# タグ付け +docker tag clawdbot:latest .dkr.ecr.ap-northeast-1.amazonaws.com/clawdbot:latest + +# プッシュ +docker push .dkr.ecr.ap-northeast-1.amazonaws.com/clawdbot:latest +``` + +### 5. ECSサービスの更新 + +```bash +# 新しいタスク定義でECSサービスを更新 +aws ecs update-service \ + --cluster clawdbot-cluster \ + --service clawdbot-service \ + --force-new-deployment +``` + +## セキュリティベストプラクティス + +- ✅ Discord Bot TokenはAWS Secrets Managerに保存 +- ✅ IAMロールで最小権限の原則を適用 +- ✅ S3バケットでAES256暗号化を有効化 +- ✅ DynamoDBでPITR(Point-in-Time Recovery)を有効化 +- ✅ VPC内にプライベートサブネットを配置 + +## コスト最適化 + +- ECS Fargate: 256 CPU, 2048 MBメモリ(最小構成) +- CloudWatch Logs: 7日間保持 +- S3ライフサイクル: 7日間で古いアーティファクトを自動削除 + +## 監視とログ + +### CloudWatch Logs + +```bash +# ログストリーム確認 +aws logs tail /ecs/clawdbot --follow +``` + +### ECSタスクの状態確認 + +```bash +# サービス状態 +aws ecs describe-services --cluster clawdbot-cluster --services clawdbot-service + +# タスク一覧 +aws ecs list-tasks --cluster clawdbot-cluster +``` + +## トラブルシューティング + +### コンテナが起動しない場合 + +1. CloudWatch Logsでエラーを確認 +2. タスク定義の環境変数を確認 +3. Secrets Managerからシークレットが取得できているか確認 + +### Discord Botが応答しない場合 + +1. ECSタスクが実行中か確認 +2. セキュリティグループでアウトバウンド通信が許可されているか確認 +3. Discord Bot Tokenが正しいか確認 + +## クリーンアップ + +```bash +terraform destroy +``` + +注意: Secrets Managerのシークレットは手動で削除する必要があります: + +```bash +aws secretsmanager delete-secret --secret-id clawdbot/discord-token +``` diff --git a/workers/clawdbot-aws/terraform/dynamodb.tf b/workers/clawdbot-aws/terraform/dynamodb.tf new file mode 100644 index 000000000..eb87a879e --- /dev/null +++ b/workers/clawdbot-aws/terraform/dynamodb.tf @@ -0,0 +1,124 @@ +/** + * DynamoDB Tables for θ-cycle Session Persistence & Artifact Storage + */ + +resource "aws_dynamodb_table" "sessions" { + name = var.sessions_table_name + billing_mode = "PAY_PER_REQUEST" + hash_key = "sessionId" + + attribute { + name = "sessionId" + type = "S" + } + + attribute { + name = "userId" + type = "S" + } + + attribute { + name = "guildId" + type = "S" + } + + attribute { + name = "status" + type = "S" + } + + global_secondary_index { + name = "UserIndex" + hash_key = "userId" + range_key = "status" + projection_type = "ALL" + } + + global_secondary_index { + name = "GuildIndex" + hash_key = "guildId" + range_key = "status" + projection_type = "ALL" + } + + point_in_time_recovery { + enabled = true + } + + ttl { + attribute_name = "expiresAt" + } + + tags = { + Name = "${var.project_name}-sessions" + } +} + +resource "aws_dynamodb_table" "artifacts" { + name = var.artifacts_table_name + billing_mode = "PAY_PER_REQUEST" + hash_key = "id" + + attribute { + name = "id" + type = "S" + } + + attribute { + name = "sessionId" + type = "S" + } + + attribute { + name = "userId" + type = "S" + } + + attribute { + name = "type" + type = "S" + } + + attribute { + name = "createdAt" + type = "N" + } + + attribute { + name = "expiresAt" + type = "N" + } + + global_secondary_index { + name = "SessionIndex" + hash_key = "sessionId" + range_key = "createdAt" + projection_type = "ALL" + } + + global_secondary_index { + name = "UserIndex" + hash_key = "userId" + range_key = "createdAt" + projection_type = "ALL" + } + + global_secondary_index { + name = "TypeIndex" + hash_key = "type" + range_key = "expiresAt" + projection_type = "ALL" + } + + point_in_time_recovery { + enabled = true + } + + ttl { + attribute_name = "expiresAt" + } + + tags = { + Name = "${var.project_name}-artifacts" + } +} diff --git a/workers/clawdbot-aws/terraform/ecr.tf b/workers/clawdbot-aws/terraform/ecr.tf new file mode 100644 index 000000000..10f7db6e9 --- /dev/null +++ b/workers/clawdbot-aws/terraform/ecr.tf @@ -0,0 +1,36 @@ +/** + * Amazon ECR Repository for Clawdbot Container Images + */ + +resource "aws_ecr_repository" "clawdbot" { + name = "${var.project_name}/bot" + image_tag_mutability = "MUTABLE" + + image_scanning_configuration { + scan_on_push = true + } + + tags = { + Name = "${var.project_name}-ecr" + } +} + +# ECR Lifecycle Policy (keep last 10 images) +resource "aws_ecr_lifecycle_policy" "clawdbot" { + repository = aws_ecr_repository.clawdbot.name + + policy = jsonencode({ + rules = [{ + rulePriority = 1 + description = "Keep last 10 images" + selection = { + tagStatus = "any" + countType = "imageCountMoreThan" + countNumber = 10 + } + action = { + type = "expire" + } + }] + }) +} diff --git a/workers/clawdbot-aws/terraform/ecs.tf b/workers/clawdbot-aws/terraform/ecs.tf new file mode 100644 index 000000000..dc241eab3 --- /dev/null +++ b/workers/clawdbot-aws/terraform/ecs.tf @@ -0,0 +1,126 @@ +/** + * ECS Fargate Configuration for Clawdbot Discord Bot + */ + +# CloudWatch Log Group +resource "aws_cloudwatch_log_group" "clawdbot" { + name = "/ecs/${var.project_name}" + retention_in_days = 7 + + tags = { + Name = "${var.project_name}-logs" + } +} + +# ECS Cluster +resource "aws_ecs_cluster" "clawdbot" { + name = "${var.project_name}-cluster" + + tags = { + Name = "${var.project_name}-ecs" + } +} + +# Security Group (outbound only, no inbound needed for Discord bot) +resource "aws_security_group" "clawdbot" { + name = "${var.project_name}-sg" + description = "Security group for Clawdbot ECS tasks (outbound only)" + vpc_id = aws_vpc.clawdbot.id + + # No ingress rules needed - Discord bot initiates outbound WebSocket connections + + # Egress (outbound) for internet access + egress { + description = "Allow all outbound traffic" + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + } + + tags = { + Name = "${var.project_name}-sg" + } +} + +# ECS Task Definition +resource "aws_ecs_task_definition" "clawdbot" { + family = "${var.project_name}-task" + network_mode = "awsvpc" + requires_compatibilities = ["FARGATE"] + cpu = var.ecs_task_cpu + memory = var.ecs_task_memory + execution_role_arn = aws_iam_role.ecs_execution_role.arn + task_role_arn = aws_iam_role.ecs_task_role.arn + + container_definitions = jsonencode([ + { + name = "${var.project_name}-container" + image = "${aws_ecr_repository.clawdbot.repository_url}:latest" + cpu = var.ecs_task_cpu + memory = var.ecs_task_memory + essential = true + command = ["node", "dist/entry.js", "gateway", "--allow-unconfigured"] + + environment = [ + { + name = "AWS_REGION" + value = var.aws_region + }, + { + name = "SESSIONS_TABLE_NAME" + value = aws_dynamodb_table.sessions.name + }, + { + name = "ARTIFACTS_TABLE_NAME" + value = aws_dynamodb_table.artifacts.name + }, + { + name = "ARTIFACTS_S3_BUCKET" + value = aws_s3_bucket.artifacts.id + }, + { + name = "DISCORD_BOT_TOKEN" + value = data.aws_secretsmanager_secret_version.discord_token.secret_string + }, + { + name = "CLAWDBOT_FORCE_BUILD" + value = "0" + } + ] + + logConfiguration = { + logDriver = "awslogs" + options = { + "awslogs-group" = aws_cloudwatch_log_group.clawdbot.name + "awslogs-region" = var.aws_region + "awslogs-stream-prefix" = "ecs" + "awslogs-create-group" = "true" + } + } + } + ]) + + tags = { + Name = "${var.project_name}-taskdef" + } +} + +# ECS Service +resource "aws_ecs_service" "clawdbot" { + name = "${var.project_name}-service" + cluster = aws_ecs_cluster.clawdbot.id + task_definition = aws_ecs_task_definition.clawdbot.arn + desired_count = var.ecs_service_desired_count + launch_type = "FARGATE" + + network_configuration { + subnets = aws_subnet.public[*].id + security_groups = [aws_security_group.clawdbot.id] + assign_public_ip = true + } + + tags = { + Name = "${var.project_name}-service" + } +} diff --git a/workers/clawdbot-aws/terraform/iam.tf b/workers/clawdbot-aws/terraform/iam.tf new file mode 100644 index 000000000..8447bf78d --- /dev/null +++ b/workers/clawdbot-aws/terraform/iam.tf @@ -0,0 +1,100 @@ +/** + * IAM Roles for ECS Fargate Task Execution + */ + +# Assume role policy for ECS tasks +data "aws_iam_policy_document" "ecs_assume_role" { + statement { + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["ecs-tasks.amazonaws.com"] + } + } +} + +# ECS Task Execution Role (for ECR pull and CloudWatch logs) +resource "aws_iam_role" "ecs_execution_role" { + name = "${var.project_name}-ecs-execution-role" + assume_role_policy = data.aws_iam_policy_document.ecs_assume_role.json +} + +# Attach AmazonECSTaskExecutionRolePolicy (required for Fargate) +resource "aws_iam_role_policy_attachment" "ecs_execution_role_policy" { + role = aws_iam_role.ecs_execution_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy" +} + +# ECS Task Role (for application access to DynamoDB and S3) +resource "aws_iam_role" "ecs_task_role" { + name = "${var.project_name}-ecs-task-role" + assume_role_policy = data.aws_iam_policy_document.ecs_assume_role.json +} + +# Task Role Policy: DynamoDB access +resource "aws_iam_role_policy" "ecs_task_dynamodb" { + name = "${var.project_name}-dynamodb-access" + role = aws_iam_role.ecs_task_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "dynamodb:GetItem", + "dynamodb:PutItem", + "dynamodb:UpdateItem", + "dynamodb:DeleteItem", + "dynamodb:Query", + "dynamodb:Scan" + ] + Resource = [ + aws_dynamodb_table.sessions.arn, + "${aws_dynamodb_table.sessions.arn}/index/*", + aws_dynamodb_table.artifacts.arn, + "${aws_dynamodb_table.artifacts.arn}/index/*" + ] + } + ] + }) +} + +# Task Role Policy: S3 access +resource "aws_iam_role_policy" "ecs_task_s3" { + name = "${var.project_name}-s3-access" + role = aws_iam_role.ecs_task_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = [ + "s3:PutObject", + "s3:GetObject", + "s3:DeleteObject" + ] + Resource = "${aws_s3_bucket.artifacts.arn}/*" + } + ] + }) +} + +# Task Role Policy: Secrets Manager access for Discord token +resource "aws_iam_role_policy" "ecs_task_secrets" { + name = "${var.project_name}-secrets-access" + role = aws_iam_role.ecs_task_role.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = ["secretsmanager:GetSecretValue"] + Resource = "arn:aws:secretsmanager:${var.aws_region}:${var.aws_account_id}:secret:${var.discord_token_secret_name}-??????*" + } + ] + }) +} diff --git a/workers/clawdbot-aws/terraform/outputs.tf b/workers/clawdbot-aws/terraform/outputs.tf new file mode 100644 index 000000000..f20b0700b --- /dev/null +++ b/workers/clawdbot-aws/terraform/outputs.tf @@ -0,0 +1,99 @@ +/** + * Deployment Outputs + */ + +output "sessions_table_name" { + description = "DynamoDB table name for session storage" + value = aws_dynamodb_table.sessions.name +} + +output "artifacts_table_name" { + description = "DynamoDB table name for artifacts metadata" + value = aws_dynamodb_table.artifacts.name +} + +output "artifacts_bucket_name" { + description = "S3 bucket name for artifact storage" + value = aws_s3_bucket.artifacts.id +} + +output "artifacts_bucket_arn" { + description = "S3 bucket ARN" + value = aws_s3_bucket.artifacts.arn +} + +output "sessions_table_arn" { + description = "DynamoDB sessions table ARN" + value = aws_dynamodb_table.sessions.arn +} + +output "artifacts_table_arn" { + description = "DynamoDB artifacts table ARN" + value = aws_dynamodb_table.artifacts.arn +} + +output "environment_variables" { + description = "Required environment variables for clawdbot" + value = { + SESSIONS_TABLE_NAME = aws_dynamodb_table.sessions.name + ARTIFACTS_TABLE_NAME = aws_dynamodb_table.artifacts.name + ARTIFACTS_S3_BUCKET = aws_s3_bucket.artifacts.id + AWS_REGION = var.aws_region + } +} + +# ECS Outputs +output "ecs_cluster_name" { + description = "ECS cluster name" + value = aws_ecs_cluster.clawdbot.name +} + +output "ecs_service_name" { + description = "ECS service name" + value = aws_ecs_service.clawdbot.name +} + +output "ecs_task_definition_family" { + description = "ECS task definition family" + value = aws_ecs_task_definition.clawdbot.family +} + +output "ecr_repository_url" { + description = "ECR repository URL for container images" + value = aws_ecr_repository.clawdbot.repository_url +} + +output "ecr_repository_name" { + description = "ECR repository name" + value = aws_ecr_repository.clawdbot.name +} + +output "cloudwatch_log_group_name" { + description = "CloudWatch Log Group name" + value = aws_cloudwatch_log_group.clawdbot.name +} + +output "vpc_id" { + description = "VPC ID" + value = aws_vpc.clawdbot.id +} + +output "public_subnet_ids" { + description = "Public subnet IDs" + value = aws_subnet.public[*].id +} + +output "security_group_id" { + description = "Security Group ID" + value = aws_security_group.clawdbot.id +} + +output "ecs_task_role_arn" { + description = "ECS Task Role ARN" + value = aws_iam_role.ecs_task_role.arn +} + +output "ecs_execution_role_arn" { + description = "ECS Execution Role ARN" + value = aws_iam_role.ecs_execution_role.arn +} diff --git a/workers/clawdbot-aws/terraform/s3.tf b/workers/clawdbot-aws/terraform/s3.tf new file mode 100644 index 000000000..eb6bc5780 --- /dev/null +++ b/workers/clawdbot-aws/terraform/s3.tf @@ -0,0 +1,60 @@ +/** + * S3 Bucket for Artifact Storage + */ + +resource "aws_s3_bucket" "artifacts" { + bucket = var.artifacts_bucket_name + + tags = { + Name = "${var.project_name}-artifacts" + } +} + +# S3 bucket versioning (disabled for cost optimization) +resource "aws_s3_bucket_versioning" "artifacts" { + bucket = aws_s3_bucket.artifacts.bucket + versioning_configuration { + status = "Suspended" + } +} + +# S3 bucket server-side encryption (SSE-S3) +resource "aws_s3_bucket_server_side_encryption_configuration" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "AES256" + } + } +} + +# S3 bucket public access block (private bucket) +resource "aws_s3_bucket_public_access_block" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +# S3 bucket lifecycle configuration (auto-expire old artifacts) +resource "aws_s3_bucket_lifecycle_configuration" "artifacts" { + bucket = aws_s3_bucket.artifacts.id + + rule { + id = "expire-old-artifacts" + status = "Enabled" + + filter {} + + expiration { + days = var.artifacts_bucket_ttl_days + } + + noncurrent_version_expiration { + noncurrent_days = 1 + } + } +} diff --git a/workers/clawdbot-aws/terraform/secrets.tf b/workers/clawdbot-aws/terraform/secrets.tf new file mode 100644 index 000000000..4630a275b --- /dev/null +++ b/workers/clawdbot-aws/terraform/secrets.tf @@ -0,0 +1,8 @@ +/** + * AWS Secrets Manager Data Sources + */ + +# Discord Bot Token from Secrets Manager +data "aws_secretsmanager_secret_version" "discord_token" { + secret_id = var.discord_token_secret_name +} diff --git a/workers/clawdbot-aws/terraform/terraform.tfvars.example b/workers/clawdbot-aws/terraform/terraform.tfvars.example new file mode 100644 index 000000000..26d6e9e33 --- /dev/null +++ b/workers/clawdbot-aws/terraform/terraform.tfvars.example @@ -0,0 +1,34 @@ +# AWS Terraform Variables for Clawdbot ECS Fargate Deployment +# Copy this file to terraform.tfvars and fill in the values + +# AWS Configuration +aws_region = "ap-northeast-1" +aws_account_id = "123456789012" # Your AWS account ID + +# Environment +environment = "production" +project_name = "clawdbot" + +# ECS Configuration +ecs_task_cpu = 256 # 0.25 vCPU +ecs_task_memory = 2048 # 2 GB +ecs_service_desired_count = 1 + +# Discord Token (DEPRECATED - Use Secrets Manager instead) +# discord_token = "your_bot_token_here" # DO NOT use in production + +# Secrets Manager Secret Name for Discord Bot Token +discord_token_secret_name = "clawdbot/discord-token" +# Create the secret in AWS Secrets Manager before running Terraform: +# aws secretsmanager create-secret \ +# --name clawdbot/discord-token \ +# --description "Discord Bot Token for Clawdbot" \ +# --secret-string "your_bot_token_here" + +# DynamoDB Tables +sessions_table_name = "clawdbot-sessions" +artifacts_table_name = "clawdbot-artifacts" + +# S3 Bucket +artifacts_bucket_name = "clawdbot-artifacts" +artifacts_bucket_ttl_days = 7 diff --git a/workers/clawdbot-aws/terraform/variables.tf b/workers/clawdbot-aws/terraform/variables.tf new file mode 100644 index 000000000..b11a16edc --- /dev/null +++ b/workers/clawdbot-aws/terraform/variables.tf @@ -0,0 +1,79 @@ +variable "aws_region" { + description = "AWS region for resources" + type = string + default = "ap-northeast-1" +} + +variable "aws_account_id" { + description = "AWS account ID (for Secrets Manager ARN)" + type = string +} + +variable "environment" { + description = "Environment name (e.g., production, staging)" + type = string + default = "production" +} + +variable "project_name" { + description = "Project name used for resource naming" + type = string + default = "clawdbot" +} + +variable "sessions_table_name" { + description = "DynamoDB table name for session storage" + type = string + default = "clawdbot-sessions" +} + +variable "artifacts_table_name" { + description = "DynamoDB table name for artifacts metadata" + type = string + default = "clawdbot-artifacts" +} + +variable "artifacts_bucket_name" { + description = "S3 bucket name for artifact storage" + type = string + default = "clawdbot-artifacts" +} + +variable "artifacts_bucket_ttl_days" { + description = "S3 bucket lifecycle TTL in days" + type = number + default = 7 +} + +# ECS Fargate Configuration +variable "ecs_task_cpu" { + description = "ECS task CPU units (256 = 0.25 vCPU)" + type = number + default = 256 +} + +variable "ecs_task_memory" { + description = "ECS task memory in MB" + type = number + default = 2048 +} + +variable "ecs_service_desired_count" { + description = "Desired number of ECS tasks" + type = number + default = 1 +} + +variable "discord_token_secret_name" { + description = "AWS Secrets Manager secret name for Discord bot token" + type = string + default = "clawdbot/discord-token" +} + +# DEPRECATED: Use discord_token_secret_name with AWS Secrets Manager instead +variable "discord_token" { + description = "DEPRECATED: Discord bot token (use Secrets Manager instead)" + type = string + sensitive = true + default = null +} diff --git a/workers/clawdbot-aws/terraform/versions.tf b/workers/clawdbot-aws/terraform/versions.tf new file mode 100644 index 000000000..55d3796f6 --- /dev/null +++ b/workers/clawdbot-aws/terraform/versions.tf @@ -0,0 +1,23 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } +} + +provider "aws" { + region = var.aws_region + + default_tags { + tags = { + Project = var.project_name + Environment = var.environment + ManagedBy = "Terraform" + Component = "clawdbot-backend" + } + } +} diff --git a/workers/clawdbot-aws/terraform/vpc.tf b/workers/clawdbot-aws/terraform/vpc.tf new file mode 100644 index 000000000..79b0b92d0 --- /dev/null +++ b/workers/clawdbot-aws/terraform/vpc.tf @@ -0,0 +1,63 @@ +/** + * VPC for Clawdbot ECS Fargate Deployment + * Discord bot requires outbound internet access for WebSocket connections + */ + +# VPC (using 100.64.0.0/16 to avoid conflicts with default 10.0.0.0/8) +resource "aws_vpc" "clawdbot" { + cidr_block = "100.64.0.0/16" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "${var.project_name}-vpc" + } +} + +# Internet Gateway for outbound internet access +resource "aws_internet_gateway" "clawdbot" { + vpc_id = aws_vpc.clawdbot.id + + tags = { + Name = "${var.project_name}-igw" + } +} + +# Public Subnets (for Fargate tasks with public IP) +resource "aws_subnet" "public" { + count = 2 + vpc_id = aws_vpc.clawdbot.id + cidr_block = cidrsubnet(aws_vpc.clawdbot.cidr_block, 8, count.index) + availability_zone = data.aws_availability_zones.available.names[count.index] + map_public_ip_on_launch = true + + tags = { + Name = "${var.project_name}-public-${count.index + 1}" + } +} + +# Route Table for public subnets +resource "aws_route_table" "public" { + vpc_id = aws_vpc.clawdbot.id + + route { + cidr_block = "0.0.0.0/0" + gateway_id = aws_internet_gateway.clawdbot.id + } + + tags = { + Name = "${var.project_name}-public-rt" + } +} + +# Route Table Association for public subnets +resource "aws_route_table_association" "public" { + count = 2 + subnet_id = aws_subnet.public[count.index].id + route_table_id = aws_route_table.public.id +} + +# Data source for available AZs +data "aws_availability_zones" "available" { + state = "available" +}