diff --git a/README.de.md b/README.de.md index 343f01e..56a9fb9 100644 --- a/README.de.md +++ b/README.de.md @@ -35,14 +35,14 @@ ## ✨ Warum dieses Projekt - ✅ Leichtgewichtig: keine schweren Frameworks, volle Transparenz - ✅ Multi-Bot: mehrere Chats, mehrere Sessions - - ✅ Telegram zum Steuern von Codex / Copilot CLI verwenden + - ✅ Telegram zum Steuern von Codex / Copilot / Claude Code CLI verwenden - ✅ Antworten und geänderte Dateien bequem in Code-Blöcken prüfen - ✅ Folgefragen während eines laufenden Agentenlaufs in die Queue stellen - ✅ Akzeptiert ✏️ Text-, 🌄 Bild- und 🎙️ Sprachnachrichten ## 🔁 Nahtlos zwischen Geräten und Sessions wechseln - Starte eine Session in Telegram und setze dieselbe Codex/Copilot CLI-Session später ohne Umwege am Computer fort. Mit `/switch` kannst du auch wieder sauber von Telegram zurück ins Terminal wechseln. + Starte eine Session in Telegram und setze dieselbe Codex/Copilot/Claude Code CLI-Session später ohne Umwege am Computer fort. Mit `/switch` kannst du auch wieder sauber von Telegram zurück ins Terminal wechseln. - Nutze `/switch`, um eine lokale Session weiterzuführen - Historische Sessions werden ebenfalls unterstützt @@ -86,6 +86,7 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - Bestehende Ordner können vor schreibenden Git-Operationen eine Vertrauensbestätigung verlangen - Der Server führt keine versteckten externen Aufrufe aus. Alles bleibt unter deiner Kontrolle. - Funktioniert gut mit dem Codex Sandbox mode; du musst `danger-full-access` nicht freigeben +- Die Claude Code-Integration unterstützt konfigurierbare Permission-Modi sowie Allow-/Deny-Listen für Tools @@ -96,16 +97,74 @@ Vor dem Start des Servers brauchst du: - Python 3.9 oder neuer - Einen Telegram-Bot-Token von _@BotFather_ - Deine Telegram-Chat-ID -- Lokal installiertes Codex CLI und/oder Copilot CLI -- [Codex CLI Installation](https://developers.openai.com/codex/cli) -- [Copilot CLI Installation](https://github.com/features/copilot/cli) +- Lokal installiertes Codex CLI und/oder Copilot CLI und/oder Claude Code CLI +- [Codex CLI Installation](https://developers.openai.com/codex/cli) / [Copilot CLI Installation](https://github.com/features/copilot/cli) / [Claude Code CLI Installation](https://code.claude.com/docs/en/quickstart) - [Optional] Whisper, ffmpeg ## 🦞 Warum brauche ich das, wenn ich Openclaw bereits habe? -Openclaw bietet dir sehr umfassende Funktionen und hat mit Pi-Agent bereits eine integrierte Agent-Loop. Es ist vielseitig und für breitere Einsatzfälle gedacht. Ich mag Openclaw ebenfalls und habe selbst damit entwickelt. Für Coding ist es aber nicht immer die beste Wahl, weil der eingebaute große System-Prompt und der zusätzliche Kontext eher ablenken können. Claude Code / Codex / Copilot sind fürs Coding oft effizienter, präziser, weniger abgelenkt und direkter. Dieses Projekt bleibt bewusst einfach und integriert nur Codex / Copilot CLI. Du delegierst also direkt an Codex / Copilot. +Openclaw bietet dir sehr umfassende Funktionen und hat mit Pi-Agent bereits eine integrierte Agent-Loop. Es ist vielseitig und für breitere Einsatzfälle gedacht. Ich mag Openclaw ebenfalls und habe selbst damit entwickelt. Für Coding ist es aber nicht immer die beste Wahl, weil der eingebaute große System-Prompt und der zusätzliche Kontext eher ablenken können. Claude Code / Codex / Copilot sind fürs Coding oft effizienter, präziser, weniger abgelenkt und direkter. Dieses Projekt bleibt bewusst einfach und integriert nur Codex / Copilot / Claude Code CLI. Du delegierst also direkt an Codex / Copilot / Claude Code. + +## 🆚 Warum brauche ich coding-agent-telegram, wenn ich bereits Claude Code + Telegram Plugin habe? + +| Funktion | Claude Code + offizielles Telegram Plugin | coding-agent-telegram (mit Claude-Unterstützung) | +|----------|--------------------------------------------|--------------------------------------------------| +| KI-Chat über Telegram | ✅ | ✅ | +| Lokalen Code bearbeiten und Befehle ausführen | ✅ | ✅ | +| Erfordert eine bereits laufende CLI-Sitzung | **Ja** | **Nein** (startet oder setzt Sitzungen automatisch fort) | +| Unterstützung mehrerer KI-Anbieter | ❌ Nur Claude | ✅ Claude Code, Codex CLI, GitHub Copilot CLI | +| Projektverwaltung über Telegram | ❌ | ✅ `/project` | +| Branch-Verwaltung über Telegram | Manuelle Git-Befehle | ✅ `/branch`-Workflow | +| Sitzungen erstellen und wechseln | Auf die aktive Claude-Sitzung beschränkt | ✅ `/new`, `/switch`, `/current`, `/compact` | +| Vorhandene lokale CLI-Sitzungen fortsetzen | ❌ | ✅ | +| Sitzungen geräteübergreifend fortsetzen | Eingeschränkt | ✅ | +| Schutz vor gleichzeitigen Änderungen im Workspace | ❌ | ✅ Verhindert, dass mehrere Agents gleichzeitig dasselbe Projekt bearbeiten | +| Aufgabenwarteschlange bei ausgelastetem Agent | ❌ | ✅ | +| Unabhängige Dateisystem-Snapshots und Diffs | ❌ | ✅ | +| Strukturierte Datei-Diffs direkt in Telegram anzeigen | ❌ | ✅ | +| Filterung von Secrets und sensiblen Diffs | ❌ | ✅ | +| Integrierter Git-Workflow (pull / push / commit) | Manuell | ✅ | +| Unterstützung mehrerer Bots | Mehrere Instanzen erforderlich | ✅ Verwaltung über einen einzigen Server | +| Projektbezogene Statusverwaltung | ❌ | ✅ | +| Anbieterunabhängige Architektur | ❌ | ✅ | + +> **Wichtigster Unterschied** +> +> Das offizielle Claude Code Telegram Plugin verbindet Telegram mit **einer bereits laufenden Claude Code-Sitzung**. +> +> **coding-agent-telegram** fungiert als **Telegram-Kontrollzentrale**, die Projekte, Branches, Sitzungen, Git-Workflows und mehrere Coding-Agents (Claude Code, Codex CLI, GitHub Copilot CLI) über eine einzige Oberfläche verwaltet. + +### 🏛️ Architektur + +#### Claude Code + Telegram Plugin + +```text +Telegram + │ + ▼ +Claude Code Channel + │ + ▼ +Eine laufende Claude Code-Sitzung +``` + +#### coding-agent-telegram + +```text +Telegram + │ + ▼ +coding-agent-telegram + │ + ├── Claude Code + ├── Codex CLI + └── GitHub Copilot CLI + │ + ▼ +Projekt • Branch • Sitzung • Warteschlange • Git • Diff • Secret-Filter +``` ## 🚀 Schnellstart @@ -203,7 +262,7 @@ Der Bot akzeptiert derzeit: - Textnachrichten - Fotos - Sprachnachrichten und Audiodateien, wenn `ENABLE_OPENAI_WHISPER_SPEECH_TO_TEXT=true` gesetzt ist und die lokalen Whisper-Voraussetzungen installiert sind -- Codex und Copilot unterstützen aktuell nur Text und Bilder, kein Video. +- Codex- und Claude Code-Sessions unterstützen Text und Bilder; Copilot-Sessions unterstützen aktuell nur Text. Video wird von keinem Provider unterstützt. ## 🤖 Telegram-Befehle @@ -234,7 +293,7 @@ Der Bot akzeptiert derzeit: /switch - Die neuesten Sessions anzeigen, zuerst die neuesten. Die Liste enthält sowohl vom Bot verwaltete Sessions als auch lokale Codex/Copilot CLI-Sessions für das aktuelle Projekt. + Die neuesten Sessions anzeigen, zuerst die neuesten. Die Liste enthält sowohl vom Bot verwaltete Sessions als auch lokale Codex/Copilot/Claude Code CLI-Sessions für das aktuelle Projekt. /switch page <number> @@ -313,13 +372,21 @@ Der Bot akzeptiert derzeit: APP_LOCALE UI-Sprache für gemeinsame Bot-Meldungen und Befehlsbeschreibungen. Unterstützte Werte: en, de, fr, ja, ko, nl, th, vi, zh-CN, zh-HK, zh-TW. + + DEFAULT_AGENT_PROVIDER + Standard-Provider für neue Sessions: codex, copilot oder claude. Standard: codex. + CODEX_BIN - Befehl zum Starten von Codex CLI. Standard: codex. + Befehl zum Starten von Codex CLI. Die Anwendung versucht beim Initialisieren der .env_coding_agent_telegram-Datei, den lokal installierten Codex-Pfad automatisch zu erkennen. Alternativ können Sie mit which codex den Pfad anzeigen. COPILOT_BIN - Befehl zum Starten von Copilot CLI. Standard: copilot. + Befehl zum Starten von Copilot CLI. Die Anwendung versucht beim Initialisieren der .env_coding_agent_telegram-Datei, den lokal installierten Copilot-Pfad automatisch zu erkennen. Alternativ können Sie mit which copilot den Pfad anzeigen. + + + CLAUDE_BIN + Befehl zum Starten von Claude Code CLI. Die Anwendung versucht beim Initialisieren der .env_coding_agent_telegram-Datei, den lokal installierten Claude-Pfad automatisch zu erkennen. Alternativ können Sie mit which claude den Pfad anzeigen. CODEX_MODEL @@ -329,6 +396,14 @@ Der Bot akzeptiert derzeit: COPILOT_MODEL Optionale Model-Überschreibung für Copilot. Leer lassen, um das Standardmodell von Copilot CLI zu verwenden. Beispiele: gpt-5.4, claude-sonnet-4.6 GitHub Copilot unterstützte modelle + + CLAUDE_MODEL + Optionale Model-Überschreibung für Claude Code. + Leer lassen, um das Standardmodell von Claude Code CLI zu verwenden. + Beispiele: sonnet, opus, haiku + Claude Code Modellkonfiguration + + CODEX_APPROVAL_POLICY An Codex übergebener Freigabemodus. Standard: never. @@ -341,6 +416,18 @@ Der Bot akzeptiert derzeit: CODEX_SKIP_GIT_REPO_CHECK Wenn aktiviert, werden Codex-Prüfungen für vertrauenswürdige Repositories immer übersprungen. + + CLAUDE_PERMISSION_MODE + An Claude Code übergebener Permission-Modus. Einer von default, acceptEdits, plan, auto, dontAsk, bypassPermissions, manual. Standard: bypassPermissions (vollständig autonom, da kein interaktives Terminal zum Bestätigen von Prompts vorhanden ist). + + + CLAUDE_ALLOWED_TOOLS + Kommagetrennte Allowlist für Claude Code-Tools, nach der Permission-Regel-Syntax von Claude Code. Beispiel: Read,Edit,Bash(git *) + + + CLAUDE_DISALLOWED_TOOLS + Kommagetrennte Denylist für Claude Code-Tools. Beispiel: Bash(rm *) + ENABLE_COMMIT_COMMAND Den Telegram-Befehl /commit aktivieren. Standard: false. @@ -422,8 +509,10 @@ ALLOWED_CHAT_IDS=123456789 DEFAULT_AGENT_PROVIDER=codex CODEX_BIN=codex COPILOT_BIN=copilot +CLAUDE_BIN=claude CODEX_APPROVAL_POLICY=never CODEX_SANDBOX_MODE=workspace-write +CLAUDE_PERMISSION_MODE=bypassPermissions ENABLE_SENSITIVE_DIFF_FILTER=true ENABLE_SECRET_SCRUB_FILTER=true ``` @@ -489,7 +578,7 @@ Wird der aktuelle Lauf abgebrochen und es warten noch Fragen, setzt der Bot nich ## ⚠️ Diff (Dateiänderungen) -_Während jedes Agentenlaufs erstellt der Bot außerdem einen leichten Vorher/Nachher-Snapshot des Projekts, damit er geänderte Dateien zusammenfassen und Diffs an Telegram senden kann. Dieser Snapshot wird von der Bot-App selbst erstellt, nicht von Codex oder Copilot._ +_Während jedes Agentenlaufs erstellt der Bot außerdem einen leichten Vorher/Nachher-Snapshot des Projekts, damit er geänderte Dateien zusammenfassen und Diffs an Telegram senden kann. Dieser Snapshot wird von der Bot-App selbst erstellt, nicht von Codex, Copilot oder Claude Code._ **Hinweise zum Snapshot:** diff --git a/README.fr.md b/README.fr.md index c221f31..73df0cc 100644 --- a/README.fr.md +++ b/README.fr.md @@ -35,14 +35,14 @@ ## ✨ Pourquoi l’utiliser - ✅ Léger : pas de framework lourd, transparence totale - ✅ Multi-bot : plusieurs chats, plusieurs sessions - - ✅ Utiliser Telegram pour piloter Codex / Copilot CLI + - ✅ Utiliser Telegram pour piloter Codex / Copilot / Claude Code CLI - ✅ Révision facile des réponses et des fichiers modifiés dans des blocs de code - ✅ Les messages de suivi peuvent être mis en file d’attente pendant qu’un agent travaille - ✅ Accepte les messages ✏️ texte, 🌄 image et 🎙️ vocaux ## 🔁 Changement fluide entre appareils et sessions - Démarrez une session dans Telegram, puis reprenez la même session Codex/Copilot CLI plus tard sur votre ordinateur sans friction. Avec `/switch`, vous pouvez aussi repasser simplement de Telegram à la ligne de commande. + Démarrez une session dans Telegram, puis reprenez la même session Codex/Copilot/Claude Code CLI plus tard sur votre ordinateur sans friction. Avec `/switch`, vous pouvez aussi repasser simplement de Telegram à la ligne de commande. - Utilisez `/switch` pour reprendre une session locale - Les sessions historiques sont également prises en charge @@ -86,6 +86,7 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - Les dossiers existants peuvent exiger une confirmation de confiance avant les opérations Git modifiantes - Le serveur n’effectue aucun appel externe caché. Vous gardez le contrôle. - Fonctionne bien avec le mode sandbox de Codex, sans devoir accorder `danger-full-access` +- L’intégration Claude Code prend en charge des modes de permission configurables ainsi que des listes d’autorisation/interdiction d’outils @@ -96,16 +97,74 @@ Avant de démarrer le serveur, assurez-vous d’avoir : - Python 3.9 ou plus récent - Un token de bot Telegram créé via _@BotFather_ - Votre identifiant de chat Telegram -- Codex CLI et/ou Copilot CLI installés localement -- [Installation Codex CLI](https://developers.openai.com/codex/cli) -- [Installation Copilot CLI](https://github.com/features/copilot/cli) +- Codex CLI et/ou Copilot CLI et/ou Claude Code CLI installés localement +- [Installation Codex CLI](https://developers.openai.com/codex/cli) / [Installation Copilot CLI](https://github.com/features/copilot/cli) / [Installation Claude Code CLI](https://code.claude.com/docs/en/quickstart) - [Optionnel] Whisper, ffmpeg ## 🦞 Pourquoi en ai-je besoin si j’ai déjà Openclaw ? -Openclaw offre des capacités très complètes et intègre déjà une boucle d’agent appelée Pi-Agent. C’est un outil riche, pensé pour des cas d’usage plus variés. J’aime aussi Openclaw et j’ai déjà codé avec lui. Mais pour le coding pur, ce n’est pas toujours le meilleur choix à cause du gros system prompt intégré et du contexte supplémentaire. Pour coder, Claude Code / Codex / Copilot restent souvent plus efficaces, plus précis, moins distraits et plus directs. Ce projet reste volontairement simple et se contente d’intégrer Codex / Copilot CLI. Vous déléguez donc directement le travail à Codex / Copilot. +Openclaw offre des capacités très complètes et intègre déjà une boucle d’agent appelée Pi-Agent. C’est un outil riche, pensé pour des cas d’usage plus variés. J’aime aussi Openclaw et j’ai déjà codé avec lui. Mais pour le coding pur, ce n’est pas toujours le meilleur choix à cause du gros system prompt intégré et du contexte supplémentaire. Pour coder, Claude Code / Codex / Copilot restent souvent plus efficaces, plus précis, moins distraits et plus directs. Ce projet reste volontairement simple et se contente d’intégrer Codex / Copilot / Claude Code CLI. Vous déléguez donc directement le travail à Codex / Copilot / Claude Code. + +## 🆚 Pourquoi utiliser coding-agent-telegram si j'ai déjà Claude Code + Telegram Plugin ? + +| Fonctionnalité | Claude Code + Plugin Telegram officiel | coding-agent-telegram (avec prise en charge de Claude) | +|----------------|-----------------------------------------|--------------------------------------------------------| +| Discussion avec l'IA via Telegram | ✅ | ✅ | +| Modifier le code local et exécuter des commandes | ✅ | ✅ | +| Nécessite une session CLI déjà en cours d'exécution | **Oui** | **Non** (démarre ou reprend automatiquement les sessions) | +| Prise en charge de plusieurs fournisseurs d'IA | ❌ Claude uniquement | ✅ Claude Code, Codex CLI, GitHub Copilot CLI | +| Gestion des projets depuis Telegram | ❌ | ✅ `/project` | +| Gestion des branches depuis Telegram | Commandes Git manuelles | ✅ Workflow `/branch` | +| Créer et changer de session | Limité à la session Claude active | ✅ `/new`, `/switch`, `/current`, `/compact` | +| Reprendre des sessions CLI locales existantes | ❌ | ✅ | +| Continuité des sessions entre plusieurs appareils | Limitée | ✅ | +| Protection contre les modifications simultanées d'un même workspace | ❌ | ✅ Empêche plusieurs agents de modifier le même projet en même temps | +| File d'attente des tâches lorsqu'un agent est occupé | ❌ | ✅ | +| Snapshots et diffs du système de fichiers indépendants | ❌ | ✅ | +| Affichage des diffs structurés directement dans Telegram | ❌ | ✅ | +| Filtrage des secrets et des diffs sensibles | ❌ | ✅ | +| Workflow Git intégré (pull / push / commit) | Manuel | ✅ | +| Prise en charge de plusieurs bots | Plusieurs instances nécessaires | ✅ Gérés par un serveur unique | +| Gestion de l'état au niveau du projet | ❌ | ✅ | +| Architecture indépendante du fournisseur | ❌ | ✅ | + +> **Différence principale** +> +> Le plugin Telegram officiel de Claude Code connecte Telegram à **une seule session Claude Code déjà en cours d'exécution**. +> +> **coding-agent-telegram** agit comme un **plan de contrôle Telegram** permettant de gérer les projets, les branches, les sessions, les workflows Git et plusieurs agents de développement (Claude Code, Codex CLI, GitHub Copilot CLI) depuis une interface unique. + +### 🏛️ Architecture + +#### Claude Code + Telegram Plugin + +```text +Telegram + │ + ▼ +Canal Claude Code + │ + ▼ +Une session Claude Code en cours d'exécution +``` + +#### coding-agent-telegram + +```text +Telegram + │ + ▼ +coding-agent-telegram + │ + ├── Claude Code + ├── Codex CLI + └── GitHub Copilot CLI + │ + ▼ +Projet • Branche • Session • File d'attente • Git • Diff • Filtre des secrets +``` ## 🚀 Démarrage rapide @@ -203,7 +262,7 @@ Le bot accepte actuellement : - les messages texte - les photos - les messages vocaux et les fichiers audio quand `ENABLE_OPENAI_WHISPER_SPEECH_TO_TEXT=true` et que les prérequis locaux de Whisper sont installés -- Codex et Copilot prennent actuellement en charge uniquement le texte et les images, pas la vidéo +- Codex et Claude Code prennent en charge le texte et les images ; Copilot ne prend actuellement en charge que le texte. Aucun fournisseur ne prend en charge la vidéo. ## 🤖 Commandes Telegram @@ -234,7 +293,7 @@ Le bot accepte actuellement : /switch - Afficher les sessions les plus récentes, de la plus récente à la plus ancienne. La liste inclut les sessions gérées par le bot et les sessions locales Codex/Copilot CLI du projet courant. + Afficher les sessions les plus récentes, de la plus récente à la plus ancienne. La liste inclut les sessions gérées par le bot et les sessions locales Codex/Copilot/Claude Code CLI du projet courant. /switch page <number> @@ -313,13 +372,21 @@ Le bot accepte actuellement : APP_LOCALE Langue de l’interface pour les messages partagés du bot et les descriptions de commandes. Valeurs prises en charge : en, de, fr, ja, ko, nl, th, vi, zh-CN, zh-HK, zh-TW. + + DEFAULT_AGENT_PROVIDER + Fournisseur par défaut pour les nouvelles sessions : codex, copilot, ou claude. Valeur par défaut : codex. + CODEX_BIN - Commande utilisée pour lancer Codex CLI. Valeur par défaut : codex. + Commande utilisée pour lancer Codex CLI. L'application essaiera de détecter automatiquement le chemin d'installation local de Codex lors de l'initialisation de .env_coding_agent_telegram. Vous pouvez également utiliser which codex pour afficher ce chemin. COPILOT_BIN - Commande utilisée pour lancer Copilot CLI. Valeur par défaut : copilot. + Commande utilisée pour lancer Copilot CLI. L'application essaiera de détecter automatiquement le chemin d'installation local de Copilot lors de l'initialisation de .env_coding_agent_telegram. Vous pouvez également utiliser which copilot pour afficher ce chemin. + + + CLAUDE_BIN + Commande utilisée pour lancer Claude Code CLI. L'application essaiera de détecter automatiquement le chemin d'installation local de Claude lors de l'initialisation de .env_coding_agent_telegram. Vous pouvez également utiliser which claude pour afficher ce chemin. CODEX_MODEL @@ -329,6 +396,14 @@ Le bot accepte actuellement : COPILOT_MODEL Remplacement optionnel du modèle Copilot. Laissez vide pour utiliser le modèle par défaut de Copilot CLI. Exemples : gpt-5.4, claude-sonnet-4.6 Modèles pris en charge par GitHub Copilot + + CLAUDE_MODEL + Remplacement optionnel du modèle Claude Code. + Laissez vide pour utiliser le modèle par défaut de Claude Code CLI. + Exemples : sonnet, opus, haiku + Configuration des modèles Claude Code + + CODEX_APPROVAL_POLICY Mode d’approbation transmis à Codex. Défaut : never. @@ -341,6 +416,18 @@ Le bot accepte actuellement : CODEX_SKIP_GIT_REPO_CHECK Si activé, contourne toujours les vérifications de dépôt trusted de Codex. + + CLAUDE_PERMISSION_MODE + Mode de permission transmis à Claude Code. Un parmi default, acceptEdits, plan, auto, dontAsk, bypassPermissions, manual. Défaut : bypassPermissions (entièrement autonome, puisqu’il n’y a pas de terminal interactif pour approuver les invites). + + + CLAUDE_ALLOWED_TOOLS + Liste blanche d’outils Claude Code séparés par des virgules, utilisant la syntaxe des règles de permission de Claude Code. Exemple : Read,Edit,Bash(git *) + + + CLAUDE_DISALLOWED_TOOLS + Liste noire d’outils Claude Code séparés par des virgules. Exemple : Bash(rm *) + ENABLE_COMMIT_COMMAND Active la commande Telegram /commit. Défaut : false. @@ -422,8 +509,10 @@ ALLOWED_CHAT_IDS=123456789 DEFAULT_AGENT_PROVIDER=codex CODEX_BIN=codex COPILOT_BIN=copilot +CLAUDE_BIN=claude CODEX_APPROVAL_POLICY=never CODEX_SANDBOX_MODE=workspace-write +CLAUDE_PERMISSION_MODE=bypassPermissions ENABLE_SENSITIVE_DIFF_FILTER=true ENABLE_SECRET_SCRUB_FILTER=true ``` @@ -487,7 +576,7 @@ Si l'exécution en cours est annulée et que des questions attendent encore, le ## ⚠️ Diff (modifications de fichiers) -_Pendant chaque exécution d'agent, le bot prend aussi un léger instantané avant/après du projet afin de résumer les fichiers modifiés et d'envoyer des diffs vers Telegram. Ce instantané est produit par le bot lui-même, pas par Codex ou Copilot._ +_Pendant chaque exécution d'agent, le bot prend aussi un léger instantané avant/après du projet afin de résumer les fichiers modifiés et d'envoyer des diffs vers Telegram. Ce instantané est produit par le bot lui-même, pas par Codex, Copilot ou Claude Code._ **À savoir sur le instantané :** diff --git a/README.ja.md b/README.ja.md index 410c329..8fbe137 100644 --- a/README.ja.md +++ b/README.ja.md @@ -35,14 +35,14 @@ ## ✨ このプロジェクトを使う理由 - ✅ 軽量: 重いフレームワーク不要、挙動が見えやすい - ✅ マルチBot: 複数チャット、複数セッションに対応 - - ✅ Telegram で Codex / Copilot CLI を操作できる + - ✅ Telegram で Codex / Copilot / Claude Code CLI を操作できる - ✅ エージェントの回答や変更ファイルをコードブロックで確認しやすい - ✅ エージェント実行中でも追加入力をキューに積める - ✅ ✏️ テキスト、🌄 画像、🎙️ 音声メッセージに対応 ## 🔁 デバイス/セッションをシームレスに切り替え - Telegram で始めたセッションを、あとで同じ Codex/Copilot CLI セッションとして PC 上でそのまま続けられます。`/switch` を使えば Telegram とコマンドラインの行き来も簡単です。 + Telegram で始めたセッションを、あとで同じ Codex/Copilot/Claude Code CLI セッションとして PC 上でそのまま続けられます。`/switch` を使えば Telegram とコマンドラインの行き来も簡単です。 - `/switch` でローカルセッションを再開 - 過去のセッションも利用可能 @@ -86,6 +86,7 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - 既存フォルダでは、変更を伴う Git operation の前に trust 確認を求める場合あり - サーバーは隠れた外部呼び出しを行いません。すべて自分で管理できます。 - Codex Sandbox mode と相性がよく、`danger-full-access` を許可する必要はありません +- Claude Code の統合は、設定可能な permission mode と tool の許可/拒否リストに対応しています @@ -96,16 +97,74 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - Python 3.9 以上 - _@BotFather_ で作成した Telegram Bot Token - 自分の Telegram Chat ID -- ローカルにインストール済みの Codex CLI または Copilot CLI -- [Codex CLI インストール](https://developers.openai.com/codex/cli) -- [Copilot CLI インストール](https://github.com/features/copilot/cli) +- ローカルにインストール済みの Codex CLI または Copilot CLI または Claude Code CLI +- [Codex CLI インストール](https://developers.openai.com/codex/cli) / [Copilot CLI インストール](https://github.com/features/copilot/cli) / [Claude Code CLI インストール](https://code.claude.com/docs/en/quickstart) - [任意] Whisper、ffmpeg ## 🦞 すでに Openclaw があるのに、なぜこれが必要ですか? -Openclaw は非常に多機能で、Pi-Agent という統合 agent loop も備えています。用途の幅も広く、とても包括的です。私自身も Openclaw が好きで、以前は Openclaw で coding していました。ですが、coding に限ると、組み込みの大きな system prompt や追加 context のせいで、必ずしも最適とは限りません。Claude Code / Codex / Copilot のほうが、coding ではより効率的で、精度が高く、気が散りにくく、まっすぐです。この project は意図的にシンプルで、Codex / Copilot CLI だけを統合します。つまり、Codex / Copilot に直接作業を委ねられます。 +Openclaw は非常に多機能で、Pi-Agent という統合 agent loop も備えています。用途の幅も広く、とても包括的です。私自身も Openclaw が好きで、以前は Openclaw で coding していました。ですが、coding に限ると、組み込みの大きな system prompt や追加 context のせいで、必ずしも最適とは限りません。Claude Code / Codex / Copilot のほうが、coding ではより効率的で、精度が高く、気が散りにくく、まっすぐです。この project は意図的にシンプルで、Codex / Copilot / Claude Code CLI だけを統合します。つまり、Codex / Copilot / Claude Code に直接作業を委ねられます。 + +## 🆚 Claude Code + Telegram Plugin があるのに、なぜ coding-agent-telegram が必要なのですか? + +| 機能 | Claude Code + 公式 Telegram Plugin | coding-agent-telegram(Claude 対応) | +|------|------------------------------------|--------------------------------------| +| Telegram で AI とチャット | ✅ | ✅ | +| ローカルコードの編集とコマンド実行 | ✅ | ✅ | +| あらかじめ起動済みの CLI セッションが必要 | **はい** | **いいえ**(セッションを自動的に開始または再開) | +| 複数の AI プロバイダーをサポート | ❌ Claude のみ | ✅ Claude Code、Codex CLI、GitHub Copilot CLI | +| Telegram からプロジェクトを管理 | ❌ | ✅ `/project` | +| Telegram からブランチを管理 | Git コマンドを手動で実行 | ✅ `/branch` ワークフロー | +| セッションの作成・切り替え | アクティブな Claude セッションのみ | ✅ `/new`、`/switch`、`/current`、`/compact` | +| 既存のローカル CLI セッションを再開 | ❌ | ✅ | +| デバイス間でセッションを継続 | 制限あり | ✅ | +| ワークスペースの同時編集を防止 | ❌ | ✅ 複数のエージェントが同じプロジェクトを同時に編集することを防止 | +| エージェントがビジー時のタスクキュー | ❌ | ✅ | +| 独立したファイルシステムスナップショットと差分取得 | ❌ | ✅ | +| 構造化されたファイル差分を Telegram 上で表示 | ❌ | ✅ | +| シークレットや機密情報を含む差分のフィルタリング | ❌ | ✅ | +| Git ワークフロー内蔵(pull / push / commit) | 手動 | ✅ | +| 複数 Bot のサポート | 複数インスタンスが必要 | ✅ 1 台のサーバーで一元管理 | +| プロジェクト単位の状態管理 | ❌ | ✅ | +| プロバイダーに依存しないアーキテクチャ | ❌ | ✅ | + +> **主な違い** +> +> 公式の Claude Code Telegram Plugin は、Telegram を **すでに実行中の 1 つの Claude Code セッション** に接続します。 +> +> **coding-agent-telegram** は **Telegram コントロールプレーン** として機能し、単一のインターフェースからプロジェクト、ブランチ、セッション、Git ワークフロー、さらに複数のコーディングエージェント(Claude Code、Codex CLI、GitHub Copilot CLI)を管理できます。 + +### 🏛️ アーキテクチャ + +#### Claude Code + Telegram Plugin + +```text +Telegram + │ + ▼ +Claude Code Channel + │ + ▼ +実行中の Claude Code セッション +``` + +#### coding-agent-telegram + +```text +Telegram + │ + ▼ +coding-agent-telegram + │ + ├── Claude Code + ├── Codex CLI + └── GitHub Copilot CLI + │ + ▼ +プロジェクト • ブランチ • セッション • キュー • Git • 差分 • シークレットフィルター +``` ## 🚀 クイックスタート @@ -203,7 +262,7 @@ https://api.telegram.org/bot/getUpdates - テキストメッセージ - 写真 - `ENABLE_OPENAI_WHISPER_SPEECH_TO_TEXT=true` が設定され、ローカル Whisper の前提条件がインストールされている場合の音声メッセージと音声ファイル -- Codex と Copilot は現在、テキストと画像のみをサポートしており、動画はサポートしていません +- Codex と Claude Code のセッションはテキストと画像に対応していますが、Copilot のセッションは現在テキストのみに対応しています。動画はどのプロバイダーもサポートしていません ## 🤖 Telegram コマンド @@ -234,7 +293,7 @@ https://api.telegram.org/bot/getUpdates /switch - 最新のセッションを新しい順で表示します。現在のプロジェクトに対する bot 管理セッションとローカルの Codex/Copilot CLI セッションの両方を含みます。 + 最新のセッションを新しい順で表示します。現在のプロジェクトに対する bot 管理セッションとローカルの Codex/Copilot/Claude Code CLI セッションの両方を含みます。 /switch page <number> @@ -313,13 +372,21 @@ https://api.telegram.org/bot/getUpdates APP_LOCALE 共有 bot メッセージとコマンド説明の UI 言語です。対応値: en, de, fr, ja, ko, nl, th, vi, zh-CN, zh-HK, zh-TW. + + DEFAULT_AGENT_PROVIDER + 新しいセッションの既定プロバイダーです: codex, copilot, claude のいずれか。既定値: codex. + CODEX_BIN - Codex CLI を起動するコマンドです。既定値: codex. + Codex CLI を起動するコマンドです。アプリは .env_coding_agent_telegram の初期化時に、ローカルにインストールされている Codex のパスを自動的に検出します。別の方法として、which codex を使用してパスを確認できます。 COPILOT_BIN - Copilot CLI を起動するコマンドです。既定値: copilot. + Copilot CLI を起動するコマンドです。アプリは .env_coding_agent_telegram の初期化時に、ローカルにインストールされている Copilot のパスを自動的に検出します。別の方法として、which copilot を使用してパスを確認できます。 + + + CLAUDE_BIN + Claude Code CLI を起動するコマンドです。アプリは .env_coding_agent_telegram の初期化時に、ローカルにインストールされている Claude のパスを自動的に検出します。別の方法として、which claude を使用してパスを確認できます。 CODEX_MODEL @@ -329,6 +396,14 @@ https://api.telegram.org/bot/getUpdates COPILOT_MODEL Copilot モデルの任意上書きです。空欄なら Copilot CLI の既定モデルを使います。例: gpt-5.4, claude-sonnet-4.6 GitHub Copilot supported models + + CLAUDE_MODEL + Claude Code モデルの任意上書きです。 + 空欄なら Claude Code CLI の既定モデルを使います。 + 例: sonnet, opus, haiku + Claude Code model configuration + + CODEX_APPROVAL_POLICY Codex に渡す approval mode。既定: never. @@ -341,6 +416,18 @@ https://api.telegram.org/bot/getUpdates CODEX_SKIP_GIT_REPO_CHECK 有効にすると Codex の trusted-repo check を常にスキップします。 + + CLAUDE_PERMISSION_MODE + Claude Code に渡す permission mode です。次のいずれか: default, acceptEdits, plan, auto, dontAsk, bypassPermissions, manual。既定値: bypassPermissions(対話的な terminal で prompt を承認できないため、完全に自律動作します)。 + + + CLAUDE_ALLOWED_TOOLS + Claude Code の permission rule 構文を使った、カンマ区切りの tool 許可リストです。例: Read,Edit,Bash(git *) + + + CLAUDE_DISALLOWED_TOOLS + カンマ区切りの Claude Code tool 拒否リストです。例: Bash(rm *) + ENABLE_COMMIT_COMMAND Telegram の /commit コマンドを有効にします。既定: false. @@ -420,8 +507,10 @@ ALLOWED_CHAT_IDS=123456789 DEFAULT_AGENT_PROVIDER=codex CODEX_BIN=codex COPILOT_BIN=copilot +CLAUDE_BIN=claude CODEX_APPROVAL_POLICY=never CODEX_SANDBOX_MODE=workspace-write +CLAUDE_PERMISSION_MODE=bypassPermissions ENABLE_SENSITIVE_DIFF_FILTER=true ENABLE_SECRET_SCRUB_FILTER=true ``` @@ -485,7 +574,7 @@ lock はディスクではなくメモリ上に保持されるため、agent 完 ## ⚠️ Diff(ファイル変更) -_各 エージェント実行 のたびに、bot はプロジェクトの軽量な 実行前後のスナップショット も取得し、変更ファイルの要約と diff を Telegram に送ります。この スナップショット は Codex や Copilot ではなく、bot 自身が作成します。_ +_各 エージェント実行 のたびに、bot はプロジェクトの軽量な 実行前後のスナップショット も取得し、変更ファイルの要約と diff を Telegram に送ります。この スナップショット は Codex や Copilot、Claude Code ではなく、bot 自身が作成します。_ **Snapshot のポイント:** diff --git a/README.ko.md b/README.ko.md index 2143d8c..014cad4 100644 --- a/README.ko.md +++ b/README.ko.md @@ -35,14 +35,14 @@ ## ✨ 왜 써야 하나요 - ✅ 가벼움: 무거운 프레임워크 없이 투명한 동작 - ✅ 멀티 봇: 여러 채팅, 여러 세션 지원 - - ✅ Telegram 으로 Codex / Copilot CLI 를 제어 + - ✅ Telegram 으로 Codex / Copilot / Claude Code CLI 를 제어 - ✅ 에이전트 응답과 변경 파일을 코드 블록으로 쉽게 검토 - ✅ 에이전트가 작업 중일 때도 후속 질문을 큐에 저장 - ✅ ✏️ 텍스트, 🌄 이미지, 🎙️ 음성 메시지 지원 ## 🔁 기기/세션 간 자연스러운 전환 - Telegram에서 시작한 세션을 나중에 컴퓨터에서 같은 Codex/Copilot CLI 세션으로 이어서 작업할 수 있습니다. `/switch`만으로 Telegram과 터미널 사이를 자연스럽게 오갈 수 있습니다. + Telegram에서 시작한 세션을 나중에 컴퓨터에서 같은 Codex/Copilot/Claude Code CLI 세션으로 이어서 작업할 수 있습니다. `/switch`만으로 Telegram과 터미널 사이를 자연스럽게 오갈 수 있습니다. - `/switch`로 로컬 세션 이어서 작업 - 이전 세션도 지원 @@ -86,6 +86,7 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - 기존 폴더는 변경성 Git operation 전에 trust 확인을 요구할 수 있음 - 서버는 숨겨진 외부 호출을 하지 않습니다. 모든 제어권은 사용자에게 있습니다. - Codex Sandbox mode 와 잘 동작하며 `danger-full-access` 를 허용할 필요가 없습니다 +- Claude Code 통합은 설정 가능한 permission mode 와 tool allow/deny 목록을 지원합니다 @@ -96,16 +97,76 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - Python 3.9 이상 - _@BotFather_ 에서 만든 Telegram bot token - 자신의 Telegram chat ID -- 로컬에 설치된 Codex CLI 및/또는 Copilot CLI +- 로컬에 설치된 Codex CLI 및/또는 Copilot CLI 및/또는 Claude Code CLI - [Codex CLI 설치](https://developers.openai.com/codex/cli) - [Copilot CLI 설치](https://github.com/features/copilot/cli) +- [Claude Code CLI 설치](https://code.claude.com/docs/en/quickstart) - [선택 사항] Whisper, ffmpeg ## 🦞 이미 Openclaw 가 있는데 왜 이것이 필요한가요? -Openclaw 는 매우 다양한 기능을 제공하고 Pi-Agent 라는 통합 agent loop 도 갖추고 있습니다. 더 폭넓은 use case 를 위한 상당히 종합적인 도구입니다. 저도 Openclaw 를 좋아하고 예전에는 Openclaw 로 coding 했습니다. 하지만 coding 에 한정하면, 내장된 큰 system prompt 와 추가 context 때문에 항상 최선의 선택은 아닙니다. Claude Code / Codex / Copilot 는 coding 에서 더 효율적이고, 더 정확하며, 덜 산만하고, 더 직접적입니다. 이 project 는 의도적으로 단순하며 Codex / Copilot CLI 만 통합합니다. 즉, Codex / Copilot 에 직접 일을 맡길 수 있습니다. +Openclaw 는 매우 다양한 기능을 제공하고 Pi-Agent 라는 통합 agent loop 도 갖추고 있습니다. 더 폭넓은 use case 를 위한 상당히 종합적인 도구입니다. 저도 Openclaw 를 좋아하고 예전에는 Openclaw 로 coding 했습니다. 하지만 coding 에 한정하면, 내장된 큰 system prompt 와 추가 context 때문에 항상 최선의 선택은 아닙니다. Claude Code / Codex / Copilot 는 coding 에서 더 효율적이고, 더 정확하며, 덜 산만하고, 더 직접적입니다. 이 project 는 의도적으로 단순하며 Codex / Copilot / Claude Code CLI 만 통합합니다. 즉, Codex / Copilot / Claude Code 에 직접 일을 맡길 수 있습니다. + +## 🆚 이미 Claude Code + Telegram Plugin이 있는데, 왜 coding-agent-telegram이 필요한가요? + +| 기능 | Claude Code + 공식 Telegram Plugin | coding-agent-telegram (Claude 지원) | +|------|------------------------------------|--------------------------------------| +| Telegram에서 AI와 채팅 | ✅ | ✅ | +| 로컬 코드 편집 및 명령 실행 | ✅ | ✅ | +| 이미 실행 중인 CLI 세션 필요 | **예** | **아니요** (세션을 자동으로 시작하거나 이어서 사용) | +| 여러 AI 제공업체 지원 | ❌ Claude만 지원 | ✅ Claude Code, Codex CLI, GitHub Copilot CLI | +| Telegram에서 프로젝트 관리 | ❌ | ✅ `/project` | +| Telegram에서 브랜치 관리 | Git 명령을 수동으로 실행 | ✅ `/branch` 워크플로 | +| 세션 생성 및 전환 | 현재 활성화된 Claude 세션으로 제한 | ✅ `/new`, `/switch`, `/current`, `/compact` | +| 기존 로컬 CLI 세션 이어서 사용 | ❌ | ✅ | +| 여러 기기에서 세션 연속성 | 제한적 | ✅ | +| 동일한 워크스페이스에 대한 동시 작업 방지 | ❌ | ✅ 여러 에이전트가 동일한 프로젝트를 동시에 수정하지 못하도록 방지 | +| 에이전트가 작업 중일 때 작업 대기열 | ❌ | ✅ | +| 독립적인 파일 시스템 스냅샷 및 Diff | ❌ | ✅ | +| Telegram에서 구조화된 파일 Diff 보기 | ❌ | ✅ | +| Secret 및 민감한 Diff 필터링 | ❌ | ✅ | +| 내장 Git 워크플로 (pull / push / commit) | 수동 | ✅ | +| 여러 Bot 지원 | 여러 인스턴스 필요 | ✅ 단일 서버에서 관리 | +| 프로젝트 단위 상태 관리 | ❌ | ✅ | +| Provider에 독립적인 아키텍처 | ❌ | ✅ | + +> **핵심 차이점** +> +> 공식 Claude Code Telegram Plugin은 Telegram을 **이미 실행 중인 하나의 Claude Code 세션**에 연결합니다. +> +> **coding-agent-telegram**은 **Telegram 제어 플랫폼(Control Plane)** 역할을 하며, 하나의 인터페이스에서 프로젝트, 브랜치, 세션, Git 워크플로 및 여러 코딩 에이전트(Claude Code, Codex CLI, GitHub Copilot CLI)를 관리할 수 있습니다. + +### 🏛️ 아키텍처 + +#### Claude Code + Telegram Plugin + +```text +Telegram + │ + ▼ +Claude Code Channel + │ + ▼ +실행 중인 Claude Code 세션 +``` + +#### coding-agent-telegram + +```text +Telegram + │ + ▼ +coding-agent-telegram + │ + ├── Claude Code + ├── Codex CLI + └── GitHub Copilot CLI + │ + ▼ +프로젝트 • 브랜치 • 세션 • 대기열 • Git • Diff • Secret 필터 +``` ## 🚀 빠른 시작 @@ -203,7 +264,7 @@ https://api.telegram.org/bot/getUpdates - 텍스트 메시지 - 사진 - `ENABLE_OPENAI_WHISPER_SPEECH_TO_TEXT=true` 로 설정되어 있고 로컬 Whisper 전제 조건이 설치된 경우의 음성 메시지와 오디오 파일 -- Codex 와 Copilot 은 현재 텍스트와 이미지만 지원하며, 비디오는 지원하지 않습니다 +- Codex 와 Claude Code 세션은 텍스트와 이미지를 지원하며, Copilot 세션은 현재 텍스트만 지원합니다. 어떤 제공자도 비디오는 지원하지 않습니다 ## 🤖 Telegram 명령어 @@ -234,7 +295,7 @@ https://api.telegram.org/bot/getUpdates /switch - 가장 최근 세션을 최신순으로 보여줍니다. 현재 프로젝트의 bot 관리 세션과 로컬 Codex/Copilot CLI 세션이 함께 표시됩니다. + 가장 최근 세션을 최신순으로 보여줍니다. 현재 프로젝트의 bot 관리 세션과 로컬 Codex/Copilot/Claude Code CLI 세션이 함께 표시됩니다. /switch page <number> @@ -313,13 +374,21 @@ https://api.telegram.org/bot/getUpdates APP_LOCALE 공용 bot 메시지와 명령 설명에 사용할 UI locale 입니다. 지원 값: en, de, fr, ja, ko, nl, th, vi, zh-CN, zh-HK, zh-TW. + + DEFAULT_AGENT_PROVIDER + 새 세션의 기본 제공자입니다: codex, copilot, 또는 claude. 기본값: codex. + CODEX_BIN - Codex CLI 를 실행할 명령입니다. 기본값: codex. + Codex CLI 를 실행할 명령입니다. 앱은 .env_coding_agent_telegram를 초기화할 때 로컬에 설치된 Codex 경로를 자동으로 감지합니다. 또는 which codex를 사용하여 경로를 확인할 수 있습니다. COPILOT_BIN - Copilot CLI 를 실행할 명령입니다. 기본값: copilot. + Copilot CLI 를 실행할 명령입니다. 앱은 .env_coding_agent_telegram를 초기화할 때 로컬에 설치된 Copilot 경로를 자동으로 감지합니다. 또는 which copilot를 사용하여 경로를 확인할 수 있습니다. + + + CLAUDE_BIN + Claude Code CLI 를 실행할 명령입니다. 앱은 .env_coding_agent_telegram를 초기화할 때 로컬에 설치된 Claude 경로를 자동으로 감지합니다. 또는 which claude를 사용하여 경로를 확인할 수 있습니다. CODEX_MODEL @@ -329,6 +398,10 @@ https://api.telegram.org/bot/getUpdates COPILOT_MODEL 선택적 Copilot model override 입니다. 비워 두면 Copilot CLI 기본 model 을 사용합니다. 예: gpt-5.4, claude-sonnet-4.6 GitHub Copilot supported models + + CLAUDE_MODEL + 선택적 Claude Code model override 입니다. 비워 두면 Claude Code CLI 기본 model 을 사용합니다. 예: sonnet, opus, haiku Claude Code model configuration + CODEX_APPROVAL_POLICY Codex 에 전달할 approval mode 입니다. 기본값: never. @@ -341,6 +414,18 @@ https://api.telegram.org/bot/getUpdates CODEX_SKIP_GIT_REPO_CHECK 활성화하면 Codex trusted-repo check 를 항상 건너뜁니다. + + CLAUDE_PERMISSION_MODE + Claude Code 에 전달할 permission mode 입니다. 다음 중 하나: default, acceptEdits, plan, auto, dontAsk, bypassPermissions, manual. 기본값: bypassPermissions (대화형 터미널이 없어 prompt 를 승인할 수 없으므로 완전 자동 실행됩니다). + + + CLAUDE_ALLOWED_TOOLS + Claude Code 의 permission rule 문법을 사용하는, 쉼표로 구분된 tool allowlist 입니다. 예: Read,Edit,Bash(git *) + + + CLAUDE_DISALLOWED_TOOLS + 쉼표로 구분된 Claude Code tool denylist 입니다. 예: Bash(rm *) + ENABLE_COMMIT_COMMAND Telegram /commit 명령을 활성화합니다. 기본값: false. @@ -420,8 +505,10 @@ ALLOWED_CHAT_IDS=123456789 DEFAULT_AGENT_PROVIDER=codex CODEX_BIN=codex COPILOT_BIN=copilot +CLAUDE_BIN=claude CODEX_APPROVAL_POLICY=never CODEX_SANDBOX_MODE=workspace-write +CLAUDE_PERMISSION_MODE=bypassPermissions ENABLE_SENSITIVE_DIFF_FILTER=true ENABLE_SECRET_SCRUB_FILTER=true ``` @@ -485,7 +572,7 @@ lock 은 디스크가 아니라 메모리에만 유지되므로 agent 가 끝나 ## ⚠️ Diff (파일 변경) -_각 에이전트 실행 동안 bot 은 project 의 가벼운 실행 전후 스냅샷 도 만들어 변경 파일 요약과 diff 를 Telegram 으로 보낼 수 있게 합니다. 이 스냅샷 은 Codex 나 Copilot 이 아니라 bot 앱 자체가 만듭니다._ +_각 에이전트 실행 동안 bot 은 project 의 가벼운 실행 전후 스냅샷 도 만들어 변경 파일 요약과 diff 를 Telegram 으로 보낼 수 있게 합니다. 이 스냅샷 은 Codex, Copilot, Claude Code 가 아니라 bot 앱 자체가 만듭니다._ **Snapshot 참고 사항:** diff --git a/README.md b/README.md index 9cdd06b..e9b8097 100644 --- a/README.md +++ b/README.md @@ -35,14 +35,14 @@ ## ✨ Why Use It - ✅ Lightweight: no heavy frameworks, full transparency - ✅ Multi-bot: multiple chats, multiple sessions - - ✅ Use Telegram to control Codex / Copilot CLI + - ✅ Use Telegram to control Codex / Copilot / Claude Code CLI - ✅ Easily review files changed by agent in code block - ✅ Queue follow-up messages while the agent is working - ✅ Accept ✏️ Text, 🌄 Image, and 🎙️ Voice messages ## 🔁 Seamless Device/Session Switching - Start a session on Telegram, later on you can still continue the same Codex/Copilot CLI session on your computer, and switch back again without hassle. + Start a session on Telegram, later on you can still continue the same Codex/Copilot/Claude Code CLI session on your computer, and switch back again without hassle. - Use `/switch` in Telegram to continue a local session - Support historical sessions @@ -86,6 +86,7 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - Existing folders can require trust before mutating git operations - The server does not make hidden external calls. Everything stays under your control. - Works well with Codex Sandbox mode (you don't have to grant `danger-full-access`) + - Claude Code integration supports configurable permission modes and tool allow/deny lists @@ -96,15 +97,74 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - Python 3.9 or newer - Telegram bot token created from _@BotFather_ - Your Telegram chat ID - - Codex CLI and/or Copilot CLI installed locally - - [Codex CLI install](https://developers.openai.com/codex/cli) / [Copilot CLI install](https://github.com/features/copilot/cli) + - Codex CLI and/or Copilot CLI and/or Claude Code CLI installed locally + - [Codex CLI install](https://developers.openai.com/codex/cli) / [Copilot CLI install](https://github.com/features/copilot/cli) / [Claude Code CLI install](https://code.claude.com/docs/en/quickstart) - [Optional] `Whisper`, `ffmpeg` ## 🦞 Why do I need it if I have Openclaw already? -Openclaw offers you full capabilities and has integrated agent loop called Pi-Agent. It's quite comprehensive and designed for more diversified use cases. I am also Openclaw lover and I used to code with Openclaw. However, that is not the best choice for coding due to the built in large system prompt and context. Using Claude Code / Codex / Copilot for coding is still more effecient, accurate, less distracted, and straightforward. This project is quite simple, purely integrating with Codex / Copilot CLI. So you are directly delegating Codex / Copilot to work for you. +Openclaw offers you full capabilities and has integrated agent loop called Pi-Agent. It's quite comprehensive and designed for more diversified use cases. I am also Openclaw lover and I used to code with Openclaw. However, that is not the best choice for coding due to the built in large system prompt and context. Using Claude Code / Codex / Copilot for coding is still more effecient, accurate, less distracted, and straightforward. This project is quite simple, purely integrating with Codex / Copilot / Claude Code CLI. So you are directly delegating Codex / Copilot / Claude Code to work for you. + +## 🆚 Why do I need it if I have Claude Code + Telegram Plugin already? + +| Capability | Claude Code + Official Telegram Plugin | coding-agent-telegram (with Claude support) | +|------------|-----------------------------------------|---------------------------------------------| +| Telegram chat with AI | ✅ | ✅ | +| Edit local code and run commands | ✅ | ✅ | +| Requires an already-running CLI session | **Yes** | **No** (automatically starts or resumes sessions) | +| Multiple AI providers | ❌ Claude only | ✅ Claude Code, Codex CLI, GitHub Copilot CLI | +| Project management from Telegram | ❌ | ✅ `/project` | +| Branch management from Telegram | Manual Git commands | ✅ `/branch` workflow | +| Create and switch sessions | Limited to active Claude session | ✅ `/new`, `/switch`, `/current`, `/compact` | +| Resume existing local CLI sessions | ❌ | ✅ | +| Cross-device session continuity | Limited | ✅ | +| Workspace concurrency protection | ❌ | ✅ Prevents multiple agents from editing the same project simultaneously | +| Task queue while an agent is busy | ❌ | ✅ | +| Independent filesystem snapshot & diff | ❌ | ✅ | +| View structured file diffs in Telegram | ❌ | ✅ | +| Secret / sensitive diff filtering | ❌ | ✅ | +| Built-in Git workflow (pull / push / commit) | Manual | ✅ | +| Multi-bot support | Multiple instances required | ✅ Managed by a single server | +| Project-level state management | ❌ | ✅ | +| Provider-agnostic architecture | ❌ | ✅ | + +> **Key difference** +> +> The official Claude Code Telegram plugin connects Telegram to **one already-running Claude Code session**. +> +> **coding-agent-telegram** acts as a **Telegram control plane** that manages projects, branches, sessions, Git workflows, and multiple coding agents (Claude Code, Codex CLI, GitHub Copilot CLI) from a single interface. + +### 🏛️ Architecture + +#### Claude Code + Telegram Plugin + +```text +Telegram + │ + ▼ +Claude Code Channel + │ + ▼ +One running Claude Code session +``` + +#### coding-agent-telegram + +```text +Telegram + │ + ▼ +coding-agent-telegram + │ + ├── Claude Code + ├── Codex CLI + └── GitHub Copilot CLI + │ + ▼ +Project • Branch • Session • Queue • Git • Diff • Secret Filter +``` ## 🚀 Quick Start @@ -214,7 +274,7 @@ The bot currently accepts: - Text messages - photos - voice messages when `ENABLE_OPENAI_WHISPER_SPEECH_TO_TEXT=true` and local Whisper prerequisites are installed -- Codex and Copilot currently supports text and image only, video is not supported. +- Codex and Claude Code sessions support text and image; Copilot sessions currently support text only. Video is not supported by any provider. ## 🤖 Telegram Commands @@ -245,7 +305,7 @@ The bot currently accepts: /switch - Show the latest sessions, newest first. The list includes both bot-managed sessions and local Codex/Copilot CLI sessions for the current project. + Show the latest sessions, newest first. The list includes both bot-managed sessions and local Codex/Copilot/Claude Code CLI sessions for the current project. /switch page <number> @@ -323,13 +383,21 @@ The bot currently accepts: APP_LOCALE UI locale for shared bot messages and command descriptions. Supported values: en, de, fr, ja, ko, nl, th, vi, zh-CN, zh-HK, zh-TW. + + DEFAULT_AGENT_PROVIDER + Default provider for new sessions: codex, copilot, or claude. Default: codex. + CODEX_BIN - Command used to launch Codex CLI. Default: codex. + Command used to launch Codex CLI. The app would try to detect the locally installed Codex path when initializing the .env_coding_agent_telegram. Alternatively use which codex to view the path. COPILOT_BIN - Command used to launch Copilot CLI. Default: copilot. + Command used to launch Copilot CLI. The app would try to detect the locally installed Copilot path when initializing the .env_coding_agent_telegram. Alternatively use which copilot to view the path. + + + CLAUDE_BIN + Command used to launch Claude Code CLI. The app would try to detect the locally installed Claude path when initializing the .env_coding_agent_telegram. Alternatively use which claude to view the path. CODEX_MODEL @@ -347,6 +415,14 @@ The bot currently accepts: GitHub Copilot supported models + + CLAUDE_MODEL + Optional Claude Code model override. + Leave empty to use the Claude Code CLI default model. + Examples: sonnet, opus, haiku + Claude Code model configuration + + CODEX_APPROVAL_POLICY Approval mode passed to Codex. Default: never. @@ -359,6 +435,18 @@ The bot currently accepts: CODEX_SKIP_GIT_REPO_CHECK If enabled, always bypass Codex trusted-repo checks. + + CLAUDE_PERMISSION_MODE + Permission mode passed to Claude Code. One of default, acceptEdits, plan, auto, dontAsk, bypassPermissions, manual. Default: bypassPermissions (fully autonomous, since there is no interactive terminal to approve prompts). + + + CLAUDE_ALLOWED_TOOLS + Comma-separated Claude Code tool allowlist, using Claude Code's permission rule syntax. Example: Read,Edit,Bash(git *) + + + CLAUDE_DISALLOWED_TOOLS + Comma-separated Claude Code tool denylist. Example: Bash(rm *) + ENABLE_COMMIT_COMMAND Enable the /commit Telegram command. Default: false. @@ -440,8 +528,10 @@ ALLOWED_CHAT_IDS=123456789 DEFAULT_AGENT_PROVIDER=codex CODEX_BIN=codex COPILOT_BIN=copilot +CLAUDE_BIN=claude CODEX_APPROVAL_POLICY=never CODEX_SANDBOX_MODE=workspace-write +CLAUDE_PERMISSION_MODE=bypassPermissions ENABLE_SENSITIVE_DIFF_FILTER=true ENABLE_SECRET_SCRUB_FILTER=true ``` @@ -507,7 +597,7 @@ If the current run is aborted and there are queued questions waiting, the bot do ## ⚠️ Diff (file changes) -_During each agent run, the bot also takes a lightweight before/after project snapshot so it can summarize changed files and send diffs back to Telegram. This snapshot is taken by the bot app itself, not by Codex or Copilot._ +_During each agent run, the bot also takes a lightweight before/after project snapshot so it can summarize changed files and send diffs back to Telegram. This snapshot is taken by the bot app itself, not by Codex, Copilot, or Claude Code._ **Snapshot notes:** diff --git a/README.nl.md b/README.nl.md index 887bb7a..74de966 100644 --- a/README.nl.md +++ b/README.nl.md @@ -35,14 +35,14 @@ ## ✨ Waarom dit project - ✅ Lichtgewicht: geen zware frameworks, volledig transparant - ✅ Multi-bot: meerdere chats, meerdere sessies - - ✅ Gebruik Telegram om Codex / Copilot CLI te bedienen + - ✅ Gebruik Telegram om Codex / Copilot / Claude Code CLI te bedienen - ✅ Antwoorden en gewijzigde bestanden eenvoudig beoordelen in codeblokken - ✅ Vervolgvragen kunnen in de wachtrij terwijl de agent werkt - ✅ Accepteert ✏️ tekst-, 🌄 afbeelding- en 🎙️ spraakberichten ## 🔁 Naadloos wisselen tussen apparaten en sessies - Start een sessie in Telegram en ga later verder met dezelfde Codex/Copilot CLI-sessie op je computer, zonder gedoe. Met `/switch` kun je ook eenvoudig van Telegram terug naar de command line schakelen. + Start een sessie in Telegram en ga later verder met dezelfde Codex/Copilot/Claude Code CLI-sessie op je computer, zonder gedoe. Met `/switch` kun je ook eenvoudig van Telegram terug naar de command line schakelen. - Gebruik `/switch` om een lokale sessie voort te zetten - Historische sessies worden ook ondersteund @@ -86,6 +86,7 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - Bestaande mappen kunnen een trust-bevestiging vereisen vóór muterende Git-bewerkingen - De server doet geen verborgen externe calls. Jij houdt de controle. - Werkt goed met de Codex Sandbox mode; je hoeft `danger-full-access` niet toe te staan +- De Claude Code-integratie ondersteunt configureerbare permission modes en allow/deny-lijsten voor tools @@ -96,16 +97,74 @@ Voordat je de server start, zorg dat je hebt: - Python 3.9 of nieuwer - Een Telegram bot token van _@BotFather_ - Je Telegram chat ID -- Codex CLI en/of Copilot CLI lokaal geïnstalleerd -- [Codex CLI installatie](https://developers.openai.com/codex/cli) -- [Copilot CLI installatie](https://github.com/features/copilot/cli) +- Codex CLI en/of Copilot CLI en/of Claude Code CLI lokaal geïnstalleerd +- [Codex CLI installatie](https://developers.openai.com/codex/cli) / [Copilot CLI installatie](https://github.com/features/copilot/cli) / [Claude Code CLI installatie](https://code.claude.com/docs/en/quickstart) - [Optioneel] Whisper, ffmpeg ## 🦞 Waarom heb ik dit nodig als ik Openclaw al heb? -Openclaw biedt zeer uitgebreide mogelijkheden en heeft al een geïntegreerde agent-loop genaamd Pi-Agent. Het is behoorlijk compleet en bedoeld voor bredere use cases. Ik ben zelf ook fan van Openclaw en heb er eerder mee gecodeerd. Voor coding is het echter niet altijd de beste keuze, door de ingebouwde grote system prompt en extra context. Claude Code / Codex / Copilot zijn voor coding vaak efficiënter, nauwkeuriger, minder afleidend en directer. Dit project blijft bewust eenvoudig en integreert alleen Codex / Copilot CLI. Je delegeert het werk dus rechtstreeks aan Codex / Copilot. +Openclaw biedt zeer uitgebreide mogelijkheden en heeft al een geïntegreerde agent-loop genaamd Pi-Agent. Het is behoorlijk compleet en bedoeld voor bredere use cases. Ik ben zelf ook fan van Openclaw en heb er eerder mee gecodeerd. Voor coding is het echter niet altijd de beste keuze, door de ingebouwde grote system prompt en extra context. Claude Code / Codex / Copilot zijn voor coding vaak efficiënter, nauwkeuriger, minder afleidend en directer. Dit project blijft bewust eenvoudig en integreert alleen Codex / Copilot / Claude Code CLI. Je delegeert het werk dus rechtstreeks aan Codex / Copilot / Claude Code. + +## 🆚 Waarom heb ik coding-agent-telegram nodig als ik Claude Code + Telegram Plugin al heb? + +| Mogelijkheid | Claude Code + officiële Telegram Plugin | coding-agent-telegram (met Claude-ondersteuning) | +|--------------|------------------------------------------|--------------------------------------------------| +| Chatten met AI via Telegram | ✅ | ✅ | +| Lokale code bewerken en opdrachten uitvoeren | ✅ | ✅ | +| Vereist een reeds actieve CLI-sessie | **Ja** | **Nee** (start of hervat sessies automatisch) | +| Ondersteuning voor meerdere AI-providers | ❌ Alleen Claude | ✅ Claude Code, Codex CLI, GitHub Copilot CLI | +| Projectbeheer via Telegram | ❌ | ✅ `/project` | +| Branchbeheer via Telegram | Handmatige Git-opdrachten | ✅ `/branch`-workflow | +| Sessies aanmaken en wisselen | Beperkt tot de actieve Claude-sessie | ✅ `/new`, `/switch`, `/current`, `/compact` | +| Bestaande lokale CLI-sessies hervatten | ❌ | ✅ | +| Sessies naadloos voortzetten tussen apparaten | Beperkt | ✅ | +| Bescherming tegen gelijktijdige wijzigingen in dezelfde workspace | ❌ | ✅ Voorkomt dat meerdere agents tegelijkertijd hetzelfde project wijzigen | +| Taakwachtrij wanneer een agent bezig is | ❌ | ✅ | +| Onafhankelijke bestandssnapshots en diffs | ❌ | ✅ | +| Gestructureerde bestandsdiffs bekijken in Telegram | ❌ | ✅ | +| Filteren van secrets en gevoelige diffs | ❌ | ✅ | +| Ingebouwde Git-workflow (pull / push / commit) | Handmatig | ✅ | +| Ondersteuning voor meerdere bots | Meerdere instanties vereist | ✅ Beheerd door één server | +| Statusbeheer op projectniveau | ❌ | ✅ | +| Provider-onafhankelijke architectuur | ❌ | ✅ | + +> **Belangrijkste verschil** +> +> De officiële Claude Code Telegram Plugin verbindt Telegram met **één reeds actieve Claude Code-sessie**. +> +> **coding-agent-telegram** fungeert als een **Telegram control plane** waarmee je projecten, branches, sessies, Git-workflows en meerdere coding agents (Claude Code, Codex CLI en GitHub Copilot CLI) vanuit één enkele interface kunt beheren. + +### 🏛️ Architectuur + +#### Claude Code + Telegram Plugin + +```text +Telegram + │ + ▼ +Claude Code Channel + │ + ▼ +Eén actieve Claude Code-sessie +``` + +#### coding-agent-telegram + +```text +Telegram + │ + ▼ +coding-agent-telegram + │ + ├── Claude Code + ├── Codex CLI + └── GitHub Copilot CLI + │ + ▼ +Project • Branch • Sessie • Wachtrij • Git • Diff • Secretfilter +``` ## 🚀 Snel starten @@ -203,7 +262,7 @@ De bot accepteert momenteel: - tekstberichten - foto’s - spraakberichten en audiobestanden wanneer `ENABLE_OPENAI_WHISPER_SPEECH_TO_TEXT=true` is ingesteld en de lokale Whisper-vereisten zijn geïnstalleerd -- Codex en Copilot ondersteunen momenteel alleen tekst en afbeeldingen, geen video +- Codex- en Claude Code-sessies ondersteunen tekst en afbeeldingen; Copilot-sessies ondersteunen momenteel alleen tekst. Video wordt door geen enkele aanbieder ondersteund ## 🤖 Telegram-commando's @@ -234,7 +293,7 @@ De bot accepteert momenteel: /switch - Toon de nieuwste sessies, nieuwste eerst. De lijst bevat zowel bot-managed sessies als lokale Codex/Copilot CLI-sessies voor het huidige project. + Toon de nieuwste sessies, nieuwste eerst. De lijst bevat zowel bot-managed sessies als lokale Codex/Copilot/Claude Code CLI-sessies voor het huidige project. /switch page <number> @@ -313,13 +372,21 @@ De bot accepteert momenteel: APP_LOCALE UI-locale voor gedeelde botmeldingen en commandobeschrijvingen. Ondersteunde waarden: en, de, fr, ja, ko, nl, th, vi, zh-CN, zh-HK, zh-TW. + + DEFAULT_AGENT_PROVIDER + Standaardaanbieder voor nieuwe sessies: codex, copilot of claude. Standaard: codex. + CODEX_BIN - Commando om Codex CLI te starten. Standaard: codex. + Commando om Codex CLI te starten. De applicatie probeert tijdens het initialiseren van .env_coding_agent_telegram automatisch het lokaal geïnstalleerde Codex-pad te detecteren. U kunt ook which codex gebruiken om het pad weer te geven. COPILOT_BIN - Commando om Copilot CLI te starten. Standaard: copilot. + Commando om Copilot CLI te starten. De applicatie probeert tijdens het initialiseren van .env_coding_agent_telegram automatisch het lokaal geïnstalleerde Copilot-pad te detecteren. U kunt ook which copilot gebruiken om het pad weer te geven. + + + CLAUDE_BIN + Commando om Claude Code CLI te starten. De applicatie probeert tijdens het initialiseren van .env_coding_agent_telegram automatisch het lokaal geïnstalleerde Claude-pad te detecteren. U kunt ook which claude gebruiken om het pad weer te geven. CODEX_MODEL @@ -329,6 +396,14 @@ De bot accepteert momenteel: COPILOT_MODEL Optionele Copilot-modeloverride. Laat leeg om het standaardmodel van Copilot CLI te gebruiken. Voorbeelden: gpt-5.4, claude-sonnet-4.6 Ondersteunde GitHub Copilot-modellen + + CLAUDE_MODEL + Optionele Claude Code-modeloverride. + Laat leeg om het standaardmodel van Claude Code CLI te gebruiken. + Voorbeelden: sonnet, opus, haiku + Claude Code-modelconfiguratie + + CODEX_APPROVAL_POLICY Goedkeuringsmodus die aan Codex wordt doorgegeven. Standaard: never. @@ -341,6 +416,18 @@ De bot accepteert momenteel: CODEX_SKIP_GIT_REPO_CHECK Als dit is ingeschakeld, worden trusted-repo-checks van Codex altijd overgeslagen. + + CLAUDE_PERMISSION_MODE + Permission mode die aan Claude Code wordt doorgegeven. Een van default, acceptEdits, plan, auto, dontAsk, bypassPermissions, manual. Standaard: bypassPermissions (volledig autonoom, omdat er geen interactieve terminal is om prompts goed te keuren). + + + CLAUDE_ALLOWED_TOOLS + Door komma's gescheiden allowlist van Claude Code-tools, volgens de permission rule syntax van Claude Code. Voorbeeld: Read,Edit,Bash(git *) + + + CLAUDE_DISALLOWED_TOOLS + Door komma's gescheiden denylist van Claude Code-tools. Voorbeeld: Bash(rm *) + ENABLE_COMMIT_COMMAND Schakelt het Telegram-commando /commit in. Standaard: false. @@ -422,8 +509,10 @@ ALLOWED_CHAT_IDS=123456789 DEFAULT_AGENT_PROVIDER=codex CODEX_BIN=codex COPILOT_BIN=copilot +CLAUDE_BIN=claude CODEX_APPROVAL_POLICY=never CODEX_SANDBOX_MODE=workspace-write +CLAUDE_PERMISSION_MODE=bypassPermissions ENABLE_SENSITIVE_DIFF_FILTER=true ENABLE_SECRET_SCRUB_FILTER=true ``` @@ -487,7 +576,7 @@ Wordt de huidige run afgebroken terwijl er nog vragen wachten, dan gaat de bot n ## ⚠️ Diff (bestandswijzigingen) -_Tijdens elke agent-run maakt de bot ook een lichte voor/na-momentopname van het project, zodat gewijzigde bestanden kunnen worden samengevat en diffs naar Telegram kunnen worden gestuurd. Deze momentopname wordt door de bot-app zelf gemaakt, niet door Codex of Copilot._ +_Tijdens elke agent-run maakt de bot ook een lichte voor/na-momentopname van het project, zodat gewijzigde bestanden kunnen worden samengevat en diffs naar Telegram kunnen worden gestuurd. Deze momentopname wordt door de bot-app zelf gemaakt, niet door Codex, Copilot of Claude Code._ **Snapshot-opmerkingen:** diff --git a/README.th.md b/README.th.md index 0f27168..f6d0370 100644 --- a/README.th.md +++ b/README.th.md @@ -35,14 +35,14 @@ ## ✨ ทำไมถึงควรใช้ - ✅ เบา: ไม่ต้องใช้เฟรมเวิร์กหนัก ๆ และตรวจสอบการทำงานได้ง่าย - ✅ หลายบอต: รองรับหลายแชต หลายเซสชัน - - ✅ ใช้ Telegram เพื่อควบคุม Codex / Copilot CLI + - ✅ ใช้ Telegram เพื่อควบคุม Codex / Copilot / Claude Code CLI - ✅ ตรวจคำตอบและไฟล์ที่ถูกแก้ได้ง่ายใน code block - ✅ ส่งคำถามต่อคิวไว้ได้ระหว่างที่ agent กำลังทำงาน - ✅ รองรับ ✏️ ข้อความ, 🌄 รูปภาพ และ 🎙️ ข้อความเสียง ## 🔁 สลับอุปกรณ์และเซสชันได้ลื่นไหล - เริ่มเซสชันจาก Telegram แล้วค่อยกลับไปทำต่อบนคอมพิวเตอร์ด้วยเซสชัน Codex/Copilot CLI เดิมได้ทันที และใช้ `/switch` เพื่อสลับกลับไปมาระหว่าง Telegram กับ command line ได้อย่างง่ายดาย + เริ่มเซสชันจาก Telegram แล้วค่อยกลับไปทำต่อบนคอมพิวเตอร์ด้วยเซสชัน Codex/Copilot/Claude Code CLI เดิมได้ทันที และใช้ `/switch` เพื่อสลับกลับไปมาระหว่าง Telegram กับ command line ได้อย่างง่ายดาย - ใช้ `/switch` เพื่อทำงานต่อจากเซสชันในเครื่อง - รองรับเซสชันย้อนหลัง @@ -86,6 +86,7 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - โฟลเดอร์ที่มีอยู่เดิมอาจต้องยืนยัน trust ก่อนทำ Git operation ที่มีการแก้ไข - เซิร์ฟเวอร์ไม่มีการเรียกใช้งานภายนอกแบบซ่อน ทุกอย่างยังอยู่ในการควบคุมของคุณ - ทำงานร่วมกับ Codex Sandbox mode ได้ดี โดยไม่ต้องให้สิทธิ์ `danger-full-access` +- การเชื่อมต่อกับ Claude Code รองรับการกำหนด permission mode และรายการ allow/deny ของ tools ได้ @@ -96,16 +97,74 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - Python 3.9 ขึ้นไป - Telegram bot token จาก _@BotFather_ - Telegram chat ID ของคุณ -- ติดตั้ง Codex CLI และ/หรือ Copilot CLI ไว้ในเครื่องแล้ว -- [ติดตั้ง Codex CLI](https://developers.openai.com/codex/cli) -- [ติดตั้ง Copilot CLI](https://github.com/features/copilot/cli) +- ติดตั้ง Codex CLI และ/หรือ Copilot CLI และ/หรือ Claude Code CLI ไว้ในเครื่องแล้ว +- [ติดตั้ง Codex CLI](https://developers.openai.com/codex/cli) / [ติดตั้ง Copilot CLI](https://github.com/features/copilot/cli) / [ติดตั้ง Claude Code CLI](https://code.claude.com/docs/en/quickstart) - [ทางเลือก] Whisper, ffmpeg ## 🦞 ถ้ามี Openclaw อยู่แล้ว ทำไมยังต้องใช้ตัวนี้? -Openclaw มีความสามารถครบมาก และมี agent loop แบบรวมมาให้แล้วชื่อ Pi-Agent เหมาะกับ use case ที่หลากหลายกว่า ผมเองก็ชอบ Openclaw และเคยใช้มันเขียนโค้ดเหมือนกัน แต่สำหรับ coding โดยตรง มันไม่ใช่ตัวเลือกที่ดีที่สุดเสมอไป เพราะมี system prompt ขนาดใหญ่และ context ที่ติดมาด้วยมากกว่า Claude Code / Codex / Copilot จึงมักทำงานด้าน coding ได้มีประสิทธิภาพกว่า แม่นยำกว่า ตรงประเด็นกว่า และเสียสมาธิน้อยกว่า โปรเจ็กต์นี้ตั้งใจให้เรียบง่ายมาก โดยเชื่อมต่อกับ Codex / Copilot CLI เท่านั้น ดังนั้นคุณจึงมอบหมายงานให้ Codex / Copilot ทำได้โดยตรง +Openclaw มีความสามารถครบมาก และมี agent loop แบบรวมมาให้แล้วชื่อ Pi-Agent เหมาะกับ use case ที่หลากหลายกว่า ผมเองก็ชอบ Openclaw และเคยใช้มันเขียนโค้ดเหมือนกัน แต่สำหรับ coding โดยตรง มันไม่ใช่ตัวเลือกที่ดีที่สุดเสมอไป เพราะมี system prompt ขนาดใหญ่และ context ที่ติดมาด้วยมากกว่า Claude Code / Codex / Copilot จึงมักทำงานด้าน coding ได้มีประสิทธิภาพกว่า แม่นยำกว่า ตรงประเด็นกว่า และเสียสมาธิน้อยกว่า โปรเจ็กต์นี้ตั้งใจให้เรียบง่ายมาก โดยเชื่อมต่อกับ Codex / Copilot / Claude Code CLI เท่านั้น ดังนั้นคุณจึงมอบหมายงานให้ Codex / Copilot / Claude Code ทำได้โดยตรง + +## 🆚 ทำไมฉันจึงต้องใช้ coding-agent-telegram หากมี Claude Code + Telegram Plugin อยู่แล้ว? + +| ความสามารถ | Claude Code + Telegram Plugin อย่างเป็นทางการ | coding-agent-telegram (รองรับ Claude) | +|------------|-------------------------------------------------|----------------------------------------| +| แชทกับ AI ผ่าน Telegram | ✅ | ✅ | +| แก้ไขโค้ดในเครื่องและรันคำสั่ง | ✅ | ✅ | +| ต้องมี CLI session ที่กำลังทำงานอยู่ก่อน | **ใช่** | **ไม่ใช่** (เริ่มต้นหรือกลับมาใช้ session เดิมโดยอัตโนมัติ) | +| รองรับผู้ให้บริการ AI หลายราย | ❌ รองรับเฉพาะ Claude | ✅ Claude Code, Codex CLI, GitHub Copilot CLI | +| จัดการโปรเจกต์ผ่าน Telegram | ❌ | ✅ `/project` | +| จัดการ Branch ผ่าน Telegram | ใช้คำสั่ง Git ด้วยตนเอง | ✅ เวิร์กโฟลว์ `/branch` | +| สร้างและสลับ Session | จำกัดเฉพาะ Claude session ที่กำลังใช้งาน | ✅ `/new`, `/switch`, `/current`, `/compact` | +| กลับมาใช้ Local CLI session ที่มีอยู่ | ❌ | ✅ | +| ใช้งาน Session ต่อเนื่องข้ามอุปกรณ์ | มีข้อจำกัด | ✅ | +| ป้องกันการแก้ไข Workspace พร้อมกัน | ❌ | ✅ ป้องกันไม่ให้หลาย Agent แก้ไขโปรเจกต์เดียวกันพร้อมกัน | +| คิวงานเมื่อ Agent กำลังทำงาน | ❌ | ✅ | +| Snapshot และ Diff ของระบบไฟล์แบบอิสระ | ❌ | ✅ | +| ดู Diff ของไฟล์แบบมีโครงสร้างใน Telegram | ❌ | ✅ | +| กรอง Secrets และ Diff ที่มีข้อมูลสำคัญ | ❌ | ✅ | +| เวิร์กโฟลว์ Git ในตัว (pull / push / commit) | ทำด้วยตนเอง | ✅ | +| รองรับหลาย Bot | ต้องรันหลายอินสแตนซ์ | ✅ จัดการด้วยเซิร์ฟเวอร์เดียว | +| การจัดการสถานะในระดับโปรเจกต์ | ❌ | ✅ | +| สถาปัตยกรรมที่ไม่ผูกกับผู้ให้บริการรายใด | ❌ | ✅ | + +> **ความแตกต่างที่สำคัญ** +> +> Telegram Plugin อย่างเป็นทางการของ Claude Code เชื่อม Telegram เข้ากับ **Claude Code session ที่กำลังทำงานอยู่เพียงหนึ่ง session** +> +> **coding-agent-telegram** ทำหน้าที่เป็น **Telegram Control Plane** สำหรับจัดการโปรเจกต์, Branch, Session, เวิร์กโฟลว์ Git และ Coding Agent หลายตัว (Claude Code, Codex CLI และ GitHub Copilot CLI) ผ่านอินเทอร์เฟซเดียว + +### 🏛️ สถาปัตยกรรม + +#### Claude Code + Telegram Plugin + +```text +Telegram + │ + ▼ +Claude Code Channel + │ + ▼ +Claude Code Session ที่กำลังทำงานอยู่ +``` + +#### coding-agent-telegram + +```text +Telegram + │ + ▼ +coding-agent-telegram + │ + ├── Claude Code + ├── Codex CLI + └── GitHub Copilot CLI + │ + ▼ +โปรเจกต์ • Branch • Session • คิวงาน • Git • Diff • ตัวกรอง Secrets +``` ## 🚀 เริ่มต้นอย่างรวดเร็ว @@ -203,7 +262,7 @@ https://api.telegram.org/bot/getUpdates - ข้อความตัวอักษร - รูปภาพ - ข้อความเสียงและไฟล์เสียง เมื่อกำหนด `ENABLE_OPENAI_WHISPER_SPEECH_TO_TEXT=true` และติดตั้งส่วนที่ Whisper ต้องใช้ในเครื่องแล้ว -- ปัจจุบัน Codex และ Copilot รองรับเฉพาะข้อความและรูปภาพ ยังไม่รองรับวิดีโอ +- Codex และ Claude Code รองรับทั้งข้อความและรูปภาพ ส่วน Copilot ในปัจจุบันรองรับเฉพาะข้อความเท่านั้น ยังไม่มีผู้ให้บริการรายใดรองรับวิดีโอ ## 🤖 คำสั่ง Telegram @@ -234,7 +293,7 @@ https://api.telegram.org/bot/getUpdates /switch - แสดงเซสชันล่าสุด โดยเรียงจากใหม่ไปเก่า รายการนี้รวมทั้งเซสชันที่ bot ดูแลและ local Codex/Copilot CLI เซสชันของ project ปัจจุบัน + แสดงเซสชันล่าสุด โดยเรียงจากใหม่ไปเก่า รายการนี้รวมทั้งเซสชันที่ bot ดูแลและ local Codex/Copilot/Claude Code CLI เซสชันของ project ปัจจุบัน /switch page <number> @@ -313,13 +372,21 @@ https://api.telegram.org/bot/getUpdates APP_LOCALE ภาษา UI สำหรับข้อความของบอตและคำอธิบายคำสั่งที่ใช้ร่วมกัน ค่าที่รองรับ: en, de, fr, ja, ko, nl, th, vi, zh-CN, zh-HK, zh-TW + + DEFAULT_AGENT_PROVIDER + ผู้ให้บริการเริ่มต้นสำหรับเซสชันใหม่: codex, copilot หรือ claude ค่าเริ่มต้น: codex + CODEX_BIN - คำสั่งที่ใช้เรียก Codex CLI ค่าเริ่มต้น: codex + คำสั่งที่ใช้เรียก Codex CLI. แอปจะพยายามตรวจหาเส้นทางของ Codex ที่ติดตั้งอยู่ในเครื่องโดยอัตโนมัติระหว่างการเริ่มต้น .env_coding_agent_telegram หรือสามารถใช้ which codex เพื่อดูเส้นทางได้ COPILOT_BIN - คำสั่งที่ใช้เรียก Copilot CLI ค่าเริ่มต้น: copilot + คำสั่งที่ใช้เรียก Copilot CLI. แอปจะพยายามตรวจหาเส้นทางของ Copilot ที่ติดตั้งอยู่ในเครื่องโดยอัตโนมัติระหว่างการเริ่มต้น .env_coding_agent_telegram หรือสามารถใช้ which copilot เพื่อดูเส้นทางได้ + + + CLAUDE_BIN + คำสั่งที่ใช้เรียก Claude Code CLI. แอปจะพยายามตรวจหาเส้นทางของ Claude ที่ติดตั้งอยู่ในเครื่องโดยอัตโนมัติระหว่างการเริ่มต้น .env_coding_agent_telegram หรือสามารถใช้ which claude เพื่อดูเส้นทางได้ CODEX_MODEL @@ -329,6 +396,10 @@ https://api.telegram.org/bot/getUpdates COPILOT_MODEL กำหนด model ของ Copilot เพิ่มเติมได้แบบ optional หากปล่อยว่างจะใช้ model เริ่มต้นของ Copilot CLI ตัวอย่าง: gpt-5.4, claude-sonnet-4.6 GitHub Copilot supported models + + CLAUDE_MODEL + กำหนด model ของ Claude Code เพิ่มเติมได้แบบ optional หากปล่อยว่างจะใช้ model เริ่มต้นของ Claude Code CLI ตัวอย่าง: sonnet, opus, haiku Claude Code model configuration + CODEX_APPROVAL_POLICY โหมด approval ที่ส่งให้ Codex ค่าเริ่มต้น: never @@ -341,6 +412,18 @@ https://api.telegram.org/bot/getUpdates CODEX_SKIP_GIT_REPO_CHECK หากเปิดไว้ จะข้ามการตรวจ trusted-repo ของ Codex เสมอ + + CLAUDE_PERMISSION_MODE + Permission mode ที่ส่งให้ Claude Code เลือกได้จาก default, acceptEdits, plan, auto, dontAsk, bypassPermissions, manual ค่าเริ่มต้น: bypassPermissions (ทำงานอัตโนมัติเต็มรูปแบบ เนื่องจากไม่มี terminal แบบ interactive ให้ยืนยัน prompt) + + + CLAUDE_ALLOWED_TOOLS + allowlist ของ tools สำหรับ Claude Code แบบคั่นด้วย comma โดยใช้ syntax กฎ permission ของ Claude Code ตัวอย่าง: Read,Edit,Bash(git *) + + + CLAUDE_DISALLOWED_TOOLS + denylist ของ tools สำหรับ Claude Code แบบคั่นด้วย comma ตัวอย่าง: Bash(rm *) + ENABLE_COMMIT_COMMAND เปิดใช้งานคำสั่ง Telegram /commit ค่าเริ่มต้น: false @@ -426,8 +509,10 @@ ALLOWED_CHAT_IDS=123456789 DEFAULT_AGENT_PROVIDER=codex CODEX_BIN=codex COPILOT_BIN=copilot +CLAUDE_BIN=claude CODEX_APPROVAL_POLICY=never CODEX_SANDBOX_MODE=workspace-write +CLAUDE_PERMISSION_MODE=bypassPermissions ENABLE_SENSITIVE_DIFF_FILTER=true ENABLE_SECRET_SCRUB_FILTER=true ``` @@ -491,7 +576,7 @@ lock นี้อยู่ในหน่วยความจำ ไม่ไ ## ⚠️ Diff (การเปลี่ยนไฟล์) -_ในแต่ละ การรันของเอเจนต์ บอตจะสร้าง สแนปช็อตก่อนและหลังการรัน แบบเบาของโปรเจกต์ด้วย เพื่อสรุปไฟล์ที่เปลี่ยนและส่ง diff กลับไปยัง Telegram ได้ สแนปช็อต นี้ถูกสร้างโดยตัวบอตเอง ไม่ใช่โดย Codex หรือ Copilot._ +_ในแต่ละ การรันของเอเจนต์ บอตจะสร้าง สแนปช็อตก่อนและหลังการรัน แบบเบาของโปรเจกต์ด้วย เพื่อสรุปไฟล์ที่เปลี่ยนและส่ง diff กลับไปยัง Telegram ได้ สแนปช็อต นี้ถูกสร้างโดยตัวบอตเอง ไม่ใช่โดย Codex, Copilot หรือ Claude Code._ **สิ่งที่ควรรู้เกี่ยวกับ สแนปช็อต:** diff --git a/README.vi.md b/README.vi.md index f7cd261..adfbde3 100644 --- a/README.vi.md +++ b/README.vi.md @@ -35,14 +35,14 @@ ## ✨ Vì sao nên dùng - ✅ Nhẹ: không cần framework nặng, minh bạch hoàn toàn - ✅ Nhiều bot: nhiều cuộc chat, nhiều phiên - - ✅ Dùng Telegram để điều khiển Codex / Copilot CLI + - ✅ Dùng Telegram để điều khiển Codex / Copilot / Claude Code CLI - ✅ Dễ xem câu trả lời và các tệp đã thay đổi trong code block - ✅ Có thể xếp hàng câu hỏi tiếp theo khi agent đang làm việc - ✅ Chấp nhận tin nhắn ✏️ văn bản, 🌄 hình ảnh và 🎙️ thoại ## 🔁 Chuyển thiết bị/phiên liền mạch - Bắt đầu một phiên trên Telegram rồi tiếp tục đúng phiên Codex/Copilot CLI đó trên máy tính sau mà không cần thao tác rườm rà. Với `/switch`, bạn cũng có thể chuyển qua lại mượt mà giữa Telegram và command line. + Bắt đầu một phiên trên Telegram rồi tiếp tục đúng phiên Codex/Copilot/Claude Code CLI đó trên máy tính sau mà không cần thao tác rườm rà. Với `/switch`, bạn cũng có thể chuyển qua lại mượt mà giữa Telegram và command line. - Dùng `/switch` để tiếp tục một phiên cục bộ - Hỗ trợ cả các phiên lịch sử @@ -86,6 +86,7 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - Các thư mục có sẵn có thể yêu cầu xác nhận trust trước khi chạy Git operation có thay đổi - Máy chủ không thực hiện các lời gọi ra ngoài một cách ẩn. Bạn giữ toàn quyền kiểm soát. - Hoạt động tốt với Codex Sandbox mode, bạn không cần cấp `danger-full-access` +- Tích hợp Claude Code hỗ trợ các chế độ phân quyền có thể cấu hình và danh sách tool allow/deny @@ -96,16 +97,74 @@ Trước khi khởi động server, hãy chuẩn bị: - Python 3.9 trở lên - Telegram bot token tạo từ _@BotFather_ - Telegram chat ID của bạn -- Codex CLI và/hoặc Copilot CLI đã được cài cục bộ -- [Cài Codex CLI](https://developers.openai.com/codex/cli) -- [Cài Copilot CLI](https://github.com/features/copilot/cli) +- Codex CLI và/hoặc Copilot CLI và/hoặc Claude Code CLI đã được cài cục bộ +- [Cài Codex CLI](https://developers.openai.com/codex/cli) / [Cài Copilot CLI](https://github.com/features/copilot/cli) / [Cài Claude Code CLI](https://code.claude.com/docs/en/quickstart) - [Tùy chọn] Whisper, ffmpeg ## 🦞 Vì sao tôi cần cái này nếu đã có Openclaw? -Openclaw cung cấp bộ tính năng rất đầy đủ và đã có sẵn agent loop tích hợp tên là Pi-Agent. Nó khá toàn diện và phù hợp với nhiều use case đa dạng hơn. Tôi cũng là người thích Openclaw và từng code với Openclaw. Tuy vậy, riêng cho coding thì đây không phải lúc nào cũng là lựa chọn tốt nhất vì system prompt tích hợp khá lớn và context đi kèm nhiều hơn. Claude Code / Codex / Copilot vẫn thường hiệu quả hơn, chính xác hơn, ít bị phân tán hơn và trực tiếp hơn cho việc coding. Dự án này được giữ rất đơn giản, chỉ tích hợp Codex / Copilot CLI. Nghĩa là bạn giao việc trực tiếp cho Codex / Copilot. +Openclaw cung cấp bộ tính năng rất đầy đủ và đã có sẵn agent loop tích hợp tên là Pi-Agent. Nó khá toàn diện và phù hợp với nhiều use case đa dạng hơn. Tôi cũng là người thích Openclaw và từng code với Openclaw. Tuy vậy, riêng cho coding thì đây không phải lúc nào cũng là lựa chọn tốt nhất vì system prompt tích hợp khá lớn và context đi kèm nhiều hơn. Claude Code / Codex / Copilot vẫn thường hiệu quả hơn, chính xác hơn, ít bị phân tán hơn và trực tiếp hơn cho việc coding. Dự án này được giữ rất đơn giản, chỉ tích hợp Codex / Copilot / Claude Code CLI. Nghĩa là bạn giao việc trực tiếp cho Codex / Copilot / Claude Code. + +## 🆚 Tại sao tôi cần coding-agent-telegram nếu đã có Claude Code + Telegram Plugin? + +| Tính năng | Claude Code + Telegram Plugin chính thức | coding-agent-telegram (hỗ trợ Claude) | +|-----------|------------------------------------------|---------------------------------------| +| Trò chuyện với AI qua Telegram | ✅ | ✅ | +| Chỉnh sửa mã nguồn cục bộ và chạy lệnh | ✅ | ✅ | +| Yêu cầu một phiên CLI đang chạy sẵn | **Có** | **Không** (tự động khởi động hoặc tiếp tục phiên làm việc) | +| Hỗ trợ nhiều nhà cung cấp AI | ❌ Chỉ Claude | ✅ Claude Code, Codex CLI, GitHub Copilot CLI | +| Quản lý dự án từ Telegram | ❌ | ✅ `/project` | +| Quản lý nhánh (branch) từ Telegram | Thực hiện bằng lệnh Git thủ công | ✅ Quy trình `/branch` | +| Tạo và chuyển đổi giữa các phiên | Chỉ giới hạn ở phiên Claude đang hoạt động | ✅ `/new`, `/switch`, `/current`, `/compact` | +| Tiếp tục các phiên CLI cục bộ đã có | ❌ | ✅ | +| Duy trì phiên làm việc trên nhiều thiết bị | Hạn chế | ✅ | +| Ngăn nhiều tác nhân chỉnh sửa cùng một workspace | ❌ | ✅ Ngăn nhiều agent chỉnh sửa cùng một dự án đồng thời | +| Hàng đợi tác vụ khi agent đang bận | ❌ | ✅ | +| Snapshot và diff hệ thống tệp độc lập | ❌ | ✅ | +| Xem diff tệp có cấu trúc ngay trên Telegram | ❌ | ✅ | +| Lọc secrets và diff chứa dữ liệu nhạy cảm | ❌ | ✅ | +| Quy trình Git tích hợp (pull / push / commit) | Thủ công | ✅ | +| Hỗ trợ nhiều bot | Cần chạy nhiều instance | ✅ Được quản lý bởi một máy chủ duy nhất | +| Quản lý trạng thái theo từng dự án | ❌ | ✅ | +| Kiến trúc độc lập với nhà cung cấp | ❌ | ✅ | + +> **Điểm khác biệt chính** +> +> Telegram Plugin chính thức của Claude Code kết nối Telegram với **một phiên Claude Code đang chạy**. +> +> **coding-agent-telegram** hoạt động như một **Telegram Control Plane**, cho phép quản lý dự án, nhánh, phiên làm việc, quy trình Git và nhiều coding agent (Claude Code, Codex CLI, GitHub Copilot CLI) thông qua một giao diện duy nhất. + +### 🏛️ Kiến trúc + +#### Claude Code + Telegram Plugin + +```text +Telegram + │ + ▼ +Claude Code Channel + │ + ▼ +Một phiên Claude Code đang chạy +``` + +#### coding-agent-telegram + +```text +Telegram + │ + ▼ +coding-agent-telegram + │ + ├── Claude Code + ├── Codex CLI + └── GitHub Copilot CLI + │ + ▼ +Dự án • Nhánh • Phiên • Hàng đợi • Git • Diff • Bộ lọc Secrets +``` ## 🚀 Bắt đầu nhanh @@ -203,7 +262,7 @@ Hiện tại bot chấp nhận: - tin nhắn văn bản - ảnh - tin nhắn thoại và tệp âm thanh khi `ENABLE_OPENAI_WHISPER_SPEECH_TO_TEXT=true` và các điều kiện cần cục bộ của Whisper đã được cài đặt -- hiện tại Codex và Copilot chỉ hỗ trợ văn bản và hình ảnh, chưa hỗ trợ video +- Codex và Claude Code hỗ trợ văn bản và hình ảnh; Copilot hiện chỉ hỗ trợ văn bản. Video chưa được hỗ trợ bởi bất kỳ nhà cung cấp nào. ## 🤖 Lệnh Telegram @@ -234,7 +293,7 @@ Hiện tại bot chấp nhận: /switch - Hiển thị các phiên mới nhất, mới nhất trước. Danh sách bao gồm cả phiên do bot quản lý và phiên CLI Codex/Copilot cục bộ của project hiện tại. + Hiển thị các phiên mới nhất, mới nhất trước. Danh sách bao gồm cả phiên do bot quản lý và phiên CLI Codex/Copilot/Claude Code cục bộ của project hiện tại. /switch page <number> @@ -313,13 +372,21 @@ Hiện tại bot chấp nhận: APP_LOCALE Ngôn ngữ UI cho các thông điệp bot dùng chung và mô tả lệnh. Giá trị hỗ trợ: en, de, fr, ja, ko, nl, th, vi, zh-CN, zh-HK, zh-TW. + + DEFAULT_AGENT_PROVIDER + Nhà cung cấp mặc định cho các phiên mới: codex, copilot, hoặc claude. Mặc định: codex. + CODEX_BIN - Lệnh dùng để chạy Codex CLI. Mặc định: codex. + Lệnh dùng để chạy Codex CLI. Ứng dụng sẽ cố gắng tự động phát hiện đường dẫn của Codex được cài đặt trên máy khi khởi tạo .env_coding_agent_telegram. Ngoài ra, bạn có thể sử dụng which codex để xem đường dẫn. COPILOT_BIN - Lệnh dùng để chạy Copilot CLI. Mặc định: copilot. + Lệnh dùng để chạy Copilot CLI. Ứng dụng sẽ cố gắng tự động phát hiện đường dẫn của Copilot được cài đặt trên máy khi khởi tạo .env_coding_agent_telegram. Ngoài ra, bạn có thể sử dụng which copilot để xem đường dẫn. + + + CLAUDE_BIN + Lệnh dùng để chạy Claude Code CLI. Ứng dụng sẽ cố gắng tự động phát hiện đường dẫn của Claude được cài đặt trên máy khi khởi tạo .env_coding_agent_telegram. Ngoài ra, bạn có thể sử dụng which claude để xem đường dẫn. CODEX_MODEL @@ -329,6 +396,10 @@ Hiện tại bot chấp nhận: COPILOT_MODEL Ghi đè model Copilot nếu cần. Để trống để dùng model mặc định của Copilot CLI. Ví dụ: gpt-5.4, claude-sonnet-4.6 GitHub Copilot supported models + + CLAUDE_MODEL + Ghi đè model Claude Code nếu cần. Để trống để dùng model mặc định của Claude Code CLI. Ví dụ: sonnet, opus, haiku Claude Code model configuration + CODEX_APPROVAL_POLICY Chế độ approval truyền cho Codex. Mặc định: never. @@ -341,6 +412,18 @@ Hiện tại bot chấp nhận: CODEX_SKIP_GIT_REPO_CHECK Nếu bật, luôn bỏ qua trusted-repo check của Codex. + + CLAUDE_PERMISSION_MODE + Chế độ phân quyền truyền cho Claude Code. Một trong các giá trị default, acceptEdits, plan, auto, dontAsk, bypassPermissions, manual. Mặc định: bypassPermissions (hoàn toàn tự động, vì không có terminal tương tác để xác nhận yêu cầu). + + + CLAUDE_ALLOWED_TOOLS + Danh sách allowlist các tool của Claude Code, ngăn cách bằng dấu phẩy, dùng cú pháp permission rule của Claude Code. Ví dụ: Read,Edit,Bash(git *) + + + CLAUDE_DISALLOWED_TOOLS + Danh sách denylist các tool của Claude Code, ngăn cách bằng dấu phẩy. Ví dụ: Bash(rm *) + ENABLE_COMMIT_COMMAND Bật lệnh Telegram /commit. Mặc định: false. @@ -420,8 +503,10 @@ ALLOWED_CHAT_IDS=123456789 DEFAULT_AGENT_PROVIDER=codex CODEX_BIN=codex COPILOT_BIN=copilot +CLAUDE_BIN=claude CODEX_APPROVAL_POLICY=never CODEX_SANDBOX_MODE=workspace-write +CLAUDE_PERMISSION_MODE=bypassPermissions ENABLE_SENSITIVE_DIFF_FILTER=true ENABLE_SECRET_SCRUB_FILTER=true ``` @@ -485,7 +570,7 @@ Nếu run hiện tại bị abort và vẫn còn các câu hỏi trong hàng đ ## ⚠️ Diff (thay đổi tệp) -_Trong mỗi lần chạy tác nhân, bot cũng tạo một ảnh chụp nhanh trước/sau nhẹ của project để có thể tóm tắt các tệp thay đổi và gửi diff về Telegram. Bản chụp nhanh này do chính bot app tạo ra, không phải bởi Codex hay Copilot._ +_Trong mỗi lần chạy tác nhân, bot cũng tạo một ảnh chụp nhanh trước/sau nhẹ của project để có thể tóm tắt các tệp thay đổi và gửi diff về Telegram. Bản chụp nhanh này do chính bot app tạo ra, không phải bởi Codex, Copilot, hay Claude Code._ **Ghi chú về ảnh chụp nhanh:** diff --git a/README.zh-CN.md b/README.zh-CN.md index bbb35fc..2b2c731 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -35,14 +35,14 @@ ## ✨ 为什么使用它 - ✅ 轻量:没有重型框架,行为清晰可见 - ✅ 多 Bot:支持多个聊天、多个会话 - - ✅ 使用 Telegram 控制 Codex / Copilot CLI + - ✅ 使用 Telegram 控制 Codex / Copilot / Claude Code CLI - ✅ 可以在代码块中轻松查看 agent 回复和改动文件 - ✅ agent 工作时也能继续排队后续问题 - ✅ 支持 ✏️ 文本、🌄 图片和 🎙️ 语音消息 ## 🔁 设备与会话无缝切换 - 你可以先在 Telegram 中开启一个会话,之后在电脑上继续同一个 Codex/Copilot CLI 会话,不需要折腾。使用 `/switch` 也可以在 Telegram 和命令行之间无缝切换。 + 你可以先在 Telegram 中开启一个会话,之后在电脑上继续同一个 Codex/Copilot/Claude Code CLI 会话,不需要折腾。使用 `/switch` 也可以在 Telegram 和命令行之间无缝切换。 - 使用 `/switch` 继续本地会话 - 也支持历史会话 @@ -86,6 +86,7 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - 现有文件夹在执行会修改内容的 Git 操作前,可能需要先确认 trust - 服务器不会发出隐藏的外部请求,一切都由你掌控。 - 与 Codex Sandbox mode 配合良好,你不需要授予 `danger-full-access` +- Claude Code 集成支持可配置的权限模式以及工具允许/拒绝列表 @@ -96,16 +97,76 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - Python 3.9 或更高版本 - 通过 _@BotFather_ 创建的 Telegram bot token - 你的 Telegram chat ID -- 已在本地安装 Codex CLI 和/或 Copilot CLI +- 已在本地安装 Codex CLI 和/或 Copilot CLI 和/或 Claude Code CLI - [安装 Codex CLI](https://developers.openai.com/codex/cli) - [安装 Copilot CLI](https://github.com/features/copilot/cli) +- [安装 Claude Code CLI](https://code.claude.com/docs/en/quickstart) - [可选] Whisper、ffmpeg ## 🦞 如果我已经有 Openclaw,为什么还需要它? -Openclaw 功能非常完整,也内置了名为 Pi-Agent 的 agent loop,适合更丰富、更广泛的使用场景。我自己也很喜欢 Openclaw,以前也用它来写代码。不过对纯 coding 来说,它并不总是最佳选择,因为内置的大型 system prompt 和额外 context 有时会让模型更容易分心。Claude Code / Codex / Copilot 在 coding 场景下通常仍然更高效、更准确、更不容易跑偏,也更直接。这个项目刻意保持简单,只集成 Codex / Copilot CLI,所以你是在直接把任务交给 Codex / Copilot。 +Openclaw 功能非常完整,也内置了名为 Pi-Agent 的 agent loop,适合更丰富、更广泛的使用场景。我自己也很喜欢 Openclaw,以前也用它来写代码。不过对纯 coding 来说,它并不总是最佳选择,因为内置的大型 system prompt 和额外 context 有时会让模型更容易分心。Claude Code / Codex / Copilot 在 coding 场景下通常仍然更高效、更准确、更不容易跑偏,也更直接。这个项目刻意保持简单,只集成 Codex / Copilot / Claude Code CLI,所以你是在直接把任务交给 Codex / Copilot / Claude Code。 + +## 🆚 如果我已经有 Claude Code + Telegram Plugin,为什么还需要它? + +| 功能 | Claude Code + 官方 Telegram Plugin | coding-agent-telegram(支持 Claude) | +|------|------------------------------------|--------------------------------------| +| 通过 Telegram 与 AI 对话 | ✅ | ✅ | +| 编辑本地代码并执行命令 | ✅ | ✅ | +| 需要预先启动一个 CLI 会话 | **是** | **否**(自动启动或恢复会话) | +| 支持多个 AI 提供商 | ❌ 仅支持 Claude | ✅ Claude Code、Codex CLI、GitHub Copilot CLI | +| 通过 Telegram 管理项目 | ❌ | ✅ `/project` | +| 通过 Telegram 管理分支 | 手动执行 Git 命令 | ✅ `/branch` 工作流 | +| 创建和切换会话 | 仅限当前活动的 Claude 会话 | ✅ `/new`、`/switch`、`/current`、`/compact` | +| 恢复已有的本地 CLI 会话 | ❌ | ✅ | +| 跨设备持续使用同一个会话 | 有限制 | ✅ | +| 防止多个 Agent 同时修改同一工作区 | ❌ | ✅ 防止多个 Agent 同时修改同一个项目 | +| Agent 忙碌时的任务队列 | ❌ | ✅ | +| 独立的文件系统快照与 Diff | ❌ | ✅ | +| 在 Telegram 中查看结构化文件 Diff | ❌ | ✅ | +| Secret 和敏感 Diff 过滤 | ❌ | ✅ | +| 内置 Git 工作流(pull / push / commit) | 手动操作 | ✅ | +| 支持多个 Bot | 需要运行多个实例 | ✅ 由单个服务器统一管理 | +| 项目级状态管理 | ❌ | ✅ | +| 与 AI 提供商无关的架构 | ❌ | ✅ | + +> **核心区别** +> +> 官方 Claude Code Telegram Plugin 会将 Telegram 连接到**一个已经运行中的 Claude Code 会话**。 +> +> **coding-agent-telegram** 则充当一个 **Telegram Control Plane(控制平面)**,通过统一的界面管理项目、分支、会话、Git 工作流以及多个 Coding Agent(Claude Code、Codex CLI、GitHub Copilot CLI)。 + +### 🏛️ 架构 + +#### Claude Code + Telegram Plugin + +```text +Telegram + │ + ▼ +Claude Code Channel + │ + ▼ +一个正在运行的 Claude Code 会话 +``` + +#### coding-agent-telegram + +```text +Telegram + │ + ▼ +coding-agent-telegram + │ + ├── Claude Code + ├── Codex CLI + └── GitHub Copilot CLI + │ + ▼ +项目 • 分支 • 会话 • 队列 • Git • Diff • Secret 过滤 +``` ## 🚀 快速开始 @@ -203,7 +264,7 @@ bot 当前接受: - 文本消息 - 图片 - 当 `ENABLE_OPENAI_WHISPER_SPEECH_TO_TEXT=true` 且已安装本地 Whisper 依赖时的语音消息和音频文件 -- Codex 和 Copilot 当前只支持文本和图片,不支持视频 +- Codex 和 Claude Code 会话支持文本和图片;Copilot 会话目前只支持文本。所有提供方均不支持视频 ## 🤖 Telegram 命令 @@ -234,7 +295,7 @@ bot 当前接受: /switch - 显示最新的会话,按从新到旧排序。列表同时包含 bot 管理的会话和当前项目的本地 Codex/Copilot CLI 会话。 + 显示最新的会话,按从新到旧排序。列表同时包含 bot 管理的会话和当前项目的本地 Codex/Copilot/Claude Code CLI 会话。 /switch page <number> @@ -313,13 +374,21 @@ bot 当前接受: APP_LOCALE 共享 bot 消息和命令说明所使用的 UI 语言。支持值:endefrjakonlthvizh-CNzh-HKzh-TW。 + + DEFAULT_AGENT_PROVIDER + 新会话的默认提供方:codexcopilotclaude。默认:codex。 + CODEX_BIN - 用于启动 Codex CLI 的命令。默认:codex。 + 用于启动 Codex CLI 的命令。应用程序会在初始化 .env_coding_agent_telegram 时尝试自动检测本地安装的 Codex 路径。您也可以使用 which codex 查看该路径。 COPILOT_BIN - 用于启动 Copilot CLI 的命令。默认:copilot。 + 用于启动 Copilot CLI 的命令。应用程序会在初始化 .env_coding_agent_telegram 时尝试自动检测本地安装的 Copilot 路径。您也可以使用 which copilot 查看该路径。 + + + CLAUDE_BIN + 用于启动 Claude Code CLI 的命令。应用程序会在初始化 .env_coding_agent_telegram 时尝试自动检测本地安装的 Claude 路径。您也可以使用 which claude 查看该路径。 CODEX_MODEL @@ -329,6 +398,14 @@ bot 当前接受: COPILOT_MODEL 可选的 Copilot model 覆盖。留空则使用 Copilot CLI 默认 model。示例:gpt-5.4claude-sonnet-4.6 GitHub Copilot supported models + + CLAUDE_MODEL + 可选的 Claude Code model 覆盖。 + 留空则使用 Claude Code CLI 默认 model。 + 示例:sonnetopushaiku + Claude Code model configuration + + CODEX_APPROVAL_POLICY 传递给 Codex 的 approval mode。默认:never。 @@ -341,6 +418,18 @@ bot 当前接受: CODEX_SKIP_GIT_REPO_CHECK 如果启用,将始终跳过 Codex 的 trusted-repo 检查。 + + CLAUDE_PERMISSION_MODE + 传递给 Claude Code 的 permission mode。可选值:defaultacceptEditsplanautodontAskbypassPermissionsmanual。默认:bypassPermissions(完全自主运行,因为没有交互式终端来确认提示)。 + + + CLAUDE_ALLOWED_TOOLS + 以逗号分隔的 Claude Code 工具允许列表,使用 Claude Code 的 permission rule 语法。示例:Read,Edit,Bash(git *) + + + CLAUDE_DISALLOWED_TOOLS + 以逗号分隔的 Claude Code 工具拒绝列表。示例:Bash(rm *) + ENABLE_COMMIT_COMMAND 启用 Telegram 的 /commit 命令。默认:false。 @@ -420,8 +509,10 @@ ALLOWED_CHAT_IDS=123456789 DEFAULT_AGENT_PROVIDER=codex CODEX_BIN=codex COPILOT_BIN=copilot +CLAUDE_BIN=claude CODEX_APPROVAL_POLICY=never CODEX_SANDBOX_MODE=workspace-write +CLAUDE_PERMISSION_MODE=bypassPermissions ENABLE_SENSITIVE_DIFF_FILTER=true ENABLE_SECRET_SCRUB_FILTER=true ``` @@ -485,7 +576,7 @@ bot 会强制这个限制,避免两个 agent 同时写入同一个 workspace ## ⚠️ Diff(文件变更) -_在每次 代理运行 期间,bot 也会为项目生成轻量的 前后快照,用来汇总改动文件并把 diff 发回 Telegram。这个 快照 由 bot 应用自己生成,而不是由 Codex 或 Copilot 生成。_ +_在每次 代理运行 期间,bot 也会为项目生成轻量的 前后快照,用来汇总改动文件并把 diff 发回 Telegram。这个 快照 由 bot 应用自己生成,而不是由 Codex、Copilot 或 Claude Code 生成。_ **快照说明:** diff --git a/README.zh-HK.md b/README.zh-HK.md index f787b8a..73c9c2a 100644 --- a/README.zh-HK.md +++ b/README.zh-HK.md @@ -35,14 +35,14 @@ ## ✨ 為什麼使用它 - ✅ 輕量:沒有重型框架,行為清晰透明 - ✅ 多 Bot:支援多個聊天、多個工作階段 - - ✅ 使用 Telegram 控制 Codex / Copilot CLI + - ✅ 使用 Telegram 控制 Codex / Copilot / Claude Code CLI - ✅ 可在 code block 中輕鬆查看 agent 回覆及改動檔案 - ✅ agent 執行中仍可把後續問題排入佇列 - ✅ 支援 ✏️ 文字、🌄 圖片和 🎙️ 語音訊息 ## 🔁 裝置與工作階段無縫切換 - 你可以先在 Telegram 開始一個工作階段,之後再在電腦上繼續同一個 Codex/Copilot CLI 工作階段,過程自然順暢。使用 `/switch` 亦可在 Telegram 與 command line 之間輕鬆切換。 + 你可以先在 Telegram 開始一個工作階段,之後再在電腦上繼續同一個 Codex/Copilot/Claude Code CLI 工作階段,過程自然順暢。使用 `/switch` 亦可在 Telegram 與 command line 之間輕鬆切換。 - 使用 `/switch` 繼續本機工作階段 - 亦支援歷史工作階段 @@ -86,6 +86,7 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - 現有資料夾在執行會修改內容的 Git operation 前,可能需要先確認 trust - 伺服器不會作出隱藏的外部呼叫,一切都由你掌控。 - 與 Codex Sandbox mode 配合良好,你毋須授予 `danger-full-access` +- Claude Code 整合支援可設定的 permission modes 及工具 allow/deny 清單 @@ -96,16 +97,74 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - Python 3.9 或以上 - 由 _@BotFather_ 建立的 Telegram bot token - 你的 Telegram chat ID -- 已在本機安裝 Codex CLI 及/或 Copilot CLI -- [安裝 Codex CLI](https://developers.openai.com/codex/cli) -- [安裝 Copilot CLI](https://github.com/features/copilot/cli) +- 已在本機安裝 Codex CLI 及/或 Copilot CLI 及/或 Claude Code CLI +- [安裝 Codex CLI](https://developers.openai.com/codex/cli) / [安裝 Copilot CLI](https://github.com/features/copilot/cli) / [安裝 Claude Code CLI](https://code.claude.com/docs/en/quickstart) - [可選] Whisper、ffmpeg ## 🦞 如果我已經有 Openclaw,為甚麼還需要它? -Openclaw 提供非常完整的能力,也內建了名為 Pi-Agent 的 agent loop,適合更多元的 use case。我自己也很喜歡 Openclaw,過去亦曾用它來寫 code。不過對純 coding 來說,它未必永遠是最佳選擇,因為內建的大型 system prompt 和額外 context 有時會令模型更容易分心。Claude Code / Codex / Copilot 在 coding 場景下通常仍然更有效率、更準確、更少被干擾,也更直接。這個 project 刻意保持簡單,只整合 Codex / Copilot CLI,所以你其實是在直接把工作交給 Codex / Copilot。 +Openclaw 提供非常完整的能力,也內建了名為 Pi-Agent 的 agent loop,適合更多元的 use case。我自己也很喜歡 Openclaw,過去亦曾用它來寫 code。不過對純 coding 來說,它未必永遠是最佳選擇,因為內建的大型 system prompt 和額外 context 有時會令模型更容易分心。Claude Code / Codex / Copilot 在 coding 場景下通常仍然更有效率、更準確、更少被干擾,也更直接。這個 project 刻意保持簡單,只整合 Codex / Copilot / Claude Code CLI,所以你其實是在直接把工作交給 Codex / Copilot / Claude Code。 + +## 🆚 如果我已經有 Claude Code + Telegram Plugin,為甚麼還需要它? + +| 功能 | Claude Code + 官方 Telegram Plugin | coding-agent-telegram(支援 Claude) | +|------|------------------------------------|--------------------------------------| +| 透過 Telegram 與 AI 對話 | ✅ | ✅ | +| 編輯本機程式碼並執行指令 | ✅ | ✅ | +| 需要預先啟動一個 CLI 工作階段 | **是** | **否**(自動啟動或恢復工作階段) | +| 支援多個 AI 供應商 | ❌ 僅支援 Claude | ✅ Claude Code、Codex CLI、GitHub Copilot CLI | +| 透過 Telegram 管理專案 | ❌ | ✅ `/project` | +| 透過 Telegram 管理分支 | 手動執行 Git 指令 | ✅ `/branch` 工作流程 | +| 建立及切換工作階段 | 僅限目前啟用的 Claude 工作階段 | ✅ `/new`、`/switch`、`/current`、`/compact` | +| 恢復現有的本機 CLI 工作階段 | ❌ | ✅ | +| 跨裝置延續同一個工作階段 | 有限制 | ✅ | +| 防止多個 Agent 同時修改同一個工作區 | ❌ | ✅ 防止多個 Agent 同時修改同一個專案 | +| Agent 忙碌時的工作佇列 | ❌ | ✅ | +| 獨立的檔案系統快照與 Diff | ❌ | ✅ | +| 在 Telegram 中檢視結構化檔案 Diff | ❌ | ✅ | +| Secret 及敏感 Diff 過濾 | ❌ | ✅ | +| 內建 Git 工作流程(pull / push / commit) | 手動操作 | ✅ | +| 支援多個 Bot | 需要執行多個實例 | ✅ 由單一伺服器統一管理 | +| 專案層級狀態管理 | ❌ | ✅ | +| 與 AI 供應商無關的架構 | ❌ | ✅ | + +> **主要分別** +> +> 官方 Claude Code Telegram Plugin 會將 Telegram 連接到**一個已經執行中的 Claude Code 工作階段**。 +> +> **coding-agent-telegram** 則充當一個 **Telegram Control Plane(控制平面)**,透過單一介面管理專案、分支、工作階段、Git 工作流程,以及多個 Coding Agent(Claude Code、Codex CLI、GitHub Copilot CLI)。 + +### 🏛️ 架構 + +#### Claude Code + Telegram Plugin + +```text +Telegram + │ + ▼ +Claude Code Channel + │ + ▼ +一個正在執行中的 Claude Code 工作階段 +``` + +#### coding-agent-telegram + +```text +Telegram + │ + ▼ +coding-agent-telegram + │ + ├── Claude Code + ├── Codex CLI + └── GitHub Copilot CLI + │ + ▼ +專案 • 分支 • 工作階段 • 佇列 • Git • Diff • Secret 過濾 +``` ## 🚀 快速開始 @@ -203,7 +262,7 @@ bot 目前接受: - 文字訊息 - 圖片 - 當 `ENABLE_OPENAI_WHISPER_SPEECH_TO_TEXT=true` 且已安裝本機 Whisper 依賴時的語音訊息與音訊檔案 -- Codex 與 Copilot 目前只支援文字與圖片,不支援影片 +- Codex 與 Claude Code 的工作階段支援文字與圖片;Copilot 的工作階段目前只支援文字。所有提供者均不支援影片。 ## 🤖 Telegram 指令 @@ -234,7 +293,7 @@ bot 目前接受: /switch - 顯示最新的工作階段,按由新到舊排序。列表同時包含 bot 管理的工作階段以及目前專案的本機 Codex/Copilot CLI 工作階段。 + 顯示最新的工作階段,按由新到舊排序。列表同時包含 bot 管理的工作階段以及目前專案的本機 Codex/Copilot/Claude Code CLI 工作階段。 /switch page <number> @@ -313,13 +372,21 @@ bot 目前接受: APP_LOCALE 共用 bot 訊息與指令說明所使用的 UI 語言。支援值:endefrjakonlthvizh-CNzh-HKzh-TW。 + + DEFAULT_AGENT_PROVIDER + 新工作階段的預設提供者:codexcopilotclaude。預設:codex。 + CODEX_BIN - 用來啟動 Codex CLI 的指令。預設:codex。 + 用來啟動 Codex CLI 的指令。應用程式會在初始化 .env_coding_agent_telegram 時嘗試自動偵測本機已安裝的 Codex 路徑。您亦可使用 which codex 查看該路徑。 COPILOT_BIN - 用來啟動 Copilot CLI 的指令。預設:copilot。 + 用來啟動 Copilot CLI 的指令。應用程式會在初始化 .env_coding_agent_telegram 時嘗試自動偵測本機已安裝的 Copilot 路徑。您亦可使用 which copilot 查看該路徑。 + + + CLAUDE_BIN + 用來啟動 Claude Code CLI 的指令。應用程式會在初始化 .env_coding_agent_telegram 時嘗試自動偵測本機已安裝的 Claude 路徑。您亦可使用 which claude 查看該路徑。 CODEX_MODEL @@ -329,6 +396,12 @@ bot 目前接受: COPILOT_MODEL 可選的 Copilot model override。留空則使用 Copilot CLI 預設 model。例子:gpt-5.4claude-sonnet-4.6 GitHub Copilot supported models + + CLAUDE_MODEL + 可選的 Claude Code model override。留空則使用 Claude Code CLI 預設 model。例子:sonnetopushaiku + Claude Code model configuration + + CODEX_APPROVAL_POLICY 傳遞給 Codex 的 approval mode。預設:never。 @@ -341,6 +414,18 @@ bot 目前接受: CODEX_SKIP_GIT_REPO_CHECK 如果啟用,會一直略過 Codex 的 trusted-repo 檢查。 + + CLAUDE_PERMISSION_MODE + 傳遞給 Claude Code 的 permission mode。可選值:defaultacceptEditsplanautodontAskbypassPermissionsmanual。預設:bypassPermissions(完全自動化,因為並沒有互動式 terminal 可以核准提示)。 + + + CLAUDE_ALLOWED_TOOLS + 以逗號分隔的 Claude Code 工具 allowlist,使用 Claude Code 的 permission rule 語法。例子:Read,Edit,Bash(git *) + + + CLAUDE_DISALLOWED_TOOLS + 以逗號分隔的 Claude Code 工具 denylist。例子:Bash(rm *) + ENABLE_COMMIT_COMMAND 啟用 Telegram 的 /commit 指令。預設:false。 @@ -426,8 +511,10 @@ ALLOWED_CHAT_IDS=123456789 DEFAULT_AGENT_PROVIDER=codex CODEX_BIN=codex COPILOT_BIN=copilot +CLAUDE_BIN=claude CODEX_APPROVAL_POLICY=never CODEX_SANDBOX_MODE=workspace-write +CLAUDE_PERMISSION_MODE=bypassPermissions ENABLE_SENSITIVE_DIFF_FILTER=true ENABLE_SECRET_SCRUB_FILTER=true ``` @@ -491,7 +578,7 @@ bot 會強制這個限制,避免兩個 agent 同時寫入同一個 workspace ## ⚠️ Diff(檔案變更) -_在每次 代理執行 期間,bot 也會為 project 產生輕量的 前後快照,用來總結已變更檔案並把 diff 傳回 Telegram。這個 快照 是由 bot app 自己建立,不是由 Codex 或 Copilot 建立。_ +_在每次 代理執行 期間,bot 也會為 project 產生輕量的 前後快照,用來總結已變更檔案並把 diff 傳回 Telegram。這個 快照 是由 bot app 自己建立,不是由 Codex、Copilot 或 Claude Code 建立。_ **快照說明:** diff --git a/README.zh-TW.md b/README.zh-TW.md index e8a5d40..4ca3122 100644 --- a/README.zh-TW.md +++ b/README.zh-TW.md @@ -35,14 +35,14 @@ ## ✨ 為什麼使用它 - ✅ 輕量:沒有重型框架,行為清楚透明 - ✅ 多 Bot:支援多個聊天、多個工作階段 - - ✅ 使用 Telegram 控制 Codex / Copilot CLI + - ✅ 使用 Telegram 控制 Codex / Copilot / Claude Code CLI - ✅ 可以在 code block 中輕鬆檢視 agent 回覆與改動檔案 - ✅ agent 執行期間也能把後續問題排入佇列 - ✅ 支援 ✏️ 文字、🌄 圖片和 🎙️ 語音訊息 ## 🔁 裝置與工作階段無縫切換 - 你可以先在 Telegram 開始一個工作階段,之後在電腦上繼續同一個 Codex/Copilot CLI 工作階段,不需要額外折騰。使用 `/switch` 也能在 Telegram 與 command line 之間自然切換。 + 你可以先在 Telegram 開始一個工作階段,之後在電腦上繼續同一個 Codex/Copilot/Claude Code CLI 工作階段,不需要額外折騰。使用 `/switch` 也能在 Telegram 與 command line 之間自然切換。 - 使用 `/switch` 繼續本機工作階段 - 也支援歷史工作階段 @@ -86,6 +86,7 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - 現有資料夾在執行會修改內容的 Git operation 前,可能需要先確認 trust - 伺服器不會進行隱藏的外部呼叫,一切都由你掌控。 - 與 Codex Sandbox mode 配合良好,你不需要授予 `danger-full-access` +- Claude Code 整合支援可設定的 permission modes 以及 tool allow/deny lists @@ -96,16 +97,76 @@ curl -fsSL https://raw.githubusercontent.com/daocha/coding-agent-telegram/main/i - Python 3.9 或以上 - 由 _@BotFather_ 建立的 Telegram bot token - 你的 Telegram chat ID -- 已在本機安裝 Codex CLI 及/或 Copilot CLI +- 已在本機安裝 Codex CLI 及/或 Copilot CLI 及/或 Claude Code CLI - [安裝 Codex CLI](https://developers.openai.com/codex/cli) - [安裝 Copilot CLI](https://github.com/features/copilot/cli) +- [安裝 Claude Code CLI](https://code.claude.com/docs/en/quickstart) - [可選] Whisper、ffmpeg ## 🦞 如果我已經有 Openclaw,為什麼還需要它? -Openclaw 提供非常完整的能力,也內建了名為 Pi-Agent 的 agent loop,適合更多元的 use case。我自己也很喜歡 Openclaw,過去也曾用它來寫 code。不過對純 coding 來說,它未必永遠是最佳選擇,因為內建的大型 system prompt 和額外 context 有時會讓模型更容易分心。Claude Code / Codex / Copilot 在 coding 場景下通常仍然更有效率、更準確、更不容易跑偏,也更直接。這個 project 刻意保持簡單,只整合 Codex / Copilot CLI,所以你其實是在直接把工作交給 Codex / Copilot。 +Openclaw 提供非常完整的能力,也內建了名為 Pi-Agent 的 agent loop,適合更多元的 use case。我自己也很喜歡 Openclaw,過去也曾用它來寫 code。不過對純 coding 來說,它未必永遠是最佳選擇,因為內建的大型 system prompt 和額外 context 有時會讓模型更容易分心。Claude Code / Codex / Copilot 在 coding 場景下通常仍然更有效率、更準確、更不容易跑偏,也更直接。這個 project 刻意保持簡單,只整合 Codex / Copilot / Claude Code CLI,所以你其實是在直接把工作交給 Codex / Copilot / Claude Code。 + +## 🆚 如果我已經有 Claude Code + Telegram Plugin,為什麼還需要它? + +| 功能 | Claude Code + 官方 Telegram Plugin | coding-agent-telegram(支援 Claude) | +|------|------------------------------------|--------------------------------------| +| 透過 Telegram 與 AI 對話 | ✅ | ✅ | +| 編輯本機程式碼並執行指令 | ✅ | ✅ | +| 需要預先啟動一個 CLI 工作階段 | **是** | **否**(自動啟動或恢復工作階段) | +| 支援多個 AI 提供商 | ❌ 僅支援 Claude | ✅ Claude Code、Codex CLI、GitHub Copilot CLI | +| 透過 Telegram 管理專案 | ❌ | ✅ `/project` | +| 透過 Telegram 管理分支 | 手動執行 Git 指令 | ✅ `/branch` 工作流程 | +| 建立與切換工作階段 | 僅限目前啟用的 Claude 工作階段 | ✅ `/new`、`/switch`、`/current`、`/compact` | +| 恢復既有的本機 CLI 工作階段 | ❌ | ✅ | +| 跨裝置延續同一個工作階段 | 有限制 | ✅ | +| 防止多個 Agent 同時修改同一個工作區 | ❌ | ✅ 防止多個 Agent 同時修改同一個專案 | +| Agent 忙碌時的工作佇列 | ❌ | ✅ | +| 獨立的檔案系統快照與 Diff | ❌ | ✅ | +| 在 Telegram 中檢視結構化檔案 Diff | ❌ | ✅ | +| Secret 與敏感 Diff 過濾 | ❌ | ✅ | +| 內建 Git 工作流程(pull / push / commit) | 手動操作 | ✅ | +| 支援多個 Bot | 需要執行多個實例 | ✅ 由單一伺服器統一管理 | +| 專案層級狀態管理 | ❌ | ✅ | +| 與 AI 提供商無關的架構 | ❌ | ✅ | + +> **核心差異** +> +> 官方 Claude Code Telegram Plugin 會將 Telegram 連接到**一個已經執行中的 Claude Code 工作階段**。 +> +> **coding-agent-telegram** 則充當一個 **Telegram Control Plane(控制平面)**,透過單一介面管理專案、分支、工作階段、Git 工作流程,以及多個 Coding Agent(Claude Code、Codex CLI、GitHub Copilot CLI)。 + +### 🏛️ 架構 + +#### Claude Code + Telegram Plugin + +```text +Telegram + │ + ▼ +Claude Code Channel + │ + ▼ +一個正在執行中的 Claude Code 工作階段 +``` + +#### coding-agent-telegram + +```text +Telegram + │ + ▼ +coding-agent-telegram + │ + ├── Claude Code + ├── Codex CLI + └── GitHub Copilot CLI + │ + ▼ +專案 • 分支 • 工作階段 • 佇列 • Git • Diff • Secret 過濾 +``` ## 🚀 快速開始 @@ -203,7 +264,7 @@ bot 目前接受: - 文字訊息 - 圖片 - 當 `ENABLE_OPENAI_WHISPER_SPEECH_TO_TEXT=true` 且已安裝本機 Whisper 依賴時的語音訊息與音訊檔案 -- Codex 與 Copilot 目前只支援文字與圖片,不支援影片 +- Codex 與 Claude Code 工作階段支援文字與圖片;Copilot 工作階段目前僅支援文字。所有提供者皆不支援影片 ## 🤖 Telegram 指令 @@ -234,7 +295,7 @@ bot 目前接受: /switch - 顯示最新的工作階段,依新到舊排序。列表同時包含 bot 管理的工作階段與目前專案的本機 Codex/Copilot CLI 工作階段。 + 顯示最新的工作階段,依新到舊排序。列表同時包含 bot 管理的工作階段與目前專案的本機 Codex/Copilot/Claude Code CLI 工作階段。 /switch page <number> @@ -313,13 +374,21 @@ bot 目前接受: APP_LOCALE 共用 bot 訊息與指令說明所使用的 UI 語言。支援值:endefrjakonlthvizh-CNzh-HKzh-TW。 + + DEFAULT_AGENT_PROVIDER + 新工作階段預設使用的提供者:codexcopilotclaude。預設:codex。 + CODEX_BIN - 用來啟動 Codex CLI 的指令。預設:codex。 + 用來啟動 Codex CLI 的指令。應用程式會在初始化 .env_coding_agent_telegram 時嘗試自動偵測本機安裝的 Codex 路徑。您也可以使用 which codex 查看該路徑。 COPILOT_BIN - 用來啟動 Copilot CLI 的指令。預設:copilot。 + 用來啟動 Copilot CLI 的指令。應用程式會在初始化 .env_coding_agent_telegram 時嘗試自動偵測本機安裝的 Copilot 路徑。您也可以使用 which copilot 查看該路徑。 + + + CLAUDE_BIN + 用來啟動 Claude Code CLI 的指令。應用程式會在初始化 .env_coding_agent_telegram 時嘗試自動偵測本機安裝的 Claude 路徑。您也可以使用 which claude 查看該路徑。 CODEX_MODEL @@ -329,6 +398,10 @@ bot 目前接受: COPILOT_MODEL 可選的 Copilot model override。留空則使用 Copilot CLI 預設 model。例子:gpt-5.4claude-sonnet-4.6 GitHub Copilot supported models + + CLAUDE_MODEL + 可選的 Claude Code model override。留空則使用 Claude Code CLI 預設 model。例子:sonnetopushaiku Claude Code model configuration + CODEX_APPROVAL_POLICY 傳遞給 Codex 的 approval mode。預設:never。 @@ -341,6 +414,18 @@ bot 目前接受: CODEX_SKIP_GIT_REPO_CHECK 如果啟用,會永遠略過 Codex 的 trusted-repo 檢查。 + + CLAUDE_PERMISSION_MODE + 傳遞給 Claude Code 的 permission mode。可選值:defaultacceptEditsplanautodontAskbypassPermissionsmanual。預設:bypassPermissions(完全自動化,因為沒有互動式終端機可以核准提示)。 + + + CLAUDE_ALLOWED_TOOLS + 以逗號分隔的 Claude Code tool allowlist,使用 Claude Code 的 permission rule 語法。例子:Read,Edit,Bash(git *) + + + CLAUDE_DISALLOWED_TOOLS + 以逗號分隔的 Claude Code tool denylist。例子:Bash(rm *) + ENABLE_COMMIT_COMMAND 啟用 Telegram 的 /commit 指令。預設:false。 @@ -426,8 +511,10 @@ ALLOWED_CHAT_IDS=123456789 DEFAULT_AGENT_PROVIDER=codex CODEX_BIN=codex COPILOT_BIN=copilot +CLAUDE_BIN=claude CODEX_APPROVAL_POLICY=never CODEX_SANDBOX_MODE=workspace-write +CLAUDE_PERMISSION_MODE=bypassPermissions ENABLE_SENSITIVE_DIFF_FILTER=true ENABLE_SECRET_SCRUB_FILTER=true ``` @@ -491,7 +578,7 @@ bot 會強制這個限制,避免兩個 agent 同時寫入同一個 workspace ## ⚠️ Diff(檔案變更) -_在每次 代理執行 期間,bot 也會為 project 產生輕量的 前後快照,用來彙整已變更檔案並把 diff 傳回 Telegram。這個 快照 是由 bot app 自己建立,不是由 Codex 或 Copilot 建立。_ +_在每次 代理執行 期間,bot 也會為 project 產生輕量的 前後快照,用來彙整已變更檔案並把 diff 傳回 Telegram。這個 快照 是由 bot app 自己建立,不是由 Codex、Copilot 或 Claude Code 建立。_ **快照說明:** diff --git a/src/coding_agent_telegram/agent_runner.py b/src/coding_agent_telegram/agent_runner.py index 37e8b15..d5d94ac 100644 --- a/src/coding_agent_telegram/agent_runner.py +++ b/src/coding_agent_telegram/agent_runner.py @@ -90,6 +90,11 @@ def __init__( copilot_allow_tools: tuple[str, ...] = (), copilot_deny_tools: tuple[str, ...] = (), copilot_available_tools: tuple[str, ...] = (), + claude_bin: str = "claude", + claude_model: str = "", + claude_permission_mode: str = "bypassPermissions", + claude_allowed_tools: tuple[str, ...] = (), + claude_disallowed_tools: tuple[str, ...] = (), hard_timeout_seconds: int = 0, ) -> None: self.codex_bin = codex_bin @@ -105,6 +110,11 @@ def __init__( self.copilot_allow_tools = tuple(tool.strip() for tool in copilot_allow_tools if tool.strip()) self.copilot_deny_tools = tuple(tool.strip() for tool in copilot_deny_tools if tool.strip()) self.copilot_available_tools = tuple(tool.strip() for tool in copilot_available_tools if tool.strip()) + self.claude_bin = claude_bin + self.claude_model = claude_model.strip() + self.claude_permission_mode = claude_permission_mode.strip() + self.claude_allowed_tools = tuple(tool.strip() for tool in claude_allowed_tools if tool.strip()) + self.claude_disallowed_tools = tuple(tool.strip() for tool in claude_disallowed_tools if tool.strip()) # 0 = disabled. When > 0, the agent subprocess is killed after this many seconds. self.hard_timeout_seconds = max(0, int(hard_timeout_seconds)) self._running_processes: dict[str, subprocess.Popen] = {} @@ -210,12 +220,7 @@ def _extract_copilot_assistant_text(self, event: AssistantEvent) -> str: return "" return "\n".join(self._unique_text_fragments(self._collect_text_fragments(event))) - def _parse_jsonl( - self, - stdout: str, - *, - assistant_text_extractor: Callable[[AssistantEvent], str], - ) -> Tuple[Optional[str], bool, str, Optional[str], list[dict]]: + def _parse_json_lines(self, stdout: str) -> list[dict]: events: list[dict] = [] for line in stdout.splitlines(): line = line.strip() @@ -225,6 +230,15 @@ def _parse_jsonl( events.append(json.loads(line)) except json.JSONDecodeError: continue + return events + + def _parse_jsonl( + self, + stdout: str, + *, + assistant_text_extractor: Callable[[AssistantEvent], str], + ) -> Tuple[Optional[str], bool, str, Optional[str], list[dict]]: + events = self._parse_json_lines(stdout) session_id = None success = True @@ -255,6 +269,51 @@ def _parse_codex_jsonl(self, stdout: str) -> Tuple[Optional[str], bool, str, Opt def _parse_copilot_jsonl(self, stdout: str) -> Tuple[Optional[str], bool, str, Optional[str], list[dict]]: return self._parse_jsonl(stdout, assistant_text_extractor=self._extract_copilot_assistant_text) + def _extract_claude_assistant_text(self, event: AssistantEvent) -> str: + if isinstance(event, str): + return event + if isinstance(event, list): + return "\n".join(filter(None, [self._extract_claude_assistant_text(item) for item in event])) + if not isinstance(event, dict): + return "" + if event.get("type") == "result": + result_text = event.get("result") + return result_text if isinstance(result_text, str) else "" + return "\n".join(self._unique_text_fragments(self._collect_text_fragments(event))) + + def _parse_claude_jsonl(self, stdout: str) -> Tuple[Optional[str], bool, str, Optional[str], list[dict]]: + events = self._parse_json_lines(stdout) + + session_id = None + success = True + assistant_text = "" + error_message = None + + for ev in events: + if not isinstance(ev, dict): + continue + if isinstance(ev.get("session_id"), str): + validated = _validate_session_id(ev["session_id"]) + if validated: + session_id = validated + if ev.get("type") == "result": + is_error = bool(ev.get("is_error")) + success = not is_error + result_text = ev.get("result") + if isinstance(result_text, str) and result_text: + assistant_text = result_text + if is_error: + subtype = str(ev.get("subtype") or "").strip() + error_message = ( + result_text if isinstance(result_text, str) and result_text else (subtype or "Claude run failed.") + ) + else: + extracted_text = self._extract_claude_assistant_text(ev) + if extracted_text: + assistant_text = extracted_text + + return session_id, success, assistant_text, error_message, events + def _extract_codex_progress_text(self, chunk: str, *, is_stderr: bool) -> str: stripped = chunk.strip() if not stripped: @@ -321,6 +380,67 @@ def _extract_copilot_progress_event_text(self, event: AssistantEvent) -> str: return json.dumps(event, ensure_ascii=False) return str(event) + def _extract_claude_progress_text(self, chunk: str, *, is_stderr: bool) -> str: + stripped = chunk.strip() + if not stripped: + return "" + if is_stderr: + return stripped + try: + event = json.loads(stripped) + except json.JSONDecodeError: + return stripped + return self._extract_claude_progress_event_text(event) + + def _extract_claude_progress_event_text(self, event: AssistantEvent) -> str: + if isinstance(event, list): + return json.dumps(event, ensure_ascii=False) + if not isinstance(event, dict): + return str(event) + + event_type = str(event.get("type") or "") + + if event_type == "result": + result_text = event.get("result") + if isinstance(result_text, str) and result_text: + return result_text + return self._summarize_structured_event(event) + + if event_type == "system": + if str(event.get("subtype") or "") == "api_retry": + error_category = str(event.get("error") or "").strip() or "an error" + attempt = event.get("attempt") + max_retries = event.get("max_retries") + if attempt and max_retries: + return f"Retrying after {error_category} (attempt {attempt}/{max_retries})" + return f"Retrying after {error_category}" + return "" + + if event_type in {"assistant", "user"}: + message = event.get("message") + content = message.get("content") if isinstance(message, dict) else None + if isinstance(content, list): + text_fragments: list[str] = [] + tool_summaries: list[str] = [] + for block in content: + if not isinstance(block, dict): + continue + block_type = block.get("type") + if block_type == "text" and isinstance(block.get("text"), str): + text_fragments.append(block["text"]) + elif block_type == "tool_use": + tool_summaries.append(f"Using {block.get('name') or 'tool'}") + elif block_type == "tool_result": + tool_summaries.append("Tool result received") + unique_text = self._unique_text_fragments(text_fragments) + if unique_text: + return "\n".join(unique_text) + if tool_summaries: + return "; ".join(tool_summaries) + return self._summarize_structured_event(event) + + return self._summarize_structured_event(event) + def _summarize_progress_item(self, event: dict[str, Any]) -> str: event_type = str(event.get("type") or "") phase = event_type.split(".", 1)[1] if "." in event_type else "" @@ -398,6 +518,8 @@ def record_activity(chunk: str, *, is_stderr: bool) -> None: last_stderr = chunk.strip() if provider == "codex": progress_text = self._extract_codex_progress_text(chunk, is_stderr=is_stderr) + elif provider == "claude": + progress_text = self._extract_claude_progress_text(chunk, is_stderr=is_stderr) else: progress_text = self._extract_copilot_progress_text(chunk, is_stderr=is_stderr) should_report_progress = bool( @@ -505,6 +627,8 @@ def _watchdog() -> None: stderr = "".join(stderr_chunks) if provider == "codex": session_id, parsed_success, assistant_text, error_message, events = self._parse_codex_jsonl(stdout) + elif provider == "claude": + session_id, parsed_success, assistant_text, error_message, events = self._parse_claude_jsonl(stdout) else: session_id, parsed_success, assistant_text, error_message, events = self._parse_copilot_jsonl(stdout) @@ -711,6 +835,27 @@ def _copilot_base( ) return args + def _claude_base(self, user_message: str) -> list[str]: + args = [] + if self.claude_model: + args.extend(["--model", self.claude_model]) + if self.claude_permission_mode: + args.extend(["--permission-mode", self.claude_permission_mode]) + if self.claude_allowed_tools: + args.extend(["--allowedTools", ",".join(self.claude_allowed_tools)]) + if self.claude_disallowed_tools: + args.extend(["--disallowedTools", ",".join(self.claude_disallowed_tools)]) + args.extend( + [ + "--output-format", + "stream-json", + "--verbose", + "-p", + f"{self.PROMPT_PREFIX}{user_message}", + ] + ) + return args + def create_session( self, provider: str, @@ -748,6 +893,15 @@ def create_session( on_stall=on_stall, on_progress=on_progress, ) + elif provider == "claude": + args = [self.claude_bin, *self._claude_base(user_message)] + return self._run( + args, + provider="claude", + cwd=project_path, + on_stall=on_stall, + on_progress=on_progress, + ) else: return AgentRunResult(None, False, "", f"Unsupported provider: {provider}", []) @@ -790,5 +944,14 @@ def resume_session( on_stall=on_stall, on_progress=on_progress, ) + elif provider == "claude": + args = [self.claude_bin, "--resume", session_id, *self._claude_base(user_message)] + return self._run( + args, + provider="claude", + cwd=project_path, + on_stall=on_stall, + on_progress=on_progress, + ) else: return AgentRunResult(None, False, "", f"Unsupported provider: {provider}", []) diff --git a/src/coding_agent_telegram/bot.py b/src/coding_agent_telegram/bot.py index cb0ba4d..56bc217 100644 --- a/src/coding_agent_telegram/bot.py +++ b/src/coding_agent_telegram/bot.py @@ -8,6 +8,7 @@ from coding_agent_telegram.command_router import CommandRouter from coding_agent_telegram.i18n import DEFAULT_LOCALE, translate +from coding_agent_telegram.providers import SUPPORTED_PROVIDERS from coding_agent_telegram.session_store import SessionStoreError @@ -103,6 +104,11 @@ async def handle_error(update, context) -> None: async def handle_error(update, context) -> None: await build_error_handler(DEFAULT_LOCALE)(update, context) + +def provider_callback_pattern() -> str: + return rf"^provider:set:({'|'.join(SUPPORTED_PROVIDERS)})$" + + def build_application(token: str, router: CommandRouter, *, allowed_chat_ids: set[int]) -> Application: request = HTTPXRequest( connect_timeout=TELEGRAM_CONNECT_TIMEOUT_SECONDS, @@ -155,7 +161,13 @@ async def log_incoming_private_message(update, _context) -> None: app.add_handler(CommandHandler("pull", router.handle_pull, filters=allowed_private)) app.add_handler(CommandHandler("push", router.handle_push, filters=allowed_private)) app.add_handler(CommandHandler("abort", router.handle_abort, filters=allowed_private)) - app.add_handler(CallbackQueryHandler(router.handle_provider_callback, pattern=r"^provider:set:(codex|copilot)$", block=False)) + app.add_handler( + CallbackQueryHandler( + router.handle_provider_callback, + pattern=provider_callback_pattern(), + block=False, + ) + ) app.add_handler(CallbackQueryHandler(router.handle_queue_batch_callback, pattern=r"^queuebatch:(group|single|cancel)$", block=False)) app.add_handler(CallbackQueryHandler(router.handle_queue_continue_callback, pattern=r"^queuecontinue:(yes|no)$", block=False)) app.add_handler(CallbackQueryHandler(router.handle_branch_source_callback, pattern=r"^branchsource:[0-9a-f]{12}$", block=False)) diff --git a/src/coding_agent_telegram/cli.py b/src/coding_agent_telegram/cli.py index 3cf2299..232ce31 100644 --- a/src/coding_agent_telegram/cli.py +++ b/src/coding_agent_telegram/cli.py @@ -153,6 +153,14 @@ def main() -> None: raise store = SessionStore(cfg.state_file, cfg.state_backup_file) + cleared_pending_actions = store.clear_all_pending_actions() + if cleared_pending_actions: + logger.warning( + "Cleared %d stale pending_action entr%s left over from a previous run " + "(likely an unclean shutdown mid-message); affected chats can send messages again.", + cleared_pending_actions, + "y" if cleared_pending_actions == 1 else "ies", + ) runner = MultiAgentRunner( codex_bin=cfg.codex_bin, copilot_bin=cfg.copilot_bin, @@ -167,6 +175,11 @@ def main() -> None: copilot_allow_tools=cfg.copilot_allow_tools, copilot_deny_tools=cfg.copilot_deny_tools, copilot_available_tools=cfg.copilot_available_tools, + claude_bin=cfg.claude_bin, + claude_model=cfg.claude_model, + claude_permission_mode=cfg.claude_permission_mode, + claude_allowed_tools=cfg.claude_allowed_tools, + claude_disallowed_tools=cfg.claude_disallowed_tools, hard_timeout_seconds=cfg.agent_hard_timeout_seconds, ) try: diff --git a/src/coding_agent_telegram/config.py b/src/coding_agent_telegram/config.py index 919d25f..aee9bf8 100644 --- a/src/coding_agent_telegram/config.py +++ b/src/coding_agent_telegram/config.py @@ -6,6 +6,7 @@ import os import pwd import re +import shutil import locale as system_locale from dataclasses import dataclass from pathlib import Path @@ -13,6 +14,7 @@ from dotenv import load_dotenv from coding_agent_telegram.i18n import DEFAULT_LOCALE, normalize_locale +from coding_agent_telegram.providers import SUPPORTED_PROVIDERS DEFAULT_SNAPSHOT_TEXT_FILE_MAX_BYTES = 200_000 DEFAULT_MAX_TELEGRAM_MESSAGE_LENGTH = 3_000 @@ -36,8 +38,10 @@ class AppConfig: allowed_chat_ids: set[int] codex_bin: str copilot_bin: str + claude_bin: str codex_model: str copilot_model: str + claude_model: str copilot_autopilot: bool copilot_no_ask_user: bool copilot_allow_all: bool @@ -45,6 +49,9 @@ class AppConfig: copilot_allow_tools: tuple[str, ...] copilot_deny_tools: tuple[str, ...] copilot_available_tools: tuple[str, ...] + claude_permission_mode: str + claude_allowed_tools: tuple[str, ...] + claude_disallowed_tools: tuple[str, ...] codex_approval_policy: str codex_sandbox_mode: str codex_skip_git_repo_check: bool @@ -128,6 +135,59 @@ def _apply_initial_app_locale(template_text: str, app_locale: str) -> str: return f"{replacement}\n{template_text}" +# Fallback install locations checked when a CLI isn't on PATH, e.g. because the +# process runs under a service manager or a shell profile that startup.sh +# doesn't source. Order matters: earlier entries win when a bin exists in +# more than one of them. +_BIN_CANDIDATE_DIRS = ( + "~/.local/bin", + "~/bin", + "/opt/homebrew/bin", + "/usr/local/bin", + "/usr/bin", + "~/.npm-global/bin", + "~/.volta/bin", +) + + +def detect_installed_bin(bin_name: str) -> Optional[str]: + """Return the absolute path of a locally installed CLI, or None if not found. + + Deliberately does not resolve symlinks to their final target (e.g. a + version manager's ``.local/bin/claude`` -> ``.local/share/claude/versions/x.y.z``): + keeping the symlink path lets in-place CLI upgrades keep working without + editing the env file again. + """ + found = shutil.which(bin_name) + if found: + return str(Path(found).absolute()) + for candidate_dir in _BIN_CANDIDATE_DIRS: + candidate = Path(candidate_dir).expanduser() / bin_name + if candidate.is_file() and os.access(candidate, os.X_OK): + return str(candidate.absolute()) + return None + + +# Maps each provider's env var name to the CLI executable it looks for. +_PROVIDER_BIN_ENV_VARS = ( + ("CODEX_BIN", "codex"), + ("COPILOT_BIN", "copilot"), + ("CLAUDE_BIN", "claude"), +) + + +def _apply_detected_provider_bins(template_text: str) -> str: + text = template_text + for env_var, bin_name in _PROVIDER_BIN_ENV_VARS: + detected_path = detect_installed_bin(bin_name) + if not detected_path: + continue + pattern = rf"(?m)^{env_var}=.*$" + if re.search(pattern, text): + text = re.sub(pattern, f"{env_var}={detected_path}", text, count=1) + return text + + def create_initial_env_file(env_path: Path, template_path: Optional[Path] = None) -> str: if template_path is None: template_text = importlib.resources.files("coding_agent_telegram").joinpath("resources/.env.example").read_text( @@ -137,7 +197,9 @@ def create_initial_env_file(env_path: Path, template_path: Optional[Path] = None template_text = template_path.read_text(encoding="utf-8") app_locale = detect_system_locale() env_path.parent.mkdir(parents=True, exist_ok=True) - env_path.write_text(_apply_initial_app_locale(template_text, app_locale), encoding="utf-8") + template_text = _apply_initial_app_locale(template_text, app_locale) + template_text = _apply_detected_provider_bins(template_text) + env_path.write_text(template_text, encoding="utf-8") return app_locale @@ -199,8 +261,8 @@ def load_config(env_file: Optional[Path] = None) -> AppConfig: raise ValueError("Missing required config: TELEGRAM_BOT_TOKENS") if not allowed_ids: raise ValueError("Missing required config: ALLOWED_CHAT_IDS") - if provider not in {"codex", "copilot"}: - raise ValueError("DEFAULT_AGENT_PROVIDER must be either codex or copilot") + if provider not in SUPPORTED_PROVIDERS: + raise ValueError(f"DEFAULT_AGENT_PROVIDER must be one of: {', '.join(SUPPORTED_PROVIDERS)}") workspace_root = Path(workspace_root_raw).expanduser().resolve() app_internal_root = resolve_app_internal_root(workspace_root) @@ -214,8 +276,10 @@ def load_config(env_file: Optional[Path] = None) -> AppConfig: allowed_chat_ids=allowed_ids, codex_bin=os.getenv("CODEX_BIN", "codex"), copilot_bin=os.getenv("COPILOT_BIN", "copilot"), + claude_bin=os.getenv("CLAUDE_BIN", "claude"), codex_model=os.getenv("CODEX_MODEL", "").strip(), copilot_model=os.getenv("COPILOT_MODEL", "").strip(), + claude_model=os.getenv("CLAUDE_MODEL", "").strip(), copilot_autopilot=_parse_bool(os.getenv("COPILOT_AUTOPILOT", "true"), default=True), copilot_no_ask_user=_parse_bool(os.getenv("COPILOT_NO_ASK_USER", "true"), default=True), copilot_allow_all=_parse_bool(os.getenv("COPILOT_ALLOW_ALL", "true"), default=True), @@ -223,6 +287,9 @@ def load_config(env_file: Optional[Path] = None) -> AppConfig: copilot_allow_tools=tuple(_parse_csv_env("COPILOT_ALLOW_TOOLS")), copilot_deny_tools=tuple(_parse_csv_env("COPILOT_DENY_TOOLS")), copilot_available_tools=tuple(_parse_csv_env("COPILOT_AVAILABLE_TOOLS")), + claude_permission_mode=os.getenv("CLAUDE_PERMISSION_MODE", "bypassPermissions").strip(), + claude_allowed_tools=tuple(_parse_csv_env("CLAUDE_ALLOWED_TOOLS")), + claude_disallowed_tools=tuple(_parse_csv_env("CLAUDE_DISALLOWED_TOOLS")), codex_approval_policy=os.getenv("CODEX_APPROVAL_POLICY", "never"), codex_sandbox_mode=os.getenv("CODEX_SANDBOX_MODE", "workspace-write"), codex_skip_git_repo_check=_parse_bool(os.getenv("CODEX_SKIP_GIT_REPO_CHECK", "false")), diff --git a/src/coding_agent_telegram/native_claude_sessions.py b/src/coding_agent_telegram/native_claude_sessions.py new file mode 100644 index 0000000..220c509 --- /dev/null +++ b/src/coding_agent_telegram/native_claude_sessions.py @@ -0,0 +1,112 @@ +from __future__ import annotations + +import json +import os +from pathlib import Path + +from coding_agent_telegram.native_session_types import NativeSessionRecord +from coding_agent_telegram.native_session_utils import normalize_init_text, path_matches_project + + +def claude_projects_root() -> Path: + config_dir = os.environ.get("CLAUDE_CONFIG_DIR", "").strip() + base = Path(config_dir).expanduser() if config_dir else Path.home() / ".claude" + return base / "projects" + + +def _first_text_from_content(content: object) -> str: + if isinstance(content, str): + return content + if isinstance(content, list): + for block in content: + if isinstance(block, dict) and block.get("type") == "text" and isinstance(block.get("text"), str): + return block["text"] + return "" + + +def _read_claude_session_file(jsonl_path: Path, project_path: Path, project_folder: str) -> NativeSessionRecord | None: + session_id = jsonl_path.stem + branch_name = "" + created_at = "" + updated_at = "" + first_user_message = "" + matched = False + + try: + with jsonl_path.open(encoding="utf-8") as handle: + for raw_line in handle: + line = raw_line.strip() + if not line: + continue + try: + entry = json.loads(line) + except json.JSONDecodeError: + continue + if not isinstance(entry, dict): + continue + + entry_cwd = entry.get("cwd") + if isinstance(entry_cwd, str) and entry_cwd and not matched: + if not path_matches_project(entry_cwd, project_path): + return None + matched = True + + entry_branch = entry.get("gitBranch") + if isinstance(entry_branch, str) and entry_branch: + branch_name = entry_branch + + timestamp = entry.get("timestamp") + if isinstance(timestamp, str) and timestamp: + if not created_at: + created_at = timestamp + updated_at = timestamp + + if not first_user_message and entry.get("type") == "user" and not entry.get("isMeta"): + message = entry.get("message") + if isinstance(message, dict): + text = _first_text_from_content(message.get("content")).strip() + if text: + first_user_message = text + except OSError: + return None + + if not matched: + return None + + init_text = first_user_message or session_id + return NativeSessionRecord( + session_id=session_id, + name=normalize_init_text(init_text, fallback=f"Claude session in {project_folder}"), + project_folder=project_folder, + provider="claude", + branch_name=branch_name, + created_at=created_at, + updated_at=updated_at, + source_label="native claude", + initialized_from=normalize_init_text(init_text, fallback="Native Claude session"), + ) + + +def discover_claude_sessions(project_path: Path, project_folder: str) -> list[NativeSessionRecord]: + projects_root = claude_projects_root() + if not projects_root.exists(): + return [] + + try: + project_dirs = [entry for entry in projects_root.iterdir() if entry.is_dir()] + except OSError: + return [] + + records: list[NativeSessionRecord] = [] + for project_dir in project_dirs: + try: + jsonl_paths = sorted(project_dir.glob("*.jsonl")) + except OSError: + continue + for jsonl_path in jsonl_paths: + record = _read_claude_session_file(jsonl_path, project_path, project_folder) + if record is not None: + records.append(record) + + records.sort(key=lambda item: (item.updated_at or item.created_at, item.session_id), reverse=True) + return records diff --git a/src/coding_agent_telegram/native_sessions.py b/src/coding_agent_telegram/native_sessions.py index 3413a9c..a4776f4 100644 --- a/src/coding_agent_telegram/native_sessions.py +++ b/src/coding_agent_telegram/native_sessions.py @@ -2,6 +2,7 @@ from pathlib import Path +from coding_agent_telegram.native_claude_sessions import discover_claude_sessions from coding_agent_telegram.native_codex_sessions import discover_codex_sessions from coding_agent_telegram.native_copilot_sessions import discover_copilot_sessions from coding_agent_telegram.native_session_types import NativeSessionRecord @@ -20,4 +21,7 @@ def discover_native_project_sessions( if provider in (None, "", "copilot"): for record in discover_copilot_sessions(project_path, project_folder): sessions.setdefault(record.session_id, record) + if provider in (None, "", "claude"): + for record in discover_claude_sessions(project_path, project_folder): + sessions.setdefault(record.session_id, record) return sessions diff --git a/src/coding_agent_telegram/providers.py b/src/coding_agent_telegram/providers.py new file mode 100644 index 0000000..6f8a20b --- /dev/null +++ b/src/coding_agent_telegram/providers.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +"""Shared provider identifiers and display labels used across config, router, and runtime code.""" + +SUPPORTED_PROVIDERS: tuple[str, ...] = ("codex", "copilot", "claude") + +_PROVIDER_LABELS: dict[str, str] = { + "codex": "Codex", + "copilot": "Copilot", + "claude": "Claude", +} + + +def provider_label(provider: str) -> str: + """Return a human-friendly display label for a provider identifier.""" + return _PROVIDER_LABELS.get(provider, provider.capitalize() if provider else "") diff --git a/src/coding_agent_telegram/resources/.env.example b/src/coding_agent_telegram/resources/.env.example index 582d866..408409c 100644 --- a/src/coding_agent_telegram/resources/.env.example +++ b/src/coding_agent_telegram/resources/.env.example @@ -19,18 +19,24 @@ TELEGRAM_BOT_TOKENS= # Comma-separated allowed private chat IDs. ALLOWED_CHAT_IDS= -# Codex CLI executable name or path. +# Codex CLI executable name or path, Use `which codex` to view the path CODEX_BIN=codex -# Copilot CLI executable name or path. +# Copilot CLI executable name or path. Use `which copilot` to view the path COPILOT_BIN=copilot +# Claude Code CLI executable name or path. Use `which claude` to view the path +CLAUDE_BIN=claude + # Optional Codex model override. CODEX_MODEL= # Optional Copilot model override. COPILOT_MODEL= +# Optional Claude Code model override (e.g. sonnet, opus, haiku). +CLAUDE_MODEL= + # Whether Copilot should run in autopilot mode. COPILOT_AUTOPILOT=true @@ -52,6 +58,17 @@ COPILOT_DENY_TOOLS= # Comma-separated list of all Copilot tools exposed to the agent. COPILOT_AVAILABLE_TOOLS= +# Claude Code permission mode. Controls how much Claude can do without prompting. +# One of: default, acceptEdits, plan, auto, dontAsk, bypassPermissions, manual. +# Default: bypassPermissions (fully autonomous, no interactive terminal to approve anything). +CLAUDE_PERMISSION_MODE=bypassPermissions + +# Comma-separated Claude Code tool allowlist (permission rule syntax, e.g. Read,Edit,Bash(git *)). +CLAUDE_ALLOWED_TOOLS= + +# Comma-separated Claude Code tool denylist. +CLAUDE_DISALLOWED_TOOLS= + # Codex approval policy. CODEX_APPROVAL_POLICY=never @@ -106,7 +123,7 @@ OPENAI_WHISPER_MODEL=base # Timeout for a single Whisper transcription call, in seconds. OPENAI_WHISPER_TIMEOUT_SECONDS=120 -# Default agent provider for new sessions: codex or copilot. +# Default agent provider for new sessions: codex, copilot, or claude. DEFAULT_AGENT_PROVIDER=codex # Hard time limit (seconds) for a single agent run. diff --git a/src/coding_agent_telegram/resources/locales/en.json b/src/coding_agent_telegram/resources/locales/en.json index 4d0678f..8dfd74e 100644 --- a/src/coding_agent_telegram/resources/locales/en.json +++ b/src/coding_agent_telegram/resources/locales/en.json @@ -36,7 +36,7 @@ "git.push_in_progress": "Pushing branch `{branch_name}` to `origin`...", "git.usage_pull": "Usage: /pull", "git.usage_push": "Usage: /push", - "message.photo_only_codex": "Photo attachments are currently supported only for codex sessions.", + "message.photo_only_codex": "Photo attachments are currently supported only for Codex and Claude sessions.", "message.question_queued": "Question queued as Q{question_number}. It will run after the current agent task finishes.", "message.voice_speech_to_text_disabled": "Voice messages are not enabled.\nSet ENABLE_OPENAI_WHISPER_SPEECH_TO_TEXT=true and install the local Whisper prerequisites first.", "message.unsupported_message_type": "Unsupported message type.\nThis bot currently accepts text messages, photos, voice messages, and audio files.", diff --git a/src/coding_agent_telegram/router/git_commands.py b/src/coding_agent_telegram/router/git_commands.py index 5009102..9fa626b 100644 --- a/src/coding_agent_telegram/router/git_commands.py +++ b/src/coding_agent_telegram/router/git_commands.py @@ -21,7 +21,10 @@ class GitCommandMixin: 'Only include files you intentionally modified for this task. ' 'Do not include unrelated changed files. ' 'Do not include untracked files unless they were created for this task and are clearly required. ' - 'Output only a single executable command in this format with \\ if there is line break: git add && git commit -m "".' + 'Write the message as plain text: use one -m "" flag per summary or bullet line (git joins multiple -m values into separate paragraphs automatically) instead of embedding literal newlines inside a single -m value. ' + 'Do not use $(...), backticks, heredocs (< && git commit -m "" -m "" -m "" ... ' + 'If the file list is long, you may wrap it across lines with a trailing \\ for readability, but keep every -m value on its own single line.' ) @staticmethod diff --git a/src/coding_agent_telegram/router/message_commands.py b/src/coding_agent_telegram/router/message_commands.py index a4a6769..923dfc8 100644 --- a/src/coding_agent_telegram/router/message_commands.py +++ b/src/coding_agent_telegram/router/message_commands.py @@ -95,7 +95,7 @@ async def handle_photo(self, update: Update, context: ContextTypes.DEFAULT_TYPE) if session is None or project_path is None: return - if session.get("provider", "codex") != "codex": + if session.get("provider", "codex") not in ("codex", "claude"): await send_text(update, context, self._t(update, "message.photo_only_codex")) return diff --git a/src/coding_agent_telegram/router/session_common.py b/src/coding_agent_telegram/router/session_common.py index 2ee0669..be601ca 100644 --- a/src/coding_agent_telegram/router/session_common.py +++ b/src/coding_agent_telegram/router/session_common.py @@ -2,6 +2,8 @@ from datetime import datetime +from coding_agent_telegram.providers import SUPPORTED_PROVIDERS + class SessionCommonMixin: def _next_available_session_name(self, chat_id: int, base_name: str) -> str: @@ -21,7 +23,7 @@ def _next_available_session_name(self, chat_id: int, base_name: str) -> str: def _selected_provider(self, chat_state: dict[str, object]) -> str: provider = str(chat_state.get("current_provider") or "").strip().lower() - return provider if provider in {"codex", "copilot"} else "" + return provider if provider in SUPPORTED_PROVIDERS else "" def _active_session_matches_current_context(self, chat_state: dict[str, object]) -> bool: active_session_id = chat_state.get("active_session_id") diff --git a/src/coding_agent_telegram/router/session_provider_commands.py b/src/coding_agent_telegram/router/session_provider_commands.py index e7a22e4..fded396 100644 --- a/src/coding_agent_telegram/router/session_provider_commands.py +++ b/src/coding_agent_telegram/router/session_provider_commands.py @@ -6,6 +6,7 @@ from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ContextTypes +from coding_agent_telegram.providers import SUPPORTED_PROVIDERS, provider_label from coding_agent_telegram.telegram_sender import send_text from .base import require_allowed_chat @@ -16,7 +17,11 @@ class SessionProviderCommandMixin: PROVIDER_BIN_MISSING_CACHE_TTL_SECONDS = 5 * 60 def _provider_bin(self, provider: str) -> str: - return self.deps.cfg.codex_bin if provider == "codex" else self.deps.cfg.copilot_bin + if provider == "codex": + return self.deps.cfg.codex_bin + if provider == "claude": + return self.deps.cfg.claude_bin + return self.deps.cfg.copilot_bin def _provider_available(self, provider: str) -> bool: bin_name = self._provider_bin(provider) @@ -46,34 +51,33 @@ async def _ensure_provider_available( ) -> bool: if self._provider_available(provider): return True - provider_label = "Codex" if provider == "codex" else "Copilot" await send_text( update, context, - self._t(update, "provider.cli_not_found", provider_label=provider_label, bin_name=self._provider_bin(provider)), + self._t( + update, + "provider.cli_not_found", + provider_label=provider_label(provider), + bin_name=self._provider_bin(provider), + ), ) return False def _build_provider_keyboard(self, current_provider: str) -> InlineKeyboardMarkup: def button_label(provider: str) -> str: - provider_label = "Codex" if provider == "codex" else "Copilot" status = self._t(None, "provider.status_available") if self._provider_available(provider) else self._t(None, "provider.status_missing") marker = self._t(None, "provider.status_current") if provider == current_provider else status - return f"{provider_label} ({marker})" + return f"{provider_label(provider)} ({marker})" return InlineKeyboardMarkup( [ [ InlineKeyboardButton( - button_label("codex"), - callback_data="provider:set:codex", + button_label(provider), + callback_data=f"provider:set:{provider}", api_kwargs={"style": "success"}, - ), - InlineKeyboardButton( - button_label("copilot"), - callback_data="provider:set:copilot", - api_kwargs={"style": "success"}, - ), + ) + for provider in SUPPORTED_PROVIDERS ] ] ) @@ -110,15 +114,19 @@ async def handle_provider_callback(self, update: Update, context: ContextTypes.D if await self._notify_if_current_project_busy(update, context): return _, _, provider = query.data.partition("provider:set:") - if provider not in {"codex", "copilot"}: + if provider not in SUPPORTED_PROVIDERS: return chat_id = update.effective_chat.id previous_provider = self._selected_provider(self.deps.store.get_chat_state(self.deps.bot_id, chat_id)) if not self._provider_available(provider): - provider_label = "Codex" if provider == "codex" else "Copilot" await query.edit_message_text( - self._t(update, "provider.cli_not_found_install_first", provider_label=provider_label, bin_name=self._provider_bin(provider)) + self._t( + update, + "provider.cli_not_found_install_first", + provider_label=provider_label(provider), + bin_name=self._provider_bin(provider), + ) ) return diff --git a/src/coding_agent_telegram/session_runtime.py b/src/coding_agent_telegram/session_runtime.py index 4dcf2f7..cd3fe7e 100644 --- a/src/coding_agent_telegram/session_runtime.py +++ b/src/coding_agent_telegram/session_runtime.py @@ -29,6 +29,7 @@ from coding_agent_telegram.filters import is_sensitive_path, resolve_project_path from coding_agent_telegram.git_utils import GitWorkspaceManager from coding_agent_telegram.i18n import locale_from_update, translate +from coding_agent_telegram.providers import provider_label as provider_display_label from coding_agent_telegram.session_store import SessionStore from coding_agent_telegram.telegram_sender import ( markdownish_to_html, @@ -699,7 +700,7 @@ async def _send_assistant_chunks( ) continue - provider_label = "Copilot" if provider == "copilot" else "Codex" + provider_label = provider_display_label(provider) or "Codex" title_prefix = ( self._t(update, "runtime.provider_output_single", provider=provider_label) if total == 1 diff --git a/src/coding_agent_telegram/session_store.py b/src/coding_agent_telegram/session_store.py index 58fd867..e32ba9b 100644 --- a/src/coding_agent_telegram/session_store.py +++ b/src/coding_agent_telegram/session_store.py @@ -206,6 +206,28 @@ def mutate(chat_data: dict[str, Any]) -> None: self._mutate_chat_data(bot_id, chat_id, mutate, create=True) + def clear_all_pending_actions(self) -> int: + """Drop any persisted `pending_action` left over from a previous process. + + `pending_action` marks a message as "currently being run through an agent + subprocess", which only ever makes sense within the process that started + it — in-memory locks and running-process handles never survive a restart. + If the process is killed or crashes mid-run, the flag stays set in the + state file forever, and every future incoming message gets silently + queued (never drained) because `_should_queue_incoming_message` treats a + stale pending_action the same as a real in-flight run. Call this once at + startup so a crash never permanently wedges a chat. + """ + + def mutate(state: dict[str, Any]) -> int: + cleared = 0 + for chat_data in state.get("chats", {}).values(): + if isinstance(chat_data, dict) and chat_data.pop("pending_action", None) is not None: + cleared += 1 + return cleared + + return self._mutate_state(mutate) + def create_session( self, bot_id: str, diff --git a/startup.sh b/startup.sh index 9b61be4..9775095 100755 --- a/startup.sh +++ b/startup.sh @@ -176,13 +176,14 @@ fi DEFAULT_AGENT_PROVIDER="${DEFAULT_AGENT_PROVIDER:-codex}" CODEX_BIN="${CODEX_BIN:-codex}" COPILOT_BIN="${COPILOT_BIN:-copilot}" +CLAUDE_BIN="${CLAUDE_BIN:-claude}" case "$DEFAULT_AGENT_PROVIDER" in codex) if ! command -v "$CODEX_BIN" >/dev/null 2>&1; then echo "Error: Codex CLI not found: $CODEX_BIN" >&2 echo "Check DEFAULT_AGENT_PROVIDER and CODEX_BIN in $ENV_FILE." >&2 - echo "If this machine only has Copilot, set DEFAULT_AGENT_PROVIDER=copilot." >&2 + echo "If this machine only has Copilot or Claude Code, set DEFAULT_AGENT_PROVIDER=copilot or claude." >&2 exit 1 fi ;; @@ -190,12 +191,20 @@ case "$DEFAULT_AGENT_PROVIDER" in if ! command -v "$COPILOT_BIN" >/dev/null 2>&1; then echo "Error: Copilot CLI not found: $COPILOT_BIN" >&2 echo "Check DEFAULT_AGENT_PROVIDER and COPILOT_BIN in $ENV_FILE." >&2 - echo "If this machine only has Codex, set DEFAULT_AGENT_PROVIDER=codex." >&2 + echo "If this machine only has Codex or Claude Code, set DEFAULT_AGENT_PROVIDER=codex or claude." >&2 + exit 1 + fi + ;; + claude) + if ! command -v "$CLAUDE_BIN" >/dev/null 2>&1; then + echo "Error: Claude Code CLI not found: $CLAUDE_BIN" >&2 + echo "Check DEFAULT_AGENT_PROVIDER and CLAUDE_BIN in $ENV_FILE." >&2 + echo "If this machine only has Codex or Copilot, set DEFAULT_AGENT_PROVIDER=codex or copilot." >&2 exit 1 fi ;; *) - echo "Error: DEFAULT_AGENT_PROVIDER must be codex or copilot." >&2 + echo "Error: DEFAULT_AGENT_PROVIDER must be codex, copilot, or claude." >&2 exit 1 ;; esac diff --git a/tests/test_agent_runner.py b/tests/test_agent_runner.py index 5a94a1e..7364d39 100644 --- a/tests/test_agent_runner.py +++ b/tests/test_agent_runner.py @@ -515,6 +515,253 @@ def test_copilot_runner_passes_tool_permission_flags(monkeypatch): assert "--available-tools" not in calls[0][0] +# --------------------------------------------------------------------------- +# Claude provider +# --------------------------------------------------------------------------- + + +def test_claude_runner_uses_print_mode_shape(monkeypatch): + calls = [] + monkeypatch.setattr( + "coding_agent_telegram.agent_runner.subprocess.Popen", + make_fake_popen( + calls, + process_stdout='{"type":"result","subtype":"success","is_error":false,"result":"Done.","session_id":"sess_claude"}\n', + ), + ) + + runner = MultiAgentRunner( + codex_bin="codex", + copilot_bin="copilot", + approval_policy="never", + sandbox_mode="workspace-write", + ) + + result = runner.create_session("claude", Path("/tmp/project"), "hello") + + assert calls[0][0] == [ + "claude", + "--permission-mode", + "bypassPermissions", + "--output-format", + "stream-json", + "--verbose", + "-p", + runner.PROMPT_PREFIX + "hello", + ] + assert calls[0][1] == Path("/tmp/project") + assert result.success is True + assert result.session_id == "sess_claude" + assert result.assistant_text == "Done." + + +def test_claude_runner_resume_uses_resume_flag(monkeypatch): + calls = [] + monkeypatch.setattr( + "coding_agent_telegram.agent_runner.subprocess.Popen", + make_fake_popen( + calls, + process_stdout='{"type":"result","subtype":"success","is_error":false,"result":"Done again.","session_id":"sess_claude"}\n', + ), + ) + + runner = MultiAgentRunner( + codex_bin="codex", + copilot_bin="copilot", + approval_policy="never", + sandbox_mode="workspace-write", + ) + + result = runner.resume_session("claude", "sess_1", Path("/tmp/project"), "hello again") + + assert calls[0][0][:3] == ["claude", "--resume", "sess_1"] + assert result.session_id == "sess_claude" + assert result.assistant_text == "Done again." + + +def test_claude_runner_passes_model_and_tool_flags_when_configured(monkeypatch): + calls = [] + monkeypatch.setattr("coding_agent_telegram.agent_runner.subprocess.Popen", make_fake_popen(calls)) + + runner = MultiAgentRunner( + codex_bin="codex", + copilot_bin="copilot", + approval_policy="never", + sandbox_mode="workspace-write", + claude_model="opus", + claude_permission_mode="dontAsk", + claude_allowed_tools=("Read", "Edit"), + claude_disallowed_tools=("Bash(rm *)",), + ) + + runner.create_session("claude", Path("/tmp/project"), "hello") + + assert calls[0][0] == [ + "claude", + "--model", + "opus", + "--permission-mode", + "dontAsk", + "--allowedTools", + "Read,Edit", + "--disallowedTools", + "Bash(rm *)", + "--output-format", + "stream-json", + "--verbose", + "-p", + runner.PROMPT_PREFIX + "hello", + ] + + +def test_claude_runner_reports_failure_from_result_event(monkeypatch): + calls = [] + monkeypatch.setattr( + "coding_agent_telegram.agent_runner.subprocess.Popen", + make_fake_popen( + calls, + process_stdout='{"type":"result","subtype":"error_max_turns","is_error":true,"result":"","session_id":"sess_claude"}\n', + ), + ) + + runner = MultiAgentRunner( + codex_bin="codex", + copilot_bin="copilot", + approval_policy="never", + sandbox_mode="workspace-write", + ) + + result = runner.create_session("claude", Path("/tmp/project"), "hello") + + assert result.success is False + assert result.error_message == "error_max_turns" + assert result.session_id == "sess_claude" + + +def test_claude_runner_extracts_assistant_message_text_as_progress(monkeypatch): + calls = [] + monkeypatch.setattr( + "coding_agent_telegram.agent_runner.subprocess.Popen", + make_fake_popen( + calls, + process_stdout=( + '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"Looking at the code now."}]},' + '"session_id":"sess_claude"}\n' + ), + ), + ) + + runner = MultiAgentRunner( + codex_bin="codex", + copilot_bin="copilot", + approval_policy="never", + sandbox_mode="workspace-write", + ) + runner.PROGRESS_UPDATE_INTERVAL_SECONDS = 0 + captured = [] + + runner.create_session("claude", Path("/tmp/project"), "hello", on_progress=captured.append) + + assert captured + assert captured[0].text == "Looking at the code now." + + +def test_claude_runner_summarizes_tool_use_as_progress(monkeypatch): + calls = [] + monkeypatch.setattr( + "coding_agent_telegram.agent_runner.subprocess.Popen", + make_fake_popen( + calls, + process_stdout=( + '{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","name":"Bash","input":{"command":"ls"}}]},' + '"session_id":"sess_claude"}\n' + ), + ), + ) + + runner = MultiAgentRunner( + codex_bin="codex", + copilot_bin="copilot", + approval_policy="never", + sandbox_mode="workspace-write", + ) + runner.PROGRESS_UPDATE_INTERVAL_SECONDS = 0 + captured = [] + + runner.create_session("claude", Path("/tmp/project"), "hello", on_progress=captured.append) + + assert captured + assert captured[0].text == "Using Bash" + + +def test_claude_runner_progress_surfaces_api_retry(monkeypatch): + calls = [] + monkeypatch.setattr( + "coding_agent_telegram.agent_runner.subprocess.Popen", + make_fake_popen( + calls, + process_stdout=( + '{"type":"system","subtype":"api_retry","attempt":1,"max_retries":3,"retry_delay_ms":500,' + '"error_status":429,"error":"rate_limit","uuid":"u1","session_id":"sess_claude"}\n' + ), + ), + ) + + runner = MultiAgentRunner( + codex_bin="codex", + copilot_bin="copilot", + approval_policy="never", + sandbox_mode="workspace-write", + ) + runner.PROGRESS_UPDATE_INTERVAL_SECONDS = 0 + captured = [] + + runner.create_session("claude", Path("/tmp/project"), "hello", on_progress=captured.append) + + assert captured + assert captured[0].text == "Retrying after rate_limit (attempt 1/3)" + + +def test_claude_runner_rejects_flag_like_session_id(monkeypatch): + calls = [] + monkeypatch.setattr( + "coding_agent_telegram.agent_runner.subprocess.Popen", + make_fake_popen( + calls, + process_stdout='{"type":"result","is_error":false,"result":"ok","session_id":"--malicious"}\n', + ), + ) + + runner = MultiAgentRunner( + codex_bin="codex", + copilot_bin="copilot", + approval_policy="never", + sandbox_mode="workspace-write", + ) + + result = runner.create_session("claude", Path("/tmp/project"), "hello") + + assert result.session_id is None + + +def test_claude_runner_ignores_image_paths_without_error(monkeypatch): + calls = [] + monkeypatch.setattr("coding_agent_telegram.agent_runner.subprocess.Popen", make_fake_popen(calls)) + + runner = MultiAgentRunner( + codex_bin="codex", + copilot_bin="copilot", + approval_policy="never", + sandbox_mode="workspace-write", + ) + + image_path = Path("/tmp/project/.coding-agent-telegram/telegram_attachments/img.jpg") + result = runner.create_session("claude", Path("/tmp/project"), "hello", image_paths=(image_path,)) + + assert "--image" not in calls[0][0] + assert result.success is True + + # --------------------------------------------------------------------------- # _validate_session_id # --------------------------------------------------------------------------- diff --git a/tests/test_bot.py b/tests/test_bot.py index 6348af3..24e9439 100644 --- a/tests/test_bot.py +++ b/tests/test_bot.py @@ -184,3 +184,20 @@ class FakeApp: ) assert len(set_calls) >= 1 + + +# --------------------------------------------------------------------------- +# provider_callback_pattern +# --------------------------------------------------------------------------- + + +def test_provider_callback_pattern_matches_all_supported_providers(): + import re + + from coding_agent_telegram.bot import provider_callback_pattern + + pattern = re.compile(provider_callback_pattern()) + assert pattern.match("provider:set:codex") + assert pattern.match("provider:set:copilot") + assert pattern.match("provider:set:claude") + assert pattern.match("provider:set:gemini") is None diff --git a/tests/test_command_router.py b/tests/test_command_router.py index 927d36c..e5576df 100644 --- a/tests/test_command_router.py +++ b/tests/test_command_router.py @@ -518,8 +518,10 @@ def make_config(tmp_path: Path, *, locale: str = "en") -> AppConfig: allowed_chat_ids={123}, codex_bin=sys.executable, copilot_bin=sys.executable, + claude_bin=sys.executable, codex_model="", copilot_model="", + claude_model="", copilot_autopilot=True, copilot_no_ask_user=True, copilot_allow_all=True, @@ -527,6 +529,9 @@ def make_config(tmp_path: Path, *, locale: str = "en") -> AppConfig: copilot_allow_tools=(), copilot_deny_tools=(), copilot_available_tools=(), + claude_permission_mode="bypassPermissions", + claude_allowed_tools=(), + claude_disallowed_tools=(), codex_approval_policy="never", codex_sandbox_mode="workspace-write", codex_skip_git_repo_check=False, @@ -1621,10 +1626,13 @@ def test_provider_command_sends_inline_buttons(tmp_path: Path): buttons = keyboard.inline_keyboard[0] assert buttons[0].callback_data == "provider:set:codex" assert buttons[1].callback_data == "provider:set:copilot" + assert buttons[2].callback_data == "provider:set:claude" assert "missing" in buttons[0].text assert "current" in buttons[1].text + assert "missing" in buttons[2].text assert buttons[0].api_kwargs == {"style": "success"} assert buttons[1].api_kwargs == {"style": "success"} + assert buttons[2].api_kwargs == {"style": "success"} def test_provider_callback_updates_current_provider(tmp_path: Path): @@ -1663,6 +1671,71 @@ async def fake_edit(text): assert store.get_chat_state("bot-a", 123)["current_provider"] == "copilot" +def test_provider_callback_updates_current_provider_to_claude(tmp_path: Path): + runner = DummyRunner() + cfg = make_config(tmp_path) + store = SessionStore(cfg.state_file, cfg.state_backup_file) + router = CommandRouter(RouterDeps(cfg=cfg, store=store, agent_runner=runner, bot_id="bot-a")) + router._provider_available = lambda provider: True + + answers = [] + edited = [] + update = SimpleNamespace( + effective_chat=SimpleNamespace(id=123, type="private"), + callback_query=SimpleNamespace( + data="provider:set:claude", + answer=None, + edit_message_text=None, + ), + ) + bot = FakeBot() + context = SimpleNamespace(args=[], bot=bot) + + async def fake_answer(): + answers.append("answered") + + async def fake_edit(text): + edited.append(text) + + update.callback_query.answer = fake_answer + update.callback_query.edit_message_text = fake_edit + + asyncio.run(router.handle_provider_callback(update, context)) + + assert answers == ["answered"] + assert edited == ["Current provider set to: claude"] + assert store.get_chat_state("bot-a", 123)["current_provider"] == "claude" + + +def test_provider_callback_rejects_unknown_provider(tmp_path: Path): + runner = DummyRunner() + cfg = make_config(tmp_path) + store = SessionStore(cfg.state_file, cfg.state_backup_file) + router = CommandRouter(RouterDeps(cfg=cfg, store=store, agent_runner=runner, bot_id="bot-a")) + router._provider_available = lambda provider: True + + answers = [] + update = SimpleNamespace( + effective_chat=SimpleNamespace(id=123, type="private"), + callback_query=SimpleNamespace( + data="provider:set:gemini", + answer=None, + edit_message_text=None, + ), + ) + bot = FakeBot() + context = SimpleNamespace(args=[], bot=bot) + + async def fake_answer(): + answers.append("answered") + + update.callback_query.answer = fake_answer + + asyncio.run(router.handle_provider_callback(update, context)) + + assert store.get_chat_state("bot-a", 123).get("current_provider") is None + + def test_provider_callback_continues_pending_new_session(tmp_path: Path): backend = tmp_path / "backend" backend.mkdir() @@ -2325,6 +2398,32 @@ def test_photo_message_is_saved_and_forwarded_to_codex(tmp_path: Path): assert "what is shown here?" in runner.resume_calls[-1]["user_message"] +def test_photo_message_is_saved_and_forwarded_to_claude(tmp_path: Path): + backend = tmp_path / "backend" + backend.mkdir() + runner = DummyRunner() + cfg = make_config(tmp_path) + store = SessionStore(cfg.state_file, cfg.state_backup_file) + store.create_session("bot-a", 123, "sess_photo", "photo-session", "backend", "claude") + router = CommandRouter(RouterDeps(cfg=cfg, store=store, agent_runner=runner, bot_id="bot-a")) + router.git = FakeGitManager(is_git_repo=False) + + photo = FakePhotoSize(FakeTelegramFile(b"fake-image-bytes", "photos/pic.png")) + update = SimpleNamespace( + effective_chat=SimpleNamespace(id=123, type="private"), + message=SimpleNamespace(text=None, photo=[photo], caption="what is shown here?"), + ) + bot = FakeBot() + context = SimpleNamespace(args=[], bot=bot) + + asyncio.run(router.handle_photo(update, context)) + + assert runner.resume_calls + image_paths = runner.resume_calls[-1]["image_paths"] + assert len(image_paths) == 1 + assert image_paths[0].is_file() + + def test_photo_message_rejected_for_copilot_session(tmp_path: Path): backend = tmp_path / "backend" backend.mkdir() @@ -2345,7 +2444,7 @@ def test_photo_message_rejected_for_copilot_session(tmp_path: Path): asyncio.run(router.handle_photo(update, context)) assert runner.resume_calls == [] - assert "Photo attachments are currently supported only for codex sessions." in bot.messages[-1][1] + assert "Photo attachments are currently supported only for Codex and Claude sessions." in bot.messages[-1][1] def test_voice_message_sends_transcript_preview_before_running_agent(tmp_path: Path): diff --git a/tests/test_config.py b/tests/test_config.py index e513618..ce2e822 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -31,8 +31,10 @@ def _isolate_env(monkeypatch, tmp_path): "LOG_DIR", "CODEX_BIN", "COPILOT_BIN", + "CLAUDE_BIN", "CODEX_MODEL", "COPILOT_MODEL", + "CLAUDE_MODEL", "COPILOT_AUTOPILOT", "COPILOT_NO_ASK_USER", "COPILOT_ALLOW_ALL", @@ -40,6 +42,9 @@ def _isolate_env(monkeypatch, tmp_path): "COPILOT_ALLOW_TOOLS", "COPILOT_DENY_TOOLS", "COPILOT_AVAILABLE_TOOLS", + "CLAUDE_PERMISSION_MODE", + "CLAUDE_ALLOWED_TOOLS", + "CLAUDE_DISALLOWED_TOOLS", "CODEX_APPROVAL_POLICY", "CODEX_SANDBOX_MODE", "CODEX_SKIP_GIT_REPO_CHECK", @@ -102,6 +107,7 @@ def test_load_config_required(monkeypatch, tmp_path): assert cfg.log_dir.name == "logs" assert cfg.codex_model == "" assert cfg.copilot_model == "" + assert cfg.claude_model == "" assert cfg.copilot_autopilot is True assert cfg.copilot_no_ask_user is True assert cfg.copilot_allow_all is True @@ -109,6 +115,10 @@ def test_load_config_required(monkeypatch, tmp_path): assert cfg.copilot_allow_tools == () assert cfg.copilot_deny_tools == () assert cfg.copilot_available_tools == () + assert cfg.claude_bin == "claude" + assert cfg.claude_permission_mode == "bypassPermissions" + assert cfg.claude_allowed_tools == () + assert cfg.claude_disallowed_tools == () assert cfg.state_file == home / ".coding-agent-telegram" / "state.json" assert cfg.state_backup_file == home / ".coding-agent-telegram" / "state.json.bak" @@ -120,6 +130,49 @@ def test_load_config_missing(monkeypatch, tmp_path): load_config() +def test_load_config_accepts_claude_as_default_provider(monkeypatch, tmp_path): + _isolate_env(monkeypatch, tmp_path) + monkeypatch.setenv("WORKSPACE_ROOT", "~/git") + monkeypatch.setenv("TELEGRAM_BOT_TOKENS", "token-a") + monkeypatch.setenv("ALLOWED_CHAT_IDS", "123") + monkeypatch.setenv("DEFAULT_AGENT_PROVIDER", "claude") + + cfg = load_config() + + assert cfg.default_agent_provider == "claude" + + +def test_load_config_rejects_unknown_default_provider(monkeypatch, tmp_path): + _isolate_env(monkeypatch, tmp_path) + monkeypatch.setenv("WORKSPACE_ROOT", "~/git") + monkeypatch.setenv("TELEGRAM_BOT_TOKENS", "token-a") + monkeypatch.setenv("ALLOWED_CHAT_IDS", "123") + monkeypatch.setenv("DEFAULT_AGENT_PROVIDER", "gemini") + + with pytest.raises(ValueError, match="DEFAULT_AGENT_PROVIDER"): + load_config() + + +def test_load_config_claude_overrides(monkeypatch, tmp_path): + _isolate_env(monkeypatch, tmp_path) + monkeypatch.setenv("WORKSPACE_ROOT", "~/git") + monkeypatch.setenv("TELEGRAM_BOT_TOKENS", "token-a") + monkeypatch.setenv("ALLOWED_CHAT_IDS", "123") + monkeypatch.setenv("CLAUDE_BIN", "/opt/bin/claude") + monkeypatch.setenv("CLAUDE_MODEL", "opus") + monkeypatch.setenv("CLAUDE_PERMISSION_MODE", "acceptEdits") + monkeypatch.setenv("CLAUDE_ALLOWED_TOOLS", "Read,Edit,Bash(git *)") + monkeypatch.setenv("CLAUDE_DISALLOWED_TOOLS", "Bash(rm *)") + + cfg = load_config() + + assert cfg.claude_bin == "/opt/bin/claude" + assert cfg.claude_model == "opus" + assert cfg.claude_permission_mode == "acceptEdits" + assert cfg.claude_allowed_tools == ("Read", "Edit", "Bash(git *)") + assert cfg.claude_disallowed_tools == ("Bash(rm *)",) + + def test_load_config_commit_command_enabled(monkeypatch, tmp_path): _isolate_env(monkeypatch, tmp_path) monkeypatch.setenv("WORKSPACE_ROOT", "~/git") @@ -424,6 +477,69 @@ def test_create_initial_env_file_initializes_app_locale_from_system_language(tmp assert "APP_LOCALE=ja" in env_path.read_text(encoding="utf-8") +def test_create_initial_env_file_fills_in_detected_provider_bin_paths(tmp_path, monkeypatch): + env_path = tmp_path / ".env_coding_agent_telegram" + template_path = tmp_path / ".env.example" + template_path.write_text( + "APP_LOCALE=en\nCODEX_BIN=codex\nCOPILOT_BIN=copilot\nCLAUDE_BIN=~/.local/bin/claude\n", + encoding="utf-8", + ) + + detected = { + "codex": "/opt/homebrew/bin/codex", + "claude": "/usr/local/bin/claude", + } + monkeypatch.setattr(config_module, "detect_installed_bin", lambda bin_name: detected.get(bin_name)) + + create_initial_env_file(env_path, template_path) + + written = env_path.read_text(encoding="utf-8") + assert "CODEX_BIN=/opt/homebrew/bin/codex" in written + assert "CLAUDE_BIN=/usr/local/bin/claude" in written + # copilot wasn't detected, so the template value is left untouched. + assert "COPILOT_BIN=copilot" in written + + +def test_detect_installed_bin_prefers_path_then_falls_back_to_candidate_dirs(tmp_path, monkeypatch): + from coding_agent_telegram.config import detect_installed_bin + + monkeypatch.setattr(config_module.shutil, "which", lambda name: None) + fake_home = tmp_path / "home" + local_bin = fake_home / ".local" / "bin" + local_bin.mkdir(parents=True) + claude_bin = local_bin / "claude" + claude_bin.write_text("#!/bin/sh\n", encoding="utf-8") + claude_bin.chmod(0o755) + monkeypatch.setenv("HOME", str(fake_home)) + + assert detect_installed_bin("claude") == str(claude_bin.absolute()) + assert detect_installed_bin("nonexistent-bin-xyz") is None + + +def test_detect_installed_bin_preserves_symlink_instead_of_resolving_target(tmp_path, monkeypatch): + from coding_agent_telegram.config import detect_installed_bin + + # Mirrors real installs where a version manager symlinks a stable path + # (e.g. ~/.local/bin/claude) to a versioned target that changes on upgrade. + versions_dir = tmp_path / "versions" + versions_dir.mkdir() + real_bin = versions_dir / "2.1.214" + real_bin.write_text("#!/bin/sh\n", encoding="utf-8") + real_bin.chmod(0o755) + + path_dir = tmp_path / "path_bin" + path_dir.mkdir() + symlink_bin = path_dir / "claude" + symlink_bin.symlink_to(real_bin) + + monkeypatch.setattr(config_module.shutil, "which", lambda name: str(symlink_bin)) + + detected = detect_installed_bin("claude") + + assert detected == str(symlink_bin.absolute()) + assert detected != str(real_bin) + + # --------------------------------------------------------------------------- # _parse_allowed_chat_ids: malformed value raises clear ValueError # --------------------------------------------------------------------------- diff --git a/tests/test_native_claude_sessions.py b/tests/test_native_claude_sessions.py new file mode 100644 index 0000000..7fcf157 --- /dev/null +++ b/tests/test_native_claude_sessions.py @@ -0,0 +1,250 @@ +"""Tests for native_claude_sessions.py.""" +from __future__ import annotations + +import json +import os +from pathlib import Path +from unittest.mock import patch + +from coding_agent_telegram.native_claude_sessions import claude_projects_root, discover_claude_sessions +from coding_agent_telegram.native_sessions import discover_native_project_sessions + + +def _write_jsonl(path: Path, entries: list[dict]) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("\n".join(json.dumps(entry) for entry in entries) + "\n", encoding="utf-8") + + +def test_claude_projects_root_uses_config_dir_env(tmp_path: Path): + custom = tmp_path / "custom-claude-home" + with patch.dict(os.environ, {"CLAUDE_CONFIG_DIR": str(custom)}): + assert claude_projects_root() == custom / "projects" + + +def test_claude_projects_root_defaults_to_home(tmp_path: Path): + env = {k: v for k, v in os.environ.items() if k != "CLAUDE_CONFIG_DIR"} + with patch.dict(os.environ, env, clear=True): + with patch("coding_agent_telegram.native_claude_sessions.Path.home", return_value=tmp_path): + assert claude_projects_root() == tmp_path / ".claude" / "projects" + + +def test_discover_claude_sessions_returns_empty_when_projects_root_missing(tmp_path: Path): + with patch.dict(os.environ, {"CLAUDE_CONFIG_DIR": str(tmp_path / "does-not-exist")}): + result = discover_claude_sessions(tmp_path / "proj", "proj") + assert result == [] + + +def test_discover_claude_sessions_skips_non_matching_cwd(tmp_path: Path): + claude_home = tmp_path / "claude-home" + proj = tmp_path / "myproj" + proj.mkdir() + other = tmp_path / "other" + other.mkdir() + + session_file = claude_home / "projects" / "-tmp-other" / "sess-1.jsonl" + _write_jsonl( + session_file, + [ + { + "type": "user", + "message": {"role": "user", "content": "hello"}, + "cwd": str(other), + "gitBranch": "main", + "timestamp": "2026-01-01T00:00:00.000Z", + "sessionId": "sess-1", + } + ], + ) + + with patch.dict(os.environ, {"CLAUDE_CONFIG_DIR": str(claude_home)}): + result = discover_claude_sessions(proj, "myproj") + + assert result == [] + + +def test_discover_claude_sessions_returns_matching_session(tmp_path: Path): + claude_home = tmp_path / "claude-home" + proj = tmp_path / "myproj" + proj.mkdir() + + session_file = claude_home / "projects" / "-tmp-myproj" / "sess-abc.jsonl" + _write_jsonl( + session_file, + [ + {"type": "mode", "mode": "normal", "sessionId": "sess-abc"}, + { + "type": "user", + "message": {"role": "user", "content": "Fix the failing test"}, + "cwd": str(proj), + "gitBranch": "feature-x", + "timestamp": "2026-01-01T00:00:00.000Z", + "sessionId": "sess-abc", + }, + { + "type": "assistant", + "message": {"role": "assistant", "content": [{"type": "text", "text": "Sure, looking now."}]}, + "cwd": str(proj), + "gitBranch": "feature-x", + "timestamp": "2026-01-01T00:05:00.000Z", + "sessionId": "sess-abc", + }, + ], + ) + + with patch.dict(os.environ, {"CLAUDE_CONFIG_DIR": str(claude_home)}): + result = discover_claude_sessions(proj, "myproj") + + assert len(result) == 1 + record = result[0] + assert record.session_id == "sess-abc" + assert record.project_folder == "myproj" + assert record.provider == "claude" + assert record.branch_name == "feature-x" + assert record.created_at == "2026-01-01T00:00:00.000Z" + assert record.updated_at == "2026-01-01T00:05:00.000Z" + assert "Fix the failing test" in record.name + assert record.source_label == "native claude" + + +def test_discover_claude_sessions_ignores_meta_messages_for_first_user_text(tmp_path: Path): + claude_home = tmp_path / "claude-home" + proj = tmp_path / "myproj" + proj.mkdir() + + session_file = claude_home / "projects" / "-tmp-myproj" / "sess-meta.jsonl" + _write_jsonl( + session_file, + [ + { + "type": "user", + "isMeta": True, + "message": {"role": "user", "content": "ignored"}, + "cwd": str(proj), + "timestamp": "2026-01-01T00:00:00.000Z", + "sessionId": "sess-meta", + }, + { + "type": "user", + "message": {"role": "user", "content": "Actual user prompt"}, + "cwd": str(proj), + "timestamp": "2026-01-01T00:01:00.000Z", + "sessionId": "sess-meta", + }, + ], + ) + + with patch.dict(os.environ, {"CLAUDE_CONFIG_DIR": str(claude_home)}): + result = discover_claude_sessions(proj, "myproj") + + assert len(result) == 1 + assert "Actual user prompt" in result[0].name + + +def test_discover_claude_sessions_ignores_malformed_lines(tmp_path: Path): + claude_home = tmp_path / "claude-home" + proj = tmp_path / "myproj" + proj.mkdir() + + session_file = claude_home / "projects" / "-tmp-myproj" / "sess-broken.jsonl" + session_file.parent.mkdir(parents=True, exist_ok=True) + session_file.write_text( + "not json at all\n" + + json.dumps( + { + "type": "user", + "message": {"role": "user", "content": "still works"}, + "cwd": str(proj), + "timestamp": "2026-01-01T00:00:00.000Z", + } + ) + + "\n", + encoding="utf-8", + ) + + with patch.dict(os.environ, {"CLAUDE_CONFIG_DIR": str(claude_home)}): + result = discover_claude_sessions(proj, "myproj") + + assert len(result) == 1 + assert result[0].session_id == "sess-broken" + + +def test_discover_claude_sessions_sorts_newest_first(tmp_path: Path): + claude_home = tmp_path / "claude-home" + proj = tmp_path / "myproj" + proj.mkdir() + + _write_jsonl( + claude_home / "projects" / "-tmp-myproj" / "sess-old.jsonl", + [ + { + "type": "user", + "message": {"role": "user", "content": "old"}, + "cwd": str(proj), + "timestamp": "2026-01-01T00:00:00.000Z", + } + ], + ) + _write_jsonl( + claude_home / "projects" / "-tmp-myproj" / "sess-new.jsonl", + [ + { + "type": "user", + "message": {"role": "user", "content": "new"}, + "cwd": str(proj), + "timestamp": "2026-06-01T00:00:00.000Z", + } + ], + ) + + with patch.dict(os.environ, {"CLAUDE_CONFIG_DIR": str(claude_home)}): + result = discover_claude_sessions(proj, "myproj") + + assert [record.session_id for record in result] == ["sess-new", "sess-old"] + + +def test_discover_native_project_sessions_includes_claude_records(tmp_path: Path): + claude_home = tmp_path / "claude-home" + proj = tmp_path / "myproj" + proj.mkdir() + + _write_jsonl( + claude_home / "projects" / "-tmp-myproj" / "sess-claude.jsonl", + [ + { + "type": "user", + "message": {"role": "user", "content": "hello"}, + "cwd": str(proj), + "timestamp": "2026-01-01T00:00:00.000Z", + } + ], + ) + + with patch.dict(os.environ, {"CLAUDE_CONFIG_DIR": str(claude_home), "COPILOT_HOME": str(tmp_path / "no-copilot")}): + with patch("coding_agent_telegram.native_codex_sessions.Path.home", return_value=tmp_path / "no-codex-home"): + sessions = discover_native_project_sessions(proj, "myproj") + + assert "sess-claude" in sessions + assert sessions["sess-claude"].provider == "claude" + + +def test_discover_native_project_sessions_can_filter_to_claude_only(tmp_path: Path): + claude_home = tmp_path / "claude-home" + proj = tmp_path / "myproj" + proj.mkdir() + + _write_jsonl( + claude_home / "projects" / "-tmp-myproj" / "sess-claude.jsonl", + [ + { + "type": "user", + "message": {"role": "user", "content": "hello"}, + "cwd": str(proj), + "timestamp": "2026-01-01T00:00:00.000Z", + } + ], + ) + + with patch.dict(os.environ, {"CLAUDE_CONFIG_DIR": str(claude_home)}): + sessions = discover_native_project_sessions(proj, "myproj", provider="claude") + + assert set(sessions.keys()) == {"sess-claude"} diff --git a/tests/test_session_store.py b/tests/test_session_store.py index bb33d92..9f26d9b 100644 --- a/tests/test_session_store.py +++ b/tests/test_session_store.py @@ -51,6 +51,32 @@ def test_set_pending_action_persists_and_clears(tmp_path: Path): assert "pending_action" not in store.get_chat_state("bot-a", 123) +def test_clear_all_pending_actions_removes_stale_entries_across_chats(tmp_path: Path): + state = tmp_path / "state.json" + backup = tmp_path / "state.json.bak" + store = SessionStore(state, backup) + + store.set_pending_action("bot-a", 123, {"kind": "message", "user_message": "stuck"}) + store.set_pending_action("bot-a", 456, {"kind": "message", "user_message": "also stuck"}) + store.set_current_provider("bot-a", 789, "codex") # chat with no pending_action + + cleared = store.clear_all_pending_actions() + + assert cleared == 2 + assert "pending_action" not in store.get_chat_state("bot-a", 123) + assert "pending_action" not in store.get_chat_state("bot-a", 456) + + +def test_clear_all_pending_actions_returns_zero_when_none_present(tmp_path: Path): + state = tmp_path / "state.json" + backup = tmp_path / "state.json.bak" + store = SessionStore(state, backup) + + store.set_current_provider("bot-a", 123, "codex") + + assert store.clear_all_pending_actions() == 0 + + def test_load_empty_state_file_returns_default_state(tmp_path: Path): state = tmp_path / "state.json" backup = tmp_path / "state.json.bak" diff --git a/tests/test_speech_to_text.py b/tests/test_speech_to_text.py index e405d82..8490146 100644 --- a/tests/test_speech_to_text.py +++ b/tests/test_speech_to_text.py @@ -19,8 +19,10 @@ def _cfg(tmp_path: Path, *, model: str = "base", timeout: int = 120) -> AppConfi allowed_chat_ids={123}, codex_bin="codex", copilot_bin="copilot", + claude_bin="claude", codex_model="", copilot_model="", + claude_model="", copilot_autopilot=True, copilot_no_ask_user=True, copilot_allow_all=True, @@ -28,6 +30,9 @@ def _cfg(tmp_path: Path, *, model: str = "base", timeout: int = 120) -> AppConfi copilot_allow_tools=(), copilot_deny_tools=(), copilot_available_tools=(), + claude_permission_mode="bypassPermissions", + claude_allowed_tools=(), + claude_disallowed_tools=(), codex_approval_policy="never", codex_sandbox_mode="workspace-write", codex_skip_git_repo_check=False,